Wait for the dragged body before checking the hold in e2e 41 (#282)
game-maker / gate (push) Successful in 3m16s
game-maker / suite (push) Failing after 14m6s

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 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-09-29 22:20:00 +02:00
1 parent ffedd81803
commit 9cff5282b6
1 file changed
+43 -16
+43 -16
View File
@@ -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 mouse('mouseMoved', from.x, from.y + ((to.y - from.y) * i) / 10, 'left', { buttons: 1 });
await sleep(16); await sleep(16);
} }
const target = REST - LIFT;
const crateY = "__gm.world()?.entities.find((e) => e.objectId === 'phys-crate')?.worldY"; const crateY = "__gm.world()?.entities.find((e) => e.objectId === 'phys-crate')?.worldY";
const sampleCrate = async (ms) => { // The last move reaches the body a few frames later: preview, physics step, drawn one step
const ys = []; // behind. Reading right away raced it and caught the ninth move's height (#282).
const start = Date.now(); const arrived = await waitUntil(`Math.abs(${crateY} - ${target}) < 1.5`, {
while (Date.now() - start < ms) { timeout: 2000,
ys.push(await evaluate(crateY)); label: 'the crate reached the pointer',
await sleep(50); }).then(
} () => true,
return ys; () => 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);
}; };
const held = await sampleCrate(1000); window.__dragSamplesDone = false;
requestAnimationFrame(sample);
})()`);
await sleep(1000);
await shot('41-drag-while-playing'); await shot('41-drag-while-playing');
const dragInfo = { grab, selected: (await state()).selection?.objectId }; const dragInfo = { grab, selected: (await state()).selection?.objectId };
const holdEnd = await evaluate('performance.now()');
await mouse('mouseReleased', to.x, to.y, 'left'); 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 ?? []; const log = (await json('__gm.world()'))?.log ?? [];
await stop(); 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( check(
'a body dragged while playing hangs where the pointer holds it and is let go at rest', '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) && held.every((y) => Math.abs(y - target) < 1.5) &&
Math.abs(released[0] - target) < 3 && Math.abs(released[0]?.y - target) < 3 &&
released.at(-1) > target + 1 && fell > target + 1 &&
released.at(-1) < target + 40 && fell < target + 40 &&
!log.some((line) => line.includes('moved directly')), !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); const bodies = [crateBody?.id, ballBody?.id].filter(Boolean);