import { cleanup, render } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import useBookShortcuts from '@/app/reader/hooks/useBookShortcuts';
import { eventDispatcher } from '@/utils/event';
const shortcutState = {
actions: null as Record<
string,
((event?: KeyboardEvent | MessageEvent) => void) | undefined
> | null,
};
const mockView = {
book: { dir: 'ltr' },
prev: vi.fn(),
next: vi.fn(),
pan: vi.fn(),
renderer: {
scrolled: false,
setAttribute: vi.fn(),
},
history: {
back: vi.fn(),
forward: vi.fn(),
},
};
const currentViewSettings = {
defaultFontSize: 16,
lineHeight: 1.5,
readingRulerEnabled: true,
writingMode: 'horizontal-tb',
vertical: false,
rtl: false,
paragraphMode: { enabled: false },
};
vi.mock('@/store/readerStore', () => ({
useReaderStore: () => ({
getView: () => mockView,
getViewState: () => ({ ttsEnabled: false }),
getViewSettings: () => currentViewSettings,
setViewSettings: vi.fn(),
}),
}));
vi.mock('@/store/sidebarStore', () => ({
useSidebarStore: () => ({
toggleSideBar: vi.fn(),
setSideBarBookKey: vi.fn(),
}),
}));
vi.mock('@/store/settingsStore', () => ({
useSettingsStore: () => ({
setSettingsDialogOpen: vi.fn(),
}),
}));
vi.mock('@/store/bookDataStore', () => ({
useBookDataStore: () => ({
getBookData: vi.fn(),
}),
}));
vi.mock('@/store/notebookStore', () => ({
useNotebookStore: () => ({
toggleNotebook: vi.fn(),
}),
}));
vi.mock('@/store/themeStore', () => ({
useThemeStore: () => ({
safeAreaInsets: null,
}),
}));
vi.mock('@/components/command-palette', () => ({
useCommandPalette: () => ({
open: vi.fn(),
}),
}));
vi.mock('@/hooks/useShortcuts', () => ({
default: (actions: typeof shortcutState.actions) => {
shortcutState.actions = actions;
},
}));
vi.mock('@/services/environment', () => ({
isTauriAppPlatform: () => false,
}));
vi.mock('@/utils/window', () => ({
tauriHandleClose: vi.fn(),
tauriHandleToggleFullScreen: vi.fn(),
tauriQuitApp: vi.fn(),
}));
vi.mock('@/utils/style', () => ({
getStyles: vi.fn(),
}));
vi.mock('@/services/constants', () => ({
MAX_ZOOM_LEVEL: 200,
MIN_ZOOM_LEVEL: 50,
ZOOM_STEP: 10,
}));
vi.mock('@/app/reader/hooks/useBooksManager', () => ({
default: () => ({
getNextBookKey: () => 'book-1',
}),
}));
const Harness = () => {
useBookShortcuts({ sideBarBookKey: 'book-1', bookKeys: ['book-1'] });
return null;
};
describe('useBookShortcuts', () => {
beforeEach(() => {
vi.clearAllMocks();
shortcutState.actions = null;
currentViewSettings.readingRulerEnabled = true;
currentViewSettings.writingMode = 'horizontal-tb';
currentViewSettings.vertical = false;
currentViewSettings.rtl = false;
currentViewSettings.paragraphMode.enabled = false;
mockView.book.dir = 'ltr';
});
afterEach(() => {
cleanup();
});
it('routes page-turn shortcuts to reading ruler movement when enabled', () => {
const dispatchSpy = vi.spyOn(eventDispatcher, 'dispatchSync').mockReturnValue(true);
render();
shortcutState.actions?.['onGoNext']?.();
expect(dispatchSpy).toHaveBeenCalledWith('reading-ruler-move', {
bookKey: 'book-1',
direction: 'forward',
});
expect(mockView.next).not.toHaveBeenCalled();
});
it('uses reading order when directional shortcuts are handled in rtl books', () => {
const dispatchSpy = vi.spyOn(eventDispatcher, 'dispatchSync').mockReturnValue(true);
mockView.book.dir = 'rtl';
render();
shortcutState.actions?.['onGoRight']?.();
expect(dispatchSpy).toHaveBeenCalledWith('reading-ruler-move', {
bookKey: 'book-1',
direction: 'backward',
});
});
it('falls back to normal page navigation when the ruler is disabled', () => {
currentViewSettings.readingRulerEnabled = false;
render();
shortcutState.actions?.['onGoNext']?.();
expect(mockView.next).toHaveBeenCalledWith(72);
});
it('falls back to normal page navigation when the ruler cannot move further', () => {
vi.spyOn(eventDispatcher, 'dispatchSync').mockReturnValue(false);
render();
shortcutState.actions?.['onGoNext']?.();
expect(mockView.next).toHaveBeenCalledWith(72);
});
});