mirror of
https://github.com/vxcontrol/pentagi.git
synced 2026-08-25 12:36:30 +00:00
test(e2e): sanction palette tokens per variant, and sweep populated providers
The gate sanctioned the flat union of every badge and button variant, so any variant's hue passed on any element — precisely the wrong-variant reuse it was built for. Match each element's palette tokens against a single variant's set, by equality rather than containment: a lone borrowed token is a subset of the variant it came from, and containment waved it through (verified — an outline badge carrying the blue variant's `text-blue-800` passed until the switch). The route sweep also reaches /settings/providers only with the empty seed, so the provider cards were never palette-scanned, unlike in the a11y and visual gates. Add the same dedicated populated sweep. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
a51e36fdc7
commit
585df2a953
@@ -7,6 +7,7 @@ import { buttonVariants } from '@/components/ui/button';
|
||||
import { routes } from '@/lib/routes';
|
||||
|
||||
import { expect, test } from '../../fixtures/test.ts';
|
||||
import { populatedSettingsProvidersCassette } from '../../mocks/cassettes/settings-providers.ts';
|
||||
import { ROUTE_MANIFEST } from '../../routes.ts';
|
||||
|
||||
const BADGE_VARIANTS = Object.keys({
|
||||
@@ -25,20 +26,30 @@ const BADGE_VARIANTS = Object.keys({
|
||||
|
||||
const BUTTON_VARIANTS = ['default', 'destructive', 'ghost', 'link', 'outline', 'secondary'] as const;
|
||||
|
||||
const SANCTIONED = new Set(
|
||||
[
|
||||
...BADGE_VARIANTS.map((variant) => badgeVariants({ variant })),
|
||||
...BUTTON_VARIANTS.map((variant) => buttonVariants({ variant })),
|
||||
]
|
||||
.join(' ')
|
||||
.split(/\s+/),
|
||||
);
|
||||
|
||||
// A hard-coded hue (`text-green-800`, `bg-[#16a34a]`) rather than a semantic token
|
||||
// (`bg-primary`, `text-muted-foreground`) or a non-colour utility (`text-xs`).
|
||||
const PALETTE_UTILITY =
|
||||
/^(?:[a-z-]+:)*(?:bg|text|border|ring|from|via|to|fill|stroke|shadow|outline|decoration|divide|accent|caret|placeholder)-(?:\[[^\]]*\]|(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-\d{2,3}(?:\/\d{1,3})?)$/;
|
||||
|
||||
/**
|
||||
* Per-variant, not one union: a flat union sanctions every variant's hue on every badge and button,
|
||||
* so the wrong-variant reuse this gate exists for reads as legal.
|
||||
*/
|
||||
const paletteTokensOf = (classes: string) => classes.split(/\s+/).filter((token) => PALETTE_UTILITY.test(token));
|
||||
|
||||
const SANCTIONED_VARIANTS = [
|
||||
...BADGE_VARIANTS.map((variant) => ({
|
||||
slot: 'badge',
|
||||
tokens: paletteTokensOf(badgeVariants({ variant })),
|
||||
variant,
|
||||
})),
|
||||
...BUTTON_VARIANTS.map((variant) => ({
|
||||
slot: 'button',
|
||||
tokens: paletteTokensOf(buttonVariants({ variant })),
|
||||
variant,
|
||||
})),
|
||||
];
|
||||
|
||||
/**
|
||||
* Off-palette colours already on the page, keyed by scan surface. A route's default view is keyed by
|
||||
* its path; a tab panel by `${path} [${tab}]`. Exact strings: they waive one node, not a rule.
|
||||
@@ -52,20 +63,51 @@ const ACCEPTED: Record<string, string[]> = {
|
||||
|
||||
const scanOffenders = (page: Page) =>
|
||||
page.evaluate(
|
||||
({ pattern, sanctioned }) => {
|
||||
({ pattern, variants }) => {
|
||||
const palette = new RegExp(pattern);
|
||||
const allowed = new Set(sanctioned);
|
||||
|
||||
return [...document.querySelectorAll('[data-slot="badge"],[data-slot="button"]')].flatMap((element) =>
|
||||
[...element.classList]
|
||||
.filter((token) => palette.test(token) && !allowed.has(token))
|
||||
.map((token) => `${element.getAttribute('data-slot')}: ${token}`),
|
||||
);
|
||||
return [...document.querySelectorAll('[data-slot="badge"],[data-slot="button"]')].flatMap((element) => {
|
||||
const slot = element.getAttribute('data-slot') ?? '';
|
||||
const used = [...element.classList].filter((token) => palette.test(token));
|
||||
|
||||
if (!used.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Equality, not containment: a single token borrowed from another variant is a
|
||||
// subset of that variant's set, so containment would wave through exactly the
|
||||
// wrong-variant reuse this gate exists to catch.
|
||||
const covered = variants.some(
|
||||
(candidate) =>
|
||||
candidate.slot === slot &&
|
||||
candidate.tokens.length === used.length &&
|
||||
used.every((token) => candidate.tokens.includes(token)),
|
||||
);
|
||||
|
||||
return covered ? [] : used.map((token) => `${slot}: ${token}`);
|
||||
});
|
||||
},
|
||||
{ pattern: PALETTE_UTILITY.source, sanctioned: [...SANCTIONED] },
|
||||
{ pattern: PALETTE_UTILITY.source, variants: SANCTIONED_VARIANTS },
|
||||
);
|
||||
|
||||
test.describe('palette compliance', { tag: '@cross' }, () => {
|
||||
// The manifest sweeps this route with the empty seed, so the provider cards — the surface the
|
||||
// shipped badge defect actually lived on — render nowhere in the loop below. The a11y and visual
|
||||
// gates each carry the same dedicated populated sweep.
|
||||
test.describe('settings providers (populated)', () => {
|
||||
test.use({ cassette: populatedSettingsProvidersCassette() });
|
||||
|
||||
test('provider cards carry no off-palette colour', async ({ page }) => {
|
||||
await page.goto(routes.settings.providers);
|
||||
await expect(page.getByText('My Custom Endpoint')).toBeVisible();
|
||||
|
||||
const offenders = await scanOffenders(page);
|
||||
const key = `${routes.settings.providers} (populated)`;
|
||||
|
||||
expect([...new Set(offenders)], `off-palette colours on ${key}`).toEqual(ACCEPTED[key] ?? []);
|
||||
});
|
||||
});
|
||||
|
||||
for (const entry of ROUTE_MANIFEST) {
|
||||
test.describe(entry.path, () => {
|
||||
test.use({ cassette: entry.cassette() });
|
||||
|
||||
Reference in New Issue
Block a user