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