Files
emdash-cms--emdash/packages/core/tests/unit/query-collection-offset.test.ts
wehub-resource-sync b3a7f98e5a
CI / E2E Cloudflare (4/8) (push) Failing after 0s
CI / E2E Cloudflare (8/8) (push) Failing after 1s
CI / Lint (push) Failing after 1s
Auto Extract / Extract (push) Failing after 4s
CI / Version Check (push) Failing after 9s
CI / Integration Tests (push) Failing after 1s
CI / E2E tests (1/8) (push) Failing after 1s
CI / E2E tests (2/8) (push) Failing after 2s
CI / E2E tests (3/8) (push) Failing after 2s
CI / Browser Tests (push) Failing after 1s
CI / E2E tests (5/8) (push) Failing after 1s
CI / E2E Cloudflare (2/8) (push) Failing after 2s
CI / Typecheck (push) Failing after 1s
CI / Changeset Validation (push) Failing after 2s
CI / E2E Cloudflare (5/8) (push) Failing after 1s
CI / E2E Cloudflare (6/8) (push) Failing after 1s
CI / E2E Cloudflare (7/8) (push) Failing after 1s
CodeQL / Analyze (javascript-typescript) (push) Failing after 1s
Format / Format (push) Failing after 0s
CodeQL / Analyze (actions) (push) Failing after 4s
CI / E2E tests (4/8) (push) Failing after 1s
CI / E2E tests (6/8) (push) Failing after 1s
CI / E2E tests (7/8) (push) Failing after 2s
CI / E2E tests (8/8) (push) Failing after 1s
CI / E2E Cloudflare (1/8) (push) Failing after 1s
CI / E2E Cloudflare (3/8) (push) Failing after 2s
Preview Releases / Publish Preview (push) Failing after 0s
zizmor / Run zizmor (push) Failing after 1s
Release / Release (push) Failing after 2s
CI / Smoke Tests (push) Failing after 5m36s
CI / Tests (push) Failing after 6m36s
Release / Sync Templates (push) Has been skipped
CI / E2E Tests (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:23:53 +08:00

117 lines
3.8 KiB
TypeScript

import type { Kysely } from "kysely";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { Database } from "../../src/database/types.js";
import type { CollectionFilter } from "../../src/query.js";
import { getEmDashCollection } from "../../src/query.js";
import { runWithContext } from "../../src/request-context.js";
import { setupTestDatabaseWithCollections, teardownTestDatabase } from "../utils/test-db.js";
vi.mock("astro:content", () => ({
getLiveCollection: vi.fn(),
}));
import { getLiveCollection } from "astro:content";
describe("getEmDashCollection offset pagination", () => {
let db: Kysely<Database>;
beforeEach(async () => {
db = await setupTestDatabaseWithCollections();
});
afterEach(async () => {
await teardownTestDatabase(db);
vi.mocked(getLiveCollection).mockReset();
});
function makeEntries(count: number) {
return Array.from({ length: count }, (_, i) => ({
id: `slug-${i + 1}`,
data: {
id: `db-id-${i + 1}`,
title: `Post ${i + 1}`,
createdAt: new Date(Date.UTC(2026, 0, 1, 0, 0, count - i)).toISOString(),
status: "published",
},
}));
}
async function run<T>(fn: () => Promise<T>) {
return runWithContext({ editMode: false, db }, fn);
}
it("forwards offset to the live loader", async () => {
vi.mocked(getLiveCollection).mockResolvedValue({
entries: makeEntries(3),
cacheHint: {},
} as any);
await run(() => getEmDashCollection("post", { limit: 20, offset: 40 }));
expect(getLiveCollection).toHaveBeenCalledTimes(1);
expect(vi.mocked(getLiveCollection).mock.calls[0]![1]).toMatchObject({ offset: 40 });
});
it("reports hasMore=true when the loader returns more than the requested limit", async () => {
// limit 20 over-fetches as 21; loader returns 21 ⇒ a next page exists.
vi.mocked(getLiveCollection).mockResolvedValue({
entries: makeEntries(21),
cacheHint: {},
} as any);
const result = await run(() => getEmDashCollection("post", { limit: 20, offset: 0 }));
expect(result.entries).toHaveLength(20);
expect(result.hasMore).toBe(true);
});
it("reports hasMore=false on the final page", async () => {
vi.mocked(getLiveCollection).mockResolvedValue({
entries: makeEntries(5),
cacheHint: {},
} as any);
const result = await run(() => getEmDashCollection("post", { limit: 20, offset: 40 }));
expect(result.entries).toHaveLength(5);
expect(result.hasMore).toBe(false);
});
it("leaves hasMore undefined when no limit is given", async () => {
vi.mocked(getLiveCollection).mockResolvedValue({
entries: makeEntries(5),
cacheHint: {},
} as any);
const result = await run(() => getEmDashCollection("post"));
expect(result.hasMore).toBeUndefined();
});
it("includes offset in the request-cache key so distinct pages don't collide", async () => {
vi.mocked(getLiveCollection)
.mockResolvedValueOnce({ entries: makeEntries(20), cacheHint: {} } as any)
.mockResolvedValueOnce({ entries: makeEntries(20), cacheHint: {} } as any);
await run(async () => {
await getEmDashCollection("post", { limit: 20, offset: 0 });
await getEmDashCollection("post", { limit: 20, offset: 20 });
});
expect(getLiveCollection).toHaveBeenCalledTimes(2);
const offsets = vi.mocked(getLiveCollection).mock.calls.map((c) => (c[1] as any).offset);
expect(offsets).toEqual([0, 20]);
});
it("rejects supplying both cursor and offset (compile-time)", () => {
// Each pagination mode is valid on its own...
const cursorOnly: CollectionFilter = { limit: 10, cursor: "abc" };
const offsetOnly: CollectionFilter = { limit: 10, offset: 20 };
// ...but they are mutually exclusive, so combining them is a type error.
// @ts-expect-error cursor and offset cannot be supplied together
const both: CollectionFilter = { limit: 10, cursor: "abc", offset: 20 };
expect([cursorOnly, offsetOnly, both]).toHaveLength(3);
});
});