test(reader): harden fixed-layout wheel double-scroll test against CI flake (#4978)

The readest#4727 regression test set scrollTop=0, dispatched a synthetic
wheel, waited 60ms, then asserted scrollTop stayed 0. On slow CI runners it
flaked with "expected 4 to be +0".

As sibling scroll pages finish loading, the renderer runs
restoreScrollModeAnchor asynchronously, which at scrollTop=0/page-index-0
snaps scrollTop to page 0's offsetTop, the 4px scroll-page-gap margin. The
60ms post-dispatch delay raced that re-anchoring, so the assertion observed 4
instead of 0. That 4 is unrelated to the wheel bug, which is a 120px jump.

The buggy handler was scrollBy with instant behavior, a synchronous scroll
that lands before dispatchEvent returns. Measure scrollTop synchronously
before and after the dispatch with no await in between and assert they match.
This isolates the wheel handler's own effect and is immune to the async
re-anchoring. Reintroducing the bug still fails the test (before=4, after=124,
a clean 120px delta).

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Huang Xin
2026-07-07 01:28:11 +09:00
committed by GitHub
parent 57868a138e
commit db1d63cdcc
@@ -68,17 +68,26 @@ describe('fixed-layout scroll mode wheel handling (readest#4727)', () => {
});
const scroller = renderer as unknown as HTMLElement;
scroller.scrollTop = 0;
// A wheel over the page iframe. With the bug, the iframe handler runs
// `host.scrollBy({ top: deltaY })`, moving the host scroller by ~120px on
// top of the (here absent) native scroll. With the fix it must stay put.
// `host.scrollBy({ top: deltaY, behavior: 'instant' })`, an *instant*
// (synchronous) scroll that lands by ~120px before dispatchEvent() returns.
// With the fix the handler only drops pointer-events and never scrolls.
//
// Measure the scroll position synchronously around dispatchEvent() — with no
// await in between — so we capture only the wheel handler's own effect. Do
// NOT await/settle here: as sibling pages finish loading, the renderer runs
// #restoreScrollModeAnchor asynchronously, which snaps scrollTop to a page's
// offsetTop (the 4px --scroll-page-gap). A post-dispatch delay races that
// re-anchoring and observed scrollTop === 4 instead of 0 on slow CI runners
// (readest CI flake). The buggy scrollBy is synchronous, so a synchronous
// before/after comparison still catches it while being immune to the race.
const before = scroller.scrollTop;
iframe.contentDocument!.dispatchEvent(
new WheelEvent('wheel', { deltaY: 120, bubbles: true, cancelable: true }),
);
const after = scroller.scrollTop;
await new Promise((r) => setTimeout(r, 60));
expect(scroller.scrollTop).toBe(0);
expect(after).toBe(before);
});
});