pelagia-portal/App/tests/unit/pagination.test.ts
Claude (auto-fix) 5cefe8f7ed
All checks were successful
PR checks / checks (pull_request) Successful in 44s
PR checks / integration (pull_request) Successful in 32s
feat(history): paginate PO history with items-per-page control
The /history page fetched a fixed first 200 POs in one flat table with no
way to page further and no control over page size. Replace that with real
pagination:

- page.tsx: read page/perPage from searchParams; clamp perPage to 25/50/100
  (default 25) and page to [1, totalPages] via a new shared resolvePagination
  helper. Swap the fixed take:200 for skip/take + a count() for totals.
  Replace the "first 200 results" notice with a footer ("Showing X-Y of N",
  Prev/Next, page indicator) that preserves all filters. Export PDF/CSV links
  stay on the full filtered set.
- history-filters.tsx: add a Per-page dropdown; changing it or any filter
  resets to page 1 while preserving perPage in the URL.
- lib/pagination.ts: dependency-free clamp/skip/take helper, unit-tested.

Verified: type-check clean, 272 unit tests pass (9 new), skip/take windows
and clamping checked against the test DB.

Fixes #104
2026-06-24 03:26:47 +05:30

65 lines
2.3 KiB
TypeScript

import { describe, it, expect } from "vitest";
import { resolvePagination } from "@/lib/pagination";
const OPTIONS = [25, 50, 100];
const DEFAULT = 25;
function resolve(perPageParam: string | number | undefined, pageParam: string | number | undefined, total: number) {
return resolvePagination({ perPageParam, pageParam, total, options: OPTIONS, defaultPerPage: DEFAULT });
}
describe("resolvePagination", () => {
it("defaults perPage and page when params are missing", () => {
expect(resolve(undefined, undefined, 200)).toEqual({
perPage: 25,
page: 1,
totalPages: 8,
skip: 0,
take: 25,
});
});
it("accepts allowed page sizes", () => {
expect(resolve("50", "1", 200).perPage).toBe(50);
expect(resolve("100", "1", 200).perPage).toBe(100);
expect(resolve(50, "1", 200).perPage).toBe(50);
});
it("falls back to the default for disallowed or non-numeric page sizes", () => {
expect(resolve("10", "1", 200).perPage).toBe(25);
expect(resolve("999", "1", 200).perPage).toBe(25);
expect(resolve("abc", "1", 200).perPage).toBe(25);
expect(resolve("0", "1", 200).perPage).toBe(25);
});
it("computes skip/take for a middle page", () => {
const p = resolve("25", "3", 200);
expect(p).toMatchObject({ page: 3, skip: 50, take: 25, totalPages: 8 });
});
it("clamps a page beyond the last page to the last page", () => {
expect(resolve("25", "99", 200)).toMatchObject({ page: 8, totalPages: 8, skip: 175 });
});
it("clamps non-positive / non-numeric page to 1", () => {
expect(resolve("25", "0", 200).page).toBe(1);
expect(resolve("25", "-5", 200).page).toBe(1);
expect(resolve("25", "abc", 200).page).toBe(1);
expect(resolve("25", undefined, 200).page).toBe(1);
});
it("floors fractional page numbers", () => {
expect(resolve("25", "2.9", 200).page).toBe(2);
});
it("always yields at least one page, even with zero rows", () => {
expect(resolve("25", "1", 0)).toMatchObject({ page: 1, totalPages: 1, skip: 0 });
});
it("handles a partial final page", () => {
// 23 rows, 25 per page -> single page holding all rows
expect(resolve("25", "1", 23)).toMatchObject({ page: 1, totalPages: 1, take: 25 });
// 60 rows, 25 per page -> 3 pages, last page holds 10
expect(resolve("25", "3", 60)).toMatchObject({ page: 3, totalPages: 3, skip: 50 });
});
});