From 3d092f989c18de3595474c55e91b0141e664ec3e Mon Sep 17 00:00:00 2001 From: Sergey Kozyrenko Date: Tue, 21 Jul 2026 04:43:17 +0700 Subject: [PATCH] test(e2e): scan a11y in dark theme too, waiving the known badge contrast debt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit axe only ran in the light theme, so dark-only contrast regressions were invisible. Parametrize every a11y scan over light and dark. The dark pass surfaced a real, pre-existing issue: colored Badge variants use text-{color}-600 with no dark override and fail contrast on the dark background. Waive it narrowly on the knowledges list (the blue badge) as named debt — a palette fix is queued separately — so the rest of the dark surface is now gated. Co-Authored-By: Claude Fable 5 --- frontend/e2e/specs/cross/a11y.spec.ts | 128 +++++++++++++++----------- 1 file changed, 73 insertions(+), 55 deletions(-) diff --git a/frontend/e2e/specs/cross/a11y.spec.ts b/frontend/e2e/specs/cross/a11y.spec.ts index e39ccb2b..6c2b636b 100644 --- a/frontend/e2e/specs/cross/a11y.spec.ts +++ b/frontend/e2e/specs/cross/a11y.spec.ts @@ -14,63 +14,81 @@ const ALLOWLIST: A11yAllowlist = { // targets are class chains, so the pattern pins the 50%-muted class; any // other contrast violation on the page still fails. '/flows/:flowId': [{ rule: 'color-contrast', target: /text-muted-foreground\\?\/50/ }], + // Colored Badge variants use text-{color}-600 with no dark override, so on + // the dark near-black background the text fails contrast (all seven color + // variants share this; the knowledges list renders the blue one). Real + // dark-mode debt, queued for a palette fix — remove when badge dark text + // shades land. + '/knowledges': [{ rule: 'color-contrast', target: /border-blue-500/ }], }; -test.describe('a11y', { tag: '@cross' }, () => { - test.describe('login', () => { - test.use({ cassette: loginJourneyCassette, isAuthSeeded: false }); +// Both themes are scanned: contrast waivers are theme-independent, but dark mode +// recomputes every color, so a token that passes in light can still fail in dark. +const THEMES = ['light', 'dark'] as const; - test('login page', async ({ page }) => { - await page.goto('/login'); - await expect(page.getByRole('button', { name: 'Sign in' })).toBeVisible(); - await scanA11y(page, '/login', ALLOWLIST); +for (const theme of THEMES) { + test.describe(`a11y (${theme})`, { tag: '@cross' }, () => { + if (theme === 'dark') { + test.beforeEach(async ({ page }) => { + await page.addInitScript(() => window.localStorage.setItem('theme', 'dark')); + }); + } + + test.describe('login', () => { + test.use({ cassette: loginJourneyCassette, isAuthSeeded: false }); + + test('login page', async ({ page }) => { + await page.goto('/login'); + await expect(page.getByRole('button', { name: 'Sign in' })).toBeVisible(); + await scanA11y(page, '/login', ALLOWLIST); + }); + }); + + test.describe('flows', () => { + test.use({ cassette: flowsCassette() }); + + test('flows list', async ({ page }) => { + await page.goto('/flows'); + await expect(page.getByRole('row', { name: /E2E Alpha/ })).toBeVisible(); + await scanA11y(page, '/flows', ALLOWLIST); + }); + + test('flow detail', async ({ page }) => { + await page.goto('/flows'); + await page.getByRole('row', { name: /E2E Alpha/ }).click(); + await expect(page.getByRole('button', { name: 'Flow actions' })).toBeVisible(); + await scanA11y(page, '/flows/:flowId', ALLOWLIST); + }); + }); + + test.describe('templates', () => { + test.use({ cassette: templatesCassette() }); + + test('templates list', async ({ page }) => { + await page.goto('/templates'); + await expect(page.getByRole('row', { name: /E2E Seed Template/ })).toBeVisible(); + await scanA11y(page, '/templates', ALLOWLIST); + }); + }); + + test.describe('knowledges', () => { + test.use({ cassette: knowledgesCassette() }); + + test('knowledges list', async ({ page }) => { + await page.goto('/knowledges'); + await expect(page.getByRole('row', { name: /E2E Seed Question/ })).toBeVisible(); + await scanA11y(page, '/knowledges', ALLOWLIST); + }); + }); + + test.describe('api tokens', () => { + test.use({ cassette: apiTokensCassette() }); + + test('api tokens list', async ({ page }) => { + await page.goto('/settings/api-tokens'); + await expect(page.getByRole('row', { name: /E2E seed token/ })).toBeVisible(); + await scanA11y(page, '/settings/api-tokens', ALLOWLIST); + }); }); }); - - test.describe('flows', () => { - test.use({ cassette: flowsCassette() }); - - test('flows list', async ({ page }) => { - await page.goto('/flows'); - await expect(page.getByRole('row', { name: /E2E Alpha/ })).toBeVisible(); - await scanA11y(page, '/flows', ALLOWLIST); - }); - - test('flow detail', async ({ page }) => { - await page.goto('/flows'); - await page.getByRole('row', { name: /E2E Alpha/ }).click(); - await expect(page.getByRole('button', { name: 'Flow actions' })).toBeVisible(); - await scanA11y(page, '/flows/:flowId', ALLOWLIST); - }); - }); - - test.describe('templates', () => { - test.use({ cassette: templatesCassette() }); - - test('templates list', async ({ page }) => { - await page.goto('/templates'); - await expect(page.getByRole('row', { name: /E2E Seed Template/ })).toBeVisible(); - await scanA11y(page, '/templates', ALLOWLIST); - }); - }); - - test.describe('knowledges', () => { - test.use({ cassette: knowledgesCassette() }); - - test('knowledges list', async ({ page }) => { - await page.goto('/knowledges'); - await expect(page.getByRole('row', { name: /E2E Seed Question/ })).toBeVisible(); - await scanA11y(page, '/knowledges', ALLOWLIST); - }); - }); - - test.describe('api tokens', () => { - test.use({ cassette: apiTokensCassette() }); - - test('api tokens list', async ({ page }) => { - await page.goto('/settings/api-tokens'); - await expect(page.getByRole('row', { name: /E2E seed token/ })).toBeVisible(); - await scanA11y(page, '/settings/api-tokens', ALLOWLIST); - }); - }); -}); +}