/** * Release Policy Builder — E2E Tests (Sprint 005) * * Verifies: * 1. Pack detail shows 3 tabs (Rules, Test, Activate) * 2. Old tab URLs resolve to new tabs * 3. Policy workspace loads at /ops/policy/packs * 4. No Angular runtime errors across builder routes */ import { expect, test, type Page } from '@playwright/test'; import { policyAuthorSession } from '../../src/app/testing'; const shellSession = { ...policyAuthorSession, scopes: [ ...new Set([ ...policyAuthorSession.scopes, 'ui.read', 'admin', 'ui.admin', 'orch:read', 'orch:operate', 'findings:read', 'authority:tenants.read', 'advisory:read', 'vex:read', 'exceptions:read', 'policy:read', 'policy:author', 'policy:review', 'policy:approve', 'policy:simulate', 'policy:audit', 'health:read', 'release:read', 'release:write', 'sbom:read', 'signer:read', ]), ], }; const mockConfig = { authority: { issuer: 'http://127.0.0.1:4400/authority', clientId: 'stella-ops-ui', authorizeEndpoint: 'http://127.0.0.1:4400/authority/connect/authorize', tokenEndpoint: 'http://127.0.0.1:4400/authority/connect/token', logoutEndpoint: 'http://127.0.0.1:4400/authority/connect/logout', redirectUri: 'http://127.0.0.1:4400/auth/callback', postLogoutRedirectUri: 'http://127.0.0.1:4400/', scope: 'openid profile email ui.read policy:read policy:author policy:simulate', audience: 'http://127.0.0.1:4400/gateway', dpopAlgorithms: ['ES256'], refreshLeewaySeconds: 60, }, apiBaseUrls: { authority: '/authority', scanner: '/scanner', policy: '/policy', concelier: '/concelier', attestor: '/attestor', gateway: '/gateway', }, quickstartMode: true, setup: 'complete', }; const oidcConfig = { issuer: mockConfig.authority.issuer, authorization_endpoint: mockConfig.authority.authorizeEndpoint, token_endpoint: mockConfig.authority.tokenEndpoint, jwks_uri: 'http://127.0.0.1:4400/authority/.well-known/jwks.json', response_types_supported: ['code'], subject_types_supported: ['public'], id_token_signing_alg_values_supported: ['RS256'], }; async function setupShell(page: Page): Promise { await page.addInitScript((session) => { try { window.sessionStorage.clear(); } catch { /* ignore */ } (window as { __stellaopsTestSession?: unknown }).__stellaopsTestSession = session; }, shellSession); await page.route('**/platform/envsettings.json', (route) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(mockConfig) }), ); await page.route('**/config.json', (route) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(mockConfig) }), ); await page.route('**/authority/.well-known/openid-configuration', (route) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(oidcConfig) }), ); await page.route('**/.well-known/openid-configuration', (route) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(oidcConfig) }), ); await page.route('**/authority/.well-known/jwks.json', (route) => route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ keys: [] }) }), ); await page.route('**/authority/connect/**', (route) => route.fulfill({ status: 400, contentType: 'application/json', body: JSON.stringify({ error: 'not-used' }) }), ); } async function go(page: Page, path: string): Promise { await page.goto(path, { waitUntil: 'domcontentloaded' }); await page.waitForLoadState('networkidle', { timeout: 5000 }).catch(() => null); } async function ensureShell(page: Page): Promise { await expect(page.locator('aside.sidebar')).toHaveCount(1, { timeout: 30000 }); } function collectAngularErrors(page: Page): string[] { const errors: string[] = []; page.on('console', (msg) => { const text = msg.text(); if (msg.type() === 'error' && /NG0\d{3,4}/.test(text)) { errors.push(text); } }); return errors; } test.describe.configure({ mode: 'serial' }); test.beforeEach(async ({ page }) => { await setupShell(page); }); // --------------------------------------------------------------------------- // 1. Pack shell shows 3 tabs for a pack detail // --------------------------------------------------------------------------- test.describe('Pack detail tabs', () => { test('pack shell renders with data-testid', async ({ page }) => { const errors = collectAngularErrors(page); await go(page, '/ops/policy/packs'); await ensureShell(page); const shell = page.locator('[data-testid="policy-pack-shell"]'); await expect(shell).toBeVisible({ timeout: 10000 }); expect(errors).toHaveLength(0); }); test('pack shell title says "Release Policies"', async ({ page }) => { await go(page, '/ops/policy/packs'); await ensureShell(page); const shell = page.locator('[data-testid="policy-pack-shell"]'); await expect(shell).toContainText('Release Policies'); }); }); // --------------------------------------------------------------------------- // 2. Workspace renders content // --------------------------------------------------------------------------- test.describe('Policy workspace', () => { test('/ops/policy/packs renders workspace content', async ({ page }) => { const errors = collectAngularErrors(page); await go(page, '/ops/policy/packs'); await ensureShell(page); const main = page.locator('main'); await expect(main).toBeVisible(); const text = ((await main.textContent()) ?? '').replace(/\s+/g, ''); expect(text.length).toBeGreaterThan(12); expect(errors).toHaveLength(0); }); }); // --------------------------------------------------------------------------- // 3. Policy routes render without Angular errors // --------------------------------------------------------------------------- test.describe('Policy routes stability', () => { test.setTimeout(90_000); const ROUTES = [ '/ops/policy/packs', '/ops/policy/overview', '/ops/policy/governance', '/ops/policy/vex', ]; for (const route of ROUTES) { test(`${route} renders without errors`, async ({ page }) => { const errors = collectAngularErrors(page); await go(page, route); await ensureShell(page); const main = page.locator('main'); await expect(main).toBeVisible(); expect(errors).toHaveLength(0); }); } }); // --------------------------------------------------------------------------- // 4. Full policy journey stability // --------------------------------------------------------------------------- test.describe('Policy journey', () => { test('navigating across policy pages produces no Angular errors', async ({ page }) => { test.setTimeout(120_000); const errors = collectAngularErrors(page); const journey = [ '/ops/policy/packs', '/ops/policy/overview', '/ops/policy/governance', '/ops/policy/vex', '/ops/policy/packs', ]; for (const route of journey) { await go(page, route); await ensureShell(page); } expect(errors, `Angular errors: ${errors.join('\n')}`).toHaveLength(0); }); });