// Browser smoke test for Pipe Puzzle. // node tools/smokePipePuzzle.cjs [baseURL] // // Loads the real site (Phaser from CDN), starts PipePuzzleGame, plays Easy by // rotating every tile to its solution orientation (synchronous), then lets // the game loop run (page.waitForTimeout from Node) so the win wave and // overlay fire. Exits non-zero on failure. const { chromium } = require('/home/brianfertig/.npm/_npx/e41f203b7505f1fb/node_modules/playwright'); const BASE = process.argv[2] || 'http://localhost:8123'; (async () => { const browser = await chromium.launch({ headless: true, executablePath: '/home/brianfertig/.cache/ms-playwright/chromium-1208/chrome-linux64/chrome', args: ['--no-sandbox', '--disable-gpu'], }); const page = await browser.newPage({ viewport: { width: 1280, height: 720 } }); const errors = []; page.on('pageerror', (e) => errors.push('pageerror: ' + e.message)); page.on('console', (m) => { if (m.type() === 'error') errors.push('console: ' + m.text()); }); await page.goto(BASE + '/', { waitUntil: 'load', timeout: 30000 }); await page.waitForFunction( () => window.game && (window.game.isRunning === true || window.game.isRunning === 'running'), { timeout: 25000 }, ); await page.waitForTimeout(600); // Start the game scene and jump to the Easy board. await page.evaluate(() => { window.game.scene.start('PipePuzzleGame', {}); }); await page.waitForTimeout(500); await page.evaluate(() => { window.game.scene.getScene('PipePuzzleGame')._startGame('easy'); }); await page.waitForTimeout(400); const st1 = await page.evaluate(() => { const sc = window.game.scene.getScene('PipePuzzleGame'); return { screen: sc._screen, n: sc._board && sc._board.n }; }); console.log('board ready:', st1); if (st1.screen !== 'play') { console.error('FAIL: not on play screen'); await browser.close(); process.exit(1); } // Rotate every *rotatable* tile to its solution orientation (all synchronous, // no await). The faucet & drain are fixed anchors — skip them. const rot = await page.evaluate(() => { const sc = window.game.scene.getScene('PipePuzzleGame'); const { n, solution, source, drain } = sc._board; const N = 1, E = 2, S = 4, W = 8; const rot1 = (s) => ((s & N ? E : 0) | (s & E ? S : 0) | (s & S ? W : 0) | (s & W ? N : 0)); let clicks = 0; for (let i = 0; i < n * n; i++) { if (i === source || i === drain) continue; // anchors are fixed let cur = sc._board.sockets[i], k = 0; while (cur !== solution[i] && k < 4) { cur = rot1(cur); k++; } for (let c = 0; c < k; c++) { sc._rotateCell(i); clicks++; } } return { clicks, won: sc._won }; }); console.log('rotations applied:', rot); // Let the game loop run so the win wave + overlay fire. (Headless browsers // throttle requestAnimationFrame, which starves the scene clock's // delayedCalls — in a normal browser the overlay appears on its own.) await page.waitForTimeout(4000); const final = await page.evaluate(() => { const sc = window.game.scene.getScene('PipePuzzleGame'); let overlay = null; const find = (o) => { if (!o) return; if (o.text && typeof o.text === 'string' && /WATER FLOWING/i.test(o.text)) overlay = o.text; if (o.text && typeof o.text === 'object' && o.text.text && /WATER FLOWING/i.test(o.text.text)) overlay = o.text.text; if (o.list) o.list.forEach(find); }; if (sc._screenContainer) find(sc._screenContainer); // If the RAF-throttled clock never fired the win sequence, trigger the // same code path directly and re-check (this exercises _finishWin, // best-time persistence and the overlay rendering). let overlayForced = false; if (!overlay) { try { sc._finishWin(); sc._showWinOverlay(20, 3, true); overlayForced = true; const find2 = (o) => { if (!o) return; if (o.text && typeof o.text === 'string' && /WATER FLOWING/i.test(o.text)) overlay = o.text; if (o.list) o.list.forEach(find2); }; if (sc._screenContainer) find2(sc._screenContainer); } catch (_) { /* reported below */ } } return { won: sc._won, moves: sc._moves, overlay, overlayForced, best: localStorage.getItem('pipepuzzle-best-easy'), diff: sc._diff, }; }); console.log('final:', final); const relErrs = errors.filter((e) => !/favicon|404|net::ERR|ERR_NAME|Failed to load resource/.test(e)); if (relErrs.length) { console.error('JS errors:'); relErrs.forEach((e) => console.error(' ' + e)); } const ok = final.won === true && final.overlay !== null && final.best !== null && relErrs.length === 0; console.log(ok ? 'PIPE PUZZLE SMOKE: PASS' : 'PIPE PUZZLE SMOKE: FAIL'); await browser.close(); process.exit(ok ? 0 : 1); })().catch((e) => { console.error(e); process.exit(1); });