mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 13:36:48 +00:00
11a0780a5b
Three reports from the same week, all "the layout is in my way". #2247 — the Plan tab shows both side panels from 768px up, unconditionally. A Pixel 10 Pro Fold unfolded is ~860 CSS px, so 340 + 300 of panel left the map a 200px strip; those same widths became fit padding, shrinking the framing window to 140px and opening the camera two zoom levels out over half of East Asia. The floating POI/compass cluster is centred on the viewport at z-25, which at that width put it on top of both collapse tabs (z-1 inside z-20 containers) and on the Add Place/Activity button — and its pills set pointer-events: auto, so it ate the taps meant for them. The resize handles are mousedown-only, so a touch user could not narrow anything either. Between 768 and 1023px only one panel is open now, the day plan first, the places list one tap away — the Google Maps shape the reporter asked for. The collapse flags stay the wide-layout state, so a fold back restores what was there, and a marker tap reopening "both" no longer re-crowds the screen. The control cluster centres on the corridor the panels leave (the formula PlaceInspector and DayDetailPanel already use), the tabs finally have accessible names, the dead resize handles go, and a panel dragged to 520px on a desktop is clamped so the map keeps 360px without touching the stored width. computeMapViewport refuses to frame for less than a third of the box, so absurd padding can never pick the zoom again. #2248 — the public journey hero clips its own overflow to keep its decorative circles from widening the document, which also amputated the language menu at the hero's bottom edge: 8 of 23 locales, the rest unreachable. Both share pages inlined the same menu, so it is one component now, portalled to the body and anchored to its trigger the way CustomSelect is, with a height cap and its own scroll. It closes on Escape and on an outside click, which it never did, and `zh-TW` stops labelling itself 简体中文. #2249 — the note row's pencil/trash pill is absolutely positioned over the row's own reorder chevrons, and a coarse-pointer `opacity: 1 !important` pinned it permanently visible on tablets. On a mouse it was no better: the chevrons are revealed on hover, which is exactly when the opaque pill covers them, so note reordering has never been clickable on any pointer. The pill is gone; the row opens the note the way the phone shell already does, and delete lives in that dialog behind the same confirmation. A link in the note body keeps its own tap, on both shells. Closes #2247 Closes #2248 Closes #2249
274 lines
9.7 KiB
TypeScript
274 lines
9.7 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-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);
|
|
});
|
|
});
|