Files
readest/apps/readest-app/src/__tests__/hooks/useIframeEvents.test.tsx
T
Huang Xin ad1c2d6bb0 fix(reader): filter Magic Mouse wheel events to stop accidental page turns (#4195)
A touch-surface mouse like the Magic Mouse emits a flood of tiny, low-
magnitude wheel events — plus an inertial momentum tail — for a single
physical gesture, and even a light brush of the surface produces spurious
deltas. The previous 100ms trailing debounce collapsed bursts but did not
filter by magnitude, so isolated micro-touches and the momentum tail each
turned a page, cascading into continuous accidental page turns in
paginated mode.

Add a wheel gesture detector that accumulates normalized wheel travel and
only flips once it crosses a deliberate-intent threshold, then swallows the
rest of the stream (the momentum tail) until the wheel goes idle — so one
physical gesture flips exactly one page, mirroring native readers.

Closes #4117

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 21:30:53 +02:00

90 lines
2.7 KiB
TypeScript

import { describe, test, expect, vi, afterEach } from 'vitest';
import { render, cleanup } from '@testing-library/react';
vi.mock('@/store/readerStore', () => {
return {
useReaderStore: () => ({ hoveredBookKey: null }),
};
});
vi.mock('@/store/bookDataStore', () => {
return {
useBookDataStore: () => ({ getBookData: () => null }),
};
});
vi.mock('@/utils/event', () => ({
eventDispatcher: { dispatch: vi.fn() },
}));
import { useMouseEvent } from '@/app/reader/hooks/useIframeEvents';
function dispatchWheelMessage(bookKey: string, deltaY = 100) {
// useMouseEvent listens on `message`, not `window.postMessage` directly,
// so we dispatch a MessageEvent manually for synchronous delivery.
const event = new MessageEvent('message', {
data: { bookKey, type: 'iframe-wheel', deltaY, deltaX: 0, deltaMode: 0, ctrlKey: false },
});
window.dispatchEvent(event);
}
describe('useMouseEvent wheel handling', () => {
afterEach(() => {
cleanup();
});
test('wheel flip dispatches to the latest handlePageFlip after re-render', () => {
const fn1 = vi.fn();
const fn2 = vi.fn();
function Wrapper({ handler }: { handler: (msg: MessageEvent) => void }) {
// useMouseEvent has the 2nd parameter typed as a union including
// React.MouseEvent — we cast through unknown to satisfy the typecheck
// for this focused unit test.
useMouseEvent('book-1', handler as unknown as Parameters<typeof useMouseEvent>[1]);
return null;
}
const { rerender } = render(<Wrapper handler={fn1} />);
// Re-render with a new handler reference. The wheel flip path should
// pick up the latest one rather than holding onto fn1 forever.
rerender(<Wrapper handler={fn2} />);
dispatchWheelMessage('book-1');
expect(fn1).not.toHaveBeenCalled();
expect(fn2).toHaveBeenCalledTimes(1);
});
test('a single deliberate wheel notch flips exactly one page', () => {
const handler = vi.fn();
function Wrapper() {
useMouseEvent('book-1', handler as unknown as Parameters<typeof useMouseEvent>[1]);
return null;
}
render(<Wrapper />);
dispatchWheelMessage('book-1', 120);
expect(handler).toHaveBeenCalledTimes(1);
});
test('tiny low-magnitude wheel events below the threshold do not flip', () => {
const handler = vi.fn();
function Wrapper() {
useMouseEvent('book-1', handler as unknown as Parameters<typeof useMouseEvent>[1]);
return null;
}
render(<Wrapper />);
// A Magic Mouse light brush emits a flurry of tiny deltas; on their own
// they must not turn a page.
dispatchWheelMessage('book-1', 3);
dispatchWheelMessage('book-1', 4);
expect(handler).not.toHaveBeenCalled();
});
});