import { describe, expect, it } from "@effect/vitest";import type { IntegrationSlug } from "@executor-js/sdk/shared"; import { adminUserCopyableEmail, adminUserTitle, connectionHealthStatus, connectLinkUrl, formatLastSeen, integrationConnectionStates, isConnectableIntegration, isLocalSubject, lastSeenTitle, pageNumber, shortenExternalId, splitPage, type AdminCatalogRow, type AdminConnectionRow,} from "./admin-users-display"; const slug = (value: string): IntegrationSlug => value as IntegrationSlug; /** A catalog row for a normal, connectable integration. `kind` is the owning * plugin's key on a real row, so "openapi" is what the catalog actually says. */const catalogRow = (value: string, kind = "openapi"): AdminCatalogRow => ({ slug: slug(value), kind,}); /** The built-in Executor row, exactly as a real catalog read returns it: * `kind: "built-in"` with `authMethods: []` (verified against * `executor.integrations.list()`). */const builtInRow: AdminCatalogRow = { slug: slug("executor"), kind: "built-in" }; const connection = (overrides: Partial<AdminConnectionRow> = {}): AdminConnectionRow => ({ owner: "user", integration: slug("acme"), name: "default", oauthScope: null, lastHealth: null, ...overrides,}); describe("formatLastSeen", () => { const now = Date.UTC(2026, 6, 27, 12, 0, 0); it("never seen reads as Never, not as a stale timestamp", () => { expect(formatLastSeen(null, now)).toBe("Never"); }); // The writer throttles lastSeenAt to ~1h, so anything inside that window is // one bucket. Reporting minutes would claim precision the field doesn't have. it("collapses everything inside the throttle window to one honest bucket", () => { expect(formatLastSeen(now - 60_000, now)).toBe("Within the hour"); expect(formatLastSeen(now - 45 * 60_000, now)).toBe("Within the hour"); expect(formatLastSeen(now, now)).toBe("Within the hour"); }); it("counts hours and days above the window, hedged with About", () => { expect(formatLastSeen(now - 60 * 60_000, now)).toBe("About 1 hour ago"); expect(formatLastSeen(now - 5 * 60 * 60_000, now)).toBe("About 5 hours ago"); expect(formatLastSeen(now - 24 * 60 * 60_000, now)).toBe("About 1 day ago"); expect(formatLastSeen(now - 9 * 24 * 60 * 60_000, now)).toBe("About 9 days ago"); }); it("falls back to an absolute date once relative wording stops being useful", () => { expect(formatLastSeen(now - 200 * 24 * 60 * 60_000, now)).not.toContain("ago"); }); it("the tooltip carries the exact instant and the coarseness caveat", () => { expect(lastSeenTitle(null)).toBe("Never seen on a request"); expect(lastSeenTitle(now)).toContain("at most once an hour"); });}); describe("externalId display", () => { it("recognizes the single-player host sentinel", () => { expect(isLocalSubject("local")).toBe(true); expect(isLocalSubject("user_01JABCDEF")).toBe(false); }); it("leaves short ids exactly as the server sent them", () => { expect(shortenExternalId("user_01JAB")).toBe("user_01JAB"); }); // Truncation is visual only — the id is opaque, so both ends are kept and two // ids differing in the middle stay distinguishable. it("shortens long ids from the middle, keeping both ends", () => { const long = "user_01JABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; const short = shortenExternalId(long, 21); expect(short.length).toBe(21); expect(short.startsWith("user_01JAB")).toBe(true); expect(short.endsWith("0123456789")).toBe(true); expect(short).toContain("…"); });}); describe("adminUserTitle", () => { // The email is what an operator invites, searches and supports against, so it // leads — and the opaque id drops to the supporting line rather than vanishing. it("leads with the email and keeps the id as the supporting line", () => { expect( adminUserTitle({ externalId: "user_01JAB", email: "ada@e2e.test", displayName: "Ada L" }), ).toEqual({ primary: "ada@e2e.test", secondary: "user_01JAB", primaryIsId: false }); }); it("falls back to the display name when the host resolved no email", () => { expect(adminUserTitle({ externalId: "user_01JAB", email: null, displayName: "Ada L" })).toEqual( { primary: "Ada L", secondary: "user_01JAB", primaryIsId: false }, ); }); // The pre-identity rendering, unchanged: one mono id line and no subtitle. A // row the host cannot name must not degrade to a gap where a name would be. it("renders exactly the old id-only row when the host resolved neither", () => { expect(adminUserTitle({ externalId: "user_01JAB", email: null, displayName: null })).toEqual({ primary: "user_01JAB", secondary: null, primaryIsId: true, }); }); // A member who left the org while their connections remain arrives with the // fields simply missing rather than explicitly null. it("treats missing identity fields the same as null ones", () => { expect(adminUserTitle({ externalId: "user_01JAB" }).primaryIsId).toBe(true); }); // A directory that stores empty strings must not produce a blank headline. it("does not let a blank email or name become the headline", () => { expect(adminUserTitle({ externalId: "user_01JAB", email: " ", displayName: "" })).toEqual({ primary: "user_01JAB", secondary: null, primaryIsId: true, }); }); it("spells out the single-player host sentinel, which names no member", () => { expect(adminUserTitle({ externalId: "local", email: null, displayName: null })).toEqual({ primary: "local", secondary: null, primaryIsId: true, }); });}); describe("adminUserCopyableEmail", () => { it("offers the email when the host resolved one", () => { expect(adminUserCopyableEmail({ externalId: "user_01JAB", email: "ada@e2e.test" })).toBe( "ada@e2e.test", ); }); // A name is not an address: a copy button that yields something unusable is // worse than no button, so a name-only row offers nothing to copy. it("offers nothing when only a display name is known", () => { expect( adminUserCopyableEmail({ externalId: "user_01JAB", email: null, displayName: "Ada L" }), ).toBeNull(); }); it("treats a blank email as absent", () => { expect(adminUserCopyableEmail({ externalId: "user_01JAB", email: " " })).toBeNull(); });}); describe("connectionHealthStatus", () => { it("reads the stored verdict", () => { expect(connectionHealthStatus(connection({ lastHealth: { status: "expired" } }))).toBe( "expired", ); }); it("a never-probed connection is unchecked, not healthy", () => { expect(connectionHealthStatus(connection({ lastHealth: null }))).toBe("unknown"); }); it("a tool-sync stamp is catalog state, not a degraded connection", () => { // Same rule as the console's `presentableHealth`: one failed sync sweep // must not paint an operator's view of a user's connections amber. expect( connectionHealthStatus( connection({ lastHealth: { status: "degraded", reason: "tool_sync_failed" } }), ), ).toBe("unknown"); }); it("a genuine degraded probe verdict still reads degraded", () => { expect( connectionHealthStatus( connection({ lastHealth: { status: "degraded", reason: "probe_timeout" } }), ), ).toBe("degraded"); });}); describe("isConnectableIntegration", () => { it("a built-in integration has no connect flow to send anyone to", () => { expect(isConnectableIntegration(builtInRow)).toBe(false); // The product page pins the reserved slug as well as the kind // (`integration-detail.tsx`), so both spellings are refused. expect(isConnectableIntegration({ slug: slug("executor"), kind: "openapi" })).toBe(false); }); // The trap: an openapi/mcp/graphql integration can carry `authMethods: []` // and STILL be connectable, because its accounts panel supplies // `createCustomMethod`. Empty auth methods must never be read as "no connect // flow" — only `built-in` means that. it("a normal integration stays connectable regardless of declared auth methods", () => { expect(isConnectableIntegration(catalogRow("acme"))).toBe(true); expect(isConnectableIntegration(catalogRow("beta", "mcp"))).toBe(true); expect(isConnectableIntegration(catalogRow("gamma", "graphql"))).toBe(true); });}); describe("integrationConnectionStates", () => { it("shows the whole catalog, marking what this user has connected", () => { const states = integrationConnectionStates( [catalogRow("acme"), catalogRow("beta"), catalogRow("gamma")], [connection({ integration: slug("beta") })], ); expect(states.map((state) => state.integration)).toEqual(["beta", "acme", "gamma"]); expect(states.map((state) => state.connected)).toEqual([true, false, false]); }); it("keeps an available integration listed even when nobody connected it", () => { const states = integrationConnectionStates([catalogRow("acme")], []); expect(states).toEqual([{ integration: "acme", connected: false, connections: [] }]); }); // The reported bug: the built-in Executor integration appeared under "not // connected" with a copyable link to a flow that does not exist. It is not a // slot, not a denominator, and not a link. it("drops a non-connectable integration from the view and the counts entirely", () => { const states = integrationConnectionStates( [catalogRow("acme"), builtInRow], [connection({ integration: slug("acme") })], ); expect(states.map((state) => state.integration)).toEqual(["acme"]); }); it("a catalog of nothing but non-connectable integrations yields no slots at all", () => { expect(integrationConnectionStates([builtInRow], [])).toEqual([]); }); // Data honesty beats the filter: if a user somehow HOLDS a connection on a // non-connectable integration, hiding it would misreport what they have. it("still reports a held connection on a non-connectable integration, as connected", () => { const states = integrationConnectionStates( [catalogRow("acme"), builtInRow], [connection({ integration: slug("executor"), name: "coreTools" })], ); expect(states.map((state) => state.integration)).toEqual(["executor", "acme"]); expect(states.map((state) => state.connected)).toEqual([true, false]); }); // Dropping the user's own credential because the catalog read missed it would // under-report what they actually hold. it("still reports a connection whose integration is missing from the catalog", () => { const states = integrationConnectionStates([], [connection({ integration: slug("ghost") })]); expect(states.map((state) => state.integration)).toEqual(["ghost"]); expect(states[0]?.connected).toBe(true); }); it("groups every connection a user holds on one integration", () => { const states = integrationConnectionStates( [catalogRow("acme")], [ connection({ integration: slug("acme"), name: "work" }), connection({ integration: slug("acme"), name: "personal" }), ], ); expect(states[0]?.connections.map((row) => row.name)).toEqual(["work", "personal"]); }); it("orders connected first, then alphabetically, so the order is stable per user", () => { const states = integrationConnectionStates( [catalogRow("zulu"), catalogRow("alpha"), catalogRow("mike")], [connection({ integration: slug("zulu") })], ); expect(states.map((state) => state.integration)).toEqual(["zulu", "alpha", "mike"]); });}); describe("connectLinkUrl", () => { // The org segment is the SENDING admin's. A recipient in several orgs would // otherwise land in whichever their session defaults to and connect the // credential in the wrong workspace — succeeding, silently, in the wrong place. it("carries the sending admin's org so a multi-org recipient lands in the right one", () => { expect(connectLinkUrl(slug("acme"), "https://console.example.com", "north-wind")).toBe( "https://console.example.com/north-wind/connect/acme", ); }); // Self-host and local have no orgs, so there is no segment to carry and the // bare form is the whole route. it("builds the bare form on a host with no org slug", () => { expect(connectLinkUrl(slug("acme"), "https://console.example.com")).toBe( "https://console.example.com/connect/acme", ); expect(connectLinkUrl(slug("acme"), "https://console.example.com", undefined)).toBe( "https://console.example.com/connect/acme", ); // An empty or whitespace slug is absence, not an empty path segment — it // must never render "//connect/acme". expect(connectLinkUrl(slug("acme"), "https://console.example.com", "")).toBe( "https://console.example.com/connect/acme", ); expect(connectLinkUrl(slug("acme"), "https://console.example.com", " ")).toBe( "https://console.example.com/connect/acme", ); }); it("does not double the separator when the origin carries a trailing slash", () => { expect(connectLinkUrl(slug("acme"), "https://console.example.com/")).toBe( "https://console.example.com/connect/acme", ); expect(connectLinkUrl(slug("acme"), "https://console.example.com/", "north-wind")).toBe( "https://console.example.com/north-wind/connect/acme", ); });}); describe("paging", () => { // The contract returns no total, so the page over-fetches by one row and the // extra row IS the answer to "is there a next page". it("an over-fetched page yields the page's rows plus a next-page signal", () => { expect(splitPage([1, 2, 3, 4], 3)).toEqual({ rows: [1, 2, 3], hasNext: true }); }); it("a page that came back short is the last page", () => { expect(splitPage([1, 2], 3)).toEqual({ rows: [1, 2], hasNext: false }); }); it("an exactly-full page without the extra row is also the last page", () => { expect(splitPage([1, 2, 3], 3)).toEqual({ rows: [1, 2, 3], hasNext: false }); }); it("numbers pages from one", () => { expect(pageNumber(0, 25)).toBe(1); expect(pageNumber(25, 25)).toBe(2); expect(pageNumber(75, 25)).toBe(4); });});
import { describe, expect, it } from "@effect/vitest";
import type { IntegrationSlug } from "@executor-js/sdk/shared";
import {
adminUserCopyableEmail,
adminUserTitle,
connectionHealthStatus,
connectLinkUrl,
formatLastSeen,
integrationConnectionStates,
isConnectableIntegration,
isLocalSubject,
lastSeenTitle,
pageNumber,
shortenExternalId,
splitPage,
type AdminCatalogRow,
type AdminConnectionRow,
} from "./admin-users-display";
const slug = (value: string): IntegrationSlug => value as IntegrationSlug;
/** A catalog row for a normal, connectable integration. `kind` is the owning
* plugin's key on a real row, so "openapi" is what the catalog actually says. */
const catalogRow = (value: string, kind = "openapi"): AdminCatalogRow => ({
slug: slug(value),
kind,
});
/** The built-in Executor row, exactly as a real catalog read returns it:
* `kind: "built-in"` with `authMethods: []` (verified against
* `executor.integrations.list()`). */
const builtInRow: AdminCatalogRow = { slug: slug("executor"), kind: "built-in" };
const connection = (overrides: Partial<AdminConnectionRow> = {}): AdminConnectionRow => ({
owner: "user",
integration: slug("acme"),
name: "default",
oauthScope: null,
lastHealth: null,
...overrides,
});
describe("formatLastSeen", () => {
const now = Date.UTC(2026, 6, 27, 12, 0, 0);
it("never seen reads as Never, not as a stale timestamp", () => {
expect(formatLastSeen(null, now)).toBe("Never");
});
// The writer throttles lastSeenAt to ~1h, so anything inside that window is
// one bucket. Reporting minutes would claim precision the field doesn't have.
it("collapses everything inside the throttle window to one honest bucket", () => {
expect(formatLastSeen(now - 60_000, now)).toBe("Within the hour");
expect(formatLastSeen(now - 45 * 60_000, now)).toBe("Within the hour");
expect(formatLastSeen(now, now)).toBe("Within the hour");
});
it("counts hours and days above the window, hedged with About", () => {
expect(formatLastSeen(now - 60 * 60_000, now)).toBe("About 1 hour ago");
expect(formatLastSeen(now - 5 * 60 * 60_000, now)).toBe("About 5 hours ago");
expect(formatLastSeen(now - 24 * 60 * 60_000, now)).toBe("About 1 day ago");
expect(formatLastSeen(now - 9 * 24 * 60 * 60_000, now)).toBe("About 9 days ago");
});
it("falls back to an absolute date once relative wording stops being useful", () => {
expect(formatLastSeen(now - 200 * 24 * 60 * 60_000, now)).not.toContain("ago");
});
it("the tooltip carries the exact instant and the coarseness caveat", () => {
expect(lastSeenTitle(null)).toBe("Never seen on a request");
expect(lastSeenTitle(now)).toContain("at most once an hour");
});
});
describe("externalId display", () => {
it("recognizes the single-player host sentinel", () => {
expect(isLocalSubject("local")).toBe(true);
expect(isLocalSubject("user_01JABCDEF")).toBe(false);
});
it("leaves short ids exactly as the server sent them", () => {
expect(shortenExternalId("user_01JAB")).toBe("user_01JAB");
});
// Truncation is visual only — the id is opaque, so both ends are kept and two
// ids differing in the middle stay distinguishable.
it("shortens long ids from the middle, keeping both ends", () => {
const long = "user_01JABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
const short = shortenExternalId(long, 21);
expect(short.length).toBe(21);
expect(short.startsWith("user_01JAB")).toBe(true);
expect(short.endsWith("0123456789")).toBe(true);
expect(short).toContain("…");
});
});
describe("adminUserTitle", () => {
// The email is what an operator invites, searches and supports against, so it
// leads — and the opaque id drops to the supporting line rather than vanishing.
it("leads with the email and keeps the id as the supporting line", () => {
expect(
adminUserTitle({ externalId: "user_01JAB", email: "ada@e2e.test", displayName: "Ada L" }),
).toEqual({ primary: "ada@e2e.test", secondary: "user_01JAB", primaryIsId: false });
});
it("falls back to the display name when the host resolved no email", () => {
expect(adminUserTitle({ externalId: "user_01JAB", email: null, displayName: "Ada L" })).toEqual(
{ primary: "Ada L", secondary: "user_01JAB", primaryIsId: false },
);
});
// The pre-identity rendering, unchanged: one mono id line and no subtitle. A
// row the host cannot name must not degrade to a gap where a name would be.
it("renders exactly the old id-only row when the host resolved neither", () => {
expect(adminUserTitle({ externalId: "user_01JAB", email: null, displayName: null })).toEqual({
primary: "user_01JAB",
secondary: null,
primaryIsId: true,
});
});
// A member who left the org while their connections remain arrives with the
// fields simply missing rather than explicitly null.
it("treats missing identity fields the same as null ones", () => {
expect(adminUserTitle({ externalId: "user_01JAB" }).primaryIsId).toBe(true);
});
// A directory that stores empty strings must not produce a blank headline.
it("does not let a blank email or name become the headline", () => {
expect(adminUserTitle({ externalId: "user_01JAB", email: " ", displayName: "" })).toEqual({
primary: "user_01JAB",
secondary: null,
primaryIsId: true,
});
});
it("spells out the single-player host sentinel, which names no member", () => {
expect(adminUserTitle({ externalId: "local", email: null, displayName: null })).toEqual({
primary: "local",
secondary: null,
primaryIsId: true,
});
});
});
describe("adminUserCopyableEmail", () => {
it("offers the email when the host resolved one", () => {
expect(adminUserCopyableEmail({ externalId: "user_01JAB", email: "ada@e2e.test" })).toBe(
"ada@e2e.test",
);
});
// A name is not an address: a copy button that yields something unusable is
// worse than no button, so a name-only row offers nothing to copy.
it("offers nothing when only a display name is known", () => {
expect(
adminUserCopyableEmail({ externalId: "user_01JAB", email: null, displayName: "Ada L" }),
).toBeNull();
});
it("treats a blank email as absent", () => {
expect(adminUserCopyableEmail({ externalId: "user_01JAB", email: " " })).toBeNull();
});
});
describe("connectionHealthStatus", () => {
it("reads the stored verdict", () => {
expect(connectionHealthStatus(connection({ lastHealth: { status: "expired" } }))).toBe(
"expired",
);
});
it("a never-probed connection is unchecked, not healthy", () => {
expect(connectionHealthStatus(connection({ lastHealth: null }))).toBe("unknown");
});
it("a tool-sync stamp is catalog state, not a degraded connection", () => {
// Same rule as the console's `presentableHealth`: one failed sync sweep
// must not paint an operator's view of a user's connections amber.
expect(
connectionHealthStatus(
connection({ lastHealth: { status: "degraded", reason: "tool_sync_failed" } }),
),
).toBe("unknown");
});
it("a genuine degraded probe verdict still reads degraded", () => {
expect(
connectionHealthStatus(
connection({ lastHealth: { status: "degraded", reason: "probe_timeout" } }),
),
).toBe("degraded");
});
});
describe("isConnectableIntegration", () => {
it("a built-in integration has no connect flow to send anyone to", () => {
expect(isConnectableIntegration(builtInRow)).toBe(false);
// The product page pins the reserved slug as well as the kind
// (`integration-detail.tsx`), so both spellings are refused.
expect(isConnectableIntegration({ slug: slug("executor"), kind: "openapi" })).toBe(false);
});
// The trap: an openapi/mcp/graphql integration can carry `authMethods: []`
// and STILL be connectable, because its accounts panel supplies
// `createCustomMethod`. Empty auth methods must never be read as "no connect
// flow" — only `built-in` means that.
it("a normal integration stays connectable regardless of declared auth methods", () => {
expect(isConnectableIntegration(catalogRow("acme"))).toBe(true);
expect(isConnectableIntegration(catalogRow("beta", "mcp"))).toBe(true);
expect(isConnectableIntegration(catalogRow("gamma", "graphql"))).toBe(true);
});
});
describe("integrationConnectionStates", () => {
it("shows the whole catalog, marking what this user has connected", () => {
const states = integrationConnectionStates(
[catalogRow("acme"), catalogRow("beta"), catalogRow("gamma")],
[connection({ integration: slug("beta") })],
);
expect(states.map((state) => state.integration)).toEqual(["beta", "acme", "gamma"]);
expect(states.map((state) => state.connected)).toEqual([true, false, false]);
});
it("keeps an available integration listed even when nobody connected it", () => {
const states = integrationConnectionStates([catalogRow("acme")], []);
expect(states).toEqual([{ integration: "acme", connected: false, connections: [] }]);
});
// The reported bug: the built-in Executor integration appeared under "not
// connected" with a copyable link to a flow that does not exist. It is not a
// slot, not a denominator, and not a link.
it("drops a non-connectable integration from the view and the counts entirely", () => {
const states = integrationConnectionStates(
[catalogRow("acme"), builtInRow],
[connection({ integration: slug("acme") })],
);
expect(states.map((state) => state.integration)).toEqual(["acme"]);
});
it("a catalog of nothing but non-connectable integrations yields no slots at all", () => {
expect(integrationConnectionStates([builtInRow], [])).toEqual([]);
});
// Data honesty beats the filter: if a user somehow HOLDS a connection on a
// non-connectable integration, hiding it would misreport what they have.
it("still reports a held connection on a non-connectable integration, as connected", () => {
const states = integrationConnectionStates(
[catalogRow("acme"), builtInRow],
[connection({ integration: slug("executor"), name: "coreTools" })],
);
expect(states.map((state) => state.integration)).toEqual(["executor", "acme"]);
expect(states.map((state) => state.connected)).toEqual([true, false]);
});
// Dropping the user's own credential because the catalog read missed it would
// under-report what they actually hold.
it("still reports a connection whose integration is missing from the catalog", () => {
const states = integrationConnectionStates([], [connection({ integration: slug("ghost") })]);
expect(states.map((state) => state.integration)).toEqual(["ghost"]);
expect(states[0]?.connected).toBe(true);
});
it("groups every connection a user holds on one integration", () => {
const states = integrationConnectionStates(
[catalogRow("acme")],
[
connection({ integration: slug("acme"), name: "work" }),
connection({ integration: slug("acme"), name: "personal" }),
],
);
expect(states[0]?.connections.map((row) => row.name)).toEqual(["work", "personal"]);
});
it("orders connected first, then alphabetically, so the order is stable per user", () => {
const states = integrationConnectionStates(
[catalogRow("zulu"), catalogRow("alpha"), catalogRow("mike")],
[connection({ integration: slug("zulu") })],
);
expect(states.map((state) => state.integration)).toEqual(["zulu", "alpha", "mike"]);
});
});
describe("connectLinkUrl", () => {
// The org segment is the SENDING admin's. A recipient in several orgs would
// otherwise land in whichever their session defaults to and connect the
// credential in the wrong workspace — succeeding, silently, in the wrong place.
it("carries the sending admin's org so a multi-org recipient lands in the right one", () => {
expect(connectLinkUrl(slug("acme"), "https://console.example.com", "north-wind")).toBe(
"https://console.example.com/north-wind/connect/acme",
);
});
// Self-host and local have no orgs, so there is no segment to carry and the
// bare form is the whole route.
it("builds the bare form on a host with no org slug", () => {
expect(connectLinkUrl(slug("acme"), "https://console.example.com")).toBe(
"https://console.example.com/connect/acme",
);
expect(connectLinkUrl(slug("acme"), "https://console.example.com", undefined)).toBe(
"https://console.example.com/connect/acme",
);
// An empty or whitespace slug is absence, not an empty path segment — it
// must never render "//connect/acme".
expect(connectLinkUrl(slug("acme"), "https://console.example.com", "")).toBe(
"https://console.example.com/connect/acme",
);
expect(connectLinkUrl(slug("acme"), "https://console.example.com", " ")).toBe(
"https://console.example.com/connect/acme",
);
});
it("does not double the separator when the origin carries a trailing slash", () => {
expect(connectLinkUrl(slug("acme"), "https://console.example.com/")).toBe(
"https://console.example.com/connect/acme",
);
expect(connectLinkUrl(slug("acme"), "https://console.example.com/", "north-wind")).toBe(
"https://console.example.com/north-wind/connect/acme",
);
});
});
describe("paging", () => {
// The contract returns no total, so the page over-fetches by one row and the
// extra row IS the answer to "is there a next page".
it("an over-fetched page yields the page's rows plus a next-page signal", () => {
expect(splitPage([1, 2, 3, 4], 3)).toEqual({ rows: [1, 2, 3], hasNext: true });
});
it("a page that came back short is the last page", () => {
expect(splitPage([1, 2], 3)).toEqual({ rows: [1, 2], hasNext: false });
});
it("an exactly-full page without the extra row is also the last page", () => {
expect(splitPage([1, 2, 3], 3)).toEqual({ rows: [1, 2, 3], hasNext: false });
});
it("numbers pages from one", () => {
expect(pageNumber(0, 25)).toBe(1);
expect(pageNumber(25, 25)).toBe(2);
expect(pageNumber(75, 25)).toBe(4);
});
});