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,170 @@
import { act } from 'react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { usePaginatedResource } from '../../src/hooks/usePaginatedResource';
import { renderHook } from '../helpers/renderHook';
type Item = {
id: string;
};
async function flushDebounce(delay = 250) {
await act(async () => {
vi.advanceTimersByTime(delay);
await Promise.resolve();
});
}
describe('usePaginatedResource', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it('loads first page after debounce and stores response fields', async () => {
const loadMock = vi.fn(async () => ({
items: [{ id: '1' }] as Item[],
page: 1,
pageSize: 10,
total: 25,
totalPages: 3,
}));
const { result } = renderHook(() =>
usePaginatedResource<Item>({
load: loadMock,
sort: '-createdAt',
}),
);
await flushDebounce();
expect(loadMock).toHaveBeenCalledWith({
q: '',
page: 1,
pageSize: 10,
sort: '-createdAt',
});
expect(result.current.items).toEqual([{ id: '1' }]);
expect(result.current.total).toBe(25);
expect(result.current.totalPages).toBe(3);
expect(result.current.isLoading).toBe(false);
expect(result.current.error).toBeNull();
});
it('resets page to 1 when query or page size changes', async () => {
const loadMock = vi.fn(
async ({ q, page, pageSize }: { q: string; page: number; pageSize: number }) => ({
items: [{ id: `${q}:${page}:${pageSize}` }] as Item[],
page,
pageSize,
total: 1,
totalPages: 1,
}),
);
const { result } = renderHook(() =>
usePaginatedResource<Item>({
load: loadMock,
}),
);
await flushDebounce();
act(() => {
result.current.setPage(2);
});
await flushDebounce();
expect(loadMock).toHaveBeenLastCalledWith({
q: '',
page: 2,
pageSize: 10,
sort: undefined,
});
act(() => {
result.current.setQuery('search');
});
await flushDebounce();
expect(loadMock).toHaveBeenLastCalledWith({
q: 'search',
page: 1,
pageSize: 10,
sort: undefined,
});
act(() => {
result.current.setPage(3);
});
await flushDebounce();
act(() => {
result.current.setPageSize(20);
});
await flushDebounce();
expect(loadMock).toHaveBeenLastCalledWith({
q: 'search',
page: 1,
pageSize: 20,
sort: undefined,
});
});
it('cancels stale debounce timers when inputs change quickly', async () => {
const loadMock = vi.fn(async () => ({
items: [] as Item[],
page: 1,
pageSize: 10,
total: 0,
totalPages: 0,
}));
const { result } = renderHook(() =>
usePaginatedResource<Item>({
load: loadMock,
debounceMs: 100,
}),
);
act(() => {
result.current.setQuery('latest');
});
await flushDebounce(100);
expect(loadMock).toHaveBeenCalledTimes(1);
expect(loadMock).toHaveBeenCalledWith({
q: 'latest',
page: 1,
pageSize: 10,
sort: undefined,
});
});
it('maps thrown errors into string error state', async () => {
const loadMock = vi
.fn()
.mockRejectedValueOnce(new Error('Load failed'))
.mockRejectedValueOnce('unknown');
const { result } = renderHook(() =>
usePaginatedResource<Item>({
load: loadMock,
}),
);
await flushDebounce();
expect(result.current.error).toBe('Load failed');
expect(result.current.isLoading).toBe(false);
act(() => {
result.current.setPage(2);
});
await flushDebounce();
expect(result.current.error).toBe('Request failed. Please try again.');
expect(result.current.isLoading).toBe(false);
});
});