import { fireEvent, render, screen } from '@testing-library/react'; import { act } from 'react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { LeftMenuProvider, type LeftMenuContent, useLeftMenu, } from '../../src/contexts/LeftMenuContext'; function buildDefaultContent(label = 'Default menu'): LeftMenuContent { return { ariaLabel: label, render: ({ collapsed }) =>
{collapsed ? `${label} (collapsed)` : `${label} (expanded)`}
, }; } function setViewportWidth(width: number) { Object.defineProperty(window, 'innerWidth', { configurable: true, writable: true, value: width, }); Object.defineProperty(window, 'matchMedia', { configurable: true, writable: true, value: undefined, }); } function renderLeftMenuHarness(initialPathname = '/users') { let currentPathname = initialPathname; let currentValue: ReturnType | null = null; const defaultContent = buildDefaultContent(); function Probe() { currentValue = useLeftMenu(); const renderState = { collapsed: currentValue.collapsed, mobileOpen: currentValue.mobileOpen, isDesktop: window.innerWidth >= 1024, closeMenu: currentValue.closeMenu, }; return
{currentValue.content.render(renderState)}
; } function Wrapper({ pathname }: Readonly<{ pathname: string }>) { return ( ); } const rendered = render(); return { getCurrent() { if (!currentValue) { throw new Error('Left menu context value not initialized'); } return currentValue; }, reroute(nextPathname: string) { currentPathname = nextPathname; rendered.rerender(); }, }; } describe('LeftMenuContext', () => { beforeEach(() => { localStorage.removeItem('authSidebarWidth'); localStorage.removeItem('authSidebarCollapsed'); setViewportWidth(1024); }); it('throws when useLeftMenu is used outside provider', () => { function Invalid() { useLeftMenu(); return null; } expect(() => render()).toThrow('useLeftMenu must be used within LeftMenuProvider'); }); it('supports desktop collapse/expand/toggle semantics', () => { const harness = renderLeftMenuHarness('/users'); expect(harness.getCurrent().collapsed).toBe(false); act(() => { harness.getCurrent().closeMenu(); }); expect(harness.getCurrent().collapsed).toBe(true); act(() => { harness.getCurrent().openMenu(); }); expect(harness.getCurrent().collapsed).toBe(false); act(() => { harness.getCurrent().toggleMenu(); }); expect(harness.getCurrent().collapsed).toBe(true); }); it('restores collapsed state from storage and clamps persisted width', () => { localStorage.setItem('authSidebarCollapsed', '1'); localStorage.setItem('authSidebarWidth', '999'); const harness = renderLeftMenuHarness('/users'); expect(harness.getCurrent().collapsed).toBe(true); expect(harness.getCurrent().desktopMenuStyle['--auth-sidebar-width']).toBe('56px'); }); it('locks body scroll on mobile open and unlocks on close', () => { setViewportWidth(768); const harness = renderLeftMenuHarness('/users'); act(() => { harness.getCurrent().openMenu(); }); expect(harness.getCurrent().mobileOpen).toBe(true); expect(document.body.style.overflow).toBe('hidden'); act(() => { harness.getCurrent().closeMenu(); }); expect(harness.getCurrent().mobileOpen).toBe(false); expect(document.body.style.overflow).toBe(''); }); it('reads, clamps, and persists sidebar width through pointer resize', async () => { localStorage.setItem('authSidebarWidth', '999'); const harness = renderLeftMenuHarness('/users'); expect(harness.getCurrent().desktopMenuStyle['--auth-sidebar-width']).toBe('420px'); const preventDefault = vi.fn(); act(() => { harness.getCurrent().startResize({ clientX: 420, preventDefault, } as never); }); act(() => { fireEvent.pointerMove(window, { clientX: -1000 }); fireEvent.pointerUp(window); }); await act(async () => { await Promise.resolve(); }); expect(preventDefault).toHaveBeenCalledTimes(1); expect(localStorage.getItem('authSidebarWidth')).toBe('220'); expect(harness.getCurrent().desktopMenuStyle['--auth-sidebar-width']).toBe('220px'); }); it('accepts custom content and resets to default on route changes', () => { setViewportWidth(768); const harness = renderLeftMenuHarness('/users'); act(() => { harness.getCurrent().openMenu(); harness.getCurrent().setMenuContent({ ariaLabel: 'Custom menu', render: () =>
Custom menu content
, }); }); expect(harness.getCurrent().mobileOpen).toBe(true); expect(screen.getByText('Custom menu content')).toBeInTheDocument(); harness.reroute('/posts'); expect(harness.getCurrent().mobileOpen).toBe(false); expect(screen.queryByText('Custom menu content')).not.toBeInTheDocument(); expect(screen.getByText('Default menu (expanded)')).toBeInTheDocument(); }); it('applies provided content in openMenu/toggleMenu and toggles mobile open state', () => { setViewportWidth(768); const harness = renderLeftMenuHarness('/users'); act(() => { harness.getCurrent().openMenu({ render: () =>
Open payload
, }); }); expect(screen.getByText('Open payload')).toBeInTheDocument(); expect(harness.getCurrent().mobileOpen).toBe(true); act(() => { harness.getCurrent().toggleMenu({ render: () =>
Toggle payload
, }); }); expect(screen.getByText('Toggle payload')).toBeInTheDocument(); expect(harness.getCurrent().mobileOpen).toBe(false); }); it('updates default content only when current content is still default', () => { function Harness() { const menu = useLeftMenu(); const state = { collapsed: menu.collapsed, mobileOpen: menu.mobileOpen, isDesktop: true, closeMenu: menu.closeMenu, }; return (
{menu.content.render(state)}
); } function Wrapper({ label }: Readonly<{ label: string }>) { return ( ); } const rendered = render(); expect(screen.getByTestId('content')).toHaveTextContent('Menu A (expanded)'); rendered.rerender(); expect(screen.getByTestId('content')).toHaveTextContent('Menu B (expanded)'); fireEvent.click(screen.getByRole('button', { name: 'custom' })); expect(screen.getByTestId('content')).toHaveTextContent('Custom'); rendered.rerender(); expect(screen.getByTestId('content')).toHaveTextContent('Custom'); }); });