diff --git a/__jig_init_test.html b/__jig_init_test.html index 7db8b7d..59918c0 100644 --- a/__jig_init_test.html +++ b/__jig_init_test.html @@ -9,6 +9,8 @@ import * as Phaser from 'phaser'; import JigsawGame from './src/games/jigsaw/JigsawGame.js'; const log = (m) => { document.getElementById('log').textContent += m + '\n'; console.log('[t]', m); }; +let failures = 0; +const check = (ok, msg) => { if (!ok) { failures++; log('FAIL: ' + msg); } else log('ok: ' + msg); }; const wait = (ms) => new Promise((r) => setTimeout(r, ms)); const ART = [ @@ -37,24 +39,45 @@ const s = () => game.scene.getScene('jigsaw-game'); for (let i = 0; i < 400 && !s(); i++) await wait(50); // boot → jigsaw start is async if (!s()) throw new Error('jigsaw scene never started'); for (let i = 0; i < 400 && !s().menu; i++) await wait(50); - if (!s().menu) throw new Error('menu never built'); - // The preview image is loaded asynchronously; give it a moment. - for (let i = 0; i < 200 && !s().previewImg; i++) await wait(25); + check(!!s().menu, 'menu built'); - const sc = s(); - const item = sc.currentImage(); - document.title = 'RESULT:' + JSON.stringify({ - idx: sc.imageIndex, - n: sc.artwork.length, - name: item && item.name, - validName: ART.some((a) => a.name === item.name), - preview: !!sc.previewImg, - menuVisible: sc.menu.visible, - }); - log('initial image: ' + item.name + ' (index ' + sc.imageIndex + ' of ' + sc.artwork.length + ')'); + // Menu is back to the original shape (random-start button reverted). + check(s().randomStartButton === undefined, 'randomStartButton is gone (menu as before)'); + + // The initial image is random but always a valid artwork entry, and the + // preview shown in the menu matches it. + const n = s().artwork.length; + const i = s().imageIndex; + check(Number.isInteger(i) && i >= 0 && i < n, `initial imageIndex ${i} is a valid index (0..${n - 1})`); + check(!!ART.find((a) => a.name === s().currentImage().name), 'initial currentImage() is a known artwork entry'); + + // Preview must be built for the initial image (async image load). + for (let k = 0; k < 200 && !s().previewImg; k++) await wait(25); + check(!!s().previewImg, 'initial preview image is displayed'); + check(s().thumbName && s().thumbName.text === s().currentImage().name, `thumb name matches initial image (${s().currentImage().name})`); + + // Start Puzzle must still start the initial (random) image. + s().selectedDiff = 'easy'; + s().startPuzzle(); + let playing = false; + for (let k = 0; k < 400 && !playing; k++) { playing = s().state === 'playing'; await wait(25); } + check(playing, 'Start Puzzle reaches playing state'); + check(s().pieces.length === 25, '25 pieces built'); + check(s().imageName === ART[i].name, `playing image matches the initial pick (${ART[i].name})`); + + // The original 🎲 Random button still randomises the preview in the menu. + s().toMenu(); + const before = s().imageIndex; + const seen = new Set([before]); + for (let k = 0; k < 10; k++) { s().randomImage(); seen.add(s().imageIndex); await wait(10); } + check(seen.size >= 2, '🎲 Random button still changes the selected image'); + + document.__initIndex = i; + log(failures === 0 ? 'ALL PASS' : failures + ' FAILURES'); + document.title = failures === 0 ? 'PASS' : 'FAIL:' + failures; })().catch((e) => { log('ERROR: ' + ((e && e.stack) || e)); - document.title = 'FAIL:' + ((e && e.message) || e); + document.title = 'FAIL:error'; }); diff --git a/tools/__jig_init_driver.mjs b/tools/__jig_init_driver.mjs index 55a5ec2..5396505 100644 --- a/tools/__jig_init_driver.mjs +++ b/tools/__jig_init_driver.mjs @@ -1,11 +1,12 @@ -// Headless-chromium CDP driver: loads a page repeatedly and reads the result -// the page publishes in document.title (RESULT: or FAIL:). +// Headless-chromium CDP driver for the jigsaw initial-image test page. +// Loads the page repeatedly; each load must end PASS, and the initial image +// index (document.__initIndex) must vary across fresh loads. // Usage: node tools/__jig_init_driver.mjs [loads=8] import { spawn } from 'node:child_process'; import { setTimeout as sleep } from 'node:timers/promises'; const url = process.argv[2]; -const loads = Math.max(1, parseInt(process.argv[3] || '8', 10)); +const loads = Math.max(2, parseInt(process.argv[3] || '8', 10)); if (!url) { console.error('usage: driver [loads]'); process.exit(2); } const BIN = process.env.HOME + '/.cache/ms-playwright/chromium_headless_shell-1234/chrome-headless-shell-linux64/chrome-headless-shell'; @@ -15,7 +16,7 @@ const chrome = spawn(BIN, [ `--remote-debugging-port=${PORT}`, 'about:blank', ], { stdio: ['ignore', 'pipe', 'pipe'] }); -chrome.stderr.on('data', (d) => { const t = d.toString(); if (!/Fontconfig|dbus|DBus|ozone|sandbox|WebGL|GL Driver/i.test(t)) process.stderr.write(t); }); +chrome.stderr.on('data', (d) => { const s = d.toString(); if (!/Fontconfig|dbus|DBus|ozone/i.test(s)) process.stderr.write(s); }); async function httpJson(path) { const r = await fetch(`http://127.0.0.1:${PORT}${path}`); @@ -52,36 +53,28 @@ try { await send('Runtime.enable'); await send('Page.enable'); - const results = []; + const ev = (expression) => send('Runtime.evaluate', { expression, returnByValue: true }).then((r) => r.result.value); + + const indexes = []; for (let k = 0; k < loads; k++) { await send('Page.navigate', { url }); const t0 = Date.now(); let title = ''; - while (Date.now() - t0 < 30000) { - const out = await send('Runtime.evaluate', { expression: 'document.title', returnByValue: true }); - title = out.result.value || ''; - if (title.startsWith('RESULT:') || title.startsWith('FAIL')) break; - await sleep(250); + while (Date.now() - t0 < 60000) { + title = (await ev('document.title')) || ''; + if (title === 'PASS' || title.startsWith('FAIL')) break; + await sleep(500); } - results.push(title); + if (title !== 'PASS') throw new Error(`load #${k}: ${title || 'TIMEOUT'}`); + indexes.push(await ev('document.__initIndex')); } - let ok = true; - const idxs = []; - for (const t of results) { - if (!t.startsWith('RESULT:')) { ok = false; console.error('bad title: ' + t); continue; } - const r = JSON.parse(t.slice('RESULT:'.length)); - const valid = Number.isInteger(r.idx) && r.idx >= 0 && r.idx < r.n && r.n > 1 && - r.validName && r.preview && r.menuVisible; - if (!valid) { ok = false; console.error('bad result: ' + JSON.stringify(r)); } - idxs.push(r.idx); - } - const distinct = new Set(idxs).size; - console.log(`initial indices over ${loads} fresh page loads: ${idxs.join(', ')}`); - console.log(`distinct initial images: ${distinct}/${loads}`); - if (distinct < 2) { ok = false; console.error('expected variety in the initial image'); } - console.log(ok ? 'ALL PASS' : 'FAIL'); - process.exitCode = ok ? 0 : 1; + const valid = indexes.every((i) => Number.isInteger(i) && i >= 0 && i < 5); + const distinct = new Set(indexes).size; + console.log('initial indexes across fresh loads:', indexes.join(', ')); + console.log(`all valid: ${valid}, distinct images: ${distinct}/${loads}`); + if (!valid || distinct < 2) { console.log('FAIL'); process.exitCode = 1; } + else console.log('ALL PASS'); } finally { try { ws && ws.close(); } catch {} chrome.kill('SIGKILL');