From 80c55d4b23b2d7b8fbf56424c4af551148614b73 Mon Sep 17 00:00:00 2001 From: shrugs Date: Thu, 26 Feb 2026 16:57:30 -0600 Subject: [PATCH 1/3] feat: add pagination integration tests, fix backward pagination bug MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add generic testDomainPagination helper that tests all 6 ordering permutations (3 orderBy × 2 dir) with forward and backward relay cursor iteration for Query.domains, Domain.subdomains, Account.domains, and Registry.domains. Fix cursorFilter using effectiveDesc (which includes the Relay inverted XOR) instead of the raw user orderDir for comparison operator selection. This caused backward pagination (last/before) to select rows after the cursor instead of before it. Co-Authored-By: Claude Opus 4.6 --- .../find-domains-resolver-helpers.ts | 4 +- .../lib/find-domains/find-domains-resolver.ts | 9 +- .../schema/account.integration.test.ts | 14 ++ .../schema/domain.integration.test.ts | 14 ++ .../schema/query.integration.test.ts | 39 +++-- .../schema/registry.integration.test.ts | 14 ++ .../integration/domain-pagination-queries.ts | 119 ++++++++++++++ .../src/test/integration/graphql-utils.ts | 8 +- .../integration/test-domain-pagination.ts | 152 ++++++++++++++++++ 9 files changed, 351 insertions(+), 22 deletions(-) create mode 100644 apps/ensapi/src/test/integration/domain-pagination-queries.ts create mode 100644 apps/ensapi/src/test/integration/test-domain-pagination.ts diff --git a/apps/ensapi/src/graphql-api/lib/find-domains/find-domains-resolver-helpers.ts b/apps/ensapi/src/graphql-api/lib/find-domains/find-domains-resolver-helpers.ts index 4136cb5a9..4b817d59a 100644 --- a/apps/ensapi/src/graphql-api/lib/find-domains/find-domains-resolver-helpers.ts +++ b/apps/ensapi/src/graphql-api/lib/find-domains/find-domains-resolver-helpers.ts @@ -30,7 +30,6 @@ function getOrderColumn( * @param queryOrderBy - The order field for the current query (must match cursor.by) * @param queryOrderDir - The order direction for the current query (must match cursor.dir) * @param direction - "after" for forward pagination, "before" for backward - * @param effectiveDesc - Whether the effective sort direction is descending * @throws if cursor.by does not match queryOrderBy * @throws if cursor.dir does not match queryOrderDir * @returns SQL expression for the cursor filter @@ -41,7 +40,6 @@ export function cursorFilter( queryOrderBy: typeof DomainsOrderBy.$inferType, queryOrderDir: typeof OrderDirection.$inferType, direction: "after" | "before", - effectiveDesc: boolean, ): SQL { // Validate cursor was created with the same ordering as the current query if (cursor.by !== queryOrderBy) { @@ -63,7 +61,7 @@ export function cursorFilter( // - "after" with DESC = less than cursor // - "before" with ASC = less than cursor // - "before" with DESC = greater than cursor - const useGreaterThan = (direction === "after") !== effectiveDesc; + const useGreaterThan = (direction === "after") !== (queryOrderDir === "DESC"); // Handle NULL cursor values explicitly (PostgreSQL tuple comparison with NULL yields NULL/unknown) // With NULLS LAST ordering: non-NULL values come before NULL values diff --git a/apps/ensapi/src/graphql-api/lib/find-domains/find-domains-resolver.ts b/apps/ensapi/src/graphql-api/lib/find-domains/find-domains-resolver.ts index ea14535ec..a4645f911 100644 --- a/apps/ensapi/src/graphql-api/lib/find-domains/find-domains-resolver.ts +++ b/apps/ensapi/src/graphql-api/lib/find-domains/find-domains-resolver.ts @@ -20,7 +20,7 @@ import { db } from "@/lib/db"; import { makeLogger } from "@/lib/logger"; import { DomainCursor } from "./domain-cursor"; -import { cursorFilter, isEffectiveDesc, orderFindDomains } from "./find-domains-resolver-helpers"; +import { cursorFilter, orderFindDomains } from "./find-domains-resolver-helpers"; import type { DomainOrderValue } from "./types"; /** @@ -104,9 +104,6 @@ export function resolveFindDomains( }), }, async ({ before, after, limit, inverted }: ResolveCursorConnectionArgs) => { - // identify whether the effective sort direction is descending - const effectiveDesc = isEffectiveDesc(orderDir, inverted); - // build order clauses const orderClauses = orderFindDomains(domains, orderBy, orderDir, inverted); @@ -122,10 +119,10 @@ export function resolveFindDomains( .where( and( beforeCursor - ? cursorFilter(domains, beforeCursor, orderBy, orderDir, "before", effectiveDesc) + ? cursorFilter(domains, beforeCursor, orderBy, orderDir, "before") : undefined, afterCursor - ? cursorFilter(domains, afterCursor, orderBy, orderDir, "after", effectiveDesc) + ? cursorFilter(domains, afterCursor, orderBy, orderDir, "after") : undefined, ), ) diff --git a/apps/ensapi/src/graphql-api/schema/account.integration.test.ts b/apps/ensapi/src/graphql-api/schema/account.integration.test.ts index 440415d9a..4c6b90d41 100644 --- a/apps/ensapi/src/graphql-api/schema/account.integration.test.ts +++ b/apps/ensapi/src/graphql-api/schema/account.integration.test.ts @@ -3,12 +3,17 @@ import { describe, expect, it } from "vitest"; import type { Name } from "@ensnode/ensnode-sdk"; +import { + AccountDomainsPaginated, + type PaginatedDomainResult, +} from "@/test/integration/domain-pagination-queries"; import { gql } from "@/test/integration/ensnode-graphql-api-client"; import { flattenConnection, type GraphQLConnection, request, } from "@/test/integration/graphql-utils"; +import { testDomainPagination } from "@/test/integration/test-domain-pagination"; // via devnet const DEFAULT_OWNER: Address = "0x70997970c51812dc3a010c7d01b50e0d17dc79c8"; @@ -70,3 +75,12 @@ describe("Account.domains", () => { expect(names, "expected 'newowner.eth' in new owner's domains").toContain("newowner.eth"); }); }); + +describe("Account.domains pagination", () => { + testDomainPagination(async (variables) => { + const result = await request<{ + account: { domains: GraphQLConnection }; + }>(AccountDomainsPaginated, { address: DEFAULT_OWNER, ...variables }); + return result.account.domains; + }); +}); diff --git a/apps/ensapi/src/graphql-api/schema/domain.integration.test.ts b/apps/ensapi/src/graphql-api/schema/domain.integration.test.ts index c09145aa0..bd0efbe48 100644 --- a/apps/ensapi/src/graphql-api/schema/domain.integration.test.ts +++ b/apps/ensapi/src/graphql-api/schema/domain.integration.test.ts @@ -3,12 +3,17 @@ import { describe, expect, it } from "vitest"; import type { InterpretedLabel, Name } from "@ensnode/ensnode-sdk"; import { DEVNET_ETH_LABELS } from "@/test/integration/devnet-names"; +import { + DomainSubdomainsPaginated, + type PaginatedDomainResult, +} from "@/test/integration/domain-pagination-queries"; import { gql } from "@/test/integration/ensnode-graphql-api-client"; import { flattenConnection, type GraphQLConnection, request, } from "@/test/integration/graphql-utils"; +import { testDomainPagination } from "@/test/integration/test-domain-pagination"; describe("Domain.subdomains", () => { type SubdomainsResult = { @@ -47,3 +52,12 @@ describe("Domain.subdomains", () => { } }); }); + +describe("Domain.subdomains pagination", () => { + testDomainPagination(async (variables) => { + const result = await request<{ + domain: { subdomains: GraphQLConnection }; + }>(DomainSubdomainsPaginated, variables); + return result.domain.subdomains; + }); +}); diff --git a/apps/ensapi/src/graphql-api/schema/query.integration.test.ts b/apps/ensapi/src/graphql-api/schema/query.integration.test.ts index b45120d40..f1fdd2e59 100644 --- a/apps/ensapi/src/graphql-api/schema/query.integration.test.ts +++ b/apps/ensapi/src/graphql-api/schema/query.integration.test.ts @@ -13,8 +13,17 @@ import { } from "@ensnode/ensnode-sdk"; import { DEVNET_NAMES } from "@/test/integration/devnet-names"; +import { + type PaginatedDomainResult, + QueryDomainsPaginated, +} from "@/test/integration/domain-pagination-queries"; import { gql } from "@/test/integration/ensnode-graphql-api-client"; -import { flattenConnection, request } from "@/test/integration/graphql-utils"; +import { + flattenConnection, + type GraphQLConnection, + request, +} from "@/test/integration/graphql-utils"; +import { testDomainPagination } from "@/test/integration/test-domain-pagination"; const namespace = "ens-test-env"; @@ -39,17 +48,13 @@ describe("Query.root", () => { describe("Query.domains", () => { type QueryDomainsResult = { - domains: { - edges: Array<{ - node: { - __typename: "ENSv1Domain" | "ENSv2Domain"; - id: DomainId; - name: Name; - label: { interpreted: InterpretedLabel }; - owner: { address: Address }; - }; - }>; - }; + domains: GraphQLConnection<{ + __typename: "ENSv1Domain" | "ENSv2Domain"; + id: DomainId; + name: Name; + label: { interpreted: InterpretedLabel }; + owner: { address: Address }; + }>; }; const QueryDomains = gql` @@ -126,3 +131,13 @@ describe("Query.domain", () => { ).resolves.toMatchObject({ domain: null }); }); }); + +describe("Query.domains pagination", () => { + testDomainPagination(async (variables) => { + const result = await request<{ domains: GraphQLConnection }>( + QueryDomainsPaginated, + variables, + ); + return result.domains; + }); +}); diff --git a/apps/ensapi/src/graphql-api/schema/registry.integration.test.ts b/apps/ensapi/src/graphql-api/schema/registry.integration.test.ts index 497d93085..2352d5a66 100644 --- a/apps/ensapi/src/graphql-api/schema/registry.integration.test.ts +++ b/apps/ensapi/src/graphql-api/schema/registry.integration.test.ts @@ -4,12 +4,17 @@ import { DatasourceNames } from "@ensnode/datasources"; import { getDatasourceContract, type InterpretedLabel } from "@ensnode/ensnode-sdk"; import { DEVNET_ETH_LABELS } from "@/test/integration/devnet-names"; +import { + type PaginatedDomainResult, + RegistryDomainsPaginated, +} from "@/test/integration/domain-pagination-queries"; import { gql } from "@/test/integration/ensnode-graphql-api-client"; import { flattenConnection, type GraphQLConnection, request, } from "@/test/integration/graphql-utils"; +import { testDomainPagination } from "@/test/integration/test-domain-pagination"; const namespace = "ens-test-env"; @@ -51,3 +56,12 @@ describe("Registry.domains", () => { } }); }); + +describe("Registry.domains pagination", () => { + testDomainPagination(async (variables) => { + const result = await request<{ + registry: { domains: GraphQLConnection }; + }>(RegistryDomainsPaginated, { contract: V2_ETH_REGISTRY, ...variables }); + return result.registry.domains; + }); +}); diff --git a/apps/ensapi/src/test/integration/domain-pagination-queries.ts b/apps/ensapi/src/test/integration/domain-pagination-queries.ts new file mode 100644 index 000000000..22c3956cf --- /dev/null +++ b/apps/ensapi/src/test/integration/domain-pagination-queries.ts @@ -0,0 +1,119 @@ +import type { InterpretedLabel, Name } from "@ensnode/ensnode-sdk"; + +import { gql } from "@/test/integration/ensnode-graphql-api-client"; + +const PageInfoFragment = gql` + fragment PageInfoFragment on PageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } +`; + +const PaginatedDomainFragment = gql` + fragment PaginatedDomainFragment on Domain { + name + label { interpreted } + registration { + expiry + event { timestamp } + } + } +`; + +export type PaginatedDomainResult = { + name: Name | null; + label: { interpreted: InterpretedLabel }; + registration: { + expiry: string | null; + event: { timestamp: string }; + } | null; +}; + +export const QueryDomainsPaginated = gql` + query QueryDomainsPaginated( + $order: DomainsOrderInput! + $first: Int + $after: String + $last: Int + $before: String + ) { + domains( + where: { name: "e" } + order: $order + first: $first + after: $after + last: $last + before: $before + ) { + edges { cursor node { ...PaginatedDomainFragment } } + pageInfo { ...PageInfoFragment } + } + } + + ${PageInfoFragment} + ${PaginatedDomainFragment} +`; + +export const DomainSubdomainsPaginated = gql` + query DomainSubdomainsPaginated( + $order: DomainsOrderInput! + $first: Int + $after: String + $last: Int + $before: String + ) { + domain(by: { name: "eth" }) { + subdomains(order: $order, first: $first, after: $after, last: $last, before: $before) { + edges { cursor node { ...PaginatedDomainFragment } } + pageInfo { ...PageInfoFragment } + } + } + } + + ${PageInfoFragment} + ${PaginatedDomainFragment} +`; + +export const AccountDomainsPaginated = gql` + query AccountDomainsPaginated( + $address: Address! + $order: DomainsOrderInput! + $first: Int + $after: String + $last: Int + $before: String + ) { + account(address: $address) { + domains(order: $order, first: $first, after: $after, last: $last, before: $before) { + edges { cursor node { ...PaginatedDomainFragment } } + pageInfo { ...PageInfoFragment } + } + } + } + + ${PageInfoFragment} + ${PaginatedDomainFragment} +`; + +export const RegistryDomainsPaginated = gql` + query RegistryDomainsPaginated( + $contract: AccountIdInput! + $order: DomainsOrderInput! + $first: Int + $after: String + $last: Int + $before: String + ) { + registry(by: { contract: $contract }) { + domains(order: $order, first: $first, after: $after, last: $last, before: $before) { + edges { cursor node { ...PaginatedDomainFragment } } + pageInfo { ...PageInfoFragment } + } + } + } + + ${PageInfoFragment} + ${PaginatedDomainFragment} +`; diff --git a/apps/ensapi/src/test/integration/graphql-utils.ts b/apps/ensapi/src/test/integration/graphql-utils.ts index 84fd57cfb..2d9811536 100644 --- a/apps/ensapi/src/test/integration/graphql-utils.ts +++ b/apps/ensapi/src/test/integration/graphql-utils.ts @@ -5,7 +5,13 @@ import { client } from "./ensnode-graphql-api-client"; import { highlightGraphQL, highlightJSON } from "./highlight"; export type GraphQLConnection = { - edges: { node: NODE }[]; + edges: { cursor: string; node: NODE }[]; + pageInfo: { + hasNextPage: boolean; + hasPreviousPage: boolean; + startCursor: string | null; + endCursor: string | null; + }; }; export function flattenConnection(connection?: GraphQLConnection): T[] { diff --git a/apps/ensapi/src/test/integration/test-domain-pagination.ts b/apps/ensapi/src/test/integration/test-domain-pagination.ts new file mode 100644 index 000000000..25606de9e --- /dev/null +++ b/apps/ensapi/src/test/integration/test-domain-pagination.ts @@ -0,0 +1,152 @@ +import { describe, expect, it } from "vitest"; + +import type { DomainsOrderByValue, DomainsOrderInput } from "@/graphql-api/schema/domain"; +import type { OrderDirectionValue } from "@/graphql-api/schema/order-direction"; +import type { PaginatedDomainResult } from "@/test/integration/domain-pagination-queries"; +import { flattenConnection, type GraphQLConnection } from "@/test/integration/graphql-utils"; + +type FetchPageVariables = { + order: typeof DomainsOrderInput.$inferInput; + first?: number; + after?: string; + last?: number; + before?: string; +}; + +type FetchPage = ( + variables: FetchPageVariables, +) => Promise>; + +const ORDER_PERMUTATIONS: Array<{ by: DomainsOrderByValue; dir: OrderDirectionValue }> = [ + { by: "NAME", dir: "ASC" }, + { by: "NAME", dir: "DESC" }, + { by: "REGISTRATION_TIMESTAMP", dir: "ASC" }, + { by: "REGISTRATION_TIMESTAMP", dir: "DESC" }, + { by: "REGISTRATION_EXPIRY", dir: "ASC" }, + { by: "REGISTRATION_EXPIRY", dir: "DESC" }, +]; + +function getSortValue(domain: PaginatedDomainResult, by: DomainsOrderByValue): string | null { + switch (by) { + case "NAME": + return domain.label.interpreted; + case "REGISTRATION_TIMESTAMP": + return domain.registration?.event.timestamp ?? null; + case "REGISTRATION_EXPIRY": + return domain.registration?.expiry ?? null; + } +} + +function assertOrdering( + domains: PaginatedDomainResult[], + by: DomainsOrderByValue, + dir: OrderDirectionValue, +) { + const values = domains.map((n) => getSortValue(n, by)); + + for (let i = 0; i < values.length - 1; i++) { + const a = values[i]; + const b = values[i + 1]; + + // nulls sort last regardless of direction + if (a === null) { + // a is null => b must also be null (everything after should be null) + expect( + b, + `expected null at index ${i + 1} because index ${i} was null (nulls last)`, + ).toBeNull(); + continue; + } + if (b === null) { + // a is non-null, b is null => fine (null sorts last) + continue; + } + + if (by === "NAME") { + if (dir === "ASC") { + expect(a <= b, `expected "${a}" <= "${b}" at indices ${i},${i + 1} (NAME ASC)`).toBe(true); + } else { + expect(a >= b, `expected "${a}" >= "${b}" at indices ${i},${i + 1} (NAME DESC)`).toBe(true); + } + } else { + // bigint string comparison + const av = BigInt(a); + const bv = BigInt(b); + if (dir === "ASC") { + expect(av <= bv, `expected ${av} <= ${bv} at indices ${i},${i + 1} (${by} ASC)`).toBe(true); + } else { + expect(av >= bv, `expected ${av} >= ${bv} at indices ${i},${i + 1} (${by} DESC)`).toBe( + true, + ); + } + } + } +} + +async function collectForward( + fetchPage: FetchPage, + order: typeof DomainsOrderInput.$inferInput, + pageSize: number, +): Promise { + const all: PaginatedDomainResult[] = []; + let after: string | undefined; + + while (true) { + const page = await fetchPage({ order, first: pageSize, after }); + all.push(...flattenConnection(page)); + + if (!page.pageInfo.hasNextPage) break; + after = page.pageInfo.endCursor ?? undefined; + } + + return all; +} + +async function collectBackward( + fetchPage: FetchPage, + order: typeof DomainsOrderInput.$inferInput, + pageSize: number, +): Promise { + const all: PaginatedDomainResult[] = []; + let before: string | undefined; + + while (true) { + const page = await fetchPage({ order, last: pageSize, before }); + // prepend: last pages come in forward order within the page, + // but we're iterating from the end of the full list + all.unshift(...flattenConnection(page)); + + if (!page.pageInfo.hasPreviousPage) break; + before = page.pageInfo.startCursor ?? undefined; + } + + return all; +} + +const PAGE_SIZE = 2; + +/** + * Generic pagination test suite for any domains connection field. + * Generates describe/it blocks for all 6 ordering permutations, + * testing forward pagination, ordering correctness, and backward pagination. + */ +export function testDomainPagination(fetchPage: FetchPage) { + for (const order of ORDER_PERMUTATIONS) { + describe(`order: ${order.by} ${order.dir}`, async () => { + const forwardNodes = await collectForward(fetchPage, order, PAGE_SIZE); + + it("forward pagination collects all nodes", async () => { + expect(forwardNodes.length).toBeGreaterThan(0); + }); + + it("nodes are correctly ordered", () => { + assertOrdering(forwardNodes, order.by, order.dir); + }); + + it("backward pagination yields same nodes in same order", async () => { + const backwardNodes = await collectBackward(fetchPage, order, PAGE_SIZE); + expect(backwardNodes).toEqual(forwardNodes); + }); + }); + } +} From 34b60980e01d035f6a396275f0f3d87b9f5a546a Mon Sep 17 00:00:00 2001 From: shrugs Date: Thu, 26 Feb 2026 17:18:40 -0600 Subject: [PATCH 2/3] fix: clean up notes from bots --- .../schema/account.integration.test.ts | 3 +- .../schema/domain.integration.test.ts | 3 +- .../schema/query.integration.test.ts | 3 +- .../schema/registry.integration.test.ts | 3 +- .../src/test/integration/graphql-utils.ts | 8 ++++- .../integration/test-domain-pagination.ts | 36 +++++++++++++------ 6 files changed, 41 insertions(+), 15 deletions(-) diff --git a/apps/ensapi/src/graphql-api/schema/account.integration.test.ts b/apps/ensapi/src/graphql-api/schema/account.integration.test.ts index 4c6b90d41..c934a111b 100644 --- a/apps/ensapi/src/graphql-api/schema/account.integration.test.ts +++ b/apps/ensapi/src/graphql-api/schema/account.integration.test.ts @@ -11,6 +11,7 @@ import { gql } from "@/test/integration/ensnode-graphql-api-client"; import { flattenConnection, type GraphQLConnection, + type PaginatedGraphQLConnection, request, } from "@/test/integration/graphql-utils"; import { testDomainPagination } from "@/test/integration/test-domain-pagination"; @@ -79,7 +80,7 @@ describe("Account.domains", () => { describe("Account.domains pagination", () => { testDomainPagination(async (variables) => { const result = await request<{ - account: { domains: GraphQLConnection }; + account: { domains: PaginatedGraphQLConnection }; }>(AccountDomainsPaginated, { address: DEFAULT_OWNER, ...variables }); return result.account.domains; }); diff --git a/apps/ensapi/src/graphql-api/schema/domain.integration.test.ts b/apps/ensapi/src/graphql-api/schema/domain.integration.test.ts index bd0efbe48..7006e0de3 100644 --- a/apps/ensapi/src/graphql-api/schema/domain.integration.test.ts +++ b/apps/ensapi/src/graphql-api/schema/domain.integration.test.ts @@ -11,6 +11,7 @@ import { gql } from "@/test/integration/ensnode-graphql-api-client"; import { flattenConnection, type GraphQLConnection, + type PaginatedGraphQLConnection, request, } from "@/test/integration/graphql-utils"; import { testDomainPagination } from "@/test/integration/test-domain-pagination"; @@ -56,7 +57,7 @@ describe("Domain.subdomains", () => { describe("Domain.subdomains pagination", () => { testDomainPagination(async (variables) => { const result = await request<{ - domain: { subdomains: GraphQLConnection }; + domain: { subdomains: PaginatedGraphQLConnection }; }>(DomainSubdomainsPaginated, variables); return result.domain.subdomains; }); diff --git a/apps/ensapi/src/graphql-api/schema/query.integration.test.ts b/apps/ensapi/src/graphql-api/schema/query.integration.test.ts index 59e1a3989..f5bf0ec6e 100644 --- a/apps/ensapi/src/graphql-api/schema/query.integration.test.ts +++ b/apps/ensapi/src/graphql-api/schema/query.integration.test.ts @@ -21,6 +21,7 @@ import { gql } from "@/test/integration/ensnode-graphql-api-client"; import { flattenConnection, type GraphQLConnection, + type PaginatedGraphQLConnection, request, } from "@/test/integration/graphql-utils"; import { testDomainPagination } from "@/test/integration/test-domain-pagination"; @@ -134,7 +135,7 @@ describe("Query.domain", () => { describe("Query.domains pagination", () => { testDomainPagination(async (variables) => { - const result = await request<{ domains: GraphQLConnection }>( + const result = await request<{ domains: PaginatedGraphQLConnection }>( QueryDomainsPaginated, variables, ); diff --git a/apps/ensapi/src/graphql-api/schema/registry.integration.test.ts b/apps/ensapi/src/graphql-api/schema/registry.integration.test.ts index 2352d5a66..5132b02bc 100644 --- a/apps/ensapi/src/graphql-api/schema/registry.integration.test.ts +++ b/apps/ensapi/src/graphql-api/schema/registry.integration.test.ts @@ -12,6 +12,7 @@ import { gql } from "@/test/integration/ensnode-graphql-api-client"; import { flattenConnection, type GraphQLConnection, + type PaginatedGraphQLConnection, request, } from "@/test/integration/graphql-utils"; import { testDomainPagination } from "@/test/integration/test-domain-pagination"; @@ -60,7 +61,7 @@ describe("Registry.domains", () => { describe("Registry.domains pagination", () => { testDomainPagination(async (variables) => { const result = await request<{ - registry: { domains: GraphQLConnection }; + registry: { domains: PaginatedGraphQLConnection }; }>(RegistryDomainsPaginated, { contract: V2_ETH_REGISTRY, ...variables }); return result.registry.domains; }); diff --git a/apps/ensapi/src/test/integration/graphql-utils.ts b/apps/ensapi/src/test/integration/graphql-utils.ts index 27cb8618d..d8e5dcd04 100644 --- a/apps/ensapi/src/test/integration/graphql-utils.ts +++ b/apps/ensapi/src/test/integration/graphql-utils.ts @@ -5,6 +5,10 @@ import { client } from "./ensnode-graphql-api-client"; import { highlightGraphQL, highlightJSON } from "./highlight"; export type GraphQLConnection = { + edges: { node: NODE }[]; +}; + +export type PaginatedGraphQLConnection = { edges: { cursor: string; node: NODE }[]; pageInfo: { hasNextPage: boolean; @@ -14,7 +18,9 @@ export type GraphQLConnection = { }; }; -export function flattenConnection(connection?: GraphQLConnection): T[] { +export function flattenConnection( + connection?: GraphQLConnection | PaginatedGraphQLConnection, +): T[] { return (connection?.edges ?? []).map((edge) => edge.node); } diff --git a/apps/ensapi/src/test/integration/test-domain-pagination.ts b/apps/ensapi/src/test/integration/test-domain-pagination.ts index 25606de9e..a7b520daf 100644 --- a/apps/ensapi/src/test/integration/test-domain-pagination.ts +++ b/apps/ensapi/src/test/integration/test-domain-pagination.ts @@ -1,9 +1,12 @@ -import { describe, expect, it } from "vitest"; +import { beforeAll, describe, expect, it } from "vitest"; import type { DomainsOrderByValue, DomainsOrderInput } from "@/graphql-api/schema/domain"; import type { OrderDirectionValue } from "@/graphql-api/schema/order-direction"; import type { PaginatedDomainResult } from "@/test/integration/domain-pagination-queries"; -import { flattenConnection, type GraphQLConnection } from "@/test/integration/graphql-utils"; +import { + flattenConnection, + type PaginatedGraphQLConnection, +} from "@/test/integration/graphql-utils"; type FetchPageVariables = { order: typeof DomainsOrderInput.$inferInput; @@ -15,7 +18,7 @@ type FetchPageVariables = { type FetchPage = ( variables: FetchPageVariables, -) => Promise>; +) => Promise>; const ORDER_PERMUTATIONS: Array<{ by: DomainsOrderByValue; dir: OrderDirectionValue }> = [ { by: "NAME", dir: "ASC" }, @@ -96,7 +99,10 @@ async function collectForward( all.push(...flattenConnection(page)); if (!page.pageInfo.hasNextPage) break; - after = page.pageInfo.endCursor ?? undefined; + + const nextCursor = page.pageInfo.endCursor ?? undefined; + expect(nextCursor, "endCursor must advance when hasNextPage is true").not.toBe(after); + after = nextCursor; } return all; @@ -117,23 +123,34 @@ async function collectBackward( all.unshift(...flattenConnection(page)); if (!page.pageInfo.hasPreviousPage) break; - before = page.pageInfo.startCursor ?? undefined; + + const nextCursor = page.pageInfo.startCursor ?? undefined; + expect(nextCursor, "startCursor must advance when hasPreviousPage is true").not.toBe(before); + before = nextCursor; } return all; } +// NOTE: using small page size to force multiple pages in devnet result set const PAGE_SIZE = 2; /** - * Generic pagination test suite for any domains connection field. - * Generates describe/it blocks for all 6 ordering permutations, - * testing forward pagination, ordering correctness, and backward pagination. + * Generic pagination test suite for any find-domains connection field. + * + * Generates describe/it blocks for all 6 ordering permutations, testing forward pagination, + * ordering correctness, and backward pagination. */ export function testDomainPagination(fetchPage: FetchPage) { for (const order of ORDER_PERMUTATIONS) { describe(`order: ${order.by} ${order.dir}`, async () => { - const forwardNodes = await collectForward(fetchPage, order, PAGE_SIZE); + let forwardNodes: PaginatedDomainResult[]; + let backwardNodes: PaginatedDomainResult[]; + + beforeAll(async () => { + forwardNodes = await collectForward(fetchPage, order, PAGE_SIZE); + backwardNodes = await collectBackward(fetchPage, order, PAGE_SIZE); + }); it("forward pagination collects all nodes", async () => { expect(forwardNodes.length).toBeGreaterThan(0); @@ -144,7 +161,6 @@ export function testDomainPagination(fetchPage: FetchPage) { }); it("backward pagination yields same nodes in same order", async () => { - const backwardNodes = await collectBackward(fetchPage, order, PAGE_SIZE); expect(backwardNodes).toEqual(forwardNodes); }); }); From 2f59e1286e08f9e464285a5b71887c69126e5d72 Mon Sep 17 00:00:00 2001 From: shrugs Date: Thu, 26 Feb 2026 17:30:34 -0600 Subject: [PATCH 3/3] fix: no async describe --- apps/ensapi/src/test/integration/test-domain-pagination.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/ensapi/src/test/integration/test-domain-pagination.ts b/apps/ensapi/src/test/integration/test-domain-pagination.ts index a7b520daf..baf378fb9 100644 --- a/apps/ensapi/src/test/integration/test-domain-pagination.ts +++ b/apps/ensapi/src/test/integration/test-domain-pagination.ts @@ -143,7 +143,7 @@ const PAGE_SIZE = 2; */ export function testDomainPagination(fetchPage: FetchPage) { for (const order of ORDER_PERMUTATIONS) { - describe(`order: ${order.by} ${order.dir}`, async () => { + describe(`order: ${order.by} ${order.dir}`, () => { let forwardNodes: PaginatedDomainResult[]; let backwardNodes: PaginatedDomainResult[];