test(e2e): make the colour gates fail loudly when they stop measuring

Three ways these gates could go green while measuring nothing:

The editor probes name their classes by hand — the one place in the contrast
spec that does not import them from the app. A rename, or the rule moving out
from under its ancestor chain, would mount a span matching no rule, and the
measurement would fall back to the page's default text pair, which clears AA.
The probes now reject a transparent background, which only an unmatched class
produces (the check lives with the editor probes, not in the shared measure —
the outline badge legitimately paints no background).

The dark sweeps seed a theme key that mirrors a *default parameter value* in the
provider, so it can stop matching without any test file changing; the run would
then repeat the light theme under a dark label. Both sweeps now assert the theme
actually applied.

The resources target-size waiver matched `.rounded` unanchored, so it also
covered every `rounded-*` utility — a too-broad waiver never fails, it only
hides. Anchored to the class it documents.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Sergey Kozyrenko
2026-07-22 00:09:42 +07:00
co-authored by Claude Opus 4.8
parent 2fd4c0b685
commit d012819f6b
4 changed files with 19 additions and 1 deletions
+10
View File
@@ -62,6 +62,16 @@ export const mountEditorProbes = async (page: Page, probes: Record<string, strin
host.append(surface);
document.body.append(host);
// Both token rules paint a chip via color-mix, so a transparent probe
// means its class matched nothing — a rename, or the rule moving out
// from under the ancestor chain above. Without this the measurement
// silently falls back to the page's default text pair, which clears AA.
for (const probe of surface.children) {
if (getComputedStyle(probe).backgroundColor === 'rgba(0, 0, 0, 0)') {
throw new Error(`editor contrast probe "${probe.className}" matched no style rule`);
}
}
},
{ entries: Object.entries(probes), hostId: HOST_ID },
);
+3 -1
View File
@@ -110,7 +110,9 @@ export const ROUTE_MANIFEST: RouteManifestEntry[] = [
// target floor — widening them is a density decision for the file manager.
a11yWaivers: [
{ rule: 'color-contrast', target: /text-muted-foreground\\?\/80/ },
{ rule: 'target-size', target: /\.rounded|aria-label="Select / },
// `.rounded` unanchored would also match every rounded-* utility, so a
// future under-sized control anywhere on the page would be waived too.
{ rule: 'target-size', target: /\.rounded(?![-\w])|aria-label="Select / },
],
cassette: resourcesCassette,
path: routes.resources,
+3
View File
@@ -35,6 +35,9 @@ for (const theme of THEMES) {
test('has no axe violations', async ({ page }) => {
await page.goto(entry.path);
await expect(entry.ready(page)).toBeVisible();
// A seed that stops taking effect would otherwise re-run the light
// theme under a dark label and pass.
await expect(page.locator('html')).toHaveClass(theme === 'dark' ? /dark/ : /light/);
await scanA11y(page, entry.path, entry.a11yWaivers);
});
});
@@ -42,6 +42,7 @@ for (const theme of THEMES) {
test('every badge variant clears AA at rest and on hover', async ({ page }) => {
await page.goto('/flows');
await expect(page.getByRole('row', { name: /E2E Alpha/ })).toBeVisible();
await expect(page.locator('html')).toHaveClass(theme === 'dark' ? /dark/ : /light/);
await mountContrastProbes(
page,
@@ -63,6 +64,7 @@ for (const theme of THEMES) {
test('the destructive button clears AA at rest and on hover', async ({ page }) => {
await page.goto('/flows');
await expect(page.getByRole('row', { name: /E2E Alpha/ })).toBeVisible();
await expect(page.locator('html')).toHaveClass(theme === 'dark' ? /dark/ : /light/);
await mountContrastProbes(page, { destructive: buttonVariants({ variant: 'destructive' }) });
@@ -77,6 +79,7 @@ for (const theme of THEMES) {
test('editor highlight tokens clear AA on the editor surface', async ({ page }) => {
await page.goto('/flows');
await expect(page.getByRole('row', { name: /E2E Alpha/ })).toBeVisible();
await expect(page.locator('html')).toHaveClass(theme === 'dark' ? /dark/ : /light/);
await mountEditorProbes(page, EDITOR_TOKENS);