import { strict as assert } from 'node:assert'; import { test } from 'node:test'; import { DEFAULT_PAGE_SIZE, PAGE_SIZES } from './usePaged'; /** * `usePaged` is a hook, and there is no React renderer in this suite — the repo * runs `tsx --test`, not jsdom. So the arithmetic it depends on is written here * as the pure function the hook applies, and asserted directly. * * That is worth doing rather than skipping: every bug this hook exists to * prevent is an arithmetic one at a boundary — an empty list, a list that * shrinks under a page, a page size that divides exactly. */ interface Slice { page: number; totalPages: number; from: number; to: number; rows: number[]; } /** Mirrors the derivation in `usePaged`, including the clamp. */ function slice(rows: readonly number[], requestedPage: number, pageSize: number): Slice { const total = rows.length; const totalPages = Math.max(1, Math.ceil(total / pageSize)); const page = Math.min(requestedPage, totalPages); return { page, totalPages, from: total === 0 ? 0 : (page - 1) * pageSize + 1, to: Math.min(page * pageSize, total), rows: rows.slice((page - 1) * pageSize, page * pageSize), }; } const upTo = (n: number) => Array.from({ length: n }, (_, i) => i + 1); test('a full first page', () => { const out = slice(upTo(96), 1, 25); assert.equal(out.totalPages, 4); assert.deepEqual([out.from, out.to], [1, 25]); assert.equal(out.rows[0], 1); assert.equal(out.rows.at(-1), 25); }); test('a middle page counts from the right place', () => { // The off-by-one everybody writes at least once. const out = slice(upTo(96), 3, 25); assert.deepEqual([out.from, out.to], [51, 75]); assert.equal(out.rows[0], 51); }); test('the last page is short, and says so', () => { const out = slice(upTo(96), 4, 25); assert.deepEqual([out.from, out.to], [76, 96]); assert.equal(out.rows.length, 21); }); test('an exact multiple does not produce a trailing empty page', () => { // 100 rows at 25 is four pages, not five. assert.equal(slice(upTo(100), 1, 25).totalPages, 4); }); test('no rows is one page reading 0–0, not zero pages', () => { /* `Math.ceil(0 / 25)` is 0, and a totalPages of 0 makes the pager render "page 1 of 0" and every control dead. One empty page is the honest shape. */ const out = slice([], 1, 25); assert.equal(out.totalPages, 1); assert.deepEqual([out.from, out.to], [0, 0]); assert.deepEqual(out.rows, []); }); test('a list that shrinks under you clamps instead of going blank', () => { /* The bug this hook exists for. Sitting on page 4, somebody narrows the filter to nine results. Slicing at the requested page would read rows 76–100 of a nine-row list and render an empty table with the controls saying page 4 — which looks exactly like the data vanished. */ const out = slice(upTo(9), 4, 25); assert.equal(out.page, 1, 'clamped to the last page that exists'); assert.equal(out.rows.length, 9); assert.deepEqual([out.from, out.to], [1, 9]); }); test('clamping lands on the LAST page, not always the first', () => { // 60 rows at 25 is three pages; from page 9 you belong on 3, not on 1. const out = slice(upTo(60), 9, 25); assert.equal(out.page, 3); assert.deepEqual([out.from, out.to], [51, 60]); }); test('every offered page size divides the work sensibly', () => { // Guards the constants themselves: a 0 or a negative here would make // totalPages Infinity and hang the pager. for (const size of PAGE_SIZES) { assert.ok(size > 0 && Number.isInteger(size), `${size} is a usable page size`); assert.equal(slice(upTo(100), 1, size).rows.length, Math.min(size, 100)); } assert.ok(PAGE_SIZES.includes(DEFAULT_PAGE_SIZE), 'the default is one of the choices'); });