137 lines
6.5 KiB
JavaScript
137 lines
6.5 KiB
JavaScript
// Regression test — Pipe Puzzle: a tile's rotation animation must keep running
|
|
// after the pointer leaves the tile (mouse-out) mid-spin.
|
|
// node tools/regressionPipePuzzleUnhover.cjs [baseURL]
|
|
//
|
|
// Background: Phaser 3.90's `tweens.killTweensOf(target, 'prop')` no longer
|
|
// supports the prop filter — the argument is silently ignored and EVERY tween
|
|
// on the target is destroyed. PipePuzzleGame used it as
|
|
// `killTweensOf(tile, 'scaleX')` in the pointerout handler, which killed the
|
|
// in-flight rotation tween and froze the tile at a half-rotated angle.
|
|
//
|
|
// This test drives the same code path the pointerout event hits
|
|
// (`_setHover(i, false)`) while a rotation tween is live, and asserts:
|
|
// 1. the rotation tween SURVIVES the un-hover and completes to target;
|
|
// 2. the hover scale tweens are the only ones replaced;
|
|
// 3. mirror case: clicking/re-hovering mid-spin also must not kill the
|
|
// in-flight scale tween.
|
|
//
|
|
// Completion is verified by manually ticking the tween manager
|
|
// (`tweens.tick()`), which is deterministic even when headless RAF is
|
|
// throttled. 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 r = await page.evaluate(() => {
|
|
const rel = (a) => ((a % 360) + 360) % 360;
|
|
const sc = window.game.scene.getScene('PipePuzzleGame');
|
|
const { source, drain } = sc._board;
|
|
let i = 0;
|
|
while (i === source || i === drain) i++;
|
|
const img = sc._cells[i];
|
|
const tweensOf = () => sc.tweens.getTweensOf(img);
|
|
const hasKey = (key) => tweensOf().some((t) => t.data && t.data.some((d) => d.key === key));
|
|
// Advance every live tween on this tile to completion, deterministically.
|
|
// (Driven via Tween.update() so the check works even when the headless
|
|
// RAF clock is throttled to a crawl. `update` returns true once the
|
|
// tween has finished; the manager reaps it on its next step.)
|
|
const settle = (maxSteps = 200) => {
|
|
for (let g = 0; g < maxSteps; g++) {
|
|
let anyRunning = false;
|
|
for (const tw of sc.tweens.getTweensOf(img)) {
|
|
if (tw.isDestroyed()) continue;
|
|
if (!tw.update(16)) anyRunning = true; // advanced, still running
|
|
}
|
|
if (!anyRunning) break;
|
|
}
|
|
};
|
|
|
|
// ── Case 1: rotate, then mouse OUT mid-animation (the reported bug) ──
|
|
sc._setHover(i, true); // pointerover
|
|
const hoverTween = tweensOf().at(-1); // the hover scale tween
|
|
const moves = sc._moves;
|
|
sc._rotateCell(i); // pointerdown → 240ms spin starts
|
|
const target = img._targetAngle;
|
|
const spinAtStart = tweensOf().find((t) => t.data && t.data.some((d) => d.key === 'angle'));
|
|
sc._setHover(i, false); // pointerout while spin is live
|
|
|
|
const spinSurvivesUnhover = tweensOf().some((t) => t === spinAtStart && !t.isDestroyed());
|
|
const hoverTweenKilled = hoverTween.isDestroyed();
|
|
const restoreTweenPresent = hasKey('scaleX');
|
|
|
|
settle();
|
|
const completedAfterUnhover = rel(img.angle) === rel(target);
|
|
|
|
// ── Case 2 (mirror): rotate, then mouse IN again mid-animation ──
|
|
sc._rotateCell(i); // pre-fix this killed EVERY tween (incl. scale)
|
|
const scaleAtStart = tweensOf().find((t) => t.data && t.data.some((d) => d.key === 'scaleX'));
|
|
sc._setHover(i, true); // pointerover again mid-spin
|
|
|
|
const spinSurvivesRehover = tweensOf().some((t) => t.data && t.data.some((d) => d.key === 'angle') && !t.isDestroyed());
|
|
const scaleTweenReplaced = scaleAtStart != null && scaleAtStart.isDestroyed() && hasKey('scaleX');
|
|
|
|
settle();
|
|
const completedAfterRehover = rel(img.angle) === rel(img._targetAngle);
|
|
|
|
// ── Case 3: game state must have advanced cleanly ──
|
|
const stateAdvanced =
|
|
sc._moves === moves + 2 &&
|
|
!sc._won &&
|
|
sc._board.sockets.every((s) => Number.isInteger(s));
|
|
|
|
return {
|
|
spinSurvivesUnhover, hoverTweenKilled, restoreTweenPresent, completedAfterUnhover,
|
|
spinSurvivesRehover, scaleTweenReplaced, completedAfterRehover, stateAdvanced,
|
|
};
|
|
});
|
|
|
|
console.log('tween survival/kill state:', r);
|
|
|
|
const checks = [
|
|
['un-hover does NOT kill the in-flight rotation tween', r.spinSurvivesUnhover],
|
|
['un-hover DOES kill the old hover-scale tween', r.hoverTweenKilled],
|
|
['un-hover still starts the scale-restore tween', r.restoreTweenPresent],
|
|
['rotation COMPLETES to target angle after un-hover', r.completedAfterUnhover],
|
|
['re-hover does NOT kill the in-flight rotation tween', r.spinSurvivesRehover],
|
|
['re-hover replaces the previous scale tween', r.scaleTweenReplaced],
|
|
['rotation COMPLETES to target angle after re-hover', r.completedAfterRehover],
|
|
['game state advanced cleanly (2 moves, not won)', r.stateAdvanced],
|
|
];
|
|
let failures = 0;
|
|
for (const [name, ok] of checks) {
|
|
console.log(`${ok ? ' ok ' : 'FAIL '}${name}`);
|
|
if (!ok) failures++;
|
|
}
|
|
|
|
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)); failures++; }
|
|
|
|
await browser.close();
|
|
console.log(failures === 0 ? 'PIPE PUZZLE UNHOVER REGRESSION: PASS' : 'PIPE PUZZLE UNHOVER REGRESSION: FAIL');
|
|
process.exit(failures === 0 ? 0 : 1);
|
|
})().catch((e) => { console.error(e); process.exit(1); });
|