Files
study-bible-project/src/hooks/useResizableWidth.test.js
T
nmemmertandClaude Sonnet 5.5 e5812fd4ee Reader: resizable side panel
Drag the left edge of the verse side panel to resize it (arrow keys too,
Shift for bigger steps; double-click resets). The text reflows to make room,
and the width is remembered on this device. Reuses the Study page's resize
handle; the hook now takes the CSS variable to drive.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 16:37:50 -04:00

91 lines
4.3 KiB
JavaScript

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');
});