mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 13:02:50 +00:00
Adds the layer feature code has been missing: tokens that name a role — page surface, subtle border, danger — instead of a colour. Every one resolves to a Mantine variable rather than a literal, so they follow the colour scheme for free and cannot drift from the theme. A parallel palette of raw hexes would have recreated exactly the problem this exists to fix. Also closes three accessibility gaps that had no implementation anywhere in the codebase: no :focus-visible rule, no screen-reader-only utility, and no global prefers-reduced-motion handling. The focus work found a real bug, and nearly introduced a worse one. Mantine already rings its own controls, so the first attempt deferred to it with `outline: none` on .mantine-focus-auto. That suppressed Mantine's ring without replacing it, leaving portal buttons with no focus indicator at all — and it only showed up in one app, because which rule won depended on stylesheet order. The rules use identical values, so overlapping them is invisible and safe; opting out is not. The focus test now asserts computed outline width and style, not just pixels, since a screenshot alone would not have caught this. `status-tone.ts` establishes the six-tone vocabulary that the 48 scattered status→colour maps will eventually collapse onto. Nothing consumes it yet. No visual change: the 8 existing baselines pass unmodified. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
101 lines
3.6 KiB
TypeScript
101 lines
3.6 KiB
TypeScript
import { test, expect, type Page } from '@playwright/test';
|
|
import { VISUAL } from '../visual.config';
|
|
|
|
/**
|
|
* Theme baselines.
|
|
*
|
|
* These exist so a change to the shared theme can be reviewed as a diff rather
|
|
* than trusted. The gallery route renders every primitive the theme controls,
|
|
* so one screenshot per app per scheme per width covers the whole surface.
|
|
*
|
|
* Update baselines deliberately, never reflexively:
|
|
* npx playwright test -c apps/e2e/visual.config.ts --update-snapshots
|
|
* A diff you did not intend is the entire point of the suite.
|
|
*/
|
|
|
|
const WIDTHS = [
|
|
{ name: 'desktop', width: 1440, height: 1200 },
|
|
{ name: 'tablet', width: 768, height: 1200 },
|
|
] as const;
|
|
|
|
const SCHEMES = ['light', 'dark'] as const;
|
|
|
|
const APPS = [
|
|
{ name: 'backoffice', url: VISUAL.backofficeUrl },
|
|
{ name: 'portal', url: VISUAL.portalUrl },
|
|
] as const;
|
|
|
|
/**
|
|
* Set the scheme the way the app itself does — the pre-paint script in
|
|
* index.html reads this key. Setting it before navigation means the very first
|
|
* paint is already correct, so no screenshot catches a flash of the wrong one.
|
|
*/
|
|
async function gotoGallery(page: Page, baseUrl: string, scheme: string) {
|
|
await page.addInitScript((value) => {
|
|
window.localStorage.setItem('mantine-color-scheme-value', value);
|
|
}, scheme);
|
|
|
|
await page.goto(`${baseUrl}/__gallery`, { waitUntil: 'networkidle' });
|
|
|
|
// The gallery is static, but web fonts are not: screenshotting before they
|
|
// settle bakes a fallback-font baseline that every later run then fails
|
|
// against.
|
|
await page.evaluate(() => document.fonts.ready);
|
|
await expect(page.getByRole('heading', { name: 'Theme Gallery' })).toBeVisible();
|
|
}
|
|
|
|
for (const app of APPS) {
|
|
for (const scheme of SCHEMES) {
|
|
for (const size of WIDTHS) {
|
|
test(`${app.name} gallery — ${scheme} — ${size.name}`, async ({ page }) => {
|
|
await page.setViewportSize({ width: size.width, height: size.height });
|
|
await gotoGallery(page, app.url, scheme);
|
|
|
|
await expect(page).toHaveScreenshot(
|
|
`${app.name}-gallery-${scheme}-${size.name}.png`,
|
|
{ fullPage: true },
|
|
);
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The focus ring, captured while actually focused.
|
|
*
|
|
* The full-page shots above can't show this: nothing is focused in them, so
|
|
* a regression that removed the ring entirely would leave them all green.
|
|
* Keyboard focus specifically, because `:focus-visible` deliberately does
|
|
* not match a mouse click.
|
|
*/
|
|
test(`${app.name} — focus ring is visible`, async ({ page }) => {
|
|
await page.setViewportSize({ width: 1440, height: 900 });
|
|
await gotoGallery(page, app.url, 'light');
|
|
|
|
const section = page.locator('section, div').filter({ hasText: 'Focus states' }).last();
|
|
await section.scrollIntoViewIfNeeded();
|
|
|
|
const button = page.getByRole('button', { name: 'Button', exact: true });
|
|
await button.focus();
|
|
await expect(button).toBeFocused();
|
|
|
|
// Assert the ring in computed styles as well as pixels. A screenshot alone
|
|
// would still pass if the outline came from somewhere unintended, and a
|
|
// token that failed to resolve leaves an empty string rather than an error.
|
|
const ring = await button.evaluate((el) => {
|
|
const s = getComputedStyle(el);
|
|
return {
|
|
width: s.outlineWidth,
|
|
style: s.outlineStyle,
|
|
token: getComputedStyle(document.documentElement)
|
|
.getPropertyValue('--ema-focus-ring')
|
|
.trim(),
|
|
};
|
|
});
|
|
expect(ring.style).toBe('solid');
|
|
expect(ring.width).toBe('2px');
|
|
expect(ring.token).not.toBe('');
|
|
|
|
await expect(button).toHaveScreenshot(`${app.name}-focus-ring.png`);
|
|
});
|
|
}
|