Timer Calm (T10 / §Design pct.10): ceas campanie opt-in
Ceas M:SS in bara chrome a campaniei. Opt-in din builder (camp "Timp limita (minute)", default 0 = fara; cleanState coerce 0..120). - porneste exact la "Incepe aventura" (intro necronometrat) - deadline ABSOLUT in sessionStorage -> resume nu reseteaza ceasul - sub 1 min -> auriu (.low); expirare -> ingheata 0:00 + marcaj discret (.expired), jocul curge nestingherit (zero penalizare, stelele raman) - fara rosu pulsant (public copii) -> reduced-motion safe by default - exemplu-campanie.html regenerat (ramane fara timer - opt-in, ca vocea) Fundatie pentru muzica T10 (accelerare sub 1 min) + footer diploma. Test nou (smoke 29/29): format M:SS, prag auriu, freeze la expirare, jocul continua dupa expirare, resume pastreaza ceasul. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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 (~29 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`
|
||||
(15 — 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).
|
||||
- **Status țintă: 29/29 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 # 29/29
|
||||
npx playwright test tests/smoke.mjs --grep @regresie
|
||||
npx playwright test tests/smoke.mjs --grep @campanie
|
||||
```
|
||||
|
||||
@@ -671,6 +671,74 @@ 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 3: Camera moartă — timeout 4s → skip-banner + cod eroare
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user