Skip to content

Commit

Permalink
Accept min and max delay in createSchemaFetch options (#11774)
Browse files Browse the repository at this point in the history
  • Loading branch information
alessbell committed Apr 15, 2024
1 parent 440563a commit 2583488
Show file tree
Hide file tree
Showing 6 changed files with 173 additions and 56 deletions.
8 changes: 6 additions & 2 deletions .api-reports/api-report-testing_experimental.md
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
@@ -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
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
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
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
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

0 comments on commit 2583488

Please sign in to comment.