kgames

KGames — free keyboard games for kids
git clone https://www.keyboard.games/code/kgames.git
Log | Files | Refs | README | LICENSE

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:
Mtests/browser/runner.js | 51++++++++++++++++++++++++++++++---------------------
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; });