Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Accept min and max delay in createSchemaFetch options #11774

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
7273e3c
feat: accept min and max delay in createSchemaFetch
alessbell Apr 10, 2024
62488f5
chore: add snapshot of invariant error and add tests
alessbell Apr 10, 2024
822685c
chore: update api reports and .size-limits.json
alessbell Apr 10, 2024
c31fb6a
fix: remove explicit inner Promise
alessbell Apr 11, 2024
61b3be8
fix: error case test, update default values
alessbell Apr 11, 2024
4cd4f38
fix: throw earlier
alessbell Apr 11, 2024
def4b0d
fix: remove unused const
alessbell Apr 11, 2024
a0cd7d2
chore: commit failing test
alessbell Apr 11, 2024
ba929d3
fix: one failing test, a second is still failing unexpectedly
alessbell Apr 11, 2024
534626c
fix profiler bug
phryneas Apr 12, 2024
d76338f
remove debugging console.log
phryneas Apr 12, 2024
c5b3757
Merge branch 'release-3.10' into issue-11748-min-max-createSchemaFetc…
alessbell Apr 12, 2024
ae5d8ee
chore: extract api
alessbell Apr 12, 2024
e930e53
Merge branch 'release-3.10' into issue-11748-min-max-createSchemaFetc…
alessbell Apr 12, 2024
22a7692
chore: fix merge conflicts
alessbell Apr 12, 2024
4532732
fix: add missing call to .mockGlobal()
alessbell Apr 12, 2024
f63cce3
fix: remove old comment
alessbell Apr 12, 2024
a7747a7
chore: fix merge conflicts
alessbell Apr 15, 2024
71ee1c8
chore: add changeset and update .api-reports/api-report-testing_exper…
alessbell Apr 15, 2024
b97e9d6
Clean up Prettier, Size-limit, and Api-Extractor
alessbell Apr 15, 2024
06b4389
chore: add comment and change timeout in test
alessbell Apr 15, 2024
d8891ea
Merge branch 'issue-11748-min-max-createSchemaFetch-threshold' of git…
alessbell Apr 15, 2024
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
8 changes: 6 additions & 2 deletions .api-reports/api-report-testing_experimental.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,12 @@ import type { GraphQLSchema } from 'graphql';

