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', 'orch:quota', 'findings:read', 'vuln:view', 'vuln:investigate', 'vuln:operate', 'vuln:audit', 'authority:tenants.read', 'advisory:read', 'vex:read', 'exceptions:read', 'exceptions:approve', 'aoc:verify', 'policy:read', 'policy:author', 'policy:review', 'policy:approve', 'policy:simulate', 'policy:audit', 'health:read', 'notify:viewer', 'release:read', 'release:write', 'release:publish', '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 authority:tenants.read advisory:read vex:read exceptions:read exceptions:approve aoc:verify findings:read orch:read vuln:view vuln:investigate vuln:operate vuln:audit', 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 storage access errors in restricted contexts } (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: [] }), }), ); } 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: 15000 }); } test.describe.configure({ mode: 'serial' }); test.describe('IA v2 accessibility and regression', () => { test.beforeEach(async ({ page }) => { await setupShell(page); }); test('canonical roots expose landmarks and navigation controls', async ({ page }) => { test.setTimeout(90_000); const roots = ['/mission-control/board', '/releases', '/security', '/evidence', '/ops', '/setup']; for (const path of roots) { await go(page, path); await ensureShell(page); const landmarkCount = await page.locator('main, [role="main"], nav, [role="navigation"]').count(); expect(landmarkCount).toBeGreaterThan(1); await expect(page.locator('aside.sidebar a, aside.sidebar button').first()).toBeVisible(); } }); test('keyboard navigation moves focus across shell controls', async ({ page }) => { await go(page, '/mission-control/board'); await ensureShell(page); const focusedElements: string[] = []; for (let i = 0; i < 10; i += 1) { await page.keyboard.press('Tab'); const focused = await page.evaluate(() => { const element = document.activeElement as HTMLElement | null; if (!element) return 'none'; return `${element.tagName.toLowerCase()}::${element.className || element.id || 'no-id'}`; }); focusedElements.push(focused); } expect(new Set(focusedElements).size).toBeGreaterThan(3); }); test('deprecated root labels are absent from primary nav', async ({ page }) => { await go(page, '/mission-control/board'); await ensureShell(page); const navText = (await page.locator('aside.sidebar nav').textContent()) ?? ''; expect(navText).not.toContain('Security & Risk'); expect(navText).not.toContain('Evidence & Audit'); expect(navText).not.toContain('Platform Ops'); expect(navText).not.toContain('Administration'); expect(navText).not.toContain('Policy Studio'); }); test('breadcrumbs render canonical ownership on key shell routes', async ({ page }) => { test.setTimeout(90_000); const checks: Array<{ path: string; expected: string }> = [ { path: '/mission-control/board', expected: 'Mission Board' }, { path: '/releases/versions', expected: 'Release Versions' }, { path: '/security/advisories-vex', expected: 'Advisories & VEX' }, { path: '/evidence/verify-replay', expected: 'Verify & Replay' }, { path: '/ops/operations/data-integrity', expected: 'Data Integrity' }, { path: '/setup/topology/agents', expected: 'Agent Fleet' }, ]; for (const check of checks) { await go(page, check.path); await ensureShell(page); const breadcrumb = page.locator('app-breadcrumb nav.breadcrumb'); await expect(breadcrumb).toHaveCount(1); await expect(breadcrumb).toContainText(check.expected); } }); test('mobile viewport keeps shell usable without horizontal overflow', async ({ page }) => { await page.setViewportSize({ width: 390, height: 844 }); await go(page, '/mission-control/board'); await expect(page.locator('.topbar__menu-toggle')).toBeVisible(); const hasHorizontalScroll = await page.evaluate( () => document.documentElement.scrollWidth > document.documentElement.clientWidth, ); expect(hasHorizontalScroll).toBe(false); }); });