Files
Classeo/frontend/tests/unit/lib/components/molecules/SearchInput/SearchInput.test.ts
Mathias STRASSER 76e16db0d8 feat: Pagination et recherche des sections admin
Les listes admin (utilisateurs, classes, matières, affectations) chargeaient
toutes les données d'un coup, ce qui dégradait l'expérience avec un volume
croissant. La pagination côté serveur existait dans la config API Platform
mais aucun Provider ne l'exploitait.

Cette implémentation ajoute la pagination serveur (30 items/page, max 100)
avec recherche textuelle sur toutes les sections, des composants frontend
réutilisables (Pagination + SearchInput avec debounce), et la synchronisation
URL pour le partage de liens filtrés.

Les Query valident leurs paramètres (clamp page/limit, trim search) pour
éviter les abus. Les affectations utilisent des lookup maps pour résoudre
les noms sans N+1 queries. Les pages admin gèrent les race conditions
via AbortController.
2026-02-15 13:54:51 +01:00

112 lines
3.1 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/svelte';
import SearchInput from '$lib/components/molecules/SearchInput/SearchInput.svelte';
describe('SearchInput', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it('renders input with default placeholder', () => {
render(SearchInput, {
props: { onSearch: vi.fn() }
});
expect(screen.getByPlaceholderText('Rechercher...')).toBeTruthy();
});
it('renders input with custom placeholder', () => {
render(SearchInput, {
props: { onSearch: vi.fn(), placeholder: 'Chercher un utilisateur...' }
});
expect(screen.getByPlaceholderText('Chercher un utilisateur...')).toBeTruthy();
});
it('debounces input and calls onSearch after delay', async () => {
const onSearch = vi.fn();
render(SearchInput, {
props: { onSearch, debounceMs: 300 }
});
const input = screen.getByRole('searchbox');
await fireEvent.input(input, { target: { value: 'test' } });
// Should not be called immediately
expect(onSearch).not.toHaveBeenCalled();
// Advance past the debounce time
vi.advanceTimersByTime(300);
expect(onSearch).toHaveBeenCalledOnce();
expect(onSearch).toHaveBeenCalledWith('test');
});
it('cancels pending debounce on new input', async () => {
const onSearch = vi.fn();
render(SearchInput, {
props: { onSearch, debounceMs: 300 }
});
const input = screen.getByRole('searchbox');
// Type first value
await fireEvent.input(input, { target: { value: 'te' } });
vi.advanceTimersByTime(200);
// Type second value before debounce fires
await fireEvent.input(input, { target: { value: 'test' } });
vi.advanceTimersByTime(300);
// Should only be called once with the final value
expect(onSearch).toHaveBeenCalledOnce();
expect(onSearch).toHaveBeenCalledWith('test');
});
it('clears input on Escape key', async () => {
const onSearch = vi.fn();
render(SearchInput, {
props: { value: 'initial', onSearch }
});
const input = screen.getByRole('searchbox');
await fireEvent.keyDown(input, { key: 'Escape' });
expect(onSearch).toHaveBeenCalledWith('');
});
it('calls onSearch immediately on clear button click', async () => {
const onSearch = vi.fn();
render(SearchInput, {
props: { value: 'something', onSearch }
});
const clearButton = screen.getByRole('button', { name: 'Effacer la recherche' });
await fireEvent.click(clearButton);
// Should be called immediately (no debounce)
expect(onSearch).toHaveBeenCalledWith('');
});
it('shows clear button only when input has value', () => {
const { container } = render(SearchInput, {
props: { onSearch: vi.fn() }
});
// No clear button initially (empty value)
expect(container.querySelector('.search-clear')).toBeNull();
});
it('shows clear button when initial value is provided', () => {
const { container } = render(SearchInput, {
props: { value: 'test', onSearch: vi.fn() }
});
expect(container.querySelector('.search-clear')).not.toBeNull();
});
});