105 lines
3.7 KiB
TypeScript
105 lines
3.7 KiB
TypeScript
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');
|
||
});
|