All checks were successful
continuous-integration/drone/push Build is passing
250 lines
7.3 KiB
TypeScript
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');
|
|
});
|
|
});
|