From cf535ce25bf68d3d85d72e4b4f71f6babcbbfae6 Mon Sep 17 00:00:00 2001 From: Pavel Glac Date: Thu, 26 Mar 2026 11:14:55 +0000 Subject: [PATCH 1/3] support async resolvers. --- packages/apollo-mock-client/src/index.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/apollo-mock-client/src/index.ts b/packages/apollo-mock-client/src/index.ts index d706064c0..7d3528a98 100644 --- a/packages/apollo-mock-client/src/index.ts +++ b/packages/apollo-mock-client/src/index.ts @@ -112,7 +112,9 @@ export interface MockFunctions { * as per https://www.apollographql.com/docs/react/development-testing/testing/ */ resolveMostRecentOperation( - resolver: (operation: OperationDescriptor) => ExecutionResult, + resolver: ( + operation: OperationDescriptor, + ) => ExecutionResult | Promise, ): Promise; /** @@ -290,10 +292,12 @@ class Mock implements MockFunctions { } public async resolveMostRecentOperation( - resolver: (operation: OperationDescriptor) => ExecutionResult, + resolver: ( + operation: OperationDescriptor, + ) => ExecutionResult | Promise, ): Promise { const operation = this.getMostRecentOperation(); - this.resolve(operation, resolver(operation)); + this.resolve(operation, await resolver(operation)); } public async rejectMostRecentOperation( From d8150697e11b8ec64745e4c00aae9f1513c68641 Mon Sep 17 00:00:00 2001 From: Pavel Glac Date: Thu, 26 Mar 2026 11:15:15 +0000 Subject: [PATCH 2/3] Change files --- ...o-mock-client-545253d1-b628-433c-8572-2a7e13f59807.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 change/@graphitation-apollo-mock-client-545253d1-b628-433c-8572-2a7e13f59807.json diff --git a/change/@graphitation-apollo-mock-client-545253d1-b628-433c-8572-2a7e13f59807.json b/change/@graphitation-apollo-mock-client-545253d1-b628-433c-8572-2a7e13f59807.json new file mode 100644 index 000000000..16be930a7 --- /dev/null +++ b/change/@graphitation-apollo-mock-client-545253d1-b628-433c-8572-2a7e13f59807.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "support async resolvers.", + "packageName": "@graphitation/apollo-mock-client", + "email": "pavelglac@gmail.com", + "dependentChangeType": "patch" +} From 16f4f0a802fcf9a2aaca0b9eb5215555b1467396 Mon Sep 17 00:00:00 2001 From: Pavel Glac Date: Thu, 26 Mar 2026 11:21:47 +0000 Subject: [PATCH 3/3] tests --- .../src/__tests__/AsyncResolvers.test.tsx | 145 ++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 packages/apollo-mock-client/src/__tests__/AsyncResolvers.test.tsx diff --git a/packages/apollo-mock-client/src/__tests__/AsyncResolvers.test.tsx b/packages/apollo-mock-client/src/__tests__/AsyncResolvers.test.tsx new file mode 100644 index 000000000..91140b180 --- /dev/null +++ b/packages/apollo-mock-client/src/__tests__/AsyncResolvers.test.tsx @@ -0,0 +1,145 @@ +import * as React from "react"; +import { graphql } from "@graphitation/graphql-js-tag"; +import { readFileSync } from "fs"; +import { buildSchema } from "graphql"; +import * as ReactTestRenderer from "react-test-renderer"; +import { ApolloProvider, useQuery } from "@apollo/client"; +import * as MockPayloadGenerator from "@graphitation/graphql-js-operation-payload-generator"; + +import { ApolloMockClient, createMockClient } from "../index"; + +const schema = buildSchema( + readFileSync( + require.resolve("relay-test-utils-internal/lib/testschema.graphql"), + "utf8", + ), +); + +const TestQuery = graphql` + query AsyncResolverTestQuery($id: ID = "") { + user: node(id: $id) { + id + name + } + } +`; + +describe("Async resolver support", () => { + let client: ApolloMockClient; + + beforeEach(() => { + client = createMockClient(schema); + }); + + it("should resolve with an async resolver", async () => { + const TestComponent: React.FC = () => { + const { data, loading } = useQuery<{ + user: { id: string; name: string }; + }>(TestQuery as any); + if (loading) return
Loading...
; + if (data) return
{data.user.name}
; + return null; + }; + + let tree: ReactTestRenderer.ReactTestRenderer; + ReactTestRenderer.act(() => { + tree = ReactTestRenderer.create( + + + , + ); + }); + + expect(() => + tree.root.find((node) => node.props.id === "loading"), + ).not.toThrow(); + + await ReactTestRenderer.act(() => + client.mock.resolveMostRecentOperation(async (operation) => + MockPayloadGenerator.generate(operation), + ), + ); + + expect(() => + tree.root.find((node) => node.props.id === "data"), + ).not.toThrow(); + }); + + it("should call onCompleted with async resolver for network-only fetchPolicy", async () => { + // Verifies the fix for https://github.com/apollographql/apollo-client/issues/11327 + // + // Apollo Client 3.8+ added a networkStatus transition guard to onCompleted + // (PR #10229). When mock resolution is synchronous, markReady() mutates + // queryInfo.networkStatus before zen-observable delivers data through + // reportResult(), causing getCurrentResult() to read an inconsistent + // intermediate state. This consumes the networkStatus transition without + // data, so by the time data arrives, onCompleted is blocked. + // + // An async resolver introduces a microtask boundary that flushes pending + // zen-observable subscription microtasks before delivering data, matching + // production timing where network responses are inherently async. + const onCompletedFn = jest.fn(); + + const TestComponent: React.FC = () => { + const { data, loading } = useQuery<{ + user: { id: string; name: string }; + }>(TestQuery as any, { + fetchPolicy: "network-only", + onCompleted: onCompletedFn, + }); + if (loading) return
Loading...
; + if (data) return
{data.user.name}
; + return null; + }; + + let tree: ReactTestRenderer.ReactTestRenderer; + ReactTestRenderer.act(() => { + tree = ReactTestRenderer.create( + + + , + ); + }); + + await ReactTestRenderer.act(() => + client.mock.resolveMostRecentOperation(async (operation) => + MockPayloadGenerator.generate(operation), + ), + ); + + expect(onCompletedFn).toHaveBeenCalledTimes(1); + expect(() => + tree!.root.find((node) => node.props.id === "data"), + ).not.toThrow(); + }); + + it("should still work with sync resolvers", async () => { + const TestComponent: React.FC = () => { + const { data, loading } = useQuery<{ + user: { id: string; name: string }; + }>(TestQuery as any); + if (loading) return
Loading...
; + if (data) return
{data.user.name}
; + return null; + }; + + let tree: ReactTestRenderer.ReactTestRenderer; + ReactTestRenderer.act(() => { + tree = ReactTestRenderer.create( + + + , + ); + }); + + await ReactTestRenderer.act(() => + client.mock.resolveMostRecentOperation((operation) => + MockPayloadGenerator.generate(operation), + ), + ); + + expect(() => + tree.root.find((node) => node.props.id === "data"), + ).not.toThrow(); + }); +});