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

@@ -20,9 +20,9 @@ sursa de adevăr tehnică pentru agenți.
python3 -m http.server 8000 python3 -m http.server 8000
# Teste (Playwright; fără package.json commitat — vezi tests/AGENTS.md): # Teste (Playwright; fără package.json commitat — vezi tests/AGENTS.md):
npx playwright test tests/smoke.mjs # suita completă: 25/25 npx playwright test tests/smoke.mjs # suita completă: 26/26
npx playwright test tests/smoke.mjs --grep @regresie # regresie: 14 npx playwright test tests/smoke.mjs --grep @regresie # regresie: 14
npx playwright test tests/smoke.mjs --grep @campanie # campanie E2E: 11 npx playwright test tests/smoke.mjs --grep @campanie # campanie E2E: 12
``` ```
## Durable Rules (repo-wide) ## Durable Rules (repo-wide)

View File

@@ -15,7 +15,10 @@ Referință plan complet: `~/.gstack/projects/romfast-escape-builder/ceo-plans/2
(acoperă resume), nu doar btn-start; test rescris (headless crea ctx `running` trivial). (acoperă resume), nu doar btn-start; test rescris (headless crea ctx `running` trivial).
- [x] **Narațiune vocală (D10)** — LIVRAT (vezi §„Narațiune vocală" mai jos). Smoke 25/25. - [x] **Narațiune vocală (D10)** — LIVRAT (vezi §„Narațiune vocală" mai jos). Smoke 25/25.
- [x] **Unificare contract `_campaign` la final**`libJS.campaignDone()` (vezi §dedicată mai jos). - [x] **Unificare contract `_campaign` la final**`libJS.campaignDone()` (vezi §dedicată mai jos).
- [ ] **Audit a11y motoare** (vezi §dedicată mai jos). - [x] **Audit a11y motoare** — LIVRAT (vezi §dedicată mai jos). Smoke 26/26.
**PR2 livrat (2026-06-13):** audio camere `651025b`, voce `da93d84`, unificare `ab11089`, a11y (acest commit).
Rămas din Etapa 2: D7 (migrare classic pe libJS+SNIP) + muzică timer (T10) + Adventure Mode v0.
--- ---
@@ -123,13 +126,26 @@ Referință: planul §Etapa 2 pct. 1; D7.
- După migrare: classic folosește `libJS.campaignDone()` + `SNIP` ca celelalte 4 → 5/5 uniform. - După migrare: classic folosește `libJS.campaignDone()` + `SNIP` ca celelalte 4 → 5/5 uniform.
- Necesită regresie manuală pe classic standalone (e demo-ul implicit, cel mai vizibil). - Necesită regresie manuală pe classic standalone (e demo-ul implicit, cel mai vizibil).
### Audit a11y motoare existente (post-PR1, sub harness Playwright) ### [x] Audit a11y motoare existente — LIVRAT (sub harness Playwright)
- **Ținte tap ≥ 44px**: dpad arcade, butoane tf/choice, butonul "Trimite" din chat. Auditat faptic (măsurat, nu presupus). Ce era DEJA OK (din restyle S3, nemodificat):
- **Contrast ≥ 4.5:1**: text terminal dim (`#1f9c4a` pe `#04130a` — verifică), hint text clasic. - **Tap ≥44px**: arcade dpad 56×52, butoane classic 44/48, chat send/chip 44 — toate ✓.
- **`@media (prefers-reduced-motion: reduce)`**: dezactivează `pop`, `flip`, `flipin`, `shake`, `confetti`, `bin` — stările finale apar direct. - **Contrast**: terminal `.dim` #2ecc71 pe #040f08**9.4:1** ✓ (nota TODOS `#1f9c4a` era stale,
- **Focus & Enter**: "Deschide ușa" (campanie) focusabil + Enter; dpad arcade accesibil cu keyboard. schimbat la S3); classic `button.hint` .55 alb pe card #1a0e3d**6:1** ✓. Niciun fix necesar.
- **`aria-label` pe progres**: bara chrome din campanie (`aria-label="Camera X din Y"`). - **Focus & Enter**: butoane reale peste tot (Enter/Space nativ); arcade+overworld navigabile cu
- Referință: §Design pct. 13 (TD5, PR2); D19 din plan. săgeți (keydown pe document). „Deschide ușa" coridor = OBSOLET (overworld a înlocuit coridorul;
ușile se intră mergând cu tastatura → owCheckEnter).
Ce am REPARAT:
- **Tap**: overworld dpad era 42×42 → **44×44** (singura țintă sub prag).
- **reduced-motion** (lacune reale): `.confetti` (display:none) în classic + SNIP.baseCss + campanie;
`flipin` final (SNIP.finalCss `#fOverlay .fword span` + campanie `#fin-word span`); `dt-blink`
(cursor ușă terminal) în campanie. `pop`/`flip`/`shake`/`bin`/`tile-pop`/`tp`/`door-glow`/`crt-flicker`
erau deja acoperite. NB: `flipin`/`pop` au `backwards` fill → `animation:none` le revine la starea
vizibilă (nu rămân ascunse — verificat).
- **aria**: `#dots` `role=group`+label; fiecare dot `role=img` cu `aria-label` ce reflectă STAREA
(neînceputa/în curs/rezolvata) via `setDot`; dpad arcade+overworld au `aria-label` (Sus/Jos/Stânga/
Dreapta/Pune bomba); spacerele `.sp` overworld → `aria-hidden`+`tabindex=-1`.
Test nou smoke #9c (`a11y — tap>=44px + aria + reduced-motion`, cu `emulateMedia reducedMotion`). 26/26.
Referință: §Design pct. 13 (TD5, PR2); D19 din plan.
--- ---

