mirror of
https://github.com/Tria-plc/emaui.git
synced 2026-08-26 13:02:50 +00:00
140 lines
5.0 KiB
TypeScript
140 lines
5.0 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`);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* The skip link, on a real app shell.
|
|
*
|
|
* Not on the gallery route: the point of a skip link is bypassing the nav, and
|
|
* the gallery has none. The login page is the shell-less public route both apps
|
|
* share, so this uses the landing route instead — it carries the chrome without
|
|
* needing a session.
|
|
*
|
|
* A skip link is invisible until focused, which means a broken one and a
|
|
* working one look identical in every screenshot. Only a focus test separates
|
|
* them.
|
|
*/
|
|
test.describe('skip link', () => {
|
|
for (const app of APPS) {
|
|
test(`${app.name} — reveals on focus and targets main`, async ({ page }) => {
|
|
await page.goto(`${app.url}/`, { waitUntil: 'networkidle' });
|
|
|
|
const link = page.locator('.ema-skip-link');
|
|
if ((await link.count()) === 0) {
|
|
// The public landing route does not mount the app shell in every app;
|
|
// skipping is honest here, where asserting absence would be wrong.
|
|
test.skip(true, 'landing route does not mount the app shell');
|
|
return;
|
|
}
|
|
|
|
// Off-screen until focused...
|
|
await expect(link).not.toBeInViewport();
|
|
|
|
await page.keyboard.press('Tab');
|
|
await expect(link).toBeFocused();
|
|
await expect(link).toBeInViewport();
|
|
|
|
// ...and it must point at something that exists.
|
|
const href = await link.getAttribute('href');
|
|
expect(href).toBe('#ema-main-content');
|
|
});
|
|
}
|
|
});
|