import { fireEvent } from '@testing-library/react'; import { act } from 'react'; import { describe, expect, it, vi } from 'vitest'; import { isDesktopViewport, useSidePanelMachine } from '../../src/panels/useSidePanelMachine'; import { renderHook } from '../helpers/renderHook'; function setViewportWidth(width: number) { Object.defineProperty(window, 'innerWidth', { configurable: true, writable: true, value: width, }); } function resetMatchMedia() { Object.defineProperty(window, 'matchMedia', { configurable: true, writable: true, value: undefined, }); } describe('isDesktopViewport', () => { it('uses matchMedia when available', () => { const matchMedia = vi.fn(() => ({ matches: false })); Object.defineProperty(window, 'matchMedia', { configurable: true, writable: true, value: matchMedia, }); expect(isDesktopViewport(1200)).toBe(false); expect(matchMedia).toHaveBeenCalledWith('(min-width: 1200px)'); }); it('falls back to window.innerWidth when matchMedia is unavailable', () => { resetMatchMedia(); setViewportWidth(1024); expect(isDesktopViewport(1024)).toBe(true); setViewportWidth(800); expect(isDesktopViewport(1024)).toBe(false); }); }); describe('useSidePanelMachine', () => { it('reads stored width, clamps resize values, and persists final width', async () => { resetMatchMedia(); setViewportWidth(1200); localStorage.setItem('panel-width', '999'); const preventDefault = vi.fn(); const { result } = renderHook(() => useSidePanelMachine({ storageKey: 'panel-width', defaultWidth: 300, minWidth: 200, maxWidth: 400, resizeAxis: 'from-left', resizingBodyClass: 'resizing', isOpen: false, canResize: true, shouldPersistWidth: true, closeOnPathname: undefined, }), ); expect(result.current.width).toBe(400); expect(result.current.isDesktop).toBe(true); expect(localStorage.getItem('panel-width')).toBe('400'); act(() => { result.current.startResize({ clientX: 400, preventDefault, } as never); }); expect(preventDefault).toHaveBeenCalledTimes(1); expect(document.body.classList.contains('resizing')).toBe(true); act(() => { fireEvent.pointerMove(window, { clientX: 100 }); fireEvent.pointerUp(window); }); await act(async () => { await Promise.resolve(); }); expect(result.current.width).toBe(200); expect(localStorage.getItem('panel-width')).toBe('200'); expect(document.body.classList.contains('resizing')).toBe(false); }); it('falls back to default width for empty or invalid stored values', () => { resetMatchMedia(); setViewportWidth(1200); localStorage.setItem('panel-width', ''); const invalid = renderHook(() => useSidePanelMachine({ storageKey: 'panel-width', defaultWidth: 310, minWidth: 200, maxWidth: 400, resizeAxis: 'from-left', resizingBodyClass: 'resizing', isOpen: false, canResize: true, shouldPersistWidth: false, closeOnPathname: undefined, }), ); expect(invalid.result.current.width).toBe(310); invalid.unmount(); localStorage.setItem('panel-width', 'NaN-value'); const nonFinite = renderHook(() => useSidePanelMachine({ storageKey: 'panel-width', defaultWidth: 315, minWidth: 200, maxWidth: 400, resizeAxis: 'from-left', resizingBodyClass: 'resizing', isOpen: false, canResize: true, shouldPersistWidth: false, closeOnPathname: undefined, }), ); expect(nonFinite.result.current.width).toBe(315); }); it('does not start resizing when viewport is mobile or resizing is disabled', () => { resetMatchMedia(); setViewportWidth(700); const preventDefault = vi.fn(); const { result } = renderHook(() => useSidePanelMachine({ storageKey: 'panel-width', defaultWidth: 300, minWidth: 200, maxWidth: 400, resizeAxis: 'from-left', resizingBodyClass: 'resizing', isOpen: false, canResize: false, shouldPersistWidth: false, closeOnPathname: undefined, }), ); act(() => { result.current.startResize({ clientX: 300, preventDefault, } as never); }); expect(preventDefault).not.toHaveBeenCalled(); expect(document.body.classList.contains('resizing')).toBe(false); expect(localStorage.getItem('panel-width')).toBeNull(); }); it('ignores pointer events when not currently resizing', () => { resetMatchMedia(); setViewportWidth(1200); renderHook(() => useSidePanelMachine({ storageKey: 'panel-width', defaultWidth: 300, minWidth: 200, maxWidth: 400, resizeAxis: 'from-left', resizingBodyClass: 'resizing', isOpen: false, canResize: true, shouldPersistWidth: true, closeOnPathname: undefined, }), ); act(() => { fireEvent.pointerMove(window, { clientX: 1000 }); fireEvent.pointerUp(window); }); expect(document.body.classList.contains('resizing')).toBe(false); }); it('handles closeOnPathname, escape key callbacks and body overflow lock', () => { resetMatchMedia(); setViewportWidth(700); const onCloseOnPathname = vi.fn(); const onEscape = vi.fn(); let options = { storageKey: 'panel-width', defaultWidth: 300, minWidth: 200, maxWidth: 400, resizeAxis: 'from-right' as const, resizingBodyClass: 'resizing', isOpen: true, canResize: true, shouldPersistWidth: false, closeOnPathname: '/users', onCloseOnPathname, onEscape, }; const { rerender } = renderHook(() => useSidePanelMachine(options)); expect(onCloseOnPathname).toHaveBeenCalledTimes(1); expect(document.body.style.overflow).toBe('hidden'); act(() => { fireEvent.keyDown(window, { key: 'Escape' }); }); expect(onEscape).toHaveBeenCalledTimes(1); options = { ...options, closeOnPathname: '/posts', }; rerender(); expect(onCloseOnPathname).toHaveBeenCalledTimes(2); options = { ...options, isOpen: false, }; rerender(); expect(document.body.style.overflow).toBe(''); }); });