View File

@@ -519,6 +519,7 @@ function gameClassic(cfg) {
@keyframes fall { to { transform: translateY(105vh) rotate(720deg); } } @keyframes fall { to { transform: translateY(105vh) rotate(720deg); } }
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
.screen.on, .tile.won, .bigword span, .shake { animation: none; } .screen.on, .tile.won, .bigword span, .shake { animation: none; }
.confetti { display: none !important; }
.progress i { transition: none; } .progress i { transition: none; }
} }
</style> </style>
@@ -773,7 +774,8 @@ SNIP.baseCss = `
.confetti { position: fixed; top: -12px; width: 9px; height: 14px; z-index: 99; animation: fall linear forwards; } .confetti { position: fixed; top: -12px; width: 9px; height: 14px; z-index: 99; animation: fall linear forwards; }
@keyframes fall { to { transform: translateY(105vh) rotate(720deg); } } @keyframes fall { to { transform: translateY(105vh) rotate(720deg); } }
.shake { animation: shake .4s ease; } .shake { animation: shake .4s ease; }
@keyframes shake { 20%,60% { transform: translateX(-8px); } 40%,80% { transform: translateX(8px); } }`; @keyframes shake { 20%,60% { transform: translateX(-8px); } 40%,80% { transform: translateX(8px); } }
@media (prefers-reduced-motion: reduce){ .confetti{ display:none !important; } .shake{ animation:none !important; } }`;
SNIP.modalCss = ` SNIP.modalCss = `
#mOverlay { display: none; position: fixed; inset: 0; background: rgba(8,4,20,.72); z-index: 20; align-items: center; justify-content: center; padding: 16px; } #mOverlay { display: none; position: fixed; inset: 0; background: rgba(8,4,20,.72); z-index: 20; align-items: center; justify-content: center; padding: 16px; }
@@ -850,6 +852,7 @@ SNIP.finalCss = `
#fOverlay .fword { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; margin: 16px 0; } #fOverlay .fword { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; margin: 16px 0; }
#fOverlay .fword span { width: 44px; height: 52px; border-radius: 10px; background: var(--accent); display: flex; align-items: center; justify-content: center; font-size: 26px; font-weight: 800; animation: flipin .6s ease backwards; } #fOverlay .fword span { width: 44px; height: 52px; border-radius: 10px; background: var(--accent); display: flex; align-items: center; justify-content: center; font-size: 26px; font-weight: 800; animation: flipin .6s ease backwards; }
@keyframes flipin { from { transform: rotateX(90deg); } to { transform: rotateX(0); } } @keyframes flipin { from { transform: rotateX(90deg); } to { transform: rotateX(0); } }
@media (prefers-reduced-motion: reduce){ #fOverlay .fword span{ animation:none !important; } }
#fOverlay p { color: rgba(255,255,255,.8); line-height: 1.5; } #fOverlay p { color: rgba(255,255,255,.8); line-height: 1.5; }
#fOverlay button { font: inherit; cursor: pointer; border: none; border-radius: 10px; padding: 12px 18px; font-weight: 700; background: var(--accent); color: #fff; width: 100%; }`; #fOverlay button { font: inherit; cursor: pointer; border: none; border-radius: 10px; padding: 12px 18px; font-weight: 700; background: var(--accent); color: #fff; width: 100%; }`;
@@ -1044,7 +1047,7 @@ ${SNIP.baseCss}${SNIP.modalCss}${SNIP.finalCss}
<div id="hud"><span id="hudStep"></span><span id="hudStars"></span><div id="hudLetters"></div></div> <div id="hud"><span id="hudStep"></span><span id="hudStars"></span><div id="hudLetters"></div></div>
<canvas id="cv"></canvas> <canvas id="cv"></canvas>
<div class="help">Sageti / WASD = misca, Space sau &#128163; = bomba. Sparge cutiile, evita dusmanii. Usile rosii = intrebari; cufarul auriu = scaparea.</div> <div class="help">Sageti / WASD = misca, Space sau &#128163; = bomba. Sparge cutiile, evita dusmanii. Usile rosii = intrebari; cufarul auriu = scaparea.</div>
<div id="dpad"><button data-d="L">&#9664;</button><button data-d="U">&#9650;</button><button data-d="D">&#9660;</button><button data-d="R">&#9654;</button><button id="btnBomb">&#128163;</button></div> <div id="dpad"><button data-d="L" aria-label="Stanga">&#9664;</button><button data-d="U" aria-label="Sus">&#9650;</button><button data-d="D" aria-label="Jos">&#9660;</button><button data-d="R" aria-label="Dreapta">&#9654;</button><button id="btnBomb" aria-label="Pune bomba">&#128163;</button></div>
<div id="goOverlay"><div id="goCard"><div id="goMsg"></div><button id="goRestart">Incearca din nou</button></div></div> <div id="goOverlay"><div id="goCard"><div id="goMsg"></div><button id="goRestart">Incearca din nou</button></div></div>
${SNIP.modalHtml} ${SNIP.modalHtml}
${SNIP.finalHtml} ${SNIP.finalHtml}
@@ -1649,7 +1652,7 @@ body {
#ow-hint { position: absolute; left: 0; right: 0; bottom: 8px; text-align: center; font-size: 13px; color: rgba(255,255,255,.72); z-index: 4; pointer-events: none; padding: 0 8px; } #ow-hint { position: absolute; left: 0; right: 0; bottom: 8px; text-align: center; font-size: 13px; color: rgba(255,255,255,.72); z-index: 4; pointer-events: none; padding: 0 8px; }
#ow-toast { position: absolute; left: 50%; top: 10px; transform: translateX(-50%); background: rgba(0,0,0,.72); padding: 6px 14px; border-radius: 20px; font-size: 14px; font-weight: 700; color: var(--c-gold); z-index: 4; opacity: 0; transition: opacity .3s; pointer-events: none; } #ow-toast { position: absolute; left: 50%; top: 10px; transform: translateX(-50%); background: rgba(0,0,0,.72); padding: 6px 14px; border-radius: 20px; font-size: 14px; font-weight: 700; color: var(--c-gold); z-index: 4; opacity: 0; transition: opacity .3s; pointer-events: none; }
#ow-toast.show { opacity: 1; } #ow-toast.show { opacity: 1; }
#ow-dpad { position: absolute; right: 10px; bottom: 10px; display: grid; grid-template-columns: repeat(3, 42px); grid-template-rows: repeat(3, 42px); gap: 4px; z-index: 5; } #ow-dpad { position: absolute; right: 10px; bottom: 10px; display: grid; grid-template-columns: repeat(3, 44px); grid-template-rows: repeat(3, 44px); gap: 4px; z-index: 5; }
#ow-dpad button { border: 1px solid #4a3590; background: rgba(34,22,67,.85); color: #cdc3f0; border-radius: 9px; font-size: 16px; cursor: pointer; } #ow-dpad button { border: 1px solid #4a3590; background: rgba(34,22,67,.85); color: #cdc3f0; border-radius: 9px; font-size: 16px; cursor: pointer; }
#ow-dpad button:active { background: var(--accent); } #ow-dpad button:active { background: var(--accent); }
#ow-dpad .sp { visibility: hidden; } #ow-dpad .sp { visibility: hidden; }
@@ -1669,7 +1672,12 @@ body {
100% { transform: scale(.85) rotateY(-90deg); opacity: 0; } 100% { transform: scale(.85) rotateY(-90deg); opacity: 0; }
} }
.opening { animation: door-open .25s cubic-bezier(.4,0,1,1) forwards; transform-origin: left center; perspective: 600px; } .opening { animation: door-open .25s cubic-bezier(.4,0,1,1) forwards; transform-origin: left center; perspective: 600px; }
@media (prefers-reduced-motion: reduce) { .opening { animation: none; opacity: 0; } } @media (prefers-reduced-motion: reduce) {
.opening { animation: none; opacity: 0; }
#fin-word span { animation: none !important; }
.door-terminal .dt-cur { animation: none !important; }
.confetti { display: none !important; }
}
/* Classic */ /* Classic */
.door-classic { .door-classic {
width: 88px; height: 124px; position: relative; width: 88px; height: 124px; position: relative;
@@ -1774,7 +1782,7 @@ body {
<span id="chrome-title">${esc(cfg.title)}</span> <span id="chrome-title">${esc(cfg.title)}</span>
<div class="sp"></div> <div class="sp"></div>
<button id="btn-voice" type="button" aria-label="Naratiune vocala" hidden>&#128266;</button> <button id="btn-voice" type="button" aria-label="Naratiune vocala" hidden>&#128266;</button>
<div id="dots"></div> <div id="dots" role="group" aria-label="Progres camere"></div>
</div> </div>
<div id="room-wrap"> <div id="room-wrap">
@@ -1792,10 +1800,10 @@ body {
<div id="ow-world"></div> <div id="ow-world"></div>
<div id="ow-toast"></div> <div id="ow-toast"></div>
<div id="ow-hint"></div> <div id="ow-hint"></div>
<div id="ow-dpad"> <div id="ow-dpad" role="group" aria-label="Deplasare pe harta">
<button class="sp"></button><button data-d="U">&#9650;</button><button class="sp"></button> <button class="sp" aria-hidden="true" tabindex="-1"></button><button data-d="U" aria-label="Sus">&#9650;</button><button class="sp" aria-hidden="true" tabindex="-1"></button>
<button data-d="L">&#9664;</button><button class="sp"></button><button data-d="R">&#9654;</button> <button data-d="L" aria-label="Stanga">&#9664;</button><button class="sp" aria-hidden="true" tabindex="-1"></button><button data-d="R" aria-label="Dreapta">&#9654;</button>
<button class="sp"></button><button data-d="D">&#9660;</button><button class="sp"></button> <button class="sp" aria-hidden="true" tabindex="-1"></button><button data-d="D" aria-label="Jos">&#9660;</button><button class="sp" aria-hidden="true" tabindex="-1"></button>
</div> </div>
</div> </div>
</div> </div>
@@ -1872,11 +1880,17 @@ function buildDots(){
for(var i=0;i<N;i++){ for(var i=0;i<N;i++){
var s = document.createElement('span'); var s = document.createElement('span');
s.id = 'dot-'+i; s.id = 'dot-'+i;
s.setAttribute('aria-label','Camera '+(i+1)+' din '+N); s.setAttribute('role','img');
d.appendChild(s); d.appendChild(s);
setDot(i,''); /* setează aria-label inițial (neînceput) */
} }
} }
function setDot(i,cls){ var d=document.getElementById('dot-'+i); if(d) d.className=cls; } function setDot(i,cls){
var d=document.getElementById('dot-'+i); if(!d) return;
d.className=cls;
var st = cls==='done' ? 'rezolvata' : (cls==='active' ? 'in curs' : 'neinceputa');
d.setAttribute('aria-label','Camera '+(i+1)+' din '+N+': '+st);
}
/* ----- Ușa coridorului (§Design pct.7) ----- */ /* ----- Ușa coridorului (§Design pct.7) ----- */
function doorHtml(style, isLast, isStuck){ function doorHtml(style, isLast, isStuck){

View File

@@ -28,6 +28,7 @@
@keyframes fall { to { transform: translateY(105vh) rotate(720deg); } } @keyframes fall { to { transform: translateY(105vh) rotate(720deg); } }
.shake { animation: shake .4s ease; } .shake { animation: shake .4s ease; }
@keyframes shake { 20%,60% { transform: translateX(-8px); } 40%,80% { transform: translateX(8px); } } @keyframes shake { 20%,60% { transform: translateX(-8px); } 40%,80% { transform: translateX(8px); } }
@media (prefers-reduced-motion: reduce){ .confetti{ display:none !important; } .shake{ animation:none !important; } }
#mOverlay { display: none; position: fixed; inset: 0; background: rgba(8,4,20,.72); z-index: 20; align-items: center; justify-content: center; padding: 16px; } #mOverlay { display: none; position: fixed; inset: 0; background: rgba(8,4,20,.72); z-index: 20; align-items: center; justify-content: center; padding: 16px; }
#mCard { width: 100%; max-width: 460px; background: #221440; border: 1px solid rgba(255,255,255,.18); border-radius: 16px; padding: 22px; color: #fff; font-family: system-ui, sans-serif; box-shadow: 0 18px 50px rgba(0,0,0,.5); } #mCard { width: 100%; max-width: 460px; background: #221440; border: 1px solid rgba(255,255,255,.18); border-radius: 16px; padding: 22px; color: #fff; font-family: system-ui, sans-serif; box-shadow: 0 18px 50px rgba(0,0,0,.5); }
#mCard .mtitle { font-size: 12px; text-transform: uppercase; letter-spacing: .08em; color: #c4b5fd; font-weight: 700; } #mCard .mtitle { font-size: 12px; text-transform: uppercase; letter-spacing: .08em; color: #c4b5fd; font-weight: 700; }
@@ -49,6 +50,7 @@
#fOverlay .fword { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; margin: 16px 0; } #fOverlay .fword { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; margin: 16px 0; }
#fOverlay .fword span { width: 44px; height: 52px; border-radius: 10px; background: var(--accent); display: flex; align-items: center; justify-content: center; font-size: 26px; font-weight: 800; animation: flipin .6s ease backwards; } #fOverlay .fword span { width: 44px; height: 52px; border-radius: 10px; background: var(--accent); display: flex; align-items: center; justify-content: center; font-size: 26px; font-weight: 800; animation: flipin .6s ease backwards; }
@keyframes flipin { from { transform: rotateX(90deg); } to { transform: rotateX(0); } } @keyframes flipin { from { transform: rotateX(90deg); } to { transform: rotateX(0); } }
@media (prefers-reduced-motion: reduce){ #fOverlay .fword span{ animation:none !important; } }
#fOverlay p { color: rgba(255,255,255,.8); line-height: 1.5; } #fOverlay p { color: rgba(255,255,255,.8); line-height: 1.5; }
#fOverlay button { font: inherit; cursor: pointer; border: none; border-radius: 10px; padding: 12px 18px; font-weight: 700; background: var(--accent); color: #fff; width: 100%; } #fOverlay button { font: inherit; cursor: pointer; border: none; border-radius: 10px; padding: 12px 18px; font-weight: 700; background: var(--accent); color: #fff; width: 100%; }
</style> </style>
@@ -58,7 +60,7 @@
<div id="hud"><span id="hudStep"></span><span id="hudStars"></span><div id="hudLetters"></div></div> <div id="hud"><span id="hudStep"></span><span id="hudStars"></span><div id="hudLetters"></div></div>
<canvas id="cv"></canvas> <canvas id="cv"></canvas>
<div class="help">Sageti / WASD = misca, Space sau &#128163; = bomba. Sparge cutiile, evita dusmanii. Usile rosii = intrebari; cufarul auriu = scaparea.</div> <div class="help">Sageti / WASD = misca, Space sau &#128163; = bomba. Sparge cutiile, evita dusmanii. Usile rosii = intrebari; cufarul auriu = scaparea.</div>
<div id="dpad"><button data-d="L">&#9664;</button><button data-d="U">&#9650;</button><button data-d="D">&#9660;</button><button data-d="R">&#9654;</button><button id="btnBomb">&#128163;</button></div> <div id="dpad"><button data-d="L" aria-label="Stanga">&#9664;</button><button data-d="U" aria-label="Sus">&#9650;</button><button data-d="D" aria-label="Jos">&#9660;</button><button data-d="R" aria-label="Dreapta">&#9654;</button><button id="btnBomb" aria-label="Pune bomba">&#128163;</button></div>
<div id="goOverlay"><div id="goCard"><div id="goMsg"></div><button id="goRestart">Incearca din nou</button></div></div> <div id="goOverlay"><div id="goCard"><div id="goMsg"></div><button id="goRestart">Incearca din nou</button></div></div>
<div id="mOverlay"><div id="mCard"> <div id="mOverlay"><div id="mCard">
<div class="mtitle" id="mTitle"></div> <div class="mtitle" id="mTitle"></div>

File diff suppressed because one or more lines are too long

View File

@@ -36,6 +36,7 @@
@keyframes fall { to { transform: translateY(105vh) rotate(720deg); } } @keyframes fall { to { transform: translateY(105vh) rotate(720deg); } }
.shake { animation: shake .4s ease; } .shake { animation: shake .4s ease; }
@keyframes shake { 20%,60% { transform: translateX(-8px); } 40%,80% { transform: translateX(8px); } } @keyframes shake { 20%,60% { transform: translateX(-8px); } 40%,80% { transform: translateX(8px); } }
@media (prefers-reduced-motion: reduce){ .confetti{ display:none !important; } .shake{ animation:none !important; } }
#fOverlay { display: none; position: fixed; inset: 0; background: rgba(8,4,20,.88); z-index: 30; align-items: center; justify-content: center; padding: 16px; } #fOverlay { display: none; position: fixed; inset: 0; background: rgba(8,4,20,.88); z-index: 30; align-items: center; justify-content: center; padding: 16px; }
#fOverlay .fcard { width: 100%; max-width: 480px; text-align: center; background: #221440; border: 1px solid rgba(255,255,255,.18); border-radius: 18px; padding: 28px; color: #fff; font-family: system-ui, sans-serif; } #fOverlay .fcard { width: 100%; max-width: 480px; text-align: center; background: #221440; border: 1px solid rgba(255,255,255,.18); border-radius: 18px; padding: 28px; color: #fff; font-family: system-ui, sans-serif; }
#fOverlay h1 { margin: 0 0 8px; font-size: 26px; } #fOverlay h1 { margin: 0 0 8px; font-size: 26px; }
@@ -43,6 +44,7 @@
#fOverlay .fword { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; margin: 16px 0; } #fOverlay .fword { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; margin: 16px 0; }
#fOverlay .fword span { width: 44px; height: 52px; border-radius: 10px; background: var(--accent); display: flex; align-items: center; justify-content: center; font-size: 26px; font-weight: 800; animation: flipin .6s ease backwards; } #fOverlay .fword span { width: 44px; height: 52px; border-radius: 10px; background: var(--accent); display: flex; align-items: center; justify-content: center; font-size: 26px; font-weight: 800; animation: flipin .6s ease backwards; }
@keyframes flipin { from { transform: rotateX(90deg); } to { transform: rotateX(0); } } @keyframes flipin { from { transform: rotateX(90deg); } to { transform: rotateX(0); } }
@media (prefers-reduced-motion: reduce){ #fOverlay .fword span{ animation:none !important; } }
#fOverlay p { color: rgba(255,255,255,.8); line-height: 1.5; } #fOverlay p { color: rgba(255,255,255,.8); line-height: 1.5; }
#fOverlay button { font: inherit; cursor: pointer; border: none; border-radius: 10px; padding: 12px 18px; font-weight: 700; background: var(--accent); color: #fff; width: 100%; } #fOverlay button { font: inherit; cursor: pointer; border: none; border-radius: 10px; padding: 12px 18px; font-weight: 700; background: var(--accent); color: #fff; width: 100%; }
</style> </style>

View File

@@ -66,6 +66,7 @@
@keyframes fall { to { transform: translateY(105vh) rotate(720deg); } } @keyframes fall { to { transform: translateY(105vh) rotate(720deg); } }
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
.screen.on, .tile.won, .bigword span, .shake { animation: none; } .screen.on, .tile.won, .bigword span, .shake { animation: none; }
.confetti { display: none !important; }
.progress i { transition: none; } .progress i { transition: none; }
} }
</style> </style>

View File

@@ -28,6 +28,7 @@
@keyframes fall { to { transform: translateY(105vh) rotate(720deg); } } @keyframes fall { to { transform: translateY(105vh) rotate(720deg); } }
.shake { animation: shake .4s ease; } .shake { animation: shake .4s ease; }
@keyframes shake { 20%,60% { transform: translateX(-8px); } 40%,80% { transform: translateX(8px); } } @keyframes shake { 20%,60% { transform: translateX(-8px); } 40%,80% { transform: translateX(8px); } }
@media (prefers-reduced-motion: reduce){ .confetti{ display:none !important; } .shake{ animation:none !important; } }
#mOverlay { display: none; position: fixed; inset: 0; background: rgba(8,4,20,.72); z-index: 20; align-items: center; justify-content: center; padding: 16px; } #mOverlay { display: none; position: fixed; inset: 0; background: rgba(8,4,20,.72); z-index: 20; align-items: center; justify-content: center; padding: 16px; }
#mCard { width: 100%; max-width: 460px; background: #221440; border: 1px solid rgba(255,255,255,.18); border-radius: 16px; padding: 22px; color: #fff; font-family: system-ui, sans-serif; box-shadow: 0 18px 50px rgba(0,0,0,.5); } #mCard { width: 100%; max-width: 460px; background: #221440; border: 1px solid rgba(255,255,255,.18); border-radius: 16px; padding: 22px; color: #fff; font-family: system-ui, sans-serif; box-shadow: 0 18px 50px rgba(0,0,0,.5); }
#mCard .mtitle { font-size: 12px; text-transform: uppercase; letter-spacing: .08em; color: #c4b5fd; font-weight: 700; } #mCard .mtitle { font-size: 12px; text-transform: uppercase; letter-spacing: .08em; color: #c4b5fd; font-weight: 700; }
@@ -49,6 +50,7 @@
#fOverlay .fword { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; margin: 16px 0; } #fOverlay .fword { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; margin: 16px 0; }
#fOverlay .fword span { width: 44px; height: 52px; border-radius: 10px; background: var(--accent); display: flex; align-items: center; justify-content: center; font-size: 26px; font-weight: 800; animation: flipin .6s ease backwards; } #fOverlay .fword span { width: 44px; height: 52px; border-radius: 10px; background: var(--accent); display: flex; align-items: center; justify-content: center; font-size: 26px; font-weight: 800; animation: flipin .6s ease backwards; }
@keyframes flipin { from { transform: rotateX(90deg); } to { transform: rotateX(0); } } @keyframes flipin { from { transform: rotateX(90deg); } to { transform: rotateX(0); } }
@media (prefers-reduced-motion: reduce){ #fOverlay .fword span{ animation:none !important; } }
#fOverlay p { color: rgba(255,255,255,.8); line-height: 1.5; } #fOverlay p { color: rgba(255,255,255,.8); line-height: 1.5; }
#fOverlay button { font: inherit; cursor: pointer; border: none; border-radius: 10px; padding: 12px 18px; font-weight: 700; background: var(--accent); color: #fff; width: 100%; } #fOverlay button { font: inherit; cursor: pointer; border: none; border-radius: 10px; padding: 12px 18px; font-weight: 700; background: var(--accent); color: #fff; width: 100%; }
</style> </style>

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ă. până la ecranul final, fără erori de consolă.
## Ownership ## 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. - `playwright.config.mjs` (la root, **gitignored**) — config dev.
## Local Contracts ## 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`; - **Zero erori consolă = invariant.** `trackErrors(page)` colectează `console.error` + `pageerror`;
fiecare test asertează `errors.length === 0` la final. fiecare test asertează `errors.length === 0` la final.
- **Tag-uri:** `@regresie` (14 — exemplu-*.html + edge cases + mobil 320px + regenerare via gameHTML + - **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ă, bomberman gameplay) și `@campanie` (12 — intro→hartă→camere→final, resume, cameră moartă,
idempotență ușă, `$`/`$&`, beep, mobil, audio S1, voce/narațiune D10, navigare overworld). idempotență ușă, `$`/`$&`, beep, mobil, audio S1, voce/narațiune D10, a11y tap/aria/reduced-motion,
- **Status țintă: 25/25 PASS.** navigare overworld).
- **Status țintă: 26/26 PASS.**
## Work Guidance ## Work Guidance
- După modificări la motoare (`escape-builder.html`): rulează suita completă; extinde `@regresie` dacă - 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 ## Verification
```bash ```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 @regresie
npx playwright test tests/smoke.mjs --grep @campanie 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 // Test 10 (S4): Overworld — mers cu tastatura + iesire blocata pana la final
// ───────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────