test(e2e): gate per-tab scans on the panel's own content

The a11y and palette sweeps clicked a tab and scanned immediately, so the two
round-trip panels could be scanned while still skeletons. Skeletons carry no
axe or palette violations, so those scans passed on an empty panel instead of
the content they exist to check. Tabs now carry a readiness locator beside the
name and both sweeps wait for it.

Ordering is part of the same defect: the flow auto-opens the Assistant panel
when it has no message logs, so an Assistant-first sweep clicked a tab that was
already open and asserted a marker that predated the click. Dashboard leads the
left-hand pair, and the sweep asserts each panel is absent before its own click
so a future reordering that makes an iteration a no-op fails loudly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Sergey Kozyrenko
2026-07-24 12:59:48 +07:00
co-authored by Claude Opus 4.8
parent 7907ee5f60
commit d414ec0bb6
4 changed files with 39 additions and 26 deletions
+24 -3
View File
@@ -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,
+4 -3
View File
@@ -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);
});
}
});
+4 -3
View File
@@ -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] ?? []);
});
+7 -17
View File
@@ -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);