diff --git a/frontend/e2e/routes.ts b/frontend/e2e/routes.ts index dcccbb10..9de0908e 100644 --- a/frontend/e2e/routes.ts +++ b/frontend/e2e/routes.ts @@ -7,7 +7,7 @@ import type { Cassette } from './mocks/cassette.ts'; import { apiTokensCassette } from './mocks/cassettes/api-tokens.ts'; import { dashboardCassette } from './mocks/cassettes/dashboard.ts'; -import { flowsCassette, flowTabsCassette } from './mocks/cassettes/flows.ts'; +import { flowsCassette, flowTabsCassette, TABS_FILE_NAME, TABS_SCREENSHOT_URL } from './mocks/cassettes/flows.ts'; import { knowledgesCassette } from './mocks/cassettes/knowledges.ts'; import { resourcesCassette } from './mocks/cassettes/resources.ts'; import { settingsPromptsCassette } from './mocks/cassettes/settings-prompts.ts'; @@ -27,9 +27,30 @@ export interface RouteManifestEntry { */ sources: string[]; /** Radix unmounts inactive tab panels, so one scan of the default view sees none of them. */ - tabs?: string[]; + tabs?: RouteTab[]; } +export interface RouteTab { + name: string; + /** + * Must be absent while the panel loads: a marker that also renders on the + * skeleton lets every per-tab scan pass on an empty panel. + */ + ready: (page: Page) => Locator; +} + +/** Order matters: the flow auto-opens Assistant, so tabs.spec pins that no entry is already open. */ +export const FLOW_DETAIL_TABS: RouteTab[] = [ + { name: 'Dashboard', ready: (page) => page.getByText('Usage by Model & Provider') }, + { name: 'Assistant', ready: (page) => page.getByText('New assistant', { exact: true }) }, + { name: 'Tasks', ready: (page) => page.getByText('E2E Task Alpha') }, + { name: 'Agents', ready: (page) => page.getByText('E2E agent reconnaissance') }, + { name: 'Searches', ready: (page) => page.getByText('E2E search for the CVE') }, + { name: 'Vector Store', ready: (page) => page.getByText('E2E recall prior findings') }, + { name: 'Files', ready: (page) => page.getByText(TABS_FILE_NAME) }, + { name: 'Screenshots', ready: (page) => page.getByText(TABS_SCREENSHOT_URL) }, +]; + /** * The routes swept by NAV/visual/a11y specs and CI diff-scoping. This is a * subset of the app's routes — route-manifest.unit.test.ts pins the excluded @@ -75,7 +96,7 @@ export const ROUTE_MANIFEST: RouteManifestEntry[] = [ 'src/components/dashboard', 'src/features/resources', ], - tabs: ['Assistant', 'Dashboard', 'Tasks', 'Agents', 'Searches', 'Vector Store', 'Files', 'Screenshots'], + tabs: FLOW_DETAIL_TABS, }, { cassette: templatesCassette, diff --git a/frontend/e2e/specs/cross/a11y.spec.ts b/frontend/e2e/specs/cross/a11y.spec.ts index 9e9fdb77..184e7f54 100644 --- a/frontend/e2e/specs/cross/a11y.spec.ts +++ b/frontend/e2e/specs/cross/a11y.spec.ts @@ -52,12 +52,13 @@ for (const theme of THEMES) { }); for (const tab of entry.tabs ?? []) { - test(`tab "${tab}" has no axe violations`, async ({ page }) => { + test(`tab "${tab.name}" has no axe violations`, async ({ page }) => { await page.goto(entry.path); await expect(entry.ready(page)).toBeVisible(); await expect(page.locator('html')).toHaveClass(theme === 'dark' ? /dark/ : /light/); - await page.getByRole('tab', { name: tab }).click(); - await scanA11y(page, `${entry.path} [${tab}]`, entry.a11yWaivers); + await page.getByRole('tab', { name: tab.name }).click(); + await expect(tab.ready(page)).toBeVisible(); + await scanA11y(page, `${entry.path} [${tab.name}]`, entry.a11yWaivers); }); } }); diff --git a/frontend/e2e/specs/cross/palette.spec.ts b/frontend/e2e/specs/cross/palette.spec.ts index 98a6b7f9..241f0609 100644 --- a/frontend/e2e/specs/cross/palette.spec.ts +++ b/frontend/e2e/specs/cross/palette.spec.ts @@ -84,13 +84,14 @@ test.describe('palette compliance', { tag: '@cross' }, () => { // Tabs mount their panels lazily (Radix unmounts inactive ones), so the default-view scan // above never sees them — sweep each panel like the a11y gate does. for (const tab of entry.tabs ?? []) { - test(`tab "${tab}" carries no off-palette colour`, async ({ page }) => { + test(`tab "${tab.name}" carries no off-palette colour`, async ({ page }) => { await page.goto(entry.path); await expect(entry.ready(page)).toBeVisible(); - await page.getByRole('tab', { name: tab }).click(); + await page.getByRole('tab', { name: tab.name }).click(); + await expect(tab.ready(page)).toBeVisible(); const offenders = await scanOffenders(page); - const key = `${entry.path} [${tab}]`; + const key = `${entry.path} [${tab.name}]`; expect([...new Set(offenders)], `off-palette colours on ${key}`).toEqual(ACCEPTED[key] ?? []); }); diff --git a/frontend/e2e/specs/flows/tabs.spec.ts b/frontend/e2e/specs/flows/tabs.spec.ts index 216d75b3..0adbb7d5 100644 --- a/frontend/e2e/specs/flows/tabs.spec.ts +++ b/frontend/e2e/specs/flows/tabs.spec.ts @@ -1,20 +1,7 @@ import { expect, test } from '../../fixtures/test.ts'; import { expectCleanPage } from '../../helpers/errors.ts'; -import { - flowTabsCassette, - TABS_FILE_NAME, - TABS_SCREENSHOT_NAME, - TABS_SCREENSHOT_URL, -} from '../../mocks/cassettes/flows.ts'; - -const TABS = [ - { marker: 'E2E Task Alpha', name: 'Tasks' }, - { marker: 'E2E agent reconnaissance', name: 'Agents' }, - { marker: 'E2E search for the CVE', name: 'Searches' }, - { marker: 'E2E recall prior findings', name: 'Vector Store' }, - { marker: TABS_FILE_NAME, name: 'Files' }, - { marker: TABS_SCREENSHOT_URL, name: 'Screenshots' }, -] as const; +import { flowTabsCassette, TABS_SCREENSHOT_NAME } from '../../mocks/cassettes/flows.ts'; +import { FLOW_DETAIL_TABS } from '../../routes.ts'; test.describe('flow detail tabs', { tag: '@flows' }, () => { test.use({ cassette: flowTabsCassette() }); @@ -23,9 +10,12 @@ test.describe('flow detail tabs', { tag: '@flows' }, () => { await page.goto('/flows/5'); await expect(page.locator('header').getByRole('button', { name: 'Toggle favorite' })).toBeEnabled(); - for (const { marker, name } of TABS) { + for (const { name, ready } of FLOW_DETAIL_TABS) { + // Pins that the click is what reveals the panel. Ordering this sweep so a tab is already + // open when its turn comes (the flow auto-opens Assistant) makes its iteration a no-op. + await expect(ready(page), `"${name}" must not be on screen before its own click`).not.toBeAttached(); await page.getByRole('tab', { name }).click(); - await expect(page.getByText(marker)).toBeVisible(); + await expect(ready(page)).toBeVisible(); } expectCleanPage(pageErrorLog);