test(rsvp): stop RSVPController tests leaking real timers into teardown (#4355)

rsvp-controller.test.ts calls controller.start() in ~20 tests but never stops
the controller. start() schedules a countdown (setInterval, 500ms x3) that then
schedules the recurring word-advance (setTimeout). On the real clock those fire
~1.5s later — after the test file's jsdom env is torn down — and
emitStateChange's dispatchEvent(new CustomEvent(...)) runs against a stale realm
and throws. Vitest reports it as an unhandled error and fails the whole run
intermittently on CI:

  TypeError: Failed to execute 'dispatchEvent' on 'EventTarget': parameter 1 is
  not of type 'Event'.
   at RSVPController.advanceToNextWord -> emitStateChange (Timeout._onTimeout)

Fake only the timer functions (setTimeout/setInterval + their clears) for this
suite via vi.useFakeTimers({ toFake: [...] }); useRealTimers in afterEach
discards any still-pending fakes. The tests assert synchronously and never
advance playback, so faking the timers changes no assertion; Date/performance
stay real so CFI/position checks are unaffected. Test-only; production behaviour
is unchanged.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Huang Xin
2026-05-29 16:51:00 +08:00
committed by GitHub
parent 7bdd3ecdee
commit 89723b421e
@@ -48,6 +48,23 @@ function createMockView(primaryIndex: number, docs: Document[]): FoliateView {
}
describe('RSVPController', () => {
// start() schedules a countdown (setInterval) which then schedules the
// recurring word-advance (setTimeout). These tests assert synchronously and
// never stop the controller, so on the real clock those timers fire ~1.5s
// later — after the test file's jsdom env has been torn down — and throw an
// unhandled error from emitStateChange's dispatchEvent (a stale-realm
// CustomEvent), failing the whole run intermittently on CI. Fake only the
// timer functions so they can never fire on the real clock; useRealTimers in
// afterEach discards any still-pending fakes. Date/performance stay real, so
// the CFI/position assertions are unaffected.
beforeEach(() => {
vi.useFakeTimers({ toFake: ['setTimeout', 'setInterval', 'clearTimeout', 'clearInterval'] });
});
afterEach(() => {
vi.useRealTimers();
});
describe('start()', () => {
test('extracts words from primary spine document only', () => {
const ch1Doc = makeDoc('Hello world');