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