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