timmy-talking-turd/tests/a11y-urgent.acceptance.mjs
Timmy ae9fe0173c
Some checks failed
Quality gates / quality (pull_request) Failing after 2m25s
a11y: WCAG/mobile fixes and deterministic visual gates (Closes #14)
Strict RED-GREEN per defect, verified in a real browser:

- Nav buttons expose accessible names ('Today navigation item') and
  the active view carries aria-current=page
- Urgent red-flag box is an assertive role=alert live region so screen
  readers announce 'Pause and get medical help' the moment a red-flag
  symptom is checked
- Log sheet dismisses on Escape (keyboard path out of the modal)
- --muted darkened #756e68 -> #6a635c: muted text now meets WCAG AA
  4.5:1 on paper, surface, and soft backgrounds
- New npm run test:a11y gates (Playwright, mobile viewport):
  landmarks/labels/focus-ring/touch-targets/reduced-motion,
  urgent-alert announcement + dialog semantics, token contrast math
  and 200% text-zoom resilience; each writes a synthetic screenshot
  to artifacts/a11y-*.png for human inspection
- CI runs test:a11y and uploads the screenshots as artifacts
- sleek-chat selector updated to the new accessible nav name

Privacy/safety paths unchanged: local-first storage, consent-gated AI,
deterministic urgent override all still pass existing suites.
2026-08-22 23:18:04 +00:00

66 lines
3.1 KiB
JavaScript

import { chromium } from 'playwright';
import assert from 'node:assert/strict';
import { mkdir } from 'node:fs/promises';
await mkdir('artifacts', { recursive: true });
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({ viewport: { width: 390, height: 844 }, deviceScaleFactor: 2, serviceWorkers: 'block' });
const page = await context.newPage();
const errors = [];
page.on('console', message => { if (message.type() === 'error') errors.push(message.text()); });
page.on('pageerror', error => errors.push(error.message));
await page.goto('http://127.0.0.1:4173', { waitUntil: 'networkidle' });
await page.evaluate(() => localStorage.clear());
await page.reload({ waitUntil: 'networkidle' });
// Open the manual logger, walk to the safety-check step.
await page.locator('[data-log]').first().click();
await page.locator('[data-type="6"]').click();
await page.locator('#next').click();
await page.locator('#next').click();
// The urgent alert container must be an assertive live region so a screen
// reader announces it the moment a red-flag symptom is checked.
assert.equal(
await page.locator('#urgent-box[role="alert"]').count(), 1,
'#urgent-box is an assertive role="alert" live region',
);
assert.equal(
await page.locator('#urgent-box[aria-live="assertive"]').count(), 1,
'#urgent-box carries aria-live="alert"',
);
// Check blood → the alert appears and is announced.
await page.locator('[data-symptom="blood"]').check();
const alertBox = page.locator('#urgent-box');
assert.equal(await alertBox.getAttribute('role'), 'alert');
assert.match(await alertBox.innerText(), /Pause and get medical help/i);
const liveValue = await alertBox.getAttribute('aria-live');
assert.equal(liveValue, 'assertive');
// Dialog semantics: sheet traps description and close button has a name.
const sheet = page.locator('.sheet').last();
assert.equal(await sheet.getAttribute('role'), 'dialog');
assert.equal(await sheet.getAttribute('aria-modal'), 'true');
assert.ok(await sheet.getAttribute('aria-labelledby'), 'sheet names itself via aria-labelledby');
assert.equal(await page.locator('.icon-btn[aria-label="Close"]').last().isVisible(), true);
// Escape closes the dialog (keyboard path out of the modal).
await page.locator('.sheet').last().click(); // ensure focus is inside the dialog
await page.locator('.icon-btn[aria-label="Close"]').last().focus();
await page.keyboard.press('Escape');
assert.equal(await page.locator('.sheet-backdrop').count(), 0, 'Escape dismisses the log sheet');
// Re-open and capture the red-flag alert visual for review.
await page.locator('[data-log]').first().click();
await page.locator('[data-type="6"]').click();
await page.locator('#next').click();
await page.locator('#next').click();
await page.locator('[data-symptom="blood"]').check();
await page.waitForTimeout(150);
await page.screenshot({ path: 'artifacts/a11y-redflag-alert.png', fullPage: true });
assert.equal(await page.locator('#urgent-box .alert').isVisible(), true);
assert.deepEqual(errors, []);
await browser.close();
console.log('PASS urgent-alert screen-reader announcement + dialog keyboard semantics');