commit 3e2661dffc75de2d933851692e9ef37d3d6bc71d
parent c7565b6e5dd82825c507d06ce4ab321c58089855
Author: Kyle Barlow <kb@kylebarlow.com>
Date: Mon, 13 Jul 2026 19:19:19 -0700
tests: fix 5 flaky/broken browser tests (test bugs, not game bugs)
All five were failing due to defects in the tests themselves:
- work name/speed rounds: 0-delay page.keyboard.type() triggers OS-style
auto-repeat in headless Chromium (overlapping key-holds), garbling the
typed buffer ("ABC" -> "AABCCC"). Type with a per-key delay.
- endgame color check: the game runs Phaser's Canvas (2D) renderer in
headless Chromium, so the WebGL-only pixel read always saw 0 colors. Add
the same 2D getImageData fallback the "city colors" test already uses.
- truck starts moving: compared two late samples that both landed in the
same stationary 'waiting' state (false zero delta at 8x test speed).
Assert cumulative debugDistancePx from spawn instead.
- no-input-waiting / resumes-driving: positioned the truck at a hardcoded
segmentEnd.x-260, which assumes a horizontal approach and sits beyond the
216px stop line. Place it just short of segmentEnd along the real approach
vector so it works for any segment orientation.
npm test: 36 passed, 0 failed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Diffstat:
1 file changed, 30 insertions(+), 21 deletions(-)
diff --git a/tests/browser/runner.js b/tests/browser/runner.js
@@ -213,7 +213,9 @@ async function main() {
await page.waitForTimeout(1000);
await page.evaluate(() => window.__WORK_SCENE__.enterMode('name'));
await page.waitForFunction(() => window.__WORK_SCENE__.acceptInput === true, null, { timeout: 5000 });
- await page.keyboard.type('ABC');
+ // delay so key-holds don't overlap; 0-delay typing triggers OS-style
+ // auto-repeat in Chromium and garbles the buffer (e.g. "AABCCC").
+ await page.keyboard.type('ABC', { delay: 40 });
await page.keyboard.press('Enter');
await page.waitForTimeout(200);
const name = await page.evaluate(() => window.__WORK_SCENE__.playerName);
@@ -230,7 +232,7 @@ async function main() {
s._nameRace();
});
await page.waitForFunction(() => window.__WORK_SCENE__.acceptInput === true, null, { timeout: 5000 });
- await page.keyboard.type('Sam');
+ await page.keyboard.type('Sam', { delay: 40 });
await page.waitForFunction(() => window.__WORK_SCENE__.nameState === 'result', null, { timeout: 5000 });
const t = await page.evaluate(() => window.__WORK_SCENE__.lastTimeMs);
if (!(t >= 0)) throw new Error(`Expected a finite lastTimeMs, got ${t}`);
@@ -270,27 +272,19 @@ async function main() {
await test('fire-truck: truck starts moving automatically', async (page, url) => {
await page.goto(`${url}/games/fire-truck/`, { waitUntil: 'domcontentloaded' });
- await page.waitForTimeout(600);
- const start = await page.evaluate(() => ({
- x: window.__FT_SCENE__.truckX,
- y: window.__FT_SCENE__.truckY,
- distance: window.__FT_SCENE__.debugDistancePx || 0,
- prompt: window.__FT_SCENE__.promptDir,
- state: window.__FT_SCENE__.state,
- }));
+ // debugDistancePx is cumulative from spawn. Whether the truck is still
+ // driving or has already reached its first intersection (waiting), it can
+ // only have got there by auto-driving — so a nonzero total proves motion.
+ // (Comparing two late samples is racy: at 8x test speed both can land in
+ // the same stationary 'waiting' state, giving a false zero delta.)
await page.waitForTimeout(3000);
const end = await page.evaluate(() => ({
- x: window.__FT_SCENE__.truckX,
- y: window.__FT_SCENE__.truckY,
distance: window.__FT_SCENE__.debugDistancePx || 0,
prompt: window.__FT_SCENE__.promptDir,
state: window.__FT_SCENE__.state,
}));
- const moved = Math.hypot(end.x - start.x, end.y - start.y);
- const distanceMoved = end.distance - start.distance;
- const reachedWaiting = start.state === 'driving' && end.state === 'waiting';
- if (moved < 20 && distanceMoved < 120 && !reachedWaiting) {
- throw new Error(`Truck moved only ${moved.toFixed(1)} pixels and advanced ${distanceMoved.toFixed(1)} path pixels (start ${start.state}/${start.prompt || 'none'}, end ${end.state}/${end.prompt || 'none'})`);
+ if (end.distance < 120) {
+ throw new Error(`Truck advanced only ${end.distance.toFixed(1)} path pixels from spawn (state ${end.state}/${end.prompt || 'none'})`);
}
});
@@ -359,10 +353,15 @@ async function main() {
await page.evaluate(() => { window.__FT_SCENE__.truck.setPosition(window.__FT_SCENE__.segmentEnd.x - 500, window.__FT_SCENE__.segmentEnd.y); });
await page.waitForFunction(() => window.__FT_SCENE__ && !!window.__FT_SCENE__.promptDir, null, { timeout: 25000 });
await page.evaluate(() => { window.__FT_SCENE__.targetSpeed = 0; window.__FT_SCENE__.speed = 0; window.__FT_SCENE__.lastStepTime = performance.now(); });
- // Move truck right up to the stop line without resolving
+ // Move truck up to the stop line without resolving. Place it just short of
+ // segmentEnd *along the real approach vector* (the segment may run in any
+ // direction) so it lands inside STOP_LINE_DIST regardless of orientation.
await page.evaluate(() => {
const s = window.__FT_SCENE__;
- s.truck.setPosition(s.segmentEnd.x - 260, s.segmentEnd.y);
+ const dx = s.segmentEnd.x - s.truck.x, dy = s.segmentEnd.y - s.truck.y;
+ const d = Math.hypot(dx, dy) || 1;
+ const short = 30; // well inside STOP_LINE_DIST (~216px)
+ s.truck.setPosition(s.segmentEnd.x - (dx / d) * short, s.segmentEnd.y - (dy / d) * short);
s.step(0.016);
});
const state = await page.evaluate(() => window.__FT_SCENE__.state);
@@ -376,10 +375,14 @@ async function main() {
await page.evaluate(() => { window.__FT_SCENE__.targetSpeed = 0; window.__FT_SCENE__.speed = 0; window.__FT_SCENE__.lastStepTime = performance.now(); });
const promptDir = await page.evaluate(() => window.__FT_SCENE__.promptDir);
const correctKey = promptDir === 'left' ? 'ArrowLeft' : promptDir === 'right' ? 'ArrowRight' : 'ArrowUp';
- // Put into waiting
+ // Put into waiting: place just short of segmentEnd along the real approach
+ // vector so it reaches the stop line whatever direction the segment runs.
await page.evaluate(() => {
const s = window.__FT_SCENE__;
- s.truck.setPosition(s.segmentEnd.x - 260, s.segmentEnd.y);
+ const dx = s.segmentEnd.x - s.truck.x, dy = s.segmentEnd.y - s.truck.y;
+ const d = Math.hypot(dx, dy) || 1;
+ const short = 30;
+ s.truck.setPosition(s.segmentEnd.x - (dx / d) * short, s.segmentEnd.y - (dy / d) * short);
s.step(0.016);
});
await page.keyboard.press(correctKey);
@@ -495,6 +498,12 @@ async function main() {
const pixels = new Uint8Array(sampleW * sampleH * 4);
gl.readPixels(0, 0, sampleW, sampleH, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
for (let i = 0; i < pixels.length; i += 16) seen.add(`${pixels[i]},${pixels[i+1]},${pixels[i+2]}`);
+ } else {
+ // Phaser falls back to the Canvas (2D) renderer in headless Chromium.
+ const ctx = cv.getContext('2d');
+ const w = Math.min(cv.width, sampleW), h = Math.min(cv.height, sampleH);
+ const data = ctx.getImageData(0, 0, w, h).data;
+ for (let i = 0; i < data.length; i += 16) seen.add(`${data[i]},${data[i+1]},${data[i+2]}`);
}
return seen.size;
});