import { cleanup, render, act } from '@testing-library/react'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { useRef } from 'react'; import { useSpatialNavigation, _resetLastKeyboardTime, } from '@/app/reader/hooks/useSpatialNavigation'; function TestToolbar({ isVisible }: { isVisible: boolean }) { const ref = useRef(null); useSpatialNavigation(ref, isVisible); return (
); } function TestHeaderFooter({ isVisible }: { isVisible: boolean }) { const headerRef = useRef(null); const footerRef = useRef(null); useSpatialNavigation(headerRef, isVisible); useSpatialNavigation(footerRef, isVisible); return ( <>
); } function pressKey(element: HTMLElement, key: string) { const event = new KeyboardEvent('keydown', { key, bubbles: true, cancelable: true, }); const spy = vi.spyOn(event, 'stopPropagation'); element.dispatchEvent(event); return { event, stopPropagationCalled: spy.mock.calls.length > 0 }; } function simulateKeyboardActivation() { document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true })); } async function waitForAutoFocus() { await act(() => new Promise((r) => setTimeout(r, 150))); } describe('useToolbarKeyNavigation', () => { beforeEach(() => { _resetLastKeyboardTime(); }); afterEach(() => cleanup()); describe('auto-focus', () => { it('focuses first enabled button when keyboard-activated', async () => { simulateKeyboardActivation(); render(); await waitForAutoFocus(); expect(document.activeElement).toBe(document.querySelector('[data-testid="btn-1"]')); }); it('does not auto-focus when not visible', async () => { simulateKeyboardActivation(); render(); await waitForAutoFocus(); expect(document.activeElement).not.toBe(document.querySelector('[data-testid="btn-1"]')); }); it('does not auto-focus when mouse-activated (no recent keyboard)', async () => { // No simulateKeyboardActivation() — simulates mouse hover activation render(); await waitForAutoFocus(); expect(document.activeElement).not.toBe(document.querySelector('[data-testid="btn-1"]')); }); }); describe('left/right navigation', () => { it('ArrowRight moves focus to the next button', async () => { simulateKeyboardActivation(); render(); await waitForAutoFocus(); const btn1 = document.querySelector('[data-testid="btn-1"]')!; const btn2 = document.querySelector('[data-testid="btn-2"]')!; expect(document.activeElement).toBe(btn1); pressKey(btn1, 'ArrowRight'); expect(document.activeElement).toBe(btn2); }); it('ArrowLeft moves focus to the previous button', async () => { simulateKeyboardActivation(); render(); await waitForAutoFocus(); const btn1 = document.querySelector('[data-testid="btn-1"]')!; const btn2 = document.querySelector('[data-testid="btn-2"]')!; btn2.focus(); pressKey(btn2, 'ArrowLeft'); expect(document.activeElement).toBe(btn1); }); it('ArrowRight does not go past the last enabled button', async () => { simulateKeyboardActivation(); render(); await waitForAutoFocus(); const btn3 = document.querySelector('[data-testid="btn-3"]')!; btn3.focus(); pressKey(btn3, 'ArrowRight'); expect(document.activeElement).toBe(btn3); }); it('ArrowLeft does not go before the first button', async () => { simulateKeyboardActivation(); render(); await waitForAutoFocus(); const btn1 = document.querySelector('[data-testid="btn-1"]')!; pressKey(btn1, 'ArrowLeft'); expect(document.activeElement).toBe(btn1); }); it('ArrowRight calls stopPropagation', async () => { simulateKeyboardActivation(); render(); await waitForAutoFocus(); const btn1 = document.querySelector('[data-testid="btn-1"]')!; const { stopPropagationCalled } = pressKey(btn1, 'ArrowRight'); expect(stopPropagationCalled).toBe(true); }); }); describe('up/down navigation between header and footer', () => { it('ArrowDown from header focuses first footer button', async () => { render(); await waitForAutoFocus(); const headerBtn = document.querySelector('[data-testid="header-btn-1"]')!; const footerBtn1 = document.querySelector('[data-testid="footer-btn-1"]')!; headerBtn.focus(); pressKey(headerBtn, 'ArrowDown'); expect(document.activeElement).toBe(footerBtn1); }); it('ArrowUp from footer focuses first header button', async () => { render(); await waitForAutoFocus(); const headerBtn1 = document.querySelector('[data-testid="header-btn-1"]')!; const footerBtn = document.querySelector('[data-testid="footer-btn-1"]')!; footerBtn.focus(); pressKey(footerBtn, 'ArrowUp'); expect(document.activeElement).toBe(headerBtn1); }); it('ArrowDown from header calls stopPropagation', async () => { render(); await waitForAutoFocus(); const headerBtn = document.querySelector('[data-testid="header-btn-1"]')!; headerBtn.focus(); const { stopPropagationCalled } = pressKey(headerBtn, 'ArrowDown'); expect(stopPropagationCalled).toBe(true); }); }); });