Compare commits
3 Commits
16cd521430
...
023df382f0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
023df382f0 | ||
|
|
d8cb515545 | ||
|
|
b359bbe50a |
@@ -20,9 +20,9 @@ sursa de adevăr tehnică pentru agenți.
|
||||
python3 -m http.server 8000
|
||||
|
||||
# Teste (Playwright; fără package.json commitat — vezi tests/AGENTS.md):
|
||||
npx playwright test tests/smoke.mjs # suita completă: 28/28
|
||||
npx playwright test tests/smoke.mjs # suita completă: 31/31
|
||||
npx playwright test tests/smoke.mjs --grep @regresie # regresie: 16
|
||||
npx playwright test tests/smoke.mjs --grep @campanie # campanie E2E: 14
|
||||
npx playwright test tests/smoke.mjs --grep @campanie # campanie E2E: 17
|
||||
```
|
||||
|
||||
## Durable Rules (repo-wide)
|
||||
|
||||
10
DESIGN.md
10
DESIGN.md
@@ -217,7 +217,10 @@ Mesajul creatorului
|
||||
|
||||
## 10. Diplomă — Certificat A4 Print-First (§Design pct. 9 — Etapa 2 / PR2)
|
||||
|
||||
> Implementare în T10/PR2. Spec inclusă aici ca parte a contractului de design.
|
||||
> **LIVRAT** (2026-06-13). Overlay `#diploma`; buton „Vezi diploma →" pe finale + „Joacă din nou".
|
||||
> `buildDiploma()` citește `roomStars`/`collected`/`skipped`/`MASTER`/`_timerExpired`. Câmp builder nou
|
||||
> `creator` („Creat de"). `@media print` izolează `#diploma` (rest `visibility:hidden`). Test smoke
|
||||
> „diploma" (nume/titlu/stele-per-cameră/cuvânt/creator/înapoi). Camere sărite = 🔒 (verificat vizual).
|
||||
|
||||
- **Format:** A4 portret, fundal ALB, chenar dublu `var(--c-accent)`
|
||||
- **Titlu:** „DIPLOMĂ DE EVADARE" — **singurul** element cu font serif (limbajul certificatelor)
|
||||
@@ -231,7 +234,10 @@ Mesajul creatorului
|
||||
|
||||
## 11. Timer Calm (§Design pct. 10 — Etapa 2 / PR2)
|
||||
|
||||
> Implementare în T10/PR2.
|
||||
> **LIVRAT** (2026-06-13). Opt-in din builder (câmp „Timp limită (minute)", default 0 = fără).
|
||||
> Implementare: `#chrome-timer` în bara chrome; `startTimer/tickTimer/stopTimer`; deadline absolut
|
||||
> în `sessionStorage` (`_DEADLINE_KEY`). Sub 1 min → `.low` (auriu); expirat → `.expired` (auriu, opac).
|
||||
> Test smoke „timer calm" (format, gold, freeze, resume păstrează ceasul).
|
||||
|
||||
- Pornește **exact** la click „Începe aventura" (intro necronometrat)
|
||||
- Afișat în chrome: `M:SS`, neutru (`color: var(--c-ink)`)
|
||||
|
||||
42
TODOS.md
42
TODOS.md
@@ -18,7 +18,7 @@ Referință plan complet: `~/.gstack/projects/romfast-escape-builder/ceo-plans/2
|
||||
- [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: muzică timer (T10) + Adventure Mode v0. (D7 LIVRAT — vezi §dedicată mai jos.)
|
||||
Rămas din Etapa 2: Adventure Mode v0. (D7 + Timer Calm + Muzică T10 + Diplomă LIVRATE — vezi §§ mai jos.)
|
||||
|
||||
### [x] Bomberman polish (feedback user 2026-06-13) — LIVRAT
|
||||
Trei probleme raportate + o lipsă, toate în `gameArcade` (`escape-builder.html`):
|
||||
@@ -96,12 +96,40 @@ portează în `escape-builder.html` (un singur fișier, integrare secvențială)
|
||||
|
||||
## Post-PR1 (după ship-ul campaniei)
|
||||
|
||||
### Muzică accelerată la timer (PR2 / T10)
|
||||
- Audio ambient în campanie: track calm → accelerare progresivă sub 1 minut.
|
||||
- Ownership: părintele deține AudioContext; camerele nu știu de muzică.
|
||||
- Fallback: zero pedeapsă dacă AudioContext lipsă (webview restricitve).
|
||||
- Edge: muzica se oprește la `speechSynthesis.cancel()` dacă vocea e activă simultan.
|
||||
- Legat de: T10 (PR2), timer countdown în bara chrome (§Design pct. 10).
|
||||
### [x] Diplomă A4 print-first (§Design pct.9) — LIVRAT (2026-06-13)
|
||||
Certificat A4 portret, fundal alb, chenar dublu accent, titlu serif „DIPLOMĂ DE EVADARE" (singurul serif),
|
||||
numele copilului = cel mai mare element. Overlay `#diploma`; buton „Vezi diploma →" pe finale (+ „Joacă
|
||||
din nou"). `buildDiploma()` randează: rând de stele per cameră (★★★/★★☆; camere sărite = 🔒 „sărită"),
|
||||
cuvântul magic în dăle (aceeași iconografie ca finalul, lacăte pentru sărite), footer = dată +
|
||||
„creat de {creator}" + marcaj auriu „timpul a expirat" (dacă `_timerExpired`). Câmp builder nou `creator`.
|
||||
Per-cameră `roomStars[]` (persistat în resume). `@media print` izolează `#diploma` (rest `visibility:hidden`,
|
||||
`margin:20mm`, `print-color-adjust:exact`). Verificat: smoke 31/31 (test nou „diploma") + screenshot
|
||||
(`scratch/diploma.png`: A4, 🔒 cameră sărită, footer expirat). Rămas din Etapa 2: doar Adventure Mode v0.
|
||||
|
||||
### [x] Timer Calm (§Design pct.10 / T10) — LIVRAT (2026-06-13)
|
||||
Ceas M:SS în bara chrome a campaniei. Opt-in din builder (câmp „Timp limită (minute)", default 0 = fără;
|
||||
`cleanState` coerce la întreg 0..120). Pornește la „Începe aventura" (intro necronometrat); deadline
|
||||
ABSOLUT în `sessionStorage` (`_DEADLINE_KEY`) → resume-ul (reload mid-campanie) NU resetează ceasul.
|
||||
Sub 1 minut → auriu (`.low`); la expirare îngheață pe `0:00` + marcaj discret (`.expired`, auriu opac),
|
||||
jocul curge nestingherit (zero penalizare, stelele rămân). Fără roșu pulsant (public copii) → reduced-motion
|
||||
safe by default. `exemplu-campanie.html` regenerat (rămâne fără timer — opt-in, ca vocea). Verificat:
|
||||
smoke 29/29 (test nou „timer calm": format M:SS, prag auriu, freeze la expirare, jocul continuă, resume
|
||||
păstrează ceasul). Commit: (acest commit). Următorul: muzică T10 (accelerare sub 1 min — depinde de timer).
|
||||
|
||||
### [x] Muzică ambient accelerată la timer (PR2 / T10) — LIVRAT (2026-06-13)
|
||||
Opt-in din builder (checkbox `music`, default off). Orchestrator-only: părintele deține AudioContext
|
||||
(reutilizează `beep._ctx`, deblocat de gestul global); camerele NU știu de muzică. Arpegiu calm pe
|
||||
pentatonică minoră (`_mTick`, oscilatoare sine scurte la ~520ms); tempo **accelerează** spre ~1.8×
|
||||
pe ultimul minut (`musicTempoFactor`, legat de `_deadline`-ul Timer Calm). Buton 🎵/🔇 în bara chrome
|
||||
(`#btn-music`). Edge-uri tratate:
|
||||
- **Duck pe voce:** `voiceSay` setează `u.onstart→duckMusic(true)` / `onend|onerror→duckMusic(false)`;
|
||||
`voiceCancel` și el unduck. Vocea are prioritate (gain muzică × 0.22 cât timp vorbește).
|
||||
- **Fallback fără AudioContext:** tot în `try/catch` → no-op, buton ascuns (zero penalizare).
|
||||
- pornește la „Începe aventura" + la resume; se oprește la `showFinale` (+ toggle).
|
||||
- fără timer → tempo rămâne 1.0 (loop calm, fără accelerare).
|
||||
Hook test `window.__music` (`tempo()`, `state()`). `exemplu-campanie.html` regenerat (rămâne fără
|
||||
muzică — opt-in, ca vocea). Verificat: smoke 30/30 (test nou „muzica ambient": opt-in, start, tempo
|
||||
crește sub 1 min, duck, toggle). Următorul roadmap: Diplomă (§Design pct.9) + Adventure Mode v0.
|
||||
|
||||
### [x] Narațiune vocală (SpeechSynthesis, D10) — LIVRAT (PR2)
|
||||
Feature NOU (nu doar edge-cases — voce nu exista deloc). Opt-in din builder (checkbox
|
||||
|
||||
@@ -130,11 +130,17 @@
|
||||
</div>
|
||||
<label>Pentru cine (optional, apare in mesaje)</label>
|
||||
<input type="text" id="gPlayer" data-g="player" placeholder="ex: Paula">
|
||||
<label>Creat de (optional, apare pe diploma)</label>
|
||||
<input type="text" id="gCreator" data-g="creator" placeholder="ex: Doamna invatatoare">
|
||||
<label>Povestea de inceput</label>
|
||||
<textarea id="gStory" data-g="story" rows="3"></textarea>
|
||||
<label>Mesajul final (la castig)</label>
|
||||
<textarea id="gFinal" data-g="finalMessage" rows="2"></textarea>
|
||||
<label class="ck"><input type="checkbox" id="gVoice" data-gb="voice"> Naratiune vocala — citeste povestea si intrebarile cu vocea sistemului (doar in Campanie multi-stil)</label>
|
||||
<label class="ck"><input type="checkbox" id="gMusic" data-gb="music"> Muzica de fundal — arpegiu calm care accelereaza sub 1 minut (doar in Campanie; buton de oprire in bara)</label>
|
||||
<label>Timp limita (minute, 0 = fara) — ceas calm in bara, doar in Campanie</label>
|
||||
<input type="number" id="gTimer" data-g="timerMin" min="0" max="120" step="1" placeholder="0">
|
||||
<div class="help">Pornesti la "Incepe aventura". Sub 1 minut devine auriu. La expirare ingheata pe 0:00 — jocul continua, fara penalizare.</div>
|
||||
<label>Cuvantul final (din literele puzzle-urilor)</label>
|
||||
<div class="word" id="finalWord"> </div>
|
||||
<div class="help">Se compune automat din campul "Litera" al fiecarui puzzle, in ordine.</div>
|
||||
@@ -174,8 +180,11 @@ const defaultState = () => ({
|
||||
player: '',
|
||||
color: '#6d28d9',
|
||||
style: 'classic',
|
||||
creator: '',
|
||||
charName: 'Alex',
|
||||
voice: false,
|
||||
music: false,
|
||||
timerMin: 0,
|
||||
story: 'O comoara a fost ascunsa, iar singurul drum spre ea trece prin cateva incercari. Rezolva fiecare puzzle ca sa aduni literele cuvantului magic.',
|
||||
finalMessage: 'Felicitari! Ai gasit comoara!',
|
||||
puzzles: [
|
||||
@@ -407,6 +416,7 @@ $('#btnReload').addEventListener('click', refreshPreview);
|
||||
|
||||
function cleanState() {
|
||||
const s = JSON.parse(JSON.stringify(state));
|
||||
s.timerMin = Math.max(0, Math.min(120, parseInt(s.timerMin, 10) || 0)); /* T10: minute întregi 0..120 */
|
||||
s.puzzles.forEach(p => {
|
||||
delete p._closed;
|
||||
/* D13: letter normalizat la 1 caracter alfanumeric (bug: un < strică scena SVG din point) */
|
||||
@@ -1621,15 +1631,23 @@ body {
|
||||
}
|
||||
#chrome-title { font-size: 15px; font-weight: 700; }
|
||||
#chrome .sp { flex: 1; }
|
||||
#btn-voice {
|
||||
/* Timer Calm (§Design pct.10) — neutru; auriu sub 1 min; înghețat la expirare (fără roșu pulsant) */
|
||||
#chrome-timer {
|
||||
font-variant-numeric: tabular-nums; font-weight: 700; font-size: 15px;
|
||||
color: var(--c-ink); letter-spacing: .02em; min-width: 3.1em; text-align: right;
|
||||
}
|
||||
#chrome-timer[hidden] { display: none; }
|
||||
#chrome-timer.low { color: var(--c-gold); }
|
||||
#chrome-timer.expired { color: var(--c-gold); opacity: .55; }
|
||||
#btn-voice, #btn-music {
|
||||
width: 34px; height: 34px; min-width: 34px; padding: 0; border: 0; cursor: pointer;
|
||||
border-radius: 8px; background: rgba(255,255,255,.12); color: #fff;
|
||||
font-size: 17px; line-height: 1; display: inline-flex; align-items: center; justify-content: center;
|
||||
}
|
||||
#btn-voice[hidden] { display: none; } /* id batea specificitatea UA [hidden] */
|
||||
#btn-voice:hover { background: rgba(255,255,255,.22); }
|
||||
#btn-voice[aria-pressed="false"] { opacity: .5; }
|
||||
#btn-voice:focus-visible { outline: 2px solid #a78bfa; outline-offset: 2px; }
|
||||
#btn-voice[hidden], #btn-music[hidden] { display: none; } /* id batea specificitatea UA [hidden] */
|
||||
#btn-voice:hover, #btn-music:hover { background: rgba(255,255,255,.22); }
|
||||
#btn-voice[aria-pressed="false"], #btn-music[aria-pressed="false"] { opacity: .5; }
|
||||
#btn-voice:focus-visible, #btn-music:focus-visible { outline: 2px solid #a78bfa; outline-offset: 2px; }
|
||||
#dots { display: flex; gap: 8px; }
|
||||
#dots span {
|
||||
width: 10px; height: 10px; border-radius: 50%;
|
||||
@@ -1778,6 +1796,52 @@ body {
|
||||
}
|
||||
.btn-main:hover { filter: brightness(1.1); }
|
||||
.btn-main:disabled { opacity: .5; cursor: not-allowed; }
|
||||
.btn-sec {
|
||||
font: inherit; font-size: 15px; font-weight: 700;
|
||||
background: rgba(255,255,255,.12); color: #fff; border: 1px solid rgba(255,255,255,.22);
|
||||
border-radius: 12px; padding: 12px 24px; cursor: pointer; min-height: 44px;
|
||||
width: 100%; max-width: 320px;
|
||||
}
|
||||
.btn-sec:hover { background: rgba(255,255,255,.2); }
|
||||
.btn-main:focus-visible, .btn-sec:focus-visible { outline: 2px solid #a78bfa; outline-offset: 2px; }
|
||||
.fin-actions, .dipl-actions { display: flex; flex-direction: column; align-items: center; gap: 10px; width: 100%; }
|
||||
/* ----- Diplomă A4 print-first (§Design pct.9) ----- */
|
||||
#diploma { background: #0d0620; gap: 16px; }
|
||||
.dipl-sheet {
|
||||
width: 100%; max-width: 520px; aspect-ratio: 210 / 297; background: #fff; color: #1a1333;
|
||||
border-radius: 6px; box-shadow: 0 18px 50px rgba(0,0,0,.5);
|
||||
display: flex; padding: 10px; overflow: hidden;
|
||||
}
|
||||
.dipl-frame {
|
||||
flex: 1; border: 3px double var(--accent); border-radius: 4px;
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: flex-start;
|
||||
gap: 2.2%; padding: 6% 7%; text-align: center;
|
||||
}
|
||||
.dipl-title { font-family: Georgia, "Times New Roman", serif; font-weight: 700; letter-spacing: .04em;
|
||||
font-size: clamp(20px, 5.2vw, 30px); color: var(--accent); }
|
||||
.dipl-sub { font-size: clamp(11px, 2.4vw, 13px); color: #6b6480; text-transform: uppercase; letter-spacing: .12em; }
|
||||
.dipl-name { font-size: clamp(26px, 7vw, 42px); font-weight: 800; line-height: 1.05; color: #1a1333; word-break: break-word; }
|
||||
.dipl-game { font-size: clamp(12px, 2.8vw, 15px); color: #4a4360; font-style: italic; }
|
||||
.dipl-rooms { display: flex; flex-direction: column; gap: 3px; width: 100%; max-width: 320px; margin-top: 2%; }
|
||||
.dipl-rooms .dipl-room { display: flex; justify-content: space-between; align-items: center; gap: 8px;
|
||||
font-size: clamp(11px, 2.4vw, 13px); color: #4a4360; border-bottom: 1px dotted rgba(0,0,0,.12); padding: 2px 0; }
|
||||
.dipl-rooms .dipl-room .rstars { color: #c8952a; letter-spacing: 1px; white-space: nowrap; }
|
||||
.dipl-rooms .dipl-room .rskip { color: #9a93ad; }
|
||||
.dipl-wordlbl { font-size: clamp(10px, 2.2vw, 12px); text-transform: uppercase; letter-spacing: .12em; color: #6b6480; margin-top: 2%; }
|
||||
.dipl-word { display: flex; gap: 6px; justify-content: center; flex-wrap: wrap; }
|
||||
.dipl-word span { width: clamp(24px, 7vw, 38px); aspect-ratio: 5 / 6; background: var(--accent); color: #fff;
|
||||
border-radius: 7px; display: flex; align-items: center; justify-content: center; font-size: clamp(15px, 4vw, 22px); font-weight: 800; }
|
||||
.dipl-word span.lock { background: #d8d3e4; color: #6b6480; }
|
||||
.dipl-footer { margin-top: auto; font-size: clamp(10px, 2.2vw, 12px); color: #6b6480; line-height: 1.5; }
|
||||
.dipl-footer .dipl-expired { color: #c8952a; }
|
||||
@media print {
|
||||
body * { visibility: hidden !important; }
|
||||
#diploma, #diploma * { visibility: visible !important; }
|
||||
#diploma { position: fixed; inset: 0; display: flex !important; background: #fff !important; padding: 0; }
|
||||
.dipl-actions { display: none !important; }
|
||||
.dipl-sheet { box-shadow: none; max-width: none; width: auto; height: auto; margin: 20mm; aspect-ratio: 210 / 297; }
|
||||
.dipl-title, .dipl-word span, .dipl-frame { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
||||
}
|
||||
.btn-skip {
|
||||
font: inherit; font-size: 15px; font-weight: 700;
|
||||
background: #4b5563; color: #fff; border: none;
|
||||
@@ -1788,6 +1852,7 @@ body {
|
||||
@media (max-width: 599px) {
|
||||
#chrome { height: 40px; min-height: 40px; }
|
||||
#chrome-title { font-size: 13px; }
|
||||
#chrome-timer { font-size: 13px; }
|
||||
#dots span { width: 8px; height: 8px; }
|
||||
}
|
||||
</style>
|
||||
@@ -1797,6 +1862,8 @@ body {
|
||||
<div id="chrome">
|
||||
<span id="chrome-title">${esc(cfg.title)}</span>
|
||||
<div class="sp"></div>
|
||||
<span id="chrome-timer" role="timer" aria-label="Timp ramas" hidden>0:00</span>
|
||||
<button id="btn-music" type="button" aria-label="Muzica de fundal" hidden>🎵</button>
|
||||
<button id="btn-voice" type="button" aria-label="Naratiune vocala" hidden>🔊</button>
|
||||
<div id="dots" role="group" aria-label="Progres camere"></div>
|
||||
</div>
|
||||
@@ -1838,6 +1905,30 @@ body {
|
||||
<p>Cuvântul magic:</p>
|
||||
<div id="fin-word"></div>
|
||||
<p id="fin-msg"></p>
|
||||
<div class="fin-actions">
|
||||
<button class="btn-main" id="btn-diploma">Vezi diploma →</button>
|
||||
<button class="btn-sec" id="btn-replay">Joacă din nou</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Diplomă A4 print-first (§Design pct.9) — populată la „Vezi diploma" -->
|
||||
<div id="diploma" class="overlay" aria-hidden="true">
|
||||
<div class="dipl-sheet" role="document" aria-label="Diplomă de evadare">
|
||||
<div class="dipl-frame">
|
||||
<div class="dipl-title">DIPLOMĂ DE EVADARE</div>
|
||||
<div class="dipl-sub">se acordă lui</div>
|
||||
<div class="dipl-name" id="dipl-name"></div>
|
||||
<div class="dipl-game" id="dipl-game"></div>
|
||||
<div class="dipl-rooms" id="dipl-rooms"></div>
|
||||
<div class="dipl-wordlbl">Cuvântul magic</div>
|
||||
<div class="dipl-word" id="dipl-word"></div>
|
||||
<div class="dipl-footer" id="dipl-footer"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dipl-actions">
|
||||
<button class="btn-main" id="dipl-print">Printează diploma</button>
|
||||
<button class="btn-sec" id="dipl-back">← Înapoi</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1864,6 +1955,7 @@ document.documentElement.style.setProperty('--accent', MASTER.color || '#6d28d9'
|
||||
var N = MASTER.puzzles.length;
|
||||
var totalStars = 0;
|
||||
var collected = [];
|
||||
var roomStars = []; /* stele per cameră — pentru diplomă (§Design pct.9) */
|
||||
var skipped = {};
|
||||
|
||||
var activeIdx = -1;
|
||||
@@ -1881,14 +1973,96 @@ var _RESUME_KEY = 'esc-camp-' + djb2(JSON.stringify(MASTER));
|
||||
function safeGet(){ try{ return JSON.parse(sessionStorage.getItem(_RESUME_KEY)); }catch(e){ return null; } }
|
||||
function safeSet(v){ try{ sessionStorage.setItem(_RESUME_KEY, JSON.stringify(v)); }catch(e){ /* webview restrictat — continuă fără resume */ } }
|
||||
function saveProgress(){
|
||||
safeSet({ idx: activeIdx, totalStars: totalStars, collected: collected.slice(), skipped: skipped });
|
||||
safeSet({ idx: activeIdx, totalStars: totalStars, collected: collected.slice(), roomStars: roomStars.slice(), skipped: skipped });
|
||||
}
|
||||
function clearProgress(){ try{ sessionStorage.removeItem(_RESUME_KEY); }catch(e){} }
|
||||
function clearProgress(){ try{ sessionStorage.removeItem(_RESUME_KEY); sessionStorage.removeItem(_DEADLINE_KEY); }catch(e){} }
|
||||
|
||||
/* ----- Timer Calm (§Design pct.10) — ceas M:SS în chrome.
|
||||
Pornește la „Începe aventura"; deadline ABSOLUT în sessionStorage → resume-ul
|
||||
(reload mid-campanie) NU resetează ceasul. Sub 1 min → auriu. La expirare îngheață
|
||||
pe 0:00 + marcaj discret, jocul curge nestingherit (zero penalizare). ----- */
|
||||
var TIMER_SEC = (+MASTER.timerMin || 0) * 60;
|
||||
var _DEADLINE_KEY = _RESUME_KEY + '-dl';
|
||||
var timerEl = document.getElementById('chrome-timer');
|
||||
var _deadline = 0, _timerInt = null, _timerExpired = false;
|
||||
function _fmt(s){ var m = Math.floor(s/60), ss = s % 60; return m + ':' + (ss < 10 ? '0' : '') + ss; }
|
||||
function tickTimer(){
|
||||
if(!_deadline){ return; }
|
||||
var rem = Math.round((_deadline - Date.now()) / 1000);
|
||||
if(rem <= 0){
|
||||
rem = 0;
|
||||
if(!_timerExpired){ _timerExpired = true; timerEl.classList.add('expired'); timerEl.title = 'Timpul a expirat — jocul continua'; }
|
||||
if(_timerInt){ clearInterval(_timerInt); _timerInt = null; }
|
||||
}
|
||||
timerEl.textContent = _fmt(rem);
|
||||
if(rem <= 60) timerEl.classList.add('low');
|
||||
}
|
||||
function stopTimer(){ if(_timerInt){ clearInterval(_timerInt); _timerInt = null; } }
|
||||
function startTimer(){
|
||||
if(TIMER_SEC <= 0) return;
|
||||
timerEl.hidden = false;
|
||||
var existing = 0; try{ existing = +sessionStorage.getItem(_DEADLINE_KEY) || 0; }catch(e){}
|
||||
if(existing > 0){ _deadline = existing; } /* resume → păstrează ceasul */
|
||||
else { _deadline = Date.now() + TIMER_SEC * 1000; try{ sessionStorage.setItem(_DEADLINE_KEY, String(_deadline)); }catch(e){} }
|
||||
tickTimer();
|
||||
if(!_timerInt && !_timerExpired) _timerInt = setInterval(tickTimer, 1000);
|
||||
}
|
||||
|
||||
/* ----- Muzică ambient (T10) — opt-in MASTER.music. Orchestrator-only: părintele
|
||||
deține AudioContext (reutilizează beep._ctx, deblocat de gestul global); camerele
|
||||
nu știu de muzică. Arpegiu calm pe pentatonică minoră; tempo ACCELEREAZĂ sub 1 min
|
||||
(legat de Timer Calm). Se atenuează (duck) cât timp vocea vorbește. Fallback:
|
||||
fără AudioContext → no-op, butonul rămâne ascuns (zero penalizare). ----- */
|
||||
var MUSIC = !!MASTER.music;
|
||||
var musicOn = MUSIC;
|
||||
var _mGain = null, _mTimer = null, _mStep = 0, _mDuck = 1;
|
||||
var _MSCALE = [0, 3, 5, 7, 10]; /* pentatonică minoră (semitonuri) */
|
||||
function _mFreq(semi){ return 220 * Math.pow(2, semi / 12); }
|
||||
function musicTempoFactor(){
|
||||
/* 1.0 normal → ~1.8 pe ultimul minut (accelerare progresivă) */
|
||||
if(TIMER_SEC <= 0 || !_deadline) return 1;
|
||||
var rem = (_deadline - Date.now()) / 1000;
|
||||
if(rem < 0) return 1.8;
|
||||
if(rem > 60) return 1;
|
||||
return 1 + (1 - rem / 60) * 0.8;
|
||||
}
|
||||
function _mTick(){
|
||||
if(!musicOn || !_mGain){ _mTimer = null; return; }
|
||||
var ctx = beep._ctx;
|
||||
if(ctx){
|
||||
try{
|
||||
var oct = (Math.floor(_mStep / _MSCALE.length) % 2) ? 12 : 0;
|
||||
var semi = _MSCALE[_mStep % _MSCALE.length] + oct;
|
||||
var o = ctx.createOscillator(), g = ctx.createGain(), t = ctx.currentTime;
|
||||
o.type = 'sine'; o.frequency.value = _mFreq(semi);
|
||||
g.gain.setValueAtTime(0.0001, t);
|
||||
g.gain.linearRampToValueAtTime(0.05 * _mDuck, t + 0.05);
|
||||
g.gain.exponentialRampToValueAtTime(0.0001, t + 0.55);
|
||||
o.connect(g); g.connect(_mGain);
|
||||
o.start(t); o.stop(t + 0.6);
|
||||
}catch(e){}
|
||||
}
|
||||
_mStep++;
|
||||
_mTimer = setTimeout(_mTick, 520 / musicTempoFactor()); /* mai rapid când tempo crește */
|
||||
}
|
||||
function startMusic(){
|
||||
if(!musicOn) return;
|
||||
try{
|
||||
var ctx = beep._ctx || (beep._ctx = new (window.AudioContext || window.webkitAudioContext)());
|
||||
if(ctx.state === 'suspended') ctx.resume();
|
||||
if(!_mGain){ _mGain = ctx.createGain(); _mGain.gain.value = 1; _mGain.connect(ctx.destination); }
|
||||
if(!_mTimer){ _mStep = 0; _mTick(); }
|
||||
}catch(e){}
|
||||
}
|
||||
function stopMusic(){ if(_mTimer){ clearTimeout(_mTimer); _mTimer = null; } }
|
||||
function duckMusic(on){ _mDuck = on ? 0.22 : 1; } /* vocea are prioritate (edge T10) */
|
||||
window.__music = { tempo: musicTempoFactor, state: function(){ return { on: musicOn, playing: !!_mTimer, duck: _mDuck }; } };
|
||||
|
||||
var frameEl = document.getElementById('room-frame');
|
||||
var introEl = document.getElementById('intro');
|
||||
var skipEl = document.getElementById('skip-banner');
|
||||
var finaleEl = document.getElementById('finale');
|
||||
var diplomaEl = document.getElementById('diploma');
|
||||
|
||||
/* ----- Dots ----- */
|
||||
function buildDots(){
|
||||
@@ -1979,7 +2153,7 @@ if(SPEECH){
|
||||
_pickVoice();
|
||||
try{ window.speechSynthesis.onvoiceschanged = _pickVoice; }catch(e){}
|
||||
}
|
||||
function voiceCancel(){ if(SPEECH){ try{ window.speechSynthesis.cancel(); }catch(e){} } }
|
||||
function voiceCancel(){ if(SPEECH){ try{ window.speechSynthesis.cancel(); }catch(e){} } duckMusic(false); }
|
||||
function voiceSay(text){
|
||||
if(!SPEECH || !voiceOn || !text) return;
|
||||
try{
|
||||
@@ -1988,6 +2162,10 @@ function voiceSay(text){
|
||||
var u = new SpeechSynthesisUtterance(String(text));
|
||||
if(_roVoice){ u.voice = _roVoice; u.lang = _roVoice.lang; } else { u.lang = 'ro-RO'; }
|
||||
u.rate = 1; u.pitch = 1;
|
||||
/* vocea are prioritate → atenuează muzica cât timp vorbește (edge T10) */
|
||||
u.onstart = function(){ duckMusic(true); };
|
||||
u.onend = function(){ duckMusic(false); };
|
||||
u.onerror = function(){ duckMusic(false); };
|
||||
window.speechSynthesis.speak(u);
|
||||
}catch(e){}
|
||||
}
|
||||
@@ -2007,6 +2185,7 @@ window.nextRoom = function(data){
|
||||
clearTimeout(readyTimer);
|
||||
roomDone[idx] = true;
|
||||
totalStars += (data.stars || 0);
|
||||
roomStars[idx] = (data.stars || 0); /* pentru diplomă */
|
||||
var letter = String(data.letter || '').replace(/[^A-Za-z0-9]/g,'').charAt(0).toUpperCase();
|
||||
if(letter) collected.push(letter);
|
||||
setDot(idx,'done');
|
||||
@@ -2112,6 +2291,7 @@ function showSkipBanner(idx, code, reason){
|
||||
|
||||
/* ----- Final ----- */
|
||||
function showFinale(){
|
||||
stopTimer(); stopMusic(); /* jocul s-a încheiat — oprește ceasul + muzica */
|
||||
hideAll(); finaleEl.classList.add('show');
|
||||
var wEl = document.getElementById('fin-word'); wEl.innerHTML = '';
|
||||
collected.forEach(function(l,j){
|
||||
@@ -2136,6 +2316,43 @@ function showFinale(){
|
||||
voiceSay(document.getElementById('fin-msg').textContent); /* citește mesajul final (D10) */
|
||||
}
|
||||
|
||||
/* ----- Diplomă A4 (§Design pct.9) — populată la „Vezi diploma" ----- */
|
||||
function _starStr(n){ n = Math.max(0, Math.min(3, n|0)); var s = ''; for(var i=0;i<3;i++) s += i<n ? '\\u2605' : '\\u2606'; return s; }
|
||||
function buildDiploma(){
|
||||
document.getElementById('dipl-name').textContent = (MASTER.player||'').trim() || 'Campion';
|
||||
document.getElementById('dipl-game').textContent = '\\u201E' + (MASTER.title||'') + '\\u201D';
|
||||
var rooms = document.getElementById('dipl-rooms'); rooms.innerHTML = '';
|
||||
for(var i=0;i<N;i++){
|
||||
var row = document.createElement('div'); row.className = 'dipl-room';
|
||||
var lab = document.createElement('span'); lab.textContent = 'Camera ' + (i+1);
|
||||
var val = document.createElement('span');
|
||||
if(skipped[i]){ val.className = 'rskip'; val.textContent = '\\uD83D\\uDD12 s\\u0103rit\\u0103'; }
|
||||
else { val.className = 'rstars'; val.textContent = _starStr(roomStars[i]||0); }
|
||||
row.appendChild(lab); row.appendChild(val); rooms.appendChild(row);
|
||||
}
|
||||
var w = document.getElementById('dipl-word'); w.innerHTML = '';
|
||||
collected.forEach(function(l){ var s = document.createElement('span'); s.textContent = l; w.appendChild(s); });
|
||||
Object.keys(skipped).forEach(function(){ var s = document.createElement('span'); s.className = 'lock'; s.textContent = '\\uD83D\\uDD12'; w.appendChild(s); });
|
||||
var foot = '';
|
||||
try{ foot = new Date().toLocaleDateString('ro-RO', {year:'numeric', month:'long', day:'numeric'}); }catch(e){ foot = ''; }
|
||||
var cre = (MASTER.creator||'').trim(); if(cre) foot += ' \\u00b7 creat de ' + cre;
|
||||
var fEl = document.getElementById('dipl-footer'); fEl.textContent = foot;
|
||||
if(_timerExpired){ var ex = document.createElement('div'); ex.className = 'dipl-expired'; ex.textContent = 'timpul a expirat'; fEl.appendChild(ex); }
|
||||
}
|
||||
function showDiploma(){
|
||||
buildDiploma();
|
||||
finaleEl.classList.remove('show');
|
||||
diplomaEl.classList.add('show'); diplomaEl.setAttribute('aria-hidden','false');
|
||||
}
|
||||
function hideDiploma(){
|
||||
diplomaEl.classList.remove('show'); diplomaEl.setAttribute('aria-hidden','true');
|
||||
finaleEl.classList.add('show');
|
||||
}
|
||||
document.getElementById('btn-diploma').onclick = showDiploma;
|
||||
document.getElementById('dipl-back').onclick = hideDiploma;
|
||||
document.getElementById('dipl-print').onclick = function(){ try{ window.print(); }catch(e){} };
|
||||
document.getElementById('btn-replay').onclick = function(){ clearProgress(); location.reload(); };
|
||||
|
||||
/* ----- Confetti ----- */
|
||||
function confetti(){
|
||||
var colors=[MASTER.color||'#6d28d9','#fbbf24','#34d399','#60a5fa','#f472b6'];
|
||||
@@ -2151,7 +2368,7 @@ function confetti(){
|
||||
var overworldEl = document.getElementById('overworld');
|
||||
function hideAll(){
|
||||
voiceCancel(); /* fără replici fantomă la schimbarea scenei (D10) */
|
||||
[introEl,overworldEl,skipEl,finaleEl].forEach(function(el){ el.classList.remove('show'); });
|
||||
[introEl,overworldEl,skipEl,finaleEl,diplomaEl].forEach(function(el){ el.classList.remove('show'); });
|
||||
}
|
||||
|
||||
/* ===== Overworld (S3 pas2 — hartă top-down care înlocuiește coridorul) =====
|
||||
@@ -2280,6 +2497,8 @@ document.getElementById('intro-promise').textContent = N+' camere \\u00b7 ${nSty
|
||||
document.getElementById('btn-start').onclick = function(){
|
||||
unlockAudio(); /* gest direct pe părinte (handlerul global prinde și el, dar e idempotent) */
|
||||
clearProgress(); owResetPlayer(); showOverworld(0);
|
||||
startTimer(); /* ceasul pornește exact la „Începe aventura" (intro necronometrat) */
|
||||
startMusic(); /* muzica ambient pornește odată cu aventura (T10) */
|
||||
voiceSay(_introStory); /* după showOverworld → hideAll a rulat deja, nu mai anulează (D10) */
|
||||
};
|
||||
|
||||
@@ -2300,6 +2519,23 @@ if(SPEECH && btnVoice){
|
||||
};
|
||||
}
|
||||
|
||||
/* ----- Buton muzică în bara chrome (T10) ----- */
|
||||
var btnMusic = document.getElementById('btn-music');
|
||||
if(MUSIC && btnMusic){
|
||||
btnMusic.hidden = false;
|
||||
var _syncMusicBtn = function(){
|
||||
btnMusic.innerHTML = musicOn ? '🎵' : '🔇'; /* 🎵 / 🔇 */
|
||||
btnMusic.setAttribute('aria-pressed', musicOn ? 'true' : 'false');
|
||||
btnMusic.title = musicOn ? 'Muzica pornita — apasa ca sa opresti' : 'Muzica oprita — apasa ca sa pornesti';
|
||||
};
|
||||
_syncMusicBtn();
|
||||
btnMusic.onclick = function(){
|
||||
musicOn = !musicOn;
|
||||
if(musicOn) startMusic(); else stopMusic();
|
||||
_syncMusicBtn();
|
||||
};
|
||||
}
|
||||
|
||||
buildDots();
|
||||
|
||||
/* ----- Resume la reload (D11) ----- */
|
||||
@@ -2310,6 +2546,7 @@ buildDots();
|
||||
/* restaurăm starea */
|
||||
totalStars = saved.totalStars || 0;
|
||||
collected = saved.collected || [];
|
||||
roomStars = saved.roomStars || [];
|
||||
skipped = saved.skipped || {};
|
||||
Object.keys(skipped).forEach(function(k){ roomDone[+k] = true; setDot(+k,'done'); });
|
||||
/* repornim pe hartă, la ușa camerei next */
|
||||
@@ -2321,6 +2558,8 @@ buildDots();
|
||||
showFinale(); return;
|
||||
}
|
||||
owResetPlayer(); showOverworld(resumeIdx);
|
||||
startTimer(); /* resume → reia ceasul de la deadline-ul absolut salvat */
|
||||
startMusic(); /* resume → reia muzica (T10) */
|
||||
})();
|
||||
<\/script>
|
||||
</body>
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -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 (~28 teste).
|
||||
- `tests/smoke.mjs` — unicul fișier de teste (~31 teste).
|
||||
- `playwright.config.mjs` (la root, **gitignored**) — config dev.
|
||||
|
||||
## Local Contracts
|
||||
@@ -16,11 +16,11 @@ până la ecranul final, fără erori de consolă.
|
||||
HTML temp generat via builder (`gameHTML`) și-l încarcă de pe `file://`.
|
||||
- **Zero erori consolă = invariant.** `trackErrors(page)` colectează `console.error` + `pageerror`;
|
||||
fiecare test asertează `errors.length === 0` la final.
|
||||
- **Tag-uri:** `@regresie` (15 — exemplu-*.html + edge cases + mobil 320px + regenerare via gameHTML +
|
||||
bomberman gameplay + bomberman rază/powerup-uri) ș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ă: 28/28 PASS.**
|
||||
- **Tag-uri:** `@regresie` (16 — exemplu-*.html + edge cases + mobil 320px + regenerare via gameHTML +
|
||||
stil top-level invalid la import + bomberman gameplay + bomberman rază/powerup-uri) și `@campanie`
|
||||
(17 — intro→hartă→camere→final, resume, cameră moartă, idempotență ușă, `$`/`$&`, beep, mobil,
|
||||
audio S1, voce/narațiune D10, a11y tap/aria/reduced-motion, navigare overworld, timer calm T10, muzica ambient T10, diploma A4).
|
||||
- **Status țintă: 31/31 PASS.**
|
||||
|
||||
## Work Guidance
|
||||
- După modificări la motoare (`escape-builder.html`): rulează suita completă; extinde `@regresie` dacă
|
||||
@@ -29,7 +29,7 @@ până la ecranul final, fără erori de consolă.
|
||||
|
||||
## Verification
|
||||
```bash
|
||||
npx playwright test tests/smoke.mjs # 28/28
|
||||
npx playwright test tests/smoke.mjs # 31/31
|
||||
npx playwright test tests/smoke.mjs --grep @regresie
|
||||
npx playwright test tests/smoke.mjs --grep @campanie
|
||||
```
|
||||
|
||||
177
tests/smoke.mjs
177
tests/smoke.mjs
@@ -671,6 +671,183 @@ test.describe('Campanie E2E @campanie', () => {
|
||||
expect(errors, errors.join('\n')).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('timer calm — porneste la start, auriu sub 1 min, ingheata la expirare, resume pastreaza ceasul (T10/§Design pct.10) @campanie',
|
||||
async ({ page }) => {
|
||||
const errors = trackErrors(page);
|
||||
const cfg = campaignCfg(3, 'classic');
|
||||
cfg.timerMin = 1;
|
||||
const tmpPath = await writeCampaignHtml(page, cfg, 'timer');
|
||||
const gp = await page.context().newPage();
|
||||
|
||||
try {
|
||||
await gp.goto('file://' + tmpPath);
|
||||
|
||||
// Intro necronometrat — ceasul e ascuns pana la start
|
||||
expect(await gp.locator('#chrome-timer').isVisible(), 'timer ascuns pe intro').toBe(false);
|
||||
|
||||
await gp.locator('#btn-start').click();
|
||||
|
||||
// Dupa start: vizibil, format M:SS, aproape de 1:00
|
||||
const timer = gp.locator('#chrome-timer');
|
||||
await expect(timer).toBeVisible();
|
||||
await expect(timer).toHaveText(/^\d:\d\d$/);
|
||||
const t0 = await timer.textContent();
|
||||
const sec0 = (+t0.split(':')[0]) * 60 + (+t0.split(':')[1]);
|
||||
expect(sec0, 'ceasul porneste ~1:00').toBeGreaterThan(50);
|
||||
expect(sec0).toBeLessThanOrEqual(60);
|
||||
|
||||
// Deadline ABSOLUT salvat in sessionStorage (resume pastreaza ceasul)
|
||||
const dl = await gp.evaluate(() => +sessionStorage.getItem(_DEADLINE_KEY));
|
||||
expect(dl, 'deadline absolut in sessionStorage').toBeGreaterThan(Date.now());
|
||||
|
||||
// Sub 1 minut → auriu (.low). Manipulam deadline determinist.
|
||||
await gp.evaluate(() => { _deadline = Date.now() + 5000; tickTimer(); });
|
||||
await expect(timer).toHaveText('0:05');
|
||||
await expect(timer).toHaveClass(/low/);
|
||||
|
||||
// Expirare → ingheata pe 0:00 + .expired; jocul curge nestingherit
|
||||
await gp.evaluate(() => { _deadline = Date.now() - 1000; tickTimer(); });
|
||||
await expect(timer).toHaveText('0:00');
|
||||
await expect(timer).toHaveClass(/expired/);
|
||||
|
||||
// Jocul continua dupa expirare (zero penalizare): rezolva camera 0
|
||||
await enterRoom(gp, 0);
|
||||
await solveRoom(gp, 'classic', 'r1');
|
||||
await waitOverworld(gp);
|
||||
|
||||
// Resume pastreaza ceasul: suprascriem deadline-ul cu o valoare cunoscuta, reload
|
||||
await gp.evaluate(() => { sessionStorage.setItem(_DEADLINE_KEY, String(Date.now() + 8000)); });
|
||||
await gp.reload();
|
||||
await gp.waitForLoadState('domcontentloaded');
|
||||
await gp.waitForFunction(
|
||||
() => window.__ow && window.__ow.state.active &&
|
||||
document.getElementById('overworld')?.classList.contains('show'),
|
||||
null, { timeout: 5000 }
|
||||
);
|
||||
|
||||
// Ceasul reia de la deadline-ul salvat (~0:08), NU resetat la 1:00
|
||||
await expect(gp.locator('#chrome-timer')).toBeVisible();
|
||||
const tR = await gp.locator('#chrome-timer').textContent();
|
||||
const secR = (+tR.split(':')[0]) * 60 + (+tR.split(':')[1]);
|
||||
expect(secR, 'resume reia ceasul (nu reset la 60)').toBeLessThan(55);
|
||||
expect(secR).toBeGreaterThan(0);
|
||||
|
||||
} finally {
|
||||
await gp.close();
|
||||
try { unlinkSync(tmpPath); } catch (_) {}
|
||||
}
|
||||
expect(errors, errors.join('\n')).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('muzica ambient — opt-in, porneste la start, tempo accelereaza sub 1 min, duck pe voce, toggle (T10)',
|
||||
async ({ page }) => {
|
||||
const errors = trackErrors(page);
|
||||
const cfg = campaignCfg(3, 'classic');
|
||||
cfg.music = true;
|
||||
cfg.timerMin = 1; /* timer pornit → tempo poate accelera */
|
||||
const tmpPath = await writeCampaignHtml(page, cfg, 'music');
|
||||
const gp = await page.context().newPage();
|
||||
|
||||
try {
|
||||
await gp.goto('file://' + tmpPath);
|
||||
|
||||
// Butonul de muzica vizibil (opt-in activ); muzica inca neporita
|
||||
await expect(gp.locator('#btn-music')).toBeVisible();
|
||||
expect(await gp.evaluate(() => window.__music.state().playing), 'inca neporita pe intro').toBe(false);
|
||||
|
||||
await gp.locator('#btn-start').click();
|
||||
|
||||
// Dupa start: muzica ruleaza, buton apasat
|
||||
await gp.waitForFunction(() => window.__music.state().playing === true, null, { timeout: 4000 });
|
||||
await expect(gp.locator('#btn-music')).toHaveAttribute('aria-pressed', 'true');
|
||||
|
||||
// Tempo: 1.0 cand >60s ramase; creste progresiv sub 1 min (citit determinist)
|
||||
const tempos = await gp.evaluate(() => {
|
||||
const f = window.__music.tempo;
|
||||
_deadline = Date.now() + 90000; const t90 = f();
|
||||
_deadline = Date.now() + 30000; const t30 = f();
|
||||
_deadline = Date.now() + 1000; const t1 = f();
|
||||
return { t90, t30, t1 };
|
||||
});
|
||||
expect(tempos.t90).toBeCloseTo(1, 1);
|
||||
expect(tempos.t30, 'accelereaza sub 1 min').toBeGreaterThan(tempos.t90);
|
||||
expect(tempos.t1, 'mai rapid spre expirare').toBeGreaterThan(tempos.t30);
|
||||
|
||||
// Duck: vocea are prioritate → atenueaza muzica
|
||||
const ducked = await gp.evaluate(() => { duckMusic(true); return window.__music.state().duck; });
|
||||
const unducked = await gp.evaluate(() => { duckMusic(false); return window.__music.state().duck; });
|
||||
expect(ducked, 'duck activ < 1').toBeLessThan(1);
|
||||
expect(unducked, 'duck dezactivat = 1').toBe(1);
|
||||
|
||||
// Toggle off din buton → se opreste
|
||||
await gp.locator('#btn-music').click();
|
||||
await expect(gp.locator('#btn-music')).toHaveAttribute('aria-pressed', 'false');
|
||||
expect(await gp.evaluate(() => window.__music.state().playing), 'oprit dupa toggle').toBe(false);
|
||||
|
||||
} finally {
|
||||
await gp.close();
|
||||
try { unlinkSync(tmpPath); } catch (_) {}
|
||||
}
|
||||
expect(errors, errors.join('\n')).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('diploma — „Vezi diploma" arata certificat A4 cu nume/stele-per-camera/cuvant/creator, inapoi (§Design pct.9)',
|
||||
async ({ page }) => {
|
||||
test.setTimeout(120000);
|
||||
const errors = trackErrors(page);
|
||||
const cfg = campaignCfg(3, 'classic');
|
||||
cfg.player = 'Maria';
|
||||
cfg.creator = 'Doamna Ana';
|
||||
const tmpPath = await writeCampaignHtml(page, cfg, 'diploma');
|
||||
const gp = await page.context().newPage();
|
||||
const gameErrors = trackErrors(gp);
|
||||
|
||||
try {
|
||||
await gp.goto('file://' + tmpPath);
|
||||
await gp.locator('#btn-start').click();
|
||||
for (let i = 0; i < 3; i++) {
|
||||
await enterRoom(gp, i);
|
||||
await solveRoom(gp, 'classic', 'r' + (i + 1));
|
||||
}
|
||||
await gp.waitForFunction(() => document.getElementById('finale')?.classList.contains('show'),
|
||||
null, { timeout: 10000 });
|
||||
|
||||
// Buton „Vezi diploma" → diploma vizibila, finale ascunsa
|
||||
await gp.locator('#btn-diploma').click();
|
||||
await expect(gp.locator('#diploma')).toBeVisible();
|
||||
await expect(gp.locator('#finale')).not.toBeVisible();
|
||||
|
||||
// Numele copilului (cel mai mare element) + titlul jocului
|
||||
await expect(gp.locator('#dipl-name')).toHaveText('Maria');
|
||||
await expect(gp.locator('#dipl-game')).toContainText('Test Campanie');
|
||||
|
||||
// Rand de stele per camera: 3 randuri, fiecare cu ★ (rezolvate, nu sarite)
|
||||
await expect(gp.locator('#dipl-rooms .dipl-room')).toHaveCount(3);
|
||||
const firstRoom = await gp.locator('#dipl-rooms .dipl-room .rstars').first().innerText();
|
||||
expect(firstRoom).toMatch(/[★☆]{3}/);
|
||||
|
||||
// Cuvantul magic in dale (3 litere colectate, A B C)
|
||||
await expect(gp.locator('#dipl-word span')).toHaveCount(3);
|
||||
|
||||
// Footer: data + „creat de Doamna Ana"
|
||||
await expect(gp.locator('#dipl-footer')).toContainText('creat de Doamna Ana');
|
||||
|
||||
// Butonul de print exista (nu il apasam — window.print blocheaza headless)
|
||||
await expect(gp.locator('#dipl-print')).toBeVisible();
|
||||
|
||||
// Inapoi → finale din nou
|
||||
await gp.locator('#dipl-back').click();
|
||||
await expect(gp.locator('#finale')).toBeVisible();
|
||||
await expect(gp.locator('#diploma')).not.toBeVisible();
|
||||
|
||||
} finally {
|
||||
await gp.close();
|
||||
try { unlinkSync(tmpPath); } catch (_) {}
|
||||
}
|
||||
expect(gameErrors, 'Game errors:\n' + gameErrors.join('\n')).toHaveLength(0);
|
||||
expect(errors, errors.join('\n')).toHaveLength(0);
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
// Test 3: Camera moartă — timeout 4s → skip-banner + cod eroare
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user