Skip to content

Commit 2583488

Browse files
authored
Accept min and max delay in createSchemaFetch options (#11774)
1 parent 440563a commit 2583488

File tree

6 files changed

+173
-56
lines changed

6 files changed

+173
-56
lines changed

.api-reports/api-report-testing_experimental.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,12 @@ import type { GraphQLSchema } from 'graphql';
1212

1313
// @alpha
1414
export const createSchemaFetch: (schema: GraphQLSchema, mockFetchOpts?: {
15-
validate: boolean;
16-
}) => ((uri: any, options: any) => Promise<Response>) & {
15+
validate?: boolean;
16+
delay?: {
17+
min: number;
18+
max: number;
19+
};
20+
}) => ((uri?: any, options?: any) => Promise<Response>) & {
1721
mockGlobal: () => {
1822
restore: () => void;
1923
} & Disposable;

.changeset/strong-paws-kneel.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@apollo/client": minor
3+
---
4+
5+
Add ability to set min and max delay in `createSchemaFetch`

config/jest.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ const react17TestFileIgnoreList = [
3333
ignoreTSFiles,
3434
// We only support Suspense with React 18, so don't test suspense hooks with
3535
// React 17
36-
"src/testing/core/__tests__/createTestSchema.test.tsx",
36+
"src/testing/experimental/__tests__/createTestSchema.test.tsx",
3737
"src/react/hooks/__tests__/useSuspenseQuery.test.tsx",
3838
"src/react/hooks/__tests__/useBackgroundQuery.test.tsx",
3939
"src/react/hooks/__tests__/useLoadableQuery.test.tsx",

src/testing/experimental/__tests__/createTestSchema.test.tsx

Lines changed: 110 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {
2424
FallbackProps,
2525
ErrorBoundary as ReactErrorBoundary,
2626
} from "react-error-boundary";
27+
import { InvariantError } from "ts-invariant";
2728

2829
const typeDefs = /* GraphQL */ `
2930
type User {
@@ -396,7 +397,7 @@ describe("schema proxy", () => {
396397
return <div>Hello</div>;
397398
};
398399

399-
const { unmount } = renderWithClient(<App />, {
400+
renderWithClient(<App />, {
400401
client,
401402
wrapper: Profiler,
402403
});
@@ -422,8 +423,6 @@ describe("schema proxy", () => {
422423
},
423424
});
424425
}
425-
426-
unmount();
427426
});
428427

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

494-
const { unmount } = renderWithClient(<App />, {
493+
renderWithClient(<App />, {
495494
client,
496495
wrapper: Profiler,
497496
});
@@ -520,8 +519,6 @@ describe("schema proxy", () => {
520519
},
521520
});
522521
}
523-
524-
unmount();
525522
});
526523

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

616613
const user = userEvent.setup();
617614

618-
const { unmount } = renderWithClient(<App />, {
615+
renderWithClient(<App />, {
619616
client,
620617
wrapper: Profiler,
621618
});
@@ -666,8 +663,6 @@ describe("schema proxy", () => {
666663
},
667664
});
668665
}
669-
670-
unmount();
671666
});
672667

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

746-
const { unmount } = renderWithClient(<App />, {
741+
renderWithClient(<App />, {
747742
client,
748743
wrapper: Profiler,
749744
});
@@ -760,8 +755,6 @@ describe("schema proxy", () => {
760755
})
761756
);
762757
}
763-
764-
unmount();
765758
});
766759

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

826-
const { unmount } = renderWithClient(<App />, {
819+
renderWithClient(<App />, {
827820
client,
828821
wrapper: Profiler,
829822
});
@@ -842,8 +835,6 @@ describe("schema proxy", () => {
842835
})
843836
);
844837
}
845-
846-
unmount();
847838
});
848839

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

984975
const user = userEvent.setup();
985976

986-
const { unmount } = renderWithClient(<App />, {
977+
renderWithClient(<App />, {
987978
client,
988979
wrapper: Profiler,
989980
});
@@ -1033,7 +1024,109 @@ describe("schema proxy", () => {
10331024
},
10341025
});
10351026
}
1027+
});
10361028

1037-
unmount();
1029+
it("createSchemaFetch respects min and max delay", async () => {
1030+
const Profiler = createDefaultProfiler<ViewerQueryData>();
1031+
1032+
const minDelay = 1500;
1033+
const maxDelay = 2000;
1034+
1035+
using _fetch = createSchemaFetch(schema, {
1036+
delay: { min: minDelay, max: maxDelay },
1037+
}).mockGlobal();
1038+
1039+
const client = new ApolloClient({
1040+
cache: new InMemoryCache(),
1041+
uri,
1042+
});
1043+
1044+
const query: TypedDocumentNode<ViewerQueryData> = gql`
1045+
query {
1046+
viewer {
1047+
id
1048+
name
1049+
age
1050+
book {
1051+
id
1052+
title
1053+
publishedAt
1054+
}
1055+
}
1056+
}
1057+
`;
1058+
1059+
const Fallback = () => {
1060+
useTrackRenders();
1061+
return <div>Loading...</div>;
1062+
};
1063+
1064+
const App = () => {
1065+
return (
1066+
<React.Suspense fallback={<Fallback />}>
1067+
<Child />
1068+
</React.Suspense>
1069+
);
1070+
};
1071+
1072+
const Child = () => {
1073+
const result = useSuspenseQuery(query);
1074+
1075+
useTrackRenders();
1076+
1077+
Profiler.mergeSnapshot({
1078+
result,
1079+
} as Partial<{}>);
1080+
1081+
return <div>Hello</div>;
1082+
};
1083+
1084+
renderWithClient(<App />, {
1085+
client,
1086+
wrapper: Profiler,
1087+
});
1088+
1089+
// initial suspended render
1090+
await Profiler.takeRender();
1091+
1092+
await expect(Profiler).not.toRerender({ timeout: minDelay - 100 });
1093+
1094+
{
1095+
const { snapshot } = await Profiler.takeRender({
1096+
// This timeout doesn't start until after our `minDelay - 100`
1097+
// timeout above, so we don't have to wait the full `maxDelay`
1098+
// here.
1099+
// Instead we can just wait for the difference between `maxDelay`
1100+
// and `minDelay`, plus a bit to prevent flakiness.
1101+
timeout: maxDelay - minDelay + 110,
1102+
});
1103+
1104+
expect(snapshot.result?.data).toEqual({
1105+
viewer: {
1106+
__typename: "User",
1107+
age: 42,
1108+
id: "1",
1109+
name: "Jane Doe",
1110+
book: {
1111+
__typename: "TextBook",
1112+
id: "1",
1113+
publishedAt: "2024-01-01",
1114+
title: "The Book",
1115+
},
1116+
},
1117+
});
1118+
}
1119+
});
1120+
1121+
it("should call invariant.error if min delay is greater than max delay", async () => {
1122+
await expect(async () => {
1123+
createSchemaFetch(schema, {
1124+
delay: { min: 3000, max: 1000 },
1125+
});
1126+
}).rejects.toThrow(
1127+
new InvariantError(
1128+
"Please configure a minimum delay that is less than the maximum delay. The default minimum delay is 3ms."
1129+
)
1130+
);
10381131
});
10391132
});

src/testing/experimental/createSchemaFetch.ts

Lines changed: 41 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { execute, validate } from "graphql";
22
import type { GraphQLError, GraphQLSchema } from "graphql";
33
import { ApolloError, gql } from "../../core/index.js";
44
import { withCleanup } from "../internal/index.js";
5+
import { wait } from "../core/wait.js";
56

67
/**
78
* A function that accepts a static `schema` and a `mockFetchOpts` object and
@@ -32,47 +33,59 @@ import { withCleanup } from "../internal/index.js";
3233
*/
3334
const createSchemaFetch = (
3435
schema: GraphQLSchema,
35-
mockFetchOpts: { validate: boolean } = { validate: true }
36+
mockFetchOpts: {
37+
validate?: boolean;
38+
delay?: { min: number; max: number };
39+
} = { validate: true }
3640
) => {
3741
const prevFetch = window.fetch;
42+
const delayMin = mockFetchOpts.delay?.min ?? 3;
43+
const delayMax = mockFetchOpts.delay?.max ?? delayMin + 2;
3844

39-
const mockFetch: (uri: any, options: any) => Promise<Response> = (
45+
if (delayMin > delayMax) {
46+
throw new Error(
47+
"Please configure a minimum delay that is less than the maximum delay. The default minimum delay is 3ms."
48+
);
49+
}
50+
51+
const mockFetch: (uri?: any, options?: any) => Promise<Response> = async (
4052
_uri,
4153
options
4254
) => {
43-
return new Promise(async (resolve) => {
44-
const body = JSON.parse(options.body);
45-
const document = gql(body.query);
55+
if (delayMin > 0) {
56+
const randomDelay = Math.random() * (delayMax - delayMin) + delayMin;
57+
await wait(randomDelay);
58+
}
4659

47-
if (mockFetchOpts.validate) {
48-
let validationErrors: readonly Error[] = [];
60+
const body = JSON.parse(options.body);
61+
const document = gql(body.query);
4962

50-
try {
51-
validationErrors = validate(schema, document);
52-
} catch (e) {
53-
validationErrors = [
54-
new ApolloError({ graphQLErrors: [e as GraphQLError] }),
55-
];
56-
}
63+
if (mockFetchOpts.validate) {
64+
let validationErrors: readonly Error[] = [];
5765

58-
if (validationErrors?.length > 0) {
59-
return resolve(
60-
new Response(JSON.stringify({ errors: validationErrors }))
61-
);
62-
}
66+
try {
67+
validationErrors = validate(schema, document);
68+
} catch (e) {
69+
validationErrors = [
70+
new ApolloError({ graphQLErrors: [e as GraphQLError] }),
71+
];
6372
}
6473

65-
const result = await execute({
66-
schema,
67-
document,
68-
variableValues: body.variables,
69-
operationName: body.operationName,
70-
});
71-
72-
const stringifiedResult = JSON.stringify(result);
74+
if (validationErrors?.length > 0) {
75+
return new Response(JSON.stringify({ errors: validationErrors }));
76+
}
77+
}
7378

74-
resolve(new Response(stringifiedResult));
79+
const result = await execute({
80+
schema,
81+
document,
82+
variableValues: body.variables,
83+
operationName: body.operationName,
7584
});
85+
86+
const stringifiedResult = JSON.stringify(result);
87+
88+
return new Response(stringifiedResult);
7689
};
7790

7891
function mockGlobal() {

src/testing/internal/profile/profile.tsx

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,9 @@ export function createProfiler<Snapshot extends ValidSnapshot = void>({
151151
let nextRender: Promise<Render<Snapshot>> | undefined;
152152
let resolveNextRender: ((render: Render<Snapshot>) => void) | undefined;
153153
let rejectNextRender: ((error: unknown) => void) | undefined;
154+
function resetNextRender() {
155+
nextRender = resolveNextRender = rejectNextRender = undefined;
156+
}
154157
const snapshotRef = { current: initialSnapshot };
155158
const replaceSnapshot: ReplaceSnapshot<Snapshot> = (snap) => {
156159
if (typeof snap === "function") {
@@ -241,7 +244,7 @@ export function createProfiler<Snapshot extends ValidSnapshot = void>({
241244
});
242245
rejectNextRender?.(error);
243246
} finally {
244-
nextRender = resolveNextRender = rejectNextRender = undefined;
247+
resetNextRender();
245248
}
246249
};
247250

@@ -340,13 +343,12 @@ export function createProfiler<Snapshot extends ValidSnapshot = void>({
340343
rejectNextRender = reject;
341344
}),
342345
new Promise<Render<Snapshot>>((_, reject) =>
343-
setTimeout(
344-
() =>
345-
reject(
346-
applyStackTrace(new WaitForRenderTimeoutError(), stackTrace)
347-
),
348-
timeout
349-
)
346+
setTimeout(() => {
347+
reject(
348+
applyStackTrace(new WaitForRenderTimeoutError(), stackTrace)
349+
);
350+
resetNextRender();
351+
}, timeout)
350352
),
351353
]);
352354
}

0 commit comments

Comments
 (0)