Files
web-core/tests/contexts/RightSidebarContext.test.tsx
Beatrice Dellacà d7e144620e
All checks were successful
continuous-integration/drone/push Build is passing
add unit tests
2026-02-24 11:14:24 +01:00

250 lines
7.3 KiB
TypeScript

import { fireEvent, render } from '@testing-library/react';
import { act } from 'react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import {
RightSidebarProvider,
useRightSidebar,
} from '../../src/contexts/RightSidebarContext';
type RightSidebarHarnessOptions = {
pathname?: string;
onMobileOpenRequest?: () => void;
};
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 renderRightSidebarHarness(options: RightSidebarHarnessOptions = {}) {
const { pathname = '/users', onMobileOpenRequest } = options;
let currentPathname = pathname;
let currentValue: ReturnType<typeof useRightSidebar> | null = null;
function Probe() {
currentValue = useRightSidebar();
return null;
}
function Wrapper({ pathname }: Readonly<{ pathname: string }>) {
return (
<RightSidebarProvider
closeOnPathname={pathname}
onMobileOpenRequest={onMobileOpenRequest}
>
<Probe />
</RightSidebarProvider>
);
}
const rendered = render(<Wrapper pathname={currentPathname} />);
return {
getCurrent() {
if (!currentValue) {
throw new Error('Right sidebar context value not initialized');
}
return currentValue;
},
reroute(nextPathname: string) {
currentPathname = nextPathname;
rendered.rerender(<Wrapper pathname={currentPathname} />);
},
unmount: rendered.unmount,
};
}
describe('RightSidebarContext', () => {
beforeEach(() => {
localStorage.removeItem('authRightSidebarWidth');
setViewportWidth(1024);
});
it('throws when useRightSidebar is used outside provider', () => {
function Invalid() {
useRightSidebar();
return null;
}
expect(() => render(<Invalid />)).toThrow('useRightSidebar must be used within RightSidebarProvider');
});
it('opens and closes with content', () => {
const harness = renderRightSidebarHarness({ pathname: '/users' });
act(() => {
harness.getCurrent().openSidebar({
title: 'Meta',
content: <div>Body</div>,
});
});
expect(harness.getCurrent().isOpen).toBe(true);
expect(harness.getCurrent().content?.title).toBe('Meta');
act(() => {
harness.getCurrent().closeSidebar();
});
expect(harness.getCurrent().isOpen).toBe(false);
expect(harness.getCurrent().content).toBeNull();
});
it('does not open without current or next content', () => {
const harness = renderRightSidebarHarness({ pathname: '/users' });
act(() => {
harness.getCurrent().openSidebar();
});
expect(harness.getCurrent().isOpen).toBe(false);
expect(harness.getCurrent().content).toBeNull();
});
it('updates content live and supports toggle semantics', () => {
const harness = renderRightSidebarHarness({ pathname: '/users' });
act(() => {
harness.getCurrent().openSidebar({
title: 'Meta',
content: <div>Body</div>,
});
});
act(() => {
harness.getCurrent().setSidebarContent({
title: 'Meta Updated',
content: <div>Updated</div>,
});
});
expect(harness.getCurrent().isOpen).toBe(true);
expect(harness.getCurrent().content?.title).toBe('Meta Updated');
act(() => {
harness.getCurrent().toggleSidebar();
});
expect(harness.getCurrent().isOpen).toBe(false);
expect(harness.getCurrent().content).toBeNull();
});
it('toggleSidebar opens with provided content when closed', () => {
const harness = renderRightSidebarHarness({ pathname: '/users' });
act(() => {
harness.getCurrent().toggleSidebar({
title: 'Toggle open',
content: <div>Toggle body</div>,
});
});
expect(harness.getCurrent().isOpen).toBe(true);
expect(harness.getCurrent().content?.title).toBe('Toggle open');
});
it('closes on pathname changes', () => {
const harness = renderRightSidebarHarness({ pathname: '/users' });
act(() => {
harness.getCurrent().openSidebar({
title: 'Meta',
content: <div>Body</div>,
});
});
expect(harness.getCurrent().isOpen).toBe(true);
harness.reroute('/posts');
expect(harness.getCurrent().isOpen).toBe(false);
expect(harness.getCurrent().content).toBeNull();
});
it('reads/clamps persisted width, persists resized widths, and reacts to escape', async () => {
localStorage.setItem('authRightSidebarWidth', '999');
const harness = renderRightSidebarHarness({ pathname: '/users' });
expect(harness.getCurrent().desktopSidebarStyle['--auth-right-sidebar-width']).toBe(
'480px',
);
act(() => {
harness.getCurrent().openSidebar({
title: 'Meta',
content: <div>Body</div>,
});
});
const preventDefault = vi.fn();
act(() => {
harness.getCurrent().startResize({
clientX: 480,
preventDefault,
} as never);
});
act(() => {
fireEvent.pointerMove(window, { clientX: -400 });
fireEvent.pointerUp(window);
});
await act(async () => {
await Promise.resolve();
});
expect(preventDefault).toHaveBeenCalledTimes(1);
expect(localStorage.getItem('authRightSidebarWidth')).toBe('480');
act(() => {
fireEvent.keyDown(window, { key: 'Escape' });
});
expect(harness.getCurrent().isOpen).toBe(false);
});
it('notifies mobile open requests when opening on mobile', () => {
setViewportWidth(768);
const onMobileOpenRequest = vi.fn();
const harness = renderRightSidebarHarness({
pathname: '/users',
onMobileOpenRequest,
});
act(() => {
harness.getCurrent().openSidebar({
title: 'Meta',
content: <div>Body</div>,
});
});
expect(harness.getCurrent().isOpen).toBe(true);
expect(onMobileOpenRequest).toHaveBeenCalledTimes(1);
});
it('normalizes custom sizing values', () => {
function Probe() {
const sidebar = useRightSidebar();
return <div data-testid="width">{sidebar.desktopSidebarStyle['--auth-right-sidebar-width']}</div>;
}
const { getByTestId } = render(
<RightSidebarProvider
sizing={{
defaultWidth: 1000,
minWidth: 500,
maxWidth: 300,
}}
>
<Probe />
</RightSidebarProvider>,
);
expect(getByTestId('width').textContent).toBe('500px');
});
});