Repository navigation
Breakout Task keeps running and can report completion twice after it finishes #1641
Description
The Breakout Task keeps running after it reports completion. runTask never stops the requestAnimationFrame loop and never removes its keydown, keyup and mousemove listeners from document, so the game keeps redrawing and responding to input after it calls onComplete. Production mostly hides this, because ScalarInstrumentRenderer replaces the iframe with the summary once onSubmit resolves, and that destroys the iframe's document. Until then, or for good if the submit fails, the game is still live. After a loss, the next frame takes another life, so the canvas shows Lives: -1 and serves the ball again. After a win, the ball keeps bouncing among the empty brick rows. If it then gets past the paddle three times, onComplete fires a second time with livesRemaining: 0, and InteractiveContent submits each done event it receives.
Where
packages/instrument-library/src/interactive/DNP_BREAKOUT_TASK/index.ts:
:111-116and:188-192:onComplete(...)is called, anddrawcarries on.:211:requestAnimationFrame(draw)is scheduled unconditionally at the end of every frame.:214-216: the listeners are added and never removed.
Reproduce
- Start the Breakout Task and let the ball past the paddle three times.
- Watch the canvas after
donehas fired, before the summary replaces it. If you run the instrument outside the renderer, keep watching indefinitely.
Actual: the ball is served again and the lives counter goes to Lives: -1. The arrow keys and mouse still move the paddle.
Expected: the game freezes on the final frame once it reports completion, and done is called exactly once.
Tests
packages/instrument-library/src/__tests__/DNP_BREAKOUT_TASK.test.ts. Its afterEach removes every listener recorded through addDocumentListener, which works around the leak. When fixing:
- add a test asserting that no further frame is requested after
doneis called (nextFramestaysundefined); - add a test asserting that a key press after completion does not move the paddle;
- once the source removes its own listeners, drop the cleanup loop from
afterEach.
Suggested fix
Have a finished flag, or a single complete(data) helper, that removes the three listeners, stops scheduling requestAnimationFrame (or calls cancelAnimationFrame), and then calls onComplete once.
Found while raising unit test coverage in #1632.
Activity