// @alpha
export const createSchemaFetch: (schema: GraphQLSchema, mockFetchOpts?: {
validate: boolean;
}) => ((uri: any, options: any) => Promise<Response>) & {
validate?: boolean;
delay?: {
min: number;
max: number;
};
}) => ((uri?: any, options?: any) => Promise<Response>) & {
mockGlobal: () => {
restore: () => void;
} & Disposable;
Expand Down
5 changes: 5 additions & 0 deletions .changeset/strong-paws-kneel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@apollo/client": minor
---

Add ability to set min and max delay in `createSchemaFetch`
2 changes: 1 addition & 1 deletion config/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const react17TestFileIgnoreList = [
ignoreTSFiles,
// We only support Suspense with React 18, so don't test suspense hooks with
// React 17
"src/testing/core/__tests__/createTestSchema.test.tsx",
"src/testing/experimental/__tests__/createTestSchema.test.tsx",
"src/react/hooks/__tests__/useSuspenseQuery.test.tsx",
"src/react/hooks/__tests__/useBackgroundQuery.test.tsx",
"src/react/hooks/__tests__/useLoadableQuery.test.tsx",
Expand Down
127 changes: 110 additions & 17 deletions src/testing/experimental/__tests__/createTestSchema.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
FallbackProps,
ErrorBoundary as ReactErrorBoundary,
} from "react-error-boundary";
import { InvariantError } from "ts-invariant";

const typeDefs = /* GraphQL */ `
type User {
Expand Down Expand Up @@ -396,7 +397,7 @@ describe("schema proxy", () => {
return <div>Hello</div>;
};

const { unmount } = renderWithClient(<App />, {
renderWithClient(<App />, {
client,
wrapper: Profiler,
});
Expand All @@ -422,8 +423,6 @@ describe("schema proxy", () => {
},
});
}

unmount();
});

it("allows you to call .fork without providing resolvers", async () => {
Expand Down Expand Up @@ -491,7 +490,7 @@ describe("schema proxy", () => {
return <div>Hello</div>;
};

const { unmount } = renderWithClient(<App />, {
renderWithClient(<App />, {
client,
wrapper: Profiler,
});
Expand Down Expand Up @@ -520,8 +519,6 @@ describe("schema proxy", () => {
},
});
}

unmount();
});

it("handles mutations", async () => {
Expand Down Expand Up @@ -615,7 +612,7 @@ describe("schema proxy", () => {

const user = userEvent.setup();

const { unmount } = renderWithClient(<App />, {
renderWithClient(<App />, {
client,
wrapper: Profiler,
});
Expand Down Expand Up @@ -666,8 +663,6 @@ describe("schema proxy", () => {
},
});
}

unmount();
});

it("returns GraphQL errors", async () => {
Expand Down Expand Up @@ -743,7 +738,7 @@ describe("schema proxy", () => {
return <div>Hello</div>;
};

const { unmount } = renderWithClient(<App />, {
renderWithClient(<App />, {
client,
wrapper: Profiler,
});
Expand All @@ -760,8 +755,6 @@ describe("schema proxy", () => {
})
);
}

unmount();
});

it("validates schema by default and returns validation errors", async () => {
Expand Down Expand Up @@ -823,7 +816,7 @@ describe("schema proxy", () => {
return <div>Hello</div>;
};

const { unmount } = renderWithClient(<App />, {
renderWithClient(<App />, {
client,
wrapper: Profiler,
});
Expand All @@ -842,8 +835,6 @@ describe("schema proxy", () => {
})
);
}

unmount();
});

it("preserves resolvers from previous calls to .add on subsequent calls to .fork", async () => {
Expand Down Expand Up @@ -983,7 +974,7 @@ describe("schema proxy", () => {

const user = userEvent.setup();

const { unmount } = renderWithClient(<App />, {
renderWithClient(<App />, {
client,
wrapper: Profiler,
});
Expand Down Expand Up @@ -1033,7 +1024,109 @@ describe("schema proxy", () => {
},
});
}
});

unmount();
it("createSchemaFetch respects min and max delay", async () => {
const Profiler = createDefaultProfiler<ViewerQueryData>();

const minDelay = 1500;
const maxDelay = 2000;

using _fetch = createSchemaFetch(schema, {
delay: { min: minDelay, max: maxDelay },
}).mockGlobal();

const client = new ApolloClient({
cache: new InMemoryCache(),
uri,
});

const query: TypedDocumentNode<ViewerQueryData> = gql`
query {
viewer {
id
name
age
book {
id
title
publishedAt
}
}
}
`;

const Fallback = () => {
useTrackRenders();
return <div>Loading...</div>;
};

const App = () => {
return (
<React.Suspense fallback={<Fallback />}>
<Child />
</React.Suspense>
);
};

const Child = () => {
const result = useSuspenseQuery(query);

useTrackRenders();

Profiler.mergeSnapshot({
result,
} as Partial<{}>);

return <div>Hello</div>;
};

renderWithClient(<App />, {
client,
wrapper: Profiler,
});

// initial suspended render
await Profiler.takeRender();

await expect(Profiler).not.toRerender({ timeout: minDelay - 100 });

{
const { snapshot } = await Profiler.takeRender({
// This timeout doesn't start until after our `minDelay - 100`
// timeout above, so we don't have to wait the full `maxDelay`
// here.
// Instead we can just wait for the difference between `maxDelay`
// and `minDelay`, plus a bit to prevent flakiness.
timeout: maxDelay - minDelay + 110,
});

expect(snapshot.result?.data).toEqual({
viewer: {
__typename: "User",
age: 42,
id: "1",
name: "Jane Doe",
book: {
__typename: "TextBook",
id: "1",
publishedAt: "2024-01-01",
title: "The Book",
},
},
});
}
});

it("should call invariant.error if min delay is greater than max delay", async () => {
await expect(async () => {
createSchemaFetch(schema, {
delay: { min: 3000, max: 1000 },
});
}).rejects.toThrow(
new InvariantError(
"Please configure a minimum delay that is less than the maximum delay. The default minimum delay is 3ms."
)
);
});
});
69 changes: 41 additions & 28 deletions src/testing/experimental/createSchemaFetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { execute, validate } from "graphql";
import type { GraphQLError, GraphQLSchema } from "graphql";
import { ApolloError, gql } from "../../core/index.js";
import { withCleanup } from "../internal/index.js";
import { wait } from "../core/wait.js";

/**
* A function that accepts a static `schema` and a `mockFetchOpts` object and
Expand Down Expand Up @@ -32,47 +33,59 @@ import { withCleanup } from "../internal/index.js";
*/
const createSchemaFetch = (
schema: GraphQLSchema,
mockFetchOpts: { validate: boolean } = { validate: true }
mockFetchOpts: {
validate?: boolean;
delay?: { min: number; max: number };
} = { validate: true }
) => {
const prevFetch = window.fetch;
const delayMin = mockFetchOpts.delay?.min ?? 3;
const delayMax = mockFetchOpts.delay?.max ?? delayMin + 2;

const mockFetch: (uri: any, options: any) => Promise<Response> = (
if (delayMin > delayMax) {
throw new Error(
"Please configure a minimum delay that is less than the maximum delay. The default minimum delay is 3ms."
);
}

const mockFetch: (uri?: any, options?: any) => Promise<Response> = async (
_uri,
options
) => {
return new Promise(async (resolve) => {
const body = JSON.parse(options.body);
const document = gql(body.query);
if (delayMin > 0) {
const randomDelay = Math.random() * (delayMax - delayMin) + delayMin;
await wait(randomDelay);
}

if (mockFetchOpts.validate) {
let validationErrors: readonly Error[] = [];
const body = JSON.parse(options.body);
const document = gql(body.query);

try {
validationErrors = validate(schema, document);
} catch (e) {
validationErrors = [
new ApolloError({ graphQLErrors: [e as GraphQLError] }),
];
}
if (mockFetchOpts.validate) {
let validationErrors: readonly Error[] = [];

if (validationErrors?.length > 0) {
return resolve(
new Response(JSON.stringify({ errors: validationErrors }))
);
}
try {
validationErrors = validate(schema, document);
} catch (e) {
validationErrors = [
new ApolloError({ graphQLErrors: [e as GraphQLError] }),
];
}

const result = await execute({
schema,
document,
variableValues: body.variables,
operationName: body.operationName,
});

const stringifiedResult = JSON.stringify(result);
if (validationErrors?.length > 0) {
return new Response(JSON.stringify({ errors: validationErrors }));
}
}

resolve(new Response(stringifiedResult));
const result = await execute({
schema,
document,
variableValues: body.variables,
operationName: body.operationName,
});

const stringifiedResult = JSON.stringify(result);

return new Response(stringifiedResult);
};

function mockGlobal() {
Expand Down
18 changes: 10 additions & 8 deletions src/testing/internal/profile/profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,9 @@ export function createProfiler<Snapshot extends ValidSnapshot = void>({
let nextRender: Promise<Render<Snapshot>> | undefined;
let resolveNextRender: ((render: Render<Snapshot>) => void) | undefined;
let rejectNextRender: ((error: unknown) => void) | undefined;
function resetNextRender() {
nextRender = resolveNextRender = rejectNextRender = undefined;
}
const snapshotRef = { current: initialSnapshot };
const replaceSnapshot: ReplaceSnapshot<Snapshot> = (snap) => {
if (typeof snap === "function") {
Expand Down Expand Up @@ -241,7 +244,7 @@ export function createProfiler<Snapshot extends ValidSnapshot = void>({
});
rejectNextRender?.(error);
} finally {
nextRender = resolveNextRender = rejectNextRender = undefined;
resetNextRender();
}
};

Expand Down Expand Up @@ -340,13 +343,12 @@ export function createProfiler<Snapshot extends ValidSnapshot = void>({
rejectNextRender = reject;
}),
new Promise<Render<Snapshot>>((_, reject) =>
setTimeout(
() =>
reject(
applyStackTrace(new WaitForRenderTimeoutError(), stackTrace)
),
timeout
)
setTimeout(() => {
reject(
applyStackTrace(new WaitForRenderTimeoutError(), stackTrace)
);
resetNextRender();
}, timeout)
),
]);
}
Expand Down
Loading