add unit tests
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2026-02-24 11:14:24 +01:00
parent 7e938138ff
commit d7e144620e
23 changed files with 2766 additions and 17 deletions

View File

@@ -0,0 +1,239 @@
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('');
});
});