import { describe, it, expect } from 'vitest'; import type { MangaSort, SortOrder } from './api/mangas'; import { DEFAULT_SORT, defaultOrderFor, isTextField, dirOptions, dirLabel, sortLabel, coerceSort, coerceOrder, sortUrlParams } from './mangaSort'; const FIELDS: MangaSort[] = ['created', 'updated', 'title', 'author']; const ORDERS: SortOrder[] = ['asc', 'desc']; describe('mangaSort', () => { it('classifies text vs date fields', () => { expect(isTextField('title')).toBe(true); expect(isTextField('author')).toBe(true); expect(isTextField('created')).toBe(false); expect(isTextField('updated')).toBe(false); }); it('defaults date fields to desc and text fields to asc', () => { expect(defaultOrderFor('created')).toBe('desc'); expect(defaultOrderFor('updated')).toBe('desc'); expect(defaultOrderFor('title')).toBe('asc'); expect(defaultOrderFor('author')).toBe('asc'); }); it('labels the direction toggle per field type', () => { expect(dirOptions('title').map((o) => o.label)).toEqual(['A→Z', 'Z→A']); expect(dirOptions('updated').map((o) => o.label)).toEqual(['Newest', 'Oldest']); }); it('dirLabel never returns empty for a valid order', () => { for (const f of FIELDS) { for (const o of ORDERS) { expect(dirLabel(f, o)).not.toBe(''); } } }); it('composes a readable sort label without a dangling separator', () => { expect(sortLabel('updated', 'desc')).toBe('Last updated · Newest'); expect(sortLabel('title', 'asc')).toBe('Title · A→Z'); expect(sortLabel('author', 'desc')).toBe('Author · Z→A'); }); it('coerceSort accepts valid fields and falls back to the default otherwise', () => { for (const f of FIELDS) expect(coerceSort(f)).toBe(f); expect(coerceSort('bogus')).toBe(DEFAULT_SORT); expect(coerceSort(null)).toBe(DEFAULT_SORT); }); it('coerceSort honors the legacy `recent` alias (== created), matching the API', () => { // The backend maps sort=recent -> created; the browser must agree so a // pasted/legacy `/?sort=recent` URL resolves to the same field. expect(coerceSort('recent')).toBe('created'); }); it('coerceOrder accepts asc/desc and otherwise uses the field default', () => { expect(coerceOrder('asc', 'title')).toBe('asc'); expect(coerceOrder('desc', 'title')).toBe('desc'); // Absent/invalid → field's natural default. expect(coerceOrder(null, 'title')).toBe('asc'); expect(coerceOrder('sideways', 'updated')).toBe('desc'); }); it('omits sort/order from the URL when they equal the defaults', () => { expect(sortUrlParams('updated', 'desc')).toEqual({}); // Title at its natural default (asc) → only the field is persisted. expect(sortUrlParams('title', 'asc')).toEqual({ sort: 'title' }); // Non-default direction is persisted explicitly. expect(sortUrlParams('title', 'desc')).toEqual({ sort: 'title', order: 'desc' }); expect(sortUrlParams('updated', 'asc')).toEqual({ order: 'asc' }); }); it('round-trips every field/order combo losslessly through the URL', () => { for (const sort of FIELDS) { for (const order of ORDERS) { const params = sortUrlParams(sort, order); const back = coerceSort(params.sort ?? null); const backOrder = coerceOrder(params.order ?? null, back); expect({ sort: back, order: backOrder }).toEqual({ sort, order }); } } }); });