PR2: audit a11y - reduced-motion, tap>=44px, aria pe progres+dpad

Audit faptic (masurat) pe 5 motoare + campanie. Deja OK din restyle S3:
tap targets (arcade 56x52, classic 44/48, chat 44), contrast (terminal .dim
9.4:1, classic hint 6:1), focus/keyboard (butoane reale, navigare cu sageti).

Reparat:
- reduced-motion (lacune): .confetti display:none in classic + SNIP.baseCss +
  campanie; flipin final in SNIP.finalCss (#fOverlay .fword span) + campanie
  (#fin-word span); dt-blink in campanie. (pop/flip/shake/bin/tile-pop/tp/
  door-glow/crt-flicker erau deja acoperite.) flipin/pop au 'backwards' fill ->
  animation:none le revine la starea vizibila, nu raman ascunse.
- tap: overworld dpad 42x42 -> 44x44 (singura tinta sub prag).
- aria: #dots role=group+label; fiecare dot role=img cu aria-label ce reflecta
  starea (neinceputa/in curs/rezolvata) via setDot; dpad arcade+overworld cu
  aria-label (Sus/Jos/Stanga/Dreapta/Pune bomba); spacere .sp aria-hidden.

Test nou smoke #9c (emulateMedia reducedMotion -> confetti display:none;
tap>=44px pe dpad; aria dinamic pe dots). 26/26. Demo-uri regenerate (terminal
neatins - nu foloseste SNIP base/final).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Agent
2026-06-13 12:04:55 +00:00
parent ab11089097
commit a30441eb03
10 changed files with 141 additions and 37 deletions

View File

@@ -5,7 +5,7 @@ Smoke + regresie + campanie E2E pentru jocurile generate. Verifică faptic: fiec
până la ecranul final, fără erori de consolă.
## Ownership
- `tests/smoke.mjs` — unicul fișier de teste (~25 teste).
- `tests/smoke.mjs` — unicul fișier de teste (~26 teste).
- `playwright.config.mjs` (la root, **gitignored**) — config dev.
## Local Contracts
@@ -17,9 +17,10 @@ până la ecranul final, fără erori de consolă.
- **Zero erori consolă = invariant.** `trackErrors(page)` colectează `console.error` + `pageerror`;
fiecare test asertează `errors.length === 0` la final.
- **Tag-uri:** `@regresie` (14 — exemplu-*.html + edge cases + mobil 320px + regenerare via gameHTML +
bomberman gameplay) și `@campanie` (11 — intro→hartă→camere→final, resume, cameră moartă,
idempotență ușă, `$`/`$&`, beep, mobil, audio S1, voce/narațiune D10, navigare overworld).
- **Status țintă: 25/25 PASS.**
bomberman gameplay) și `@campanie` (12 — intro→hartă→camere→final, resume, cameră moartă,
idempotență ușă, `$`/`$&`, beep, mobil, audio S1, voce/narațiune D10, a11y tap/aria/reduced-motion,
navigare overworld).
- **Status țintă: 26/26 PASS.**
## Work Guidance
- După modificări la motoare (`escape-builder.html`): rulează suita completă; extinde `@regresie` dacă
@@ -28,7 +29,7 @@ până la ecranul final, fără erori de consolă.
## Verification
```bash
npx playwright test tests/smoke.mjs # 25/25
npx playwright test tests/smoke.mjs # 26/26
npx playwright test tests/smoke.mjs --grep @regresie
npx playwright test tests/smoke.mjs --grep @campanie
```

View File

@@ -1055,6 +1055,61 @@ test.describe('Campanie E2E @campanie', () => {
}
});
// ─────────────────────────────────────────────────────────────────────
// Test 9c (PR2): A11y — tinte tap >=44px, aria pe progres+dpad, reduced-motion
// (Playwright emuleaza prefers-reduced-motion → asertam faptic ca animatiile
// decorative sunt neutralizate).
// ─────────────────────────────────────────────────────────────────────
test('a11y — tap>=44px + aria progres/dpad + reduced-motion @campanie',
async ({ page }) => {
const cfg = campaignCfg(3, 'classic');
const tmpPath = await writeCampaignHtml(page, cfg, 'a11y');
const gp = await page.context().newPage();
try {
await gp.emulateMedia({ reducedMotion: 'reduce' });
await gp.goto('file://' + tmpPath);
// Aria pe progres: container + dot initial cu stare
await expect(gp.locator('#dots')).toHaveAttribute('role', 'group');
const dot0 = await gp.locator('#dot-0').getAttribute('aria-label');
expect(dot0).toContain('Camera 1 din 3');
expect(dot0).toContain('neinceputa');
// Start → harta; dpad cu aria-label + tinte tap >=44px
await gp.locator('#btn-start').click();
await waitOverworld(gp);
for (const [d, label] of [['U', 'Sus'], ['D', 'Jos'], ['L', 'Stanga'], ['R', 'Dreapta']]) {
const btn = gp.locator(`#ow-dpad button[data-d="${d}"]`);
await expect(btn).toHaveAttribute('aria-label', label);
const box = await btn.boundingBox();
expect(box.width, `dpad ${d} latime`).toBeGreaterThanOrEqual(44);
expect(box.height, `dpad ${d} inaltime`).toBeGreaterThanOrEqual(44);
}
// Reduced-motion: confetti decorativ -> display:none (proba directa pe regula CSS)
const confettiDisplay = await gp.evaluate(() => {
const probe = document.createElement('div');
probe.className = 'confetti';
document.body.appendChild(probe);
const disp = getComputedStyle(probe).display;
probe.remove();
return disp;
});
expect(confettiDisplay, 'confetti trebuie ascuns sub reduced-motion').toBe('none');
// Dot devine 'rezolvata' dupa ce camera 0 e gata (verifica aria dinamic)
await enterRoom(gp, 0);
await solveRoom(gp, 'classic', 'r1');
await waitOverworld(gp);
await expect.poll(
() => gp.locator('#dot-0').getAttribute('aria-label')
).toContain('rezolvata');
} finally {
await gp.close();
try { unlinkSync(tmpPath); } catch (_) {}
}
});
// ─────────────────────────────────────────────────────────────────────
// Test 10 (S4): Overworld — mers cu tastatura + iesire blocata pana la final
// ─────────────────────────────────────────────────────────────────────