Refonte front (packages/web) : coquille applicative partagée (sidebar desktop + barre d'onglets mobile + PageHeader, langue/push/logout centralisés) pilotée par route.meta.layout ; design system (BaseButton/BaseBadge/SegmentedControl/EmptyState/ SkeletonRow, anneau de focus, icônes @lucide/vue) ; tri/filtre/recherche/pagination côté client via useListControls (+ synchro URL) sur sessions/worktrees/groupes ; section « À traiter », palette de commandes ⌘K, toasts. Aucune modif serveur/protocole/DB.
88 lines
2.7 KiB
TypeScript
88 lines
2.7 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { ref } from 'vue';
|
|
import { useListControls, type FilterDef, type SortDef } from '../src/composables/useListControls';
|
|
|
|
interface Item {
|
|
n: number;
|
|
}
|
|
|
|
const sorts: SortDef<Item>[] = [
|
|
{ key: 'n', labelKey: 'n', compare: (a, b) => a.n - b.n, defaultDir: 'asc' },
|
|
];
|
|
const filters: FilterDef<Item>[] = [
|
|
{
|
|
key: 'parity',
|
|
labelKey: 'parity',
|
|
options: [
|
|
{ value: 'even', label: 'even' },
|
|
{ value: 'odd', label: 'odd' },
|
|
],
|
|
predicate: (it, active) =>
|
|
(active.has('even') && it.n % 2 === 0) || (active.has('odd') && it.n % 2 === 1),
|
|
},
|
|
];
|
|
|
|
function make() {
|
|
const source = ref<Item[]>([{ n: 5 }, { n: 2 }, { n: 4 }, { n: 1 }, { n: 3 }]);
|
|
const c = useListControls<Item>({ source: () => source.value, sorts, filters, defaultPageSize: 2 });
|
|
return { source, c };
|
|
}
|
|
|
|
describe('useListControls', () => {
|
|
it('trie en ascendant par défaut', () => {
|
|
const { c } = make();
|
|
expect(c.processed.value.map((i) => i.n)).toEqual([1, 2]); // page 1, taille 2
|
|
expect(c.total.value).toBe(5);
|
|
expect(c.pageCount.value).toBe(3);
|
|
});
|
|
|
|
it('inverse la direction au re-clic de la même clé', () => {
|
|
const { c } = make();
|
|
c.setSort('n'); // même clé → desc
|
|
expect(c.sortDir.value).toBe('desc');
|
|
expect(c.processed.value.map((i) => i.n)).toEqual([5, 4]);
|
|
});
|
|
|
|
it('pagine et borne la page', () => {
|
|
const { c } = make();
|
|
c.setPage(3);
|
|
expect(c.processed.value.map((i) => i.n)).toEqual([5]);
|
|
c.setPage(99);
|
|
expect(c.page.value).toBe(3); // borné à pageCount
|
|
});
|
|
|
|
it('filtre (OR intra-filtre) et revient page 1', () => {
|
|
const { c } = make();
|
|
c.setPage(2);
|
|
c.toggleFilter('parity', 'even');
|
|
expect(c.page.value).toBe(1);
|
|
expect(c.total.value).toBe(2); // 2 et 4
|
|
expect(c.processed.value.map((i) => i.n)).toEqual([2, 4]);
|
|
});
|
|
|
|
it('setSearch revient page 1, mais écrire search.value directement (hydratation) ne touche pas la page', () => {
|
|
const { c } = make();
|
|
c.setPage(3);
|
|
// hydratation URL : on écrit le ref directement → la page fournie par l'URL est préservée
|
|
c.search.value = '';
|
|
expect(c.page.value).toBe(3);
|
|
// saisie utilisateur via setSearch → retour page 1
|
|
c.setSearch('x');
|
|
expect(c.page.value).toBe(1);
|
|
});
|
|
|
|
it('pageSize 0 affiche tout', () => {
|
|
const { c } = make();
|
|
c.setPageSize(0);
|
|
expect(c.pageCount.value).toBe(1);
|
|
expect(c.processed.value.map((i) => i.n)).toEqual([1, 2, 3, 4, 5]);
|
|
});
|
|
|
|
it('reste réactif aux mutations de la source (upsert temps réel)', () => {
|
|
const { source, c } = make();
|
|
source.value = [...source.value, { n: 0 }];
|
|
expect(c.total.value).toBe(6);
|
|
expect(c.processed.value.map((i) => i.n)).toEqual([0, 1]);
|
|
});
|
|
});
|