mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 13:36:48 +00:00
9f0fe456a4
The handles on the day plan and the places panel only listened to the mouse, so on an iPad they did nothing, and in the portrait band they were left out. They now follow a finger as well, show a grip on touch screens, keep the page and the map from taking the gesture, and stay in the narrow layout within the room the map needs. As separators they also move with the arrow keys. (#1012)
316 lines
11 KiB
TypeScript
316 lines
11 KiB
TypeScript
import { renderHook, act } from '@testing-library/react';
|
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
import { fireEvent } from '@testing-library/react';
|
|
import { useResizablePanels } from '../../../src/hooks/useResizablePanels';
|
|
|
|
/**
|
|
* The suite's jsdom stub answers `matches: false` to everything, so the hook is
|
|
* in its wide layout unless a test says otherwise. `narrow()` puts it in the
|
|
* 768-1023 band the foldable lands in (#2247).
|
|
*/
|
|
function setViewport(width: number, narrow: boolean) {
|
|
Object.defineProperty(window, 'innerWidth', { writable: true, configurable: true, value: width });
|
|
window.matchMedia = vi.fn().mockImplementation((query: string) => ({
|
|
matches: query.includes('min-width: 768px') ? narrow : false,
|
|
media: query,
|
|
addEventListener: vi.fn(),
|
|
removeEventListener: vi.fn(),
|
|
addListener: vi.fn(),
|
|
removeListener: vi.fn(),
|
|
dispatchEvent: vi.fn(),
|
|
})) as unknown as typeof window.matchMedia;
|
|
}
|
|
|
|
describe('useResizablePanels', () => {
|
|
const realMatchMedia = window.matchMedia;
|
|
|
|
beforeEach(() => {
|
|
localStorage.clear();
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
afterEach(() => {
|
|
window.matchMedia = realMatchMedia;
|
|
});
|
|
|
|
it('FE-HOOK-PANELS-001: default leftWidth is 340 when localStorage is empty', () => {
|
|
const { result } = renderHook(() => useResizablePanels());
|
|
expect(result.current.leftWidth).toBe(340);
|
|
});
|
|
|
|
it('FE-HOOK-PANELS-002: default rightWidth is 300 when localStorage is empty', () => {
|
|
const { result } = renderHook(() => useResizablePanels());
|
|
expect(result.current.rightWidth).toBe(300);
|
|
});
|
|
|
|
it('FE-HOOK-PANELS-003: leftWidth loaded from localStorage when set', () => {
|
|
localStorage.setItem('sidebarLeftWidth', '400');
|
|
const { result } = renderHook(() => useResizablePanels());
|
|
expect(result.current.leftWidth).toBe(400);
|
|
});
|
|
|
|
it('FE-HOOK-PANELS-004: rightWidth loaded from localStorage when set', () => {
|
|
localStorage.setItem('sidebarRightWidth', '350');
|
|
const { result } = renderHook(() => useResizablePanels());
|
|
expect(result.current.rightWidth).toBe(350);
|
|
});
|
|
|
|
it('FE-HOOK-PANELS-005: startResizeLeft sets body cursor to col-resize', () => {
|
|
const { result } = renderHook(() => useResizablePanels());
|
|
act(() => {
|
|
result.current.startResizeLeft();
|
|
});
|
|
expect(document.body.style.cursor).toBe('col-resize');
|
|
});
|
|
|
|
it('FE-HOOK-PANELS-006: startResizeRight sets body cursor to col-resize', () => {
|
|
const { result } = renderHook(() => useResizablePanels());
|
|
act(() => {
|
|
result.current.startResizeRight();
|
|
});
|
|
expect(document.body.style.cursor).toBe('col-resize');
|
|
});
|
|
|
|
it('FE-HOOK-PANELS-007: mousedown → mousemove → mouseup updates leftWidth and persists to localStorage', async () => {
|
|
const { result } = renderHook(() => useResizablePanels());
|
|
|
|
act(() => {
|
|
result.current.startResizeLeft();
|
|
});
|
|
|
|
// mousemove with clientX=350 → w = max(200, min(520, 350-10)) = 340
|
|
act(() => {
|
|
fireEvent.mouseMove(document, { clientX: 350 });
|
|
});
|
|
|
|
expect(result.current.leftWidth).toBe(340);
|
|
expect(localStorage.getItem('sidebarLeftWidth')).toBe('340');
|
|
|
|
act(() => {
|
|
fireEvent.mouseUp(document);
|
|
});
|
|
|
|
expect(document.body.style.cursor).toBe('');
|
|
});
|
|
|
|
it('FE-HOOK-PANELS-008: mousedown → mousemove → mouseup updates rightWidth and persists to localStorage', () => {
|
|
// Set window.innerWidth for the right panel calculation
|
|
Object.defineProperty(window, 'innerWidth', { writable: true, configurable: true, value: 1200 });
|
|
|
|
const { result } = renderHook(() => useResizablePanels());
|
|
|
|
act(() => {
|
|
result.current.startResizeRight();
|
|
});
|
|
|
|
// mousemove with clientX=800 → w = max(200, min(520, 1200-800-10)) = max(200, min(520, 390)) = 390
|
|
act(() => {
|
|
fireEvent.mouseMove(document, { clientX: 800 });
|
|
});
|
|
|
|
expect(result.current.rightWidth).toBe(390);
|
|
expect(localStorage.getItem('sidebarRightWidth')).toBe('390');
|
|
|
|
act(() => {
|
|
fireEvent.mouseUp(document);
|
|
});
|
|
|
|
expect(document.body.style.cursor).toBe('');
|
|
});
|
|
|
|
it('FE-HOOK-PANELS-009: min width constraint (200) is enforced for left panel', () => {
|
|
const { result } = renderHook(() => useResizablePanels());
|
|
|
|
act(() => {
|
|
result.current.startResizeLeft();
|
|
});
|
|
|
|
// clientX=50 → w = max(200, min(520, 50-10)) = max(200, 40) = 200
|
|
act(() => {
|
|
fireEvent.mouseMove(document, { clientX: 50 });
|
|
});
|
|
|
|
expect(result.current.leftWidth).toBe(200);
|
|
});
|
|
|
|
it('FE-HOOK-PANELS-010: max width constraint (520) is enforced for left panel', () => {
|
|
const { result } = renderHook(() => useResizablePanels());
|
|
|
|
act(() => {
|
|
result.current.startResizeLeft();
|
|
});
|
|
|
|
// clientX=600 → w = max(200, min(520, 600-10)) = min(520, 590) = 520
|
|
act(() => {
|
|
fireEvent.mouseMove(document, { clientX: 600 });
|
|
});
|
|
|
|
expect(result.current.leftWidth).toBe(520);
|
|
});
|
|
|
|
it('FE-HOOK-PANELS-011: mousemove without prior startResize does nothing', () => {
|
|
const { result } = renderHook(() => useResizablePanels());
|
|
|
|
const initialLeft = result.current.leftWidth;
|
|
const initialRight = result.current.rightWidth;
|
|
|
|
act(() => {
|
|
fireEvent.mouseMove(document, { clientX: 400 });
|
|
});
|
|
|
|
expect(result.current.leftWidth).toBe(initialLeft);
|
|
expect(result.current.rightWidth).toBe(initialRight);
|
|
});
|
|
|
|
it('FE-HOOK-PANELS-011b: a finger drags the left panel on a tablet, and lifting it ends the drag (#1012)', () => {
|
|
const { result } = renderHook(() => useResizablePanels());
|
|
|
|
act(() => {
|
|
result.current.startResizeLeft();
|
|
});
|
|
act(() => {
|
|
fireEvent.touchMove(document, { touches: [{ clientX: 410 }] });
|
|
});
|
|
expect(result.current.leftWidth).toBe(400);
|
|
expect(localStorage.getItem('sidebarLeftWidth')).toBe('400');
|
|
|
|
act(() => {
|
|
fireEvent.touchEnd(document);
|
|
});
|
|
act(() => {
|
|
fireEvent.touchMove(document, { touches: [{ clientX: 300 }] });
|
|
});
|
|
expect(result.current.leftWidth).toBe(400);
|
|
expect(document.body.style.cursor).toBe('');
|
|
});
|
|
|
|
it('FE-HOOK-PANELS-011c: the keyboard nudges a panel within its bounds (#1012)', () => {
|
|
const { result } = renderHook(() => useResizablePanels());
|
|
const start = result.current.leftWidth;
|
|
|
|
act(() => {
|
|
result.current.nudgeLeft(16);
|
|
});
|
|
expect(result.current.leftWidth).toBe(start + 16);
|
|
|
|
act(() => {
|
|
result.current.nudgeLeft(-10_000);
|
|
});
|
|
expect(result.current.leftWidth).toBe(result.current.resizeMin);
|
|
|
|
act(() => {
|
|
result.current.nudgeRight(10_000);
|
|
});
|
|
expect(result.current.rightWidth).toBe(result.current.resizeMax);
|
|
});
|
|
|
|
it('FE-HOOK-PANELS-012: body userSelect set to none during resize, cleared on mouseup', () => {
|
|
const { result } = renderHook(() => useResizablePanels());
|
|
|
|
act(() => {
|
|
result.current.startResizeLeft();
|
|
});
|
|
|
|
expect(document.body.style.userSelect).toBe('none');
|
|
|
|
act(() => {
|
|
fireEvent.mouseUp(document);
|
|
});
|
|
|
|
expect(document.body.style.userSelect).toBe('');
|
|
});
|
|
|
|
it('FE-HOOK-PANELS-013: leftCollapsed and rightCollapsed default to false', () => {
|
|
const { result } = renderHook(() => useResizablePanels());
|
|
expect(result.current.leftCollapsed).toBe(false);
|
|
expect(result.current.rightCollapsed).toBe(false);
|
|
});
|
|
|
|
it('FE-HOOK-PANELS-014: setLeftCollapsed and setRightCollapsed are exposed', () => {
|
|
const { result } = renderHook(() => useResizablePanels());
|
|
expect(result.current.setLeftCollapsed).toBeTypeOf('function');
|
|
expect(result.current.setRightCollapsed).toBeTypeOf('function');
|
|
});
|
|
|
|
// ── The narrow band, 768-1023px (#2247) ──────────────────────────────────
|
|
|
|
it('FE-HOOK-PANELS-015: a wide layout shows whatever the collapse flags say', () => {
|
|
setViewport(1440, false);
|
|
const { result } = renderHook(() => useResizablePanels());
|
|
expect(result.current.narrow).toBe(false);
|
|
expect(result.current.leftHidden).toBe(false);
|
|
expect(result.current.rightHidden).toBe(false);
|
|
|
|
act(() => { result.current.toggleRight(); });
|
|
expect(result.current.rightHidden).toBe(true);
|
|
expect(result.current.leftHidden).toBe(false);
|
|
expect(result.current.rightCollapsed).toBe(true);
|
|
});
|
|
|
|
it('FE-HOOK-PANELS-016: the narrow band opens the day plan and hides the places list', () => {
|
|
setViewport(860, true);
|
|
const { result } = renderHook(() => useResizablePanels());
|
|
expect(result.current.narrow).toBe(true);
|
|
expect(result.current.leftHidden).toBe(false);
|
|
expect(result.current.rightHidden).toBe(true);
|
|
});
|
|
|
|
it('FE-HOOK-PANELS-017: opening one panel in the narrow band closes the other', () => {
|
|
setViewport(860, true);
|
|
const { result } = renderHook(() => useResizablePanels());
|
|
|
|
act(() => { result.current.toggleRight(); });
|
|
expect(result.current.rightHidden).toBe(false);
|
|
expect(result.current.leftHidden).toBe(true);
|
|
|
|
act(() => { result.current.toggleLeft(); });
|
|
expect(result.current.leftHidden).toBe(false);
|
|
expect(result.current.rightHidden).toBe(true);
|
|
});
|
|
|
|
it('FE-HOOK-PANELS-018: tapping the open panel in the narrow band leaves the map alone', () => {
|
|
setViewport(860, true);
|
|
const { result } = renderHook(() => useResizablePanels());
|
|
|
|
act(() => { result.current.toggleLeft(); });
|
|
expect(result.current.leftHidden).toBe(true);
|
|
expect(result.current.rightHidden).toBe(true);
|
|
});
|
|
|
|
// A marker tap calls both setters with false; in the narrow band that used to
|
|
// re-crowd the screen the user had just cleared.
|
|
it('FE-HOOK-PANELS-019: reopening both panels does not re-crowd the narrow layout', () => {
|
|
setViewport(860, true);
|
|
const { result } = renderHook(() => useResizablePanels());
|
|
|
|
act(() => { result.current.toggleRight(); });
|
|
act(() => { result.current.setLeftCollapsed(false); result.current.setRightCollapsed(false); });
|
|
expect(result.current.rightHidden).toBe(false);
|
|
expect(result.current.leftHidden).toBe(true);
|
|
});
|
|
|
|
it('FE-HOOK-PANELS-020: a panel dragged wide on a desktop is clamped so the map keeps 360px', () => {
|
|
localStorage.setItem('sidebarLeftWidth', '520');
|
|
setViewport(800, true);
|
|
const { result } = renderHook(() => useResizablePanels());
|
|
// 800 - 360 map - 20 margins = 420
|
|
expect(result.current.leftWidth).toBe(420);
|
|
// The stored value is untouched, so a wide window gets its 520 back.
|
|
expect(localStorage.getItem('sidebarLeftWidth')).toBe('520');
|
|
});
|
|
|
|
it('FE-HOOK-PANELS-021: the clamp never squeezes a panel below the drag minimum', () => {
|
|
localStorage.setItem('sidebarLeftWidth', '520');
|
|
setViewport(500, true);
|
|
const { result } = renderHook(() => useResizablePanels());
|
|
expect(result.current.leftWidth).toBe(200);
|
|
});
|
|
|
|
it('FE-HOOK-PANELS-022: a wide layout leaves the stored widths alone', () => {
|
|
localStorage.setItem('sidebarLeftWidth', '520');
|
|
setViewport(1100, false);
|
|
const { result } = renderHook(() => useResizablePanels());
|
|
expect(result.current.leftWidth).toBe(520);
|
|
});
|
|
});
|