From 9cff5282b618c1072ab037cb1910233d561edfc6 Mon Sep 17 00:00:00 2001 From: MelekTaus Date: Tue, 29 Sep 2026 22:20:00 +0200 Subject: [PATCH] Wait for the dragged body before checking the hold in e2e 41 (#282) The drag check in e2e 41 was red about every other run under WSL and twice on stein2. The diagnosis in the issue was wrong: "held" were not two samples but the first and the last one, and -3475 is the height of the ninth of ten moves. The last move reaches the body a few frames later (preview, physics step, drawn one step behind). Reading right after the move sometimes beat it, and slower frames under WSL made that likely. - The check first waits until the crate is at the pointer, for at most 2 s. If it never gets there, the check fails with "arrived: false" instead of stopping the run. - The page then records the crate's height in every frame and the check fetches the samples once. The count follows the frames, not the CDP round trips, and the check asks for at least 30 held frames. - Held and released samples are split by page timestamps taken right before and after the mouse release. "Fell" is the last sample within 200 ms of letting go. Tests: - e2e 41 on melektaus-atx (WSL): 6 of 6 runs ALL PASSED, 21 checks, 59 to 64 held frames. - Gate on melektaus-atx: 195 files, 2827 tests, CHECK EXIT 0. - Counter-check: without passing the hold to the world the crate stays on the ground and the check fails. - Not caught by this check: a body let go without zeroing its speed. After a second of holding the speed is 0 anyway. The unit test "grab too short for a physics step" covers it, as before. Co-Authored-By: Claude Opus 5.5 --- tools/e2e/41-physics.mjs | 61 +++++++++++++++++++++++++++++----------- 1 file changed, 44 insertions(+), 17 deletions(-) diff --git a/tools/e2e/41-physics.mjs b/tools/e2e/41-physics.mjs index 42eafbc8..a83b0442 100644 --- a/tools/e2e/41-physics.mjs +++ b/tools/e2e/41-physics.mjs @@ -290,33 +290,60 @@ export async function physics(t) { await mouse('mouseMoved', from.x, from.y + ((to.y - from.y) * i) / 10, 'left', { buttons: 1 }); await sleep(16); } + const target = REST - LIFT; const crateY = "__gm.world()?.entities.find((e) => e.objectId === 'phys-crate')?.worldY"; - const sampleCrate = async (ms) => { - const ys = []; - const start = Date.now(); - while (Date.now() - start < ms) { - ys.push(await evaluate(crateY)); - await sleep(50); - } - return ys; - }; - const held = await sampleCrate(1000); + // The last move reaches the body a few frames later: preview, physics step, drawn one step + // behind. Reading right away raced it and caught the ninth move's height (#282). + const arrived = await waitUntil(`Math.abs(${crateY} - ${target}) < 1.5`, { + timeout: 2000, + label: 'the crate reached the pointer', + }).then( + () => true, + () => false, + ); + // Every frame recorded in the page, so the count follows the frames and not the round trips. + await evaluate(`(() => { + const out = (window.__dragSamples = []); + const sample = () => { + out.push({ t: performance.now(), y: ${crateY} }); + if (!window.__dragSamplesDone) requestAnimationFrame(sample); + }; + window.__dragSamplesDone = false; + requestAnimationFrame(sample); + })()`); + await sleep(1000); await shot('41-drag-while-playing'); const dragInfo = { grab, selected: (await state()).selection?.objectId }; + const holdEnd = await evaluate('performance.now()'); await mouse('mouseReleased', to.x, to.y, 'left'); - const released = await sampleCrate(150); + const letGo = await evaluate('performance.now()'); + await sleep(300); + await evaluate('window.__dragSamplesDone = true'); + const dragSamples = await json('window.__dragSamples'); const log = (await json('__gm.world()'))?.log ?? []; await stop(); - const target = REST - LIFT; + const held = dragSamples.filter((s) => s.t < holdEnd).map((s) => s.y); + const released = dragSamples.filter((s) => s.t >= letGo); + // 200 ms of free fall from rest is under 20 below the hold, a body let go at speed is not. + const fell = released.filter((s) => s.t - letGo <= 200).at(-1)?.y; check( 'a body dragged while playing hangs where the pointer holds it and is let go at rest', - held.length > 10 && + arrived && + held.length >= 30 && held.every((y) => Math.abs(y - target) < 1.5) && - Math.abs(released[0] - target) < 3 && - released.at(-1) > target + 1 && - released.at(-1) < target + 40 && + Math.abs(released[0]?.y - target) < 3 && + fell > target + 1 && + fell < target + 40 && !log.some((line) => line.includes('moved directly')), - JSON.stringify({ target, held: [held[0], held.at(-1)], released, log, dragInfo }), + JSON.stringify({ + target, + arrived, + held: { frames: held.length, first: held[0], last: held.at(-1) }, + released: released.slice(0, 3).map((s) => s.y), + fell, + log, + dragInfo, + }), ); const bodies = [crateBody?.id, ballBody?.id].filter(Boolean);