import { renderHook, act } from '@testing-library/react'; import { vi, test, expect, beforeEach } from 'vitest'; import { useResizableWidth } from './useResizableWidth.js'; beforeEach(() => { localStorage.clear(); }); // A container 1400px wide whose right edge is at x=1400, and a panel currently 448px wide. const setup = (opts = {}) => { const style = { setProperty: vi.fn(), removeProperty: vi.fn() }; const containerRef = { current: { getBoundingClientRect: () => ({ right: 1400, width: 1400 }), style } }; const panelRef = { current: { getBoundingClientRect: () => ({ width: 448 }) } }; const hook = renderHook(() => useResizableWidth({ storageKey: 'w', containerRef, ...opts })); const handle = () => hook.result.current.handleProps(panelRef); return { hook, handle, style }; }; const target = { setPointerCapture: vi.fn(), releasePointerCapture: vi.fn() }; const down = (h) => h.handle().onPointerDown({ button: 0, pointerId: 1, currentTarget: target }); const move = (h, clientX) => h.handle().onPointerMove({ clientX }); const up = (h) => h.handle().onPointerUp({ pointerId: 1, currentTarget: target }); test('dragging sets the width from the pointer to the right edge, then remembers it', () => { const h = setup(); act(() => { down(h); move(h, 900); }); expect(h.style.setProperty).toHaveBeenLastCalledWith('--study-right', '500px'); // 1400 - 900 expect(h.hook.result.current.width).toBeNull(); // no React render per pointer move act(() => up(h)); expect(h.hook.result.current.width).toBe(500); expect(localStorage.getItem('w')).toBe('500'); }); test('the width is kept between a minimum and a maximum that leaves room for the passage', () => { const h = setup(); act(() => { down(h); move(h, 1350); }); // would be 50px expect(h.style.setProperty).toHaveBeenLastCalledWith('--study-right', '320px'); act(() => { move(h, 100); }); // would be 1300px; the container leaves 1400 - 600 = 800, capped at 760 expect(h.style.setProperty).toHaveBeenLastCalledWith('--study-right', '760px'); act(() => up(h)); expect(h.hook.result.current.width).toBe(760); }); test('a move without a press does nothing', () => { const h = setup(); act(() => move(h, 900)); expect(h.style.setProperty).not.toHaveBeenCalled(); }); test('arrow keys resize from the current width; shift takes bigger steps', () => { const h = setup(); act(() => h.handle().onKeyDown({ key: 'ArrowLeft', preventDefault() {} })); expect(h.hook.result.current.width).toBe(472); // 448 + 24: wider act(() => h.handle().onKeyDown({ key: 'ArrowRight', shiftKey: true, preventDefault() {} })); expect(h.hook.result.current.width).toBe(376); // 472 - 96: narrower expect(localStorage.getItem('w')).toBe('376'); }); test('Home, Escape and double-click go back to the default width', () => { localStorage.setItem('w', '600'); const h = setup(); expect(h.hook.result.current.width).toBe(600); act(() => h.handle().onKeyDown({ key: 'Home', preventDefault() {} })); expect(h.hook.result.current.width).toBeNull(); expect(localStorage.getItem('w')).toBeNull(); expect(h.style.removeProperty).toHaveBeenCalledWith('--study-right'); act(() => h.handle().onKeyDown({ key: 'ArrowLeft', preventDefault() {} })); act(() => h.handle().onDoubleClick()); expect(h.hook.result.current.width).toBeNull(); }); test('a remembered width is restored, kept within the limits, and junk is ignored', () => { localStorage.setItem('w', '100'); expect(setup().hook.result.current.width).toBe(320); localStorage.setItem('w', '5000'); expect(setup().hook.result.current.width).toBe(760); localStorage.setItem('w', 'wide'); expect(setup().hook.result.current.width).toBeNull(); }); test('the handle describes itself for assistive technology', () => { const props = setup().handle(); expect(props).toMatchObject({ role: 'separator', 'aria-orientation': 'vertical', 'aria-label': 'Resize side panel', tabIndex: 0, 'aria-valuenow': 448 }); }); test('a different panel can name its own CSS variable', () => { const h = setup({ cssVar: '--reader-pane' }); act(() => { down(h); move(h, 1000); }); expect(h.style.setProperty).toHaveBeenLastCalledWith('--reader-pane', '400px'); act(() => up(h)); act(() => { h.handle().onDoubleClick(); }); expect(h.style.removeProperty).toHaveBeenLastCalledWith('--reader-pane'); });