Il carosello dei Ringraziamenti si ferma, si sfoglia e ingrandisce le foto

Tutti gli sfondi del paese (game.reelPictures, uno per scena, col nome del posto) e ogni ritratto una volta sola
(prima tre si ripetevano). Scorre da solo ma si ferma sotto il mouse, si trascina col mouse o col dito, si spinge
con la rotella, con le frecce ai lati e con quelle della tastiera; un tocco su una foto la mostra grande col nome
(il tocco si legge al rilascio: con la cattura del puntatore il clic non arriva all'immagine). Test della fila e
prova nel browser.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GiECdBYE8csKd4tcFvwntT
This commit is contained in:
2026-10-27 22:57:01 +01:00
co-authored by Claude Fable 5.1
parent 3762f2b465
commit b262977117
6 changed files with 141 additions and 25 deletions
+31
View File
@@ -6,6 +6,8 @@ import { staticServer } from './helpers.mjs';
import { mkdirSync, readFileSync } from 'node:fs';
import assert from 'node:assert/strict';
import { solution, route } from '../src/story/solution.js';
import { story } from '../src/story/story.js';
import { reelPictures } from '../src/engine/game.js';
import { checkMultimedia } from './multimedia.browser.mjs';
const OUT = process.env.OUT ?? '/tmp/regolo-shots';
@@ -120,6 +122,35 @@ try {
await page.screenshot({ path: `${OUT}/01d-ritratto.png` });
await page.click('#face');
assert.ok(await page.isHidden('#face'));
// Il carosello: tutte le immagini (due giri), scorre da solo, si ferma sotto il mouse, le frecce lo spingono di una
// foto, un tocco su una foto la mostra grande col nome del posto.
{
const track = page.locator('#credits-reel .track');
assert.equal(await track.locator('img').count(), reelPictures(story).length * 2, 'ogni sfondo e ogni ritratto, due giri');
await page.mouse.move(5, 5);
const x0 = await track.evaluate(t => t.scrollLeft);
await page.waitForTimeout(600);
const x1 = await track.evaluate(t => t.scrollLeft);
assert.ok(x1 > x0, `il carosello scorre da solo (${x0} → ${x1})`);
await track.hover({ position: { x: 20, y: 20 } });
const x2 = await track.evaluate(t => t.scrollLeft);
await page.waitForTimeout(500);
assert.equal(await track.evaluate(t => t.scrollLeft), x2, 'sotto il mouse si ferma');
await page.click('#credits-reel .arrow.next');
await page.waitForTimeout(800);
const x3 = await track.evaluate(t => t.scrollLeft);
assert.ok(x3 - x2 > 150, `la freccia spinge di una foto (${x2} → ${x3})`);
const third = track.locator('img').nth(2);
const name = await third.getAttribute('alt');
await third.click();
assert.ok(await page.isVisible('#face img.scene'), 'la foto si vede grande');
assert.equal(await page.textContent('#face figcaption'), name);
await page.waitForFunction(() => document.querySelector('#face img').naturalWidth > 0, null, { timeout: 5000 });
await page.screenshot({ path: `${OUT}/01e-carosello-zoom.png` });
await page.keyboard.press('Escape');
assert.ok(await page.isHidden('#face'), 'Esc chiude la foto grande, non i crediti');
assert.ok(await page.isVisible('#credits'));
}
const onTop = await page.evaluate(() => { const r = document.getElementById('close-credits').getBoundingClientRect();
return document.elementFromPoint(r.x + r.width / 2, r.y + r.height / 2)?.id === 'close-credits'; });
assert.ok(onTop, 'i crediti stanno sopra il titolo, non dietro');
+15 -1
View File
@@ -3,7 +3,7 @@ import assert from 'node:assert/strict';
import { backgroundOrder } from '../src/engine/render.js';
import { story } from '../src/story/story.js';
import { solution } from '../src/story/solution.js';
import { Game, VERBS, checkSave, introText, shuffledCredits, matchPlace } from '../src/engine/game.js';
import { Game, VERBS, checkSave, introText, shuffledCredits, matchPlace, reelPictures } from '../src/engine/game.js';
import { inside } from '../src/engine/geometry.js';
import { existsSync, readFileSync } from 'node:fs';
import { settle, exploreTorch } from './helpers.mjs';
@@ -144,6 +144,20 @@ test('le fasi del giorno seguono la storia: pomeriggio, poi tramonto, poi sera,
assert.deepEqual(seen, ids);
});
test('il carosello dei Ringraziamenti: ogni sfondo una volta sola col nome del posto, tutti i ritratti, mai due ritratti vicini', () => {
const reel = reelPictures(story);
const backgrounds = new Set(Object.values(story.scenes).map(s => s.background).filter(Boolean));
assert.deepEqual(reel.filter(p => p.kind === 'scene').map(p => p.src).sort(), [...backgrounds].sort(), 'tutti gli sfondi, senza doppioni');
for (const p of reel) { assert.ok(p.name, `${p.src} senza nome`); assert.ok(existsSync(new URL('../' + p.src, import.meta.url)), `manca ${p.src}`); }
const faces = story.credits.flatMap(c => c.names).filter(n => n.face);
assert.deepEqual(reel.filter(p => p.kind !== 'scene').map(p => p.src).sort(), faces.map(n => n.face).sort(), 'tutti i ritratti, una volta');
assert.ok(reel.some(p => p.kind === 'palio'), 'il palio ha la sua cornice');
reel.forEach((p, i) => { if (i && p.kind !== 'scene') assert.equal(reel[i - 1].kind, 'scene', `due ritratti di fila: ${reel[i - 1].name}, ${p.name}`); });
// Poche scene: i ritratti che non trovano posto chiudono comunque la fila.
const few = reelPictures({ scenes: { a: { name: 'A', background: 'x.png' } }, credits: story.credits });
assert.equal(few.length, 1 + faces.length);
});
test('la ricerca della mappa: pezzi di parola, senza maiuscole, accenti e apostrofi', () => {
assert.ok(matchPlace('Campomigliaio', 'camp'));
assert.ok(matchPlace('Via de\' Medici', 'de medici'));