Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ import {
IntermediateRepresentation,
Name,
OAuthTokenEndpoint,
ObjectPropertyAccess
ObjectPropertyAccess,
Pagination,
RequestProperty,
ResponseProperty
} from "@fern-fern/ir-sdk/api";
import {
DependencyManager,
Expand Down Expand Up @@ -1158,6 +1161,20 @@ describe("${serviceName}", () => {
// (via getItems), so we don't need to traverse the response path
const pageName = "page";
const nextPageName = "nextPage";

// Calculate expected hasNextPage value based on pagination type and example data
const expectedHasNextPage =
endpoint.pagination !== undefined && endpoint.pagination.type !== "custom"
? calculateExpectedHasNextPage({
pagination: endpoint.pagination,
responseJson:
example.response.type === "ok" && example.response.value.type === "body"
? example.response.value.value?.jsonExample
: undefined,
requestJson: example.request?.jsonExample
})
: false;

const paginationBlock =
endpoint.pagination !== undefined
? code`
Expand All @@ -1167,7 +1184,7 @@ describe("${serviceName}", () => {
endpoint.pagination.type !== "custom"
? code`
expect(${expectedName}).toEqual(${pageName}.data);
expect(${pageName}.hasNextPage()).toBe(true);
expect(${pageName}.hasNextPage()).toBe(${expectedHasNextPage});
const nextPage = await ${pageName}.getNextPage();
expect(${expectedName}).toEqual(${nextPageName}.data);
`
Expand Down Expand Up @@ -1789,6 +1806,133 @@ function isCodeEmptyObject(code: Code): boolean {
return rawCode === "{}" || rawCode === "{ }";
}

/**
* Extracts a property value from a JSON object using a ResponseProperty path.
* The property path defines the nested path to traverse in the JSON object.
*/
function getPropertyValueFromJson({ json, property }: { json: unknown; property: ResponseProperty }): unknown {
if (json == null || typeof json !== "object") {
return undefined;
}

let current: unknown = json;

// Traverse the property path if it exists
if (property.propertyPath != null) {
for (const pathItem of property.propertyPath) {
if (current == null || typeof current !== "object") {
return undefined;
}
// Use originalName for Name types (wire format for nested path segments)
current = (current as Record<string, unknown>)[pathItem.name.originalName];
}
}

// Get the final property value
if (current == null || typeof current !== "object") {
return undefined;
}
// Use wireValue for NameAndWireValue types (the actual wire name)
return (current as Record<string, unknown>)[property.property.name.wireValue];
}

/**
* Extracts a property value from a JSON object using a RequestProperty path.
*/
function getRequestPropertyValueFromJson({ json, property }: { json: unknown; property: RequestProperty }): unknown {
if (json == null || typeof json !== "object") {
return undefined;
}

let current: unknown = json;

// Traverse the property path if it exists
if (property.propertyPath != null) {
for (const pathItem of property.propertyPath) {
if (current == null || typeof current !== "object") {
return undefined;
}
// Use originalName for Name types (wire format for nested path segments)
current = (current as Record<string, unknown>)[pathItem.name.originalName];
}
}

// Get the final property value
if (current == null || typeof current !== "object") {
return undefined;
}
// Use wireValue for NameAndWireValue types (the actual wire name)
return (current as Record<string, unknown>)[property.property.name.wireValue];
}

/**
* Calculates the expected hasNextPage value based on pagination type and example data.
* This mirrors the SDK's actual pagination logic:
* - Cursor pagination: hasNextPage = next != null && next !== ""
* - Offset pagination: hasNextPage = results.length >= step (or > 0 if no step)
*/
function calculateExpectedHasNextPage({
pagination,
responseJson,
requestJson
}: {
pagination: Pagination;
responseJson: unknown;
requestJson: unknown;
}): boolean {
switch (pagination.type) {
case "cursor": {
// For cursor pagination, hasNextPage is true when next property is non-null and non-empty string
const nextValue = getPropertyValueFromJson({
json: responseJson,
property: pagination.next
});
if (nextValue == null) {
return false;
}
if (typeof nextValue === "string" && nextValue === "") {
return false;
}
return true;
}
case "offset": {
// For offset pagination, hasNextPage is true when results.length >= step
const resultsValue = getPropertyValueFromJson({
json: responseJson,
property: pagination.results
});

if (!Array.isArray(resultsValue)) {
return false;
}

const resultsLength = resultsValue.length;

// If step is defined, try to get the step value from the request
if (pagination.step != null) {
const stepValue = getRequestPropertyValueFromJson({
json: requestJson,
property: pagination.step
});
// Only use the step comparison if we can actually find the step value
// (step may be in query params which aren't in jsonExample for GET requests)
if (typeof stepValue === "number") {
return resultsLength >= Math.floor(stepValue);
}
}

// Fallback: if we can't determine the step value, use resultsLength > 0
// This matches the SDK's behavior when there are results
return resultsLength > 0;
}
case "custom":
// Custom pagination doesn't use hasNextPage in tests
return false;
default:
assertNever(pagination);
}
}

/**
* Determines the casing of the variable name when used in client constructor options
*/
Expand Down
13 changes: 13 additions & 0 deletions generators/typescript/sdk/versions.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,17 @@
# yaml-language-server: $schema=../../../fern-versions-yml.schema.json
- version: 3.39.1
changelogEntry:
- summary: |
Fix wire test generator to dynamically calculate expected `hasNextPage()` value based on pagination type and example data.
Previously, the test generator hardcoded `expect(page.hasNextPage()).toBe(true)` for all pagination tests, which caused
test failures when the mock response contained fewer items than the requested limit.
Now the expected value is calculated based on:
- Cursor pagination: `hasNextPage` is true when the `next` property is non-null and non-empty
- Offset pagination: `hasNextPage` is true when `results.length >= step` (or `> 0` if no step is defined)
type: fix
createdAt: "2025-12-10"
irVersion: 62

- version: 3.39.0
changelogEntry:
- summary: |
Expand Down
10 changes: 2 additions & 8 deletions seed/ts-sdk/pagination/no-custom-config/reference.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions seed/ts-sdk/pagination/no-custom-config/snippet.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

28 changes: 7 additions & 21 deletions seed/ts-sdk/pagination/no-custom-config/tests/wire/users.test.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 2 additions & 8 deletions seed/ts-sdk/pagination/page-index-semantics/reference.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions seed/ts-sdk/pagination/page-index-semantics/snippet.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading