import { describe, it, expect } from 'vitest'; import { DEFAULT_PAGE_SIZE, PAGE_SIZE_OPTIONS, PAGE_SIZE_STORAGE_KEY, clampPage, pageFromSearchParams, pageSlice, readPageSize, readStoredPageSize, writeStoredPageSize } from '../../src/pagination'; describe('the page size a customer may choose', () => { it('defaults to 10, the conservative choice', () => { expect(DEFAULT_PAGE_SIZE).toBe(10); }); it('offers exactly the four sizes the issue settled on', () => { expect([...PAGE_SIZE_OPTIONS]).toEqual([10, 20, 50, 100]); }); it('accepts a size that is on the list', () => { expect(readPageSize('50')).toBe(50); }); // Anything else falls back rather than being honoured. A hand-edited value // of 5000 would render the whole catalogue, which is the thing this issue // exists to stop. it('falls back to the default for a size that is not on the list', () => { expect(readPageSize('5000')).toBe(DEFAULT_PAGE_SIZE); expect(readPageSize('7')).toBe(DEFAULT_PAGE_SIZE); }); it('falls back to the default for nonsense and for nothing at all', () => { expect(readPageSize('abc')).toBe(DEFAULT_PAGE_SIZE); expect(readPageSize('')).toBe(DEFAULT_PAGE_SIZE); expect(readPageSize(null)).toBe(DEFAULT_PAGE_SIZE); }); }); describe('the page number in the URL', () => { it('is 1 when the parameter is absent', () => { expect(pageFromSearchParams(new URLSearchParams())).toBe(1); }); it('reads a sensible page', () => { expect(pageFromSearchParams(new URLSearchParams('page=3'))).toBe(3); }); // A mangled link falls back to the first page rather than to an empty view, // the same rule filtersFromSearchParams follows for a mangled filter. it('falls back to 1 for zero, negatives and nonsense', () => { expect(pageFromSearchParams(new URLSearchParams('page=0'))).toBe(1); expect(pageFromSearchParams(new URLSearchParams('page=-4'))).toBe(1); expect(pageFromSearchParams(new URLSearchParams('page=abc'))).toBe(1); expect(pageFromSearchParams(new URLSearchParams('page=2.5'))).toBe(1); }); }); describe('keeping a page number inside the result set', () => { // The case that matters: someone shares a link to page 7, the catalogue // shrinks, and the link must show the last page rather than nothing at all. it('clamps a page past the end back to the last page', () => { expect(clampPage(7, 25, 10)).toBe(3); }); it('leaves a page inside the range alone', () => { expect(clampPage(2, 25, 10)).toBe(2); }); // An empty result set still has a first page to show the empty state on. it('is 1 when there is nothing to show', () => { expect(clampPage(3, 0, 10)).toBe(1); }); it('is 1 when everything fits on one page', () => { expect(clampPage(4, 6, 10)).toBe(1); }); }); describe('taking one page out of the items', () => { const items = Array.from({ length: 25 }, (_, i) => i + 1); it('takes the first page', () => { expect(pageSlice(items, 1, 10)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); }); it('takes a middle page', () => { expect(pageSlice(items, 2, 10)).toEqual([11, 12, 13, 14, 15, 16, 17, 18, 19, 20]); }); it('takes a short last page', () => { expect(pageSlice(items, 3, 10)).toEqual([21, 22, 23, 24, 25]); }); it('is empty past the end rather than throwing', () => { expect(pageSlice(items, 9, 10)).toEqual([]); }); }); /** A Storage that is present and works. */ function fakeStorage(initial: Record = {}) { const data = { ...initial }; return { getItem: (key: string) => data[key] ?? null, setItem: (key: string, value: string) => { data[key] = value; }, read: () => data }; } describe('remembering the choice', () => { it('reads a stored size', () => { expect(readStoredPageSize(fakeStorage({ [PAGE_SIZE_STORAGE_KEY]: '20' }))).toBe(20); }); it('defaults when nothing was stored', () => { expect(readStoredPageSize(fakeStorage())).toBe(DEFAULT_PAGE_SIZE); }); it('writes the choice under the agreed key', () => { const storage = fakeStorage(); writeStoredPageSize(storage, 50); expect(storage.read()[PAGE_SIZE_STORAGE_KEY]).toBe('50'); }); // Storage is absent during server rendering and throws outright in some // privacy modes. Neither may take the catalogue down over a preference. it('defaults rather than throwing when storage is missing', () => { expect(readStoredPageSize(null)).toBe(DEFAULT_PAGE_SIZE); }); it('defaults rather than throwing when storage refuses to be read', () => { const hostile = { getItem: () => { throw new Error('access denied'); } }; expect(readStoredPageSize(hostile)).toBe(DEFAULT_PAGE_SIZE); }); it('gives up quietly when storage refuses to be written', () => { const hostile = { setItem: () => { throw new Error('quota exceeded'); } }; expect(() => writeStoredPageSize(hostile, 20)).not.toThrow(); }); it('does nothing when there is no storage to write to', () => { expect(() => writeStoredPageSize(null, 20)).not.toThrow(); }); });