import { describe, expect, it } from "vitest"; import { paginate, paginateCursor } from "../db/pagination.js"; // --------------------------------------------------------------------------- // Test data // --------------------------------------------------------------------------- function makeItems(n: number) { return Array.from({ length: n }, (_, i) => ({ id: `id_${String(i + 1).padStart(3, "0")}`, name: `Item ${i + 1}`, })); } const ALL_ITEMS = makeItems(55); function mockFindMany(items: typeof ALL_ITEMS) { return async ({ skip, take }: { skip: number; take: number }) => items.slice(skip, skip + take); } function mockCount(items: typeof ALL_ITEMS) { return async () => items.length; } // --------------------------------------------------------------------------- // paginate() // --------------------------------------------------------------------------- describe("paginate", () => { it("returns first page with correct metadata", async () => { const result = await paginate( mockFindMany(ALL_ITEMS), mockCount(ALL_ITEMS), { page: 1, limit: 20 }, ); expect(result.items).toHaveLength(20); expect(result.total).toBe(55); expect(result.page).toBe(1); expect(result.limit).toBe(20); expect(result.nextCursor).toBe("id_020"); }); it("returns last page without nextCursor", async () => { const result = await paginate( mockFindMany(ALL_ITEMS), mockCount(ALL_ITEMS), { page: 3, limit: 20 }, ); expect(result.items).toHaveLength(15); expect(result.total).toBe(55); expect(result.nextCursor).toBeNull(); }); it("skips zero when cursor is provided", async () => { let capturedSkip = -1; const result = await paginate( async ({ skip, take }) => { capturedSkip = skip; return ALL_ITEMS.slice(skip, skip + take); }, mockCount(ALL_ITEMS), { page: 3, limit: 10, cursor: "irrelevant" }, ); expect(capturedSkip).toBe(0); // When cursor is given skip=0, so we get from the start of the pre-filtered data expect(result.items).toHaveLength(10); }); it("uses defaults when page and limit omitted", async () => { const result = await paginate( mockFindMany(ALL_ITEMS), mockCount(ALL_ITEMS), {}, ); expect(result.page).toBe(1); expect(result.limit).toBe(50); expect(result.items).toHaveLength(50); expect(result.nextCursor).toBe("id_050"); }); it("handles empty result set", async () => { const result = await paginate( mockFindMany([]), mockCount([]), { page: 1, limit: 20 }, ); expect(result.items).toHaveLength(0); expect(result.total).toBe(0); expect(result.nextCursor).toBeNull(); }); it("returns no nextCursor when items exactly equal limit", async () => { const items = makeItems(20); const result = await paginate( mockFindMany(items), mockCount(items), { page: 1, limit: 20 }, ); expect(result.items).toHaveLength(20); expect(result.nextCursor).toBeNull(); }); it("supports custom getId", async () => { const items = [{ uuid: "abc" }, { uuid: "def" }, { uuid: "ghi" }]; const result = await paginate( async ({ skip, take }) => items.slice(skip, skip + take), async () => items.length, { page: 1, limit: 2 }, (item) => item.uuid, ); expect(result.nextCursor).toBe("def"); expect(result.items).toHaveLength(2); }); }); // --------------------------------------------------------------------------- // paginateCursor() // --------------------------------------------------------------------------- describe("paginateCursor", () => { it("returns items and nextCursor when more exist", async () => { const result = await paginateCursor( async ({ take }) => ALL_ITEMS.slice(0, take), { limit: 10 }, ); expect(result.items).toHaveLength(10); expect(result.nextCursor).toBe("id_010"); }); it("returns null cursor when no more items", async () => { const items = makeItems(5); const result = await paginateCursor( async ({ take }) => items.slice(0, take), { limit: 10 }, ); expect(result.items).toHaveLength(5); expect(result.nextCursor).toBeNull(); }); it("handles empty result", async () => { const result = await paginateCursor( async () => [], { limit: 10 }, ); expect(result.items).toHaveLength(0); expect(result.nextCursor).toBeNull(); }); it("uses default limit", async () => { const result = await paginateCursor( async ({ take }) => ALL_ITEMS.slice(0, take), {}, ); expect(result.items).toHaveLength(50); expect(result.nextCursor).toBe("id_050"); }); });