import { test, expect, type Locator, type Page } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
// Authored portable acceptance checks. NOT executed on the authoring host:
// browser policy explicitly blocked that environment. No real email or analytics.
// Run against the dedicated fixture server in playwright.config.ts after build.
const google = /^https:\/\/(?:[^/]+\.)?(?:googletagmanager\.com|google-analytics\.com)\//;
const cast = [
['smartphone', 'Smartphone', 'Deadpan and sarcastic'],
['flip-phone', 'Flip-phone', 'Theatrical and expressive'],
['app-bot', 'App-bot', 'Eager and bouncy'],
['pager', 'Pager', 'Anxious and alert'],
['handheld', 'Handheld', 'Competitive and crisp'],
] as const;
test.beforeEach(async ({ page }) => {
await page.route('**/api/demo', route => route.fulfill({ status: 503, contentType: 'application/json', body: JSON.stringify({ ok: false, code: 'test_intercept', message: 'Synthetic request intercepted. No email was sent.' }) }));
await page.route('**/api/config', route => route.fulfill({ contentType: 'application/json', body: JSON.stringify({ analytics: { measurementId: null } }) }));
await page.route(google, route => route.abort('blockedbyclient'));
});
async function openPlayground(page: Page, motion = false) {
await page.emulateMedia({ reducedMotion: motion ? 'no-preference' : 'reduce' });
await page.goto('/work/');
const playground = page.getByTestId('character-playground');
await expect(playground.getByLabel('All expressions and moves')).toBeVisible();
await expect(playground.getByRole('button', { name: 'See this expression' })).toBeEnabled();
await page.getByTestId('character-stage').scrollIntoViewIfNeeded();
if (motion) await expect(page.getByTestId('stage-actor-handle')).toHaveAttribute('aria-disabled', 'false');
return playground;
}
async function actorX(page: Page) {
return Number(await page.getByTestId('stage-actor').getAttribute('data-x'));
}
async function expectActorInside(page: Page) {
const stage = await page.getByTestId('character-stage').boundingBox();
const actor = await page.getByTestId('stage-actor').boundingBox();
expect(stage).not.toBeNull(); expect(actor).not.toBeNull();
if (!stage || !actor) return;
expect(actor.x).toBeGreaterThanOrEqual(stage.x - 1);
expect(actor.y).toBeGreaterThanOrEqual(stage.y - 1);
expect(actor.x + actor.width).toBeLessThanOrEqual(stage.x + stage.width + 1);
expect(actor.y + actor.height).toBeLessThanOrEqual(stage.y + stage.height + 1);
}
async function expectNoOverflow(page: Page, playground: Locator) {
const widths = await page.evaluate(() => ({ viewport: document.documentElement.clientWidth, content: Math.max(document.documentElement.scrollWidth, document.body.scrollWidth) }));
expect(widths.content).toBeLessThanOrEqual(widths.viewport + 1);
const local = await playground.evaluate(element => ({ width: element.clientWidth, content: element.scrollWidth }));
expect(local.content).toBeLessThanOrEqual(local.width + 1);
}
test('the work page offers all five personalities and 32 expressions without hydration errors', async ({ page }) => {
const errors: string[] = [];
page.on('pageerror', error => errors.push(error.message));
page.on('console', message => { if (message.type() === 'error') errors.push(message.text()); });
const playground = await openPlayground(page);
await expect(page.getByRole('heading', { level: 1, name: 'You are already looking at a piece of the work.' })).toHaveCount(1);
await expect(page.getByRole('heading', { name: 'Meet the characters.', exact: true })).toHaveCount(1);
const expressions = playground.getByLabel('All expressions and moves');
await expect(expressions.locator('option')).toHaveCount(32);
expect(await expressions.locator('optgroup').evaluateAll(groups => groups.map(group => group.getAttribute('label')))).toEqual(['Moods', 'Reactions', 'Gestures', 'Movement']);
for (const [slug, name, personality] of cast) {
await playground.locator(`[data-character-choice="${slug}"]`).click();
await expect(playground).toHaveAttribute('data-character', slug);
await expect(playground.locator('.playground-personality')).toContainText(personality);
await expect(playground.getByRole('button', { name, exact: true })).toHaveAttribute('aria-pressed', 'true');
await expect(playground.getByRole('button', { name: 'See this expression' })).toBeEnabled();
for (const mood of ['Laugh', 'Suspicious', 'Facepalm', 'Shy', 'Proud']) {
await playground.getByRole('button', { name: mood, exact: true }).click();
await expect(playground).toHaveAttribute('data-selected-action', mood);
await expect(page.getByTestId('stage-actor')).toHaveAttribute('data-action', mood);
}
}
await expect(playground).toHaveAttribute('data-show', 'idle');
expect(errors).toEqual([]);
});
test('reduced motion keeps expressive still poses, disables movement, and gates the censored gesture', async ({ page }) => {
const playground = await openPlayground(page);
const select = playground.getByLabel('All expressions and moves');
await expect(page.getByTestId('character-stage')).toHaveAttribute('data-reduced-motion', 'true');
await expect(playground.getByRole('button', { name: 'Walk right', exact: true })).toBeDisabled();
await expect(playground.getByRole('button', { name: 'A little performance', exact: true })).toBeDisabled();
// Playwright 1.63 does not report