From 2d6610d3c7967e7cb942db118afc0301c7d8f66c Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 3 Sep 2025 22:06:30 -0600 Subject: [PATCH 01/97] Add graphql-alpha.9 as dev dependency --- package-lock.json | 12 ++++++++++++ package.json | 1 + 2 files changed, 13 insertions(+) diff --git a/package-lock.json b/package-lock.json index bd60c4e9976..18dbd11a0c4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -82,6 +82,7 @@ "globals": "15.14.0", "graphql": "16.9.0", "graphql-17-alpha2": "npm:graphql@17.0.0-alpha.2", + "graphql-17-alpha9": "npm:graphql@17.0.0-alpha.9", "graphql-ws": "6.0.3", "jest": "29.7.0", "jest-environment-jsdom": "29.7.0", @@ -11498,6 +11499,17 @@ "node": "^14.19.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/graphql-17-alpha9": { + "name": "graphql", + "version": "17.0.0-alpha.9", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-17.0.0-alpha.9.tgz", + "integrity": "sha512-jVK1BsvX5pUIEpRDlEgeKJr80GAxl3B8ISsFDjXHtl2xAxMXVGTEFF4Q4R8NH0Gw7yMwcHDndkNjoNT5CbwHKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^16.19.0 || ^18.14.0 || >=19.7.0" + } + }, "node_modules/graphql-config": { "version": "5.1.5", "resolved": "https://registry.npmjs.org/graphql-config/-/graphql-config-5.1.5.tgz", diff --git a/package.json b/package.json index 71218fb900e..18c08e9fdca 100644 --- a/package.json +++ b/package.json @@ -214,6 +214,7 @@ "globals": "15.14.0", "graphql": "16.9.0", "graphql-17-alpha2": "npm:graphql@17.0.0-alpha.2", + "graphql-17-alpha9": "npm:graphql@17.0.0-alpha.9", "graphql-ws": "6.0.3", "jest": "29.7.0", "jest-environment-jsdom": "29.7.0", From 217905c455defd29812543918b4c9c52e178a21e Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 3 Sep 2025 22:18:58 -0600 Subject: [PATCH 02/97] [WIP] copy over alpha.9 tests --- .../__tests__/graphql17Alpha9.test.ts | 2581 +++++++++++++++++ 1 file changed, 2581 insertions(+) create mode 100644 src/incremental/handlers/__tests__/graphql17Alpha9.test.ts diff --git a/src/incremental/handlers/__tests__/graphql17Alpha9.test.ts b/src/incremental/handlers/__tests__/graphql17Alpha9.test.ts new file mode 100644 index 00000000000..4764884965e --- /dev/null +++ b/src/incremental/handlers/__tests__/graphql17Alpha9.test.ts @@ -0,0 +1,2581 @@ +import assert from "node:assert"; + +import type { + DocumentNode, + FormattedInitialIncrementalExecutionResult, + FormattedSubsequentIncrementalExecutionResult, +} from "graphql-17-alpha9"; +import { + experimentalExecuteIncrementally, + GraphQLID, + GraphQLList, + GraphQLNonNull, + GraphQLObjectType, + GraphQLSchema, + GraphQLString, +} from "graphql-17-alpha9"; + +import { gql } from "@apollo/client"; + +// This is the test setup of the `graphql-js` v17.0.0-alpha.9 release: +// https://github.com/graphql/graphql-js/blob/3283f8adf52e77a47f148ff2f30185c8d11ff0f0/src/execution/__tests__/defer-test.ts + +const friendType = new GraphQLObjectType({ + fields: { + id: { type: GraphQLID }, + name: { type: GraphQLString }, + nonNullName: { type: new GraphQLNonNull(GraphQLString) }, + }, + name: "Friend", +}); + +const friends = [ + { name: "Han", id: 2 }, + { name: "Leia", id: 3 }, + { name: "C-3PO", id: 4 }, +]; + +const deeperObject = new GraphQLObjectType({ + fields: { + foo: { type: GraphQLString }, + bar: { type: GraphQLString }, + baz: { type: GraphQLString }, + bak: { type: GraphQLString }, + }, + name: "DeeperObject", +}); + +const nestedObject = new GraphQLObjectType({ + fields: { + deeperObject: { type: deeperObject }, + name: { type: GraphQLString }, + }, + name: "NestedObject", +}); + +const anotherNestedObject = new GraphQLObjectType({ + fields: { + deeperObject: { type: deeperObject }, + }, + name: "AnotherNestedObject", +}); + +const hero = { + name: "Luke", + id: 1, + friends, + nestedObject, + anotherNestedObject, +}; + +const c = new GraphQLObjectType({ + fields: { + d: { type: GraphQLString }, + nonNullErrorField: { type: new GraphQLNonNull(GraphQLString) }, + }, + name: "c", +}); + +const e = new GraphQLObjectType({ + fields: { + f: { type: GraphQLString }, + }, + name: "e", +}); + +const b = new GraphQLObjectType({ + fields: { + c: { type: c }, + e: { type: e }, + }, + name: "b", +}); + +const a = new GraphQLObjectType({ + fields: { + b: { type: b }, + someField: { type: GraphQLString }, + }, + name: "a", +}); + +const g = new GraphQLObjectType({ + fields: { + h: { type: GraphQLString }, + }, + name: "g", +}); + +const heroType = new GraphQLObjectType({ + fields: { + id: { type: GraphQLID }, + name: { type: GraphQLString }, + nonNullName: { type: new GraphQLNonNull(GraphQLString) }, + friends: { + type: new GraphQLList(friendType), + }, + nestedObject: { type: nestedObject }, + anotherNestedObject: { type: anotherNestedObject }, + }, + name: "Hero", +}); + +const query = new GraphQLObjectType({ + fields: { + hero: { + type: heroType, + }, + a: { type: a }, + g: { type: g }, + }, + name: "Query", +}); + +const schema = new GraphQLSchema({ query }); + +async function* run( + document: DocumentNode, + rootValue: unknown = { hero }, + enableEarlyExecution = false +): AsyncGenerator< + | FormattedInitialIncrementalExecutionResult + | FormattedSubsequentIncrementalExecutionResult +> { + const result = await experimentalExecuteIncrementally({ + schema, + document, + rootValue, + enableEarlyExecution, + }); + + if ("initialResult" in result) { + yield JSON.parse( + JSON.stringify(result.initialResult) + ) as FormattedInitialIncrementalExecutionResult; + + for await (const incremental of result.subsequentResults) { + yield JSON.parse( + JSON.stringify(incremental) + ) as FormattedSubsequentIncrementalExecutionResult; + } + } else { + return result; + } +} + +describe("graphql-js test cases", () => { + // These test cases mirror defer tests of the `graphql-js` v17.0.0-alpha.9 release: + // https://github.com/graphql/graphql-js/blob/3283f8adf52e77a47f148ff2f30185c8d11ff0f0/src/execution/__tests__/defer-test.ts + + it("Can defer fragments containing scalar types", async () => { + const query = gql` + query HeroNameQuery { + hero { + id + ...NameFragment @defer + } + } + fragment NameFragment on Hero { + name + } + `; + const incoming = run(query); + + expectJSON(incoming).toDeepEqual([ + { + data: { + hero: { + id: "1", + }, + }, + pending: [{ id: "0", path: ["hero"] }], + hasNext: true, + }, + { + incremental: [ + { + data: { + name: "Luke", + }, + id: "0", + }, + ], + completed: [{ id: "0" }], + hasNext: false, + }, + ]); + }); + it("Can disable defer using if argument", async () => { + const document = parse(` + query HeroNameQuery { + hero { + id + ...NameFragment @defer(if: false) + } + } + fragment NameFragment on Hero { + name + } + `); + const result = await run(document); + + expectJSON(result).toDeepEqual({ + data: { + hero: { + id: "1", + name: "Luke", + }, + }, + }); + }); + it("Does not disable defer with null if argument", async () => { + const document = parse(` + query HeroNameQuery($shouldDefer: Boolean) { + hero { + id + ...NameFragment @defer(if: $shouldDefer) + } + } + fragment NameFragment on Hero { + name + } + `); + const result = await run(document); + expectJSON(result).toDeepEqual([ + { + data: { hero: { id: "1" } }, + pending: [{ id: "0", path: ["hero"] }], + hasNext: true, + }, + { + incremental: [ + { + data: { name: "Luke" }, + id: "0", + }, + ], + completed: [{ id: "0" }], + hasNext: false, + }, + ]); + }); + it("Does not execute deferred fragments early when not specified", async () => { + const document = parse(` + query HeroNameQuery { + hero { + id + ...NameFragment @defer + } + } + fragment NameFragment on Hero { + name + } + `); + const order: Array = []; + const result = await run(document, { + hero: { + ...hero, + id: async () => { + await resolveOnNextTick(); + await resolveOnNextTick(); + order.push("slow-id"); + return hero.id; + }, + name: () => { + order.push("fast-name"); + return hero.name; + }, + }, + }); + + expectJSON(result).toDeepEqual([ + { + data: { + hero: { + id: "1", + }, + }, + pending: [{ id: "0", path: ["hero"] }], + hasNext: true, + }, + { + incremental: [ + { + data: { + name: "Luke", + }, + id: "0", + }, + ], + completed: [{ id: "0" }], + hasNext: false, + }, + ]); + expect(order).to.deep.equal(["slow-id", "fast-name"]); + }); + it("Does execute deferred fragments early when specified", async () => { + const document = parse(` + query HeroNameQuery { + hero { + id + ...NameFragment @defer + } + } + fragment NameFragment on Hero { + name + } + `); + const order: Array = []; + const result = await run( + document, + { + hero: { + ...hero, + id: async () => { + await resolveOnNextTick(); + await resolveOnNextTick(); + order.push("slow-id"); + return hero.id; + }, + name: () => { + order.push("fast-name"); + return hero.name; + }, + }, + }, + true + ); + + expectJSON(result).toDeepEqual([ + { + data: { + hero: { + id: "1", + }, + }, + pending: [{ id: "0", path: ["hero"] }], + hasNext: true, + }, + { + incremental: [ + { + data: { + name: "Luke", + }, + id: "0", + }, + ], + completed: [{ id: "0" }], + hasNext: false, + }, + ]); + expect(order).to.deep.equal(["fast-name", "slow-id"]); + }); + it("Can defer fragments on the top level Query field", async () => { + const document = parse(` + query HeroNameQuery { + ...QueryFragment @defer(label: "DeferQuery") + } + fragment QueryFragment on Query { + hero { + id + } + } + `); + const result = await run(document); + + expectJSON(result).toDeepEqual([ + { + data: {}, + pending: [{ id: "0", path: [], label: "DeferQuery" }], + hasNext: true, + }, + { + incremental: [ + { + data: { + hero: { + id: "1", + }, + }, + id: "0", + }, + ], + completed: [{ id: "0" }], + hasNext: false, + }, + ]); + }); + it("Can defer fragments with errors on the top level Query field", async () => { + const document = parse(` + query HeroNameQuery { + ...QueryFragment @defer(label: "DeferQuery") + } + fragment QueryFragment on Query { + hero { + name + } + } + `); + const result = await run(document, { + hero: { + ...hero, + name: () => { + throw new Error("bad"); + }, + }, + }); + + expectJSON(result).toDeepEqual([ + { + data: {}, + pending: [{ id: "0", path: [], label: "DeferQuery" }], + hasNext: true, + }, + { + incremental: [ + { + data: { + hero: { + name: null, + }, + }, + errors: [ + { + message: "bad", + locations: [{ line: 7, column: 11 }], + path: ["hero", "name"], + }, + ], + id: "0", + }, + ], + completed: [{ id: "0" }], + hasNext: false, + }, + ]); + }); + it("Can defer a fragment within an already deferred fragment", async () => { + const document = parse(` + query HeroNameQuery { + hero { + ...TopFragment @defer(label: "DeferTop") + } + } + fragment TopFragment on Hero { + id + ...NestedFragment @defer(label: "DeferNested") + } + fragment NestedFragment on Hero { + friends { + name + } + } + `); + const result = await run(document); + + expectJSON(result).toDeepEqual([ + { + data: { + hero: {}, + }, + pending: [{ id: "0", path: ["hero"], label: "DeferTop" }], + hasNext: true, + }, + { + pending: [{ id: "1", path: ["hero"], label: "DeferNested" }], + incremental: [ + { + data: { + id: "1", + }, + id: "0", + }, + { + data: { + friends: [{ name: "Han" }, { name: "Leia" }, { name: "C-3PO" }], + }, + id: "1", + }, + ], + completed: [{ id: "0" }, { id: "1" }], + hasNext: false, + }, + ]); + }); + it("Can defer a fragment that is also not deferred, deferred fragment is first", async () => { + const document = parse(` + query HeroNameQuery { + hero { + ...TopFragment @defer(label: "DeferTop") + ...TopFragment + } + } + fragment TopFragment on Hero { + name + } + `); + const result = await run(document); + expectJSON(result).toDeepEqual({ + data: { + hero: { + name: "Luke", + }, + }, + }); + }); + it("Can defer a fragment that is also not deferred, non-deferred fragment is first", async () => { + const document = parse(` + query HeroNameQuery { + hero { + ...TopFragment + ...TopFragment @defer(label: "DeferTop") + } + } + fragment TopFragment on Hero { + name + } + `); + const result = await run(document); + expectJSON(result).toDeepEqual({ + data: { + hero: { + name: "Luke", + }, + }, + }); + }); + + it("Can defer an inline fragment", async () => { + const document = parse(` + query HeroNameQuery { + hero { + id + ... on Hero @defer(label: "InlineDeferred") { + name + } + } + } + `); + const result = await run(document); + + expectJSON(result).toDeepEqual([ + { + data: { hero: { id: "1" } }, + pending: [{ id: "0", path: ["hero"], label: "InlineDeferred" }], + hasNext: true, + }, + { + incremental: [{ data: { name: "Luke" }, id: "0" }], + completed: [{ id: "0" }], + hasNext: false, + }, + ]); + }); + + it("Does not emit empty defer fragments", async () => { + const document = parse(` + query HeroNameQuery { + hero { + ... @defer { + name @skip(if: true) + } + } + } + fragment TopFragment on Hero { + name + } + `); + const result = await run(document); + expectJSON(result).toDeepEqual({ + data: { + hero: {}, + }, + }); + }); + + it("Emits children of empty defer fragments", async () => { + const document = parse(` + query HeroNameQuery { + hero { + ... @defer { + ... @defer { + name + } + } + } + } + `); + const result = await run(document); + expectJSON(result).toDeepEqual([ + { + data: { + hero: {}, + }, + pending: [{ id: "0", path: ["hero"] }], + hasNext: true, + }, + { + incremental: [{ data: { name: "Luke" }, id: "0" }], + completed: [{ id: "0" }], + hasNext: false, + }, + ]); + }); + + it("Can separately emit defer fragments with different labels with varying fields", async () => { + const document = parse(` + query HeroNameQuery { + hero { + ... @defer(label: "DeferID") { + id + } + ... @defer(label: "DeferName") { + name + } + } + } + `); + const result = await run(document); + expectJSON(result).toDeepEqual([ + { + data: { + hero: {}, + }, + pending: [ + { id: "0", path: ["hero"], label: "DeferID" }, + { id: "1", path: ["hero"], label: "DeferName" }, + ], + hasNext: true, + }, + { + incremental: [ + { + data: { + id: "1", + }, + id: "0", + }, + { + data: { + name: "Luke", + }, + id: "1", + }, + ], + completed: [{ id: "0" }, { id: "1" }], + hasNext: false, + }, + ]); + }); + + it("Separately emits defer fragments with different labels with varying subfields", async () => { + const document = parse(` + query HeroNameQuery { + ... @defer(label: "DeferID") { + hero { + id + } + } + ... @defer(label: "DeferName") { + hero { + name + } + } + } + `); + const result = await run(document); + expectJSON(result).toDeepEqual([ + { + data: {}, + pending: [ + { id: "0", path: [], label: "DeferID" }, + { id: "1", path: [], label: "DeferName" }, + ], + hasNext: true, + }, + { + incremental: [ + { + data: { hero: {} }, + id: "0", + }, + { + data: { id: "1" }, + id: "0", + subPath: ["hero"], + }, + { + data: { name: "Luke" }, + id: "1", + subPath: ["hero"], + }, + ], + completed: [{ id: "0" }, { id: "1" }], + hasNext: false, + }, + ]); + }); + + it("Separately emits defer fragments with different labels with varying subfields that return promises", async () => { + const document = parse(` + query HeroNameQuery { + ... @defer(label: "DeferID") { + hero { + id + } + } + ... @defer(label: "DeferName") { + hero { + name + } + } + } + `); + const result = await run(document, { + hero: { + id: () => Promise.resolve("1"), + name: () => Promise.resolve("Luke"), + }, + }); + expectJSON(result).toDeepEqual([ + { + data: {}, + pending: [ + { id: "0", path: [], label: "DeferID" }, + { id: "1", path: [], label: "DeferName" }, + ], + hasNext: true, + }, + { + incremental: [ + { + data: { hero: {} }, + id: "0", + }, + { + data: { id: "1" }, + id: "0", + subPath: ["hero"], + }, + { + data: { name: "Luke" }, + id: "1", + subPath: ["hero"], + }, + ], + completed: [{ id: "0" }, { id: "1" }], + hasNext: false, + }, + ]); + }); + + it("Separately emits defer fragments with varying subfields of same priorities but different level of defers", async () => { + const document = parse(` + query HeroNameQuery { + hero { + ... @defer(label: "DeferID") { + id + } + } + ... @defer(label: "DeferName") { + hero { + name + } + } + } + `); + const result = await run(document); + expectJSON(result).toDeepEqual([ + { + data: { + hero: {}, + }, + pending: [ + { id: "0", path: ["hero"], label: "DeferID" }, + { id: "1", path: [], label: "DeferName" }, + ], + hasNext: true, + }, + { + incremental: [ + { + data: { + id: "1", + }, + id: "0", + }, + { + data: { + name: "Luke", + }, + id: "1", + subPath: ["hero"], + }, + ], + completed: [{ id: "0" }, { id: "1" }], + hasNext: false, + }, + ]); + }); + + it("Separately emits nested defer fragments with varying subfields of same priorities but different level of defers", async () => { + const document = parse(` + query HeroNameQuery { + ... @defer(label: "DeferName") { + hero { + name + ... @defer(label: "DeferID") { + id + } + } + } + } + `); + const result = await run(document); + expectJSON(result).toDeepEqual([ + { + data: {}, + pending: [{ id: "0", path: [], label: "DeferName" }], + hasNext: true, + }, + { + pending: [{ id: "1", path: ["hero"], label: "DeferID" }], + incremental: [ + { + data: { + hero: { + name: "Luke", + }, + }, + id: "0", + }, + { + data: { + id: "1", + }, + id: "1", + }, + ], + completed: [{ id: "0" }, { id: "1" }], + hasNext: false, + }, + ]); + }); + + it("Initiates deferred grouped field sets only if they have been released as pending", async () => { + const document = parse(` + query { + ... @defer { + a { + ... @defer { + b { + c { d } + } + } + } + } + ... @defer { + a { + someField + ... @defer { + b { + e { f } + } + } + } + } + } + `); + + const { promise: slowFieldPromise, resolve: resolveSlowField } = + promiseWithResolvers(); + let cResolverCalled = false; + let eResolverCalled = false; + const executeResult = experimentalExecuteIncrementally({ + schema, + document, + rootValue: { + a: { + someField: slowFieldPromise, + b: { + c: () => { + cResolverCalled = true; + return { d: "d" }; + }, + e: () => { + eResolverCalled = true; + return { f: "f" }; + }, + }, + }, + }, + enableEarlyExecution: false, + }); + + assert("initialResult" in executeResult); + + const result1 = executeResult.initialResult; + expectJSON(result1).toDeepEqual({ + data: {}, + pending: [ + { id: "0", path: [] }, + { id: "1", path: [] }, + ], + hasNext: true, + }); + + const iterator = executeResult.subsequentResults[Symbol.asyncIterator](); + + expect(cResolverCalled).to.equal(false); + expect(eResolverCalled).to.equal(false); + + const result2 = await iterator.next(); + expectJSON(result2).toDeepEqual({ + value: { + pending: [{ id: "2", path: ["a"] }], + incremental: [ + { + data: { a: {} }, + id: "0", + }, + { + data: { b: {} }, + id: "2", + }, + { + data: { c: { d: "d" } }, + id: "2", + subPath: ["b"], + }, + ], + completed: [{ id: "0" }, { id: "2" }], + hasNext: true, + }, + done: false, + }); + + expect(cResolverCalled).to.equal(true); + expect(eResolverCalled).to.equal(false); + + resolveSlowField("someField"); + + const result3 = await iterator.next(); + expectJSON(result3).toDeepEqual({ + value: { + pending: [{ id: "3", path: ["a"] }], + incremental: [ + { + data: { someField: "someField" }, + id: "1", + subPath: ["a"], + }, + { + data: { e: { f: "f" } }, + id: "3", + subPath: ["b"], + }, + ], + completed: [{ id: "1" }, { id: "3" }], + hasNext: false, + }, + done: false, + }); + + expect(eResolverCalled).to.equal(true); + + const result4 = await iterator.next(); + expectJSON(result4).toDeepEqual({ + value: undefined, + done: true, + }); + }); + + it("Initiates unique deferred grouped field sets after those that are common to sibling defers", async () => { + const document = parse(` + query { + ... @defer { + a { + ... @defer { + b { + c { d } + } + } + } + } + ... @defer { + a { + ... @defer { + b { + c { d } + e { f } + } + } + } + } + } + `); + + const { promise: cPromise, resolve: resolveC } = + promiseWithResolvers(); + let cResolverCalled = false; + let eResolverCalled = false; + const executeResult = experimentalExecuteIncrementally({ + schema, + document, + rootValue: { + a: { + b: { + c: async () => { + cResolverCalled = true; + await cPromise; + return { d: "d" }; + }, + e: () => { + eResolverCalled = true; + return { f: "f" }; + }, + }, + }, + }, + enableEarlyExecution: false, + }); + + assert("initialResult" in executeResult); + + const result1 = executeResult.initialResult; + expectJSON(result1).toDeepEqual({ + data: {}, + pending: [ + { id: "0", path: [] }, + { id: "1", path: [] }, + ], + hasNext: true, + }); + + const iterator = executeResult.subsequentResults[Symbol.asyncIterator](); + + expect(cResolverCalled).to.equal(false); + expect(eResolverCalled).to.equal(false); + + const result2 = await iterator.next(); + expectJSON(result2).toDeepEqual({ + value: { + pending: [ + { id: "2", path: ["a"] }, + { id: "3", path: ["a"] }, + ], + incremental: [ + { + data: { a: {} }, + id: "0", + }, + ], + completed: [{ id: "0" }, { id: "1" }], + hasNext: true, + }, + done: false, + }); + + resolveC(); + + expect(cResolverCalled).to.equal(true); + expect(eResolverCalled).to.equal(false); + + const result3 = await iterator.next(); + expectJSON(result3).toDeepEqual({ + value: { + incremental: [ + { + data: { b: { c: { d: "d" } } }, + id: "2", + }, + { + data: { e: { f: "f" } }, + id: "3", + subPath: ["b"], + }, + ], + completed: [{ id: "2" }, { id: "3" }], + hasNext: false, + }, + done: false, + }); + + const result4 = await iterator.next(); + expectJSON(result4).toDeepEqual({ + value: undefined, + done: true, + }); + }); + + it("Can deduplicate multiple defers on the same object", async () => { + const document = parse(` + query { + hero { + friends { + ... @defer { + ...FriendFrag + ... @defer { + ...FriendFrag + ... @defer { + ...FriendFrag + ... @defer { + ...FriendFrag + } + } + } + } + } + } + } + + fragment FriendFrag on Friend { + id + name + } + `); + const result = await run(document); + + expectJSON(result).toDeepEqual([ + { + data: { hero: { friends: [{}, {}, {}] } }, + pending: [ + { id: "0", path: ["hero", "friends", 0] }, + { id: "1", path: ["hero", "friends", 1] }, + { id: "2", path: ["hero", "friends", 2] }, + ], + hasNext: true, + }, + { + incremental: [ + { data: { id: "2", name: "Han" }, id: "0" }, + { data: { id: "3", name: "Leia" }, id: "1" }, + { data: { id: "4", name: "C-3PO" }, id: "2" }, + ], + completed: [{ id: "0" }, { id: "1" }, { id: "2" }], + hasNext: false, + }, + ]); + }); + + it("Deduplicates fields present in the initial payload", async () => { + const document = parse(` + query { + hero { + nestedObject { + deeperObject { + foo + } + } + anotherNestedObject { + deeperObject { + foo + } + } + ... @defer { + nestedObject { + deeperObject { + bar + } + } + anotherNestedObject { + deeperObject { + foo + } + } + } + } + } + `); + const result = await run(document, { + hero: { + nestedObject: { deeperObject: { foo: "foo", bar: "bar" } }, + anotherNestedObject: { deeperObject: { foo: "foo" } }, + }, + }); + expectJSON(result).toDeepEqual([ + { + data: { + hero: { + nestedObject: { + deeperObject: { + foo: "foo", + }, + }, + anotherNestedObject: { + deeperObject: { + foo: "foo", + }, + }, + }, + }, + pending: [{ id: "0", path: ["hero"] }], + hasNext: true, + }, + { + incremental: [ + { + data: { bar: "bar" }, + id: "0", + subPath: ["nestedObject", "deeperObject"], + }, + ], + completed: [{ id: "0" }], + hasNext: false, + }, + ]); + }); + + it("Deduplicates fields present in a parent defer payload", async () => { + const document = parse(` + query { + hero { + ... @defer { + nestedObject { + deeperObject { + foo + ... @defer { + foo + bar + } + } + } + } + } + } + `); + const result = await run(document, { + hero: { nestedObject: { deeperObject: { foo: "foo", bar: "bar" } } }, + }); + expectJSON(result).toDeepEqual([ + { + data: { + hero: {}, + }, + pending: [{ id: "0", path: ["hero"] }], + hasNext: true, + }, + { + pending: [{ id: "1", path: ["hero", "nestedObject", "deeperObject"] }], + incremental: [ + { + data: { + nestedObject: { + deeperObject: { foo: "foo" }, + }, + }, + id: "0", + }, + { + data: { + bar: "bar", + }, + id: "1", + }, + ], + completed: [{ id: "0" }, { id: "1" }], + hasNext: false, + }, + ]); + }); + + it("Deduplicates fields with deferred fragments at multiple levels", async () => { + const document = parse(` + query { + hero { + nestedObject { + deeperObject { + foo + } + } + ... @defer { + nestedObject { + deeperObject { + foo + bar + } + ... @defer { + deeperObject { + foo + bar + baz + ... @defer { + foo + bar + baz + bak + } + } + } + } + } + } + } + `); + const result = await run(document, { + hero: { + nestedObject: { + deeperObject: { foo: "foo", bar: "bar", baz: "baz", bak: "bak" }, + }, + }, + }); + expectJSON(result).toDeepEqual([ + { + data: { + hero: { + nestedObject: { + deeperObject: { + foo: "foo", + }, + }, + }, + }, + pending: [{ id: "0", path: ["hero"] }], + hasNext: true, + }, + { + pending: [ + { id: "1", path: ["hero", "nestedObject"] }, + { id: "2", path: ["hero", "nestedObject", "deeperObject"] }, + ], + incremental: [ + { + data: { bar: "bar" }, + id: "0", + subPath: ["nestedObject", "deeperObject"], + }, + { + data: { baz: "baz" }, + id: "1", + subPath: ["deeperObject"], + }, + { + data: { bak: "bak" }, + id: "2", + }, + ], + completed: [{ id: "0" }, { id: "1" }, { id: "2" }], + hasNext: false, + }, + ]); + }); + + it("Deduplicates multiple fields from deferred fragments from different branches occurring at the same level", async () => { + const document = parse(` + query { + hero { + nestedObject { + deeperObject { + ... @defer { + foo + } + } + } + ... @defer { + nestedObject { + deeperObject { + ... @defer { + foo + bar + } + } + } + } + } + } + `); + const result = await run(document, { + hero: { nestedObject: { deeperObject: { foo: "foo", bar: "bar" } } }, + }); + expectJSON(result).toDeepEqual([ + { + data: { + hero: { + nestedObject: { + deeperObject: {}, + }, + }, + }, + pending: [ + { id: "0", path: ["hero", "nestedObject", "deeperObject"] }, + { id: "1", path: ["hero", "nestedObject", "deeperObject"] }, + ], + hasNext: true, + }, + { + incremental: [ + { + data: { + foo: "foo", + }, + id: "0", + }, + { + data: { + bar: "bar", + }, + id: "1", + }, + ], + completed: [{ id: "0" }, { id: "1" }], + hasNext: false, + }, + ]); + }); + + it("Deduplicate fields with deferred fragments in different branches at multiple non-overlapping levels", async () => { + const document = parse(` + query { + a { + b { + c { + d + } + ... @defer { + e { + f + } + } + } + } + ... @defer { + a { + b { + e { + f + } + } + } + g { + h + } + } + } + `); + const result = await run(document, { + a: { + b: { + c: { d: "d" }, + e: { f: "f" }, + }, + }, + g: { h: "h" }, + }); + expectJSON(result).toDeepEqual([ + { + data: { + a: { + b: { + c: { + d: "d", + }, + }, + }, + }, + pending: [ + { id: "0", path: ["a", "b"] }, + { id: "1", path: [] }, + ], + hasNext: true, + }, + { + incremental: [ + { + data: { e: { f: "f" } }, + id: "0", + }, + { + data: { g: { h: "h" } }, + id: "1", + }, + ], + completed: [{ id: "0" }, { id: "1" }], + hasNext: false, + }, + ]); + }); + + it("Correctly bundles varying subfields into incremental data records unique by defer combination, ignoring fields in a fragment masked by a parent defer", async () => { + const document = parse(` + query HeroNameQuery { + ... @defer { + hero { + id + } + } + ... @defer { + hero { + name + shouldBeWithNameDespiteAdditionalDefer: name + ... @defer { + shouldBeWithNameDespiteAdditionalDefer: name + } + } + } + } + `); + const result = await run(document); + expectJSON(result).toDeepEqual([ + { + data: {}, + pending: [ + { id: "0", path: [] }, + { id: "1", path: [] }, + ], + hasNext: true, + }, + { + incremental: [ + { + data: { hero: {} }, + id: "0", + }, + { + data: { id: "1" }, + id: "0", + subPath: ["hero"], + }, + { + data: { + name: "Luke", + shouldBeWithNameDespiteAdditionalDefer: "Luke", + }, + id: "1", + subPath: ["hero"], + }, + ], + completed: [{ id: "0" }, { id: "1" }], + hasNext: false, + }, + ]); + }); + + it("Nulls cross defer boundaries, null first", async () => { + const document = parse(` + query { + ... @defer { + a { + someField + b { + c { + nonNullErrorField + } + } + } + } + a { + ... @defer { + b { + c { + d + } + } + } + } + } + `); + const result = await run(document, { + a: { b: { c: { d: "d" } }, someField: "someField" }, + }); + expectJSON(result).toDeepEqual([ + { + data: { + a: {}, + }, + pending: [ + { id: "0", path: [] }, + { id: "1", path: ["a"] }, + ], + hasNext: true, + }, + { + incremental: [ + { + data: { b: { c: {} } }, + id: "1", + }, + { + data: { d: "d" }, + id: "1", + subPath: ["b", "c"], + }, + ], + completed: [ + { + id: "0", + errors: [ + { + message: + "Cannot return null for non-nullable field c.nonNullErrorField.", + locations: [{ line: 8, column: 17 }], + path: ["a", "b", "c", "nonNullErrorField"], + }, + ], + }, + { id: "1" }, + ], + hasNext: false, + }, + ]); + }); + + it("Nulls cross defer boundaries, value first", async () => { + const document = parse(` + query { + ... @defer { + a { + b { + c { + d + } + } + } + } + a { + ... @defer { + someField + b { + c { + nonNullErrorField + } + } + } + } + } + `); + const result = await run(document, { + a: { + b: { c: { d: "d" }, nonNullErrorFIeld: null }, + someField: "someField", + }, + }); + expectJSON(result).toDeepEqual([ + { + data: { + a: {}, + }, + pending: [ + { id: "0", path: [] }, + { id: "1", path: ["a"] }, + ], + hasNext: true, + }, + { + incremental: [ + { + data: { b: { c: {} } }, + id: "1", + }, + { + data: { d: "d" }, + id: "0", + subPath: ["a", "b", "c"], + }, + ], + completed: [ + { id: "0" }, + { + id: "1", + errors: [ + { + message: + "Cannot return null for non-nullable field c.nonNullErrorField.", + locations: [{ line: 17, column: 17 }], + path: ["a", "b", "c", "nonNullErrorField"], + }, + ], + }, + ], + hasNext: false, + }, + ]); + }); + + it("Handles multiple erroring deferred grouped field sets", async () => { + const document = parse(` + query { + ... @defer { + a { + b { + c { + someError: nonNullErrorField + } + } + } + } + ... @defer { + a { + b { + c { + anotherError: nonNullErrorField + } + } + } + } + } + `); + const result = await run(document, { + a: { + b: { c: { nonNullErrorField: null } }, + }, + }); + expectJSON(result).toDeepEqual([ + { + data: {}, + pending: [ + { id: "0", path: [] }, + { id: "1", path: [] }, + ], + hasNext: true, + }, + { + completed: [ + { + id: "0", + errors: [ + { + message: + "Cannot return null for non-nullable field c.nonNullErrorField.", + locations: [{ line: 7, column: 17 }], + path: ["a", "b", "c", "someError"], + }, + ], + }, + { + id: "1", + errors: [ + { + message: + "Cannot return null for non-nullable field c.nonNullErrorField.", + locations: [{ line: 16, column: 17 }], + path: ["a", "b", "c", "anotherError"], + }, + ], + }, + ], + hasNext: false, + }, + ]); + }); + + it("Handles multiple erroring deferred grouped field sets for the same fragment", async () => { + const document = parse(` + query { + ... @defer { + a { + b { + someC: c { + d: d + } + anotherC: c { + d: d + } + } + } + } + ... @defer { + a { + b { + someC: c { + someError: nonNullErrorField + } + anotherC: c { + anotherError: nonNullErrorField + } + } + } + } + } + `); + const result = await run(document, { + a: { + b: { c: { d: "d", nonNullErrorField: null } }, + }, + }); + expectJSON(result).toDeepEqual([ + { + data: {}, + pending: [ + { id: "0", path: [] }, + { id: "1", path: [] }, + ], + hasNext: true, + }, + { + incremental: [ + { + data: { a: { b: { someC: {}, anotherC: {} } } }, + id: "0", + }, + { + data: { d: "d" }, + id: "0", + subPath: ["a", "b", "someC"], + }, + { + data: { d: "d" }, + id: "0", + subPath: ["a", "b", "anotherC"], + }, + ], + completed: [ + { + id: "1", + errors: [ + { + message: + "Cannot return null for non-nullable field c.nonNullErrorField.", + locations: [{ line: 19, column: 17 }], + path: ["a", "b", "someC", "someError"], + }, + ], + }, + { id: "0" }, + ], + hasNext: false, + }, + ]); + }); + + it("filters a payload with a null that cannot be merged", async () => { + const document = parse(` + query { + ... @defer { + a { + someField + b { + c { + nonNullErrorField + } + } + } + } + a { + ... @defer { + b { + c { + d + } + } + } + } + } + `); + const result = await run( + document, + { + a: { + b: { + c: { + d: "d", + nonNullErrorField: async () => { + await resolveOnNextTick(); + return null; + }, + }, + }, + someField: "someField", + }, + }, + true + ); + expectJSON(result).toDeepEqual([ + { + data: { + a: {}, + }, + pending: [ + { id: "0", path: [] }, + { id: "1", path: ["a"] }, + ], + hasNext: true, + }, + { + incremental: [ + { + data: { b: { c: {} } }, + id: "1", + }, + { + data: { d: "d" }, + id: "1", + subPath: ["b", "c"], + }, + ], + completed: [{ id: "1" }], + hasNext: true, + }, + { + completed: [ + { + id: "0", + errors: [ + { + message: + "Cannot return null for non-nullable field c.nonNullErrorField.", + locations: [{ line: 8, column: 17 }], + path: ["a", "b", "c", "nonNullErrorField"], + }, + ], + }, + ], + hasNext: false, + }, + ]); + }); + + it("Cancels deferred fields when initial result exhibits null bubbling", async () => { + const document = parse(` + query { + hero { + nonNullName + } + ... @defer { + hero { + name + } + } + } + `); + const result = await run( + document, + { + hero: { + ...hero, + nonNullName: () => null, + }, + }, + true + ); + expectJSON(result).toDeepEqual({ + data: { + hero: null, + }, + errors: [ + { + message: + "Cannot return null for non-nullable field Hero.nonNullName.", + locations: [{ line: 4, column: 11 }], + path: ["hero", "nonNullName"], + }, + ], + }); + }); + + it("Cancels deferred fields when deferred result exhibits null bubbling", async () => { + const document = parse(` + query { + ... @defer { + hero { + nonNullName + name + } + } + } + `); + const result = await run( + document, + { + hero: { + ...hero, + nonNullName: () => null, + }, + }, + true + ); + expectJSON(result).toDeepEqual([ + { + data: {}, + pending: [{ id: "0", path: [] }], + hasNext: true, + }, + { + incremental: [ + { + data: { + hero: null, + }, + errors: [ + { + message: + "Cannot return null for non-nullable field Hero.nonNullName.", + locations: [{ line: 5, column: 13 }], + path: ["hero", "nonNullName"], + }, + ], + id: "0", + }, + ], + completed: [{ id: "0" }], + hasNext: false, + }, + ]); + }); + + it("Deduplicates list fields", async () => { + const document = parse(` + query { + hero { + friends { + name + } + ... @defer { + friends { + name + } + } + } + } + `); + const result = await run(document); + expectJSON(result).toDeepEqual({ + data: { + hero: { + friends: [{ name: "Han" }, { name: "Leia" }, { name: "C-3PO" }], + }, + }, + }); + }); + + it("Deduplicates async iterable list fields", async () => { + const document = parse(` + query { + hero { + friends { + name + } + ... @defer { + friends { + name + } + } + } + } + `); + const result = await run(document, { + hero: { + ...hero, + friends: async function* resolve() { + yield await Promise.resolve(friends[0]); + }, + }, + }); + expectJSON(result).toDeepEqual({ + data: { hero: { friends: [{ name: "Han" }] } }, + }); + }); + + it("Deduplicates empty async iterable list fields", async () => { + const document = parse(` + query { + hero { + friends { + name + } + ... @defer { + friends { + name + } + } + } + } + `); + const result = await run(document, { + hero: { + ...hero, + + friends: async function* resolve() { + await resolveOnNextTick(); + }, + }, + }); + expectJSON(result).toDeepEqual({ + data: { hero: { friends: [] } }, + }); + }); + + it("Does not deduplicate list fields with non-overlapping fields", async () => { + const document = parse(` + query { + hero { + friends { + name + } + ... @defer { + friends { + id + } + } + } + } + `); + const result = await run(document); + expectJSON(result).toDeepEqual([ + { + data: { + hero: { + friends: [{ name: "Han" }, { name: "Leia" }, { name: "C-3PO" }], + }, + }, + pending: [{ id: "0", path: ["hero"] }], + hasNext: true, + }, + { + incremental: [ + { + data: { id: "2" }, + id: "0", + subPath: ["friends", 0], + }, + { + data: { id: "3" }, + id: "0", + subPath: ["friends", 1], + }, + { + data: { id: "4" }, + id: "0", + subPath: ["friends", 2], + }, + ], + completed: [{ id: "0" }], + hasNext: false, + }, + ]); + }); + + it("Deduplicates list fields that return empty lists", async () => { + const document = parse(` + query { + hero { + friends { + name + } + ... @defer { + friends { + name + } + } + } + } + `); + const result = await run(document, { + hero: { + ...hero, + friends: () => [], + }, + }); + expectJSON(result).toDeepEqual({ + data: { hero: { friends: [] } }, + }); + }); + + it("Deduplicates null object fields", async () => { + const document = parse(` + query { + hero { + nestedObject { + name + } + ... @defer { + nestedObject { + name + } + } + } + } + `); + const result = await run(document, { + hero: { + ...hero, + nestedObject: () => null, + }, + }); + expectJSON(result).toDeepEqual({ + data: { hero: { nestedObject: null } }, + }); + }); + + it("Deduplicates promise object fields", async () => { + const document = parse(` + query { + hero { + nestedObject { + name + } + ... @defer { + nestedObject { + name + } + } + } + } + `); + const result = await run(document, { + hero: { + nestedObject: () => Promise.resolve({ name: "foo" }), + }, + }); + expectJSON(result).toDeepEqual({ + data: { hero: { nestedObject: { name: "foo" } } }, + }); + }); + + it("Handles errors thrown in deferred fragments", async () => { + const document = parse(` + query HeroNameQuery { + hero { + id + ...NameFragment @defer + } + } + fragment NameFragment on Hero { + name + } + `); + const result = await run(document, { + hero: { + ...hero, + name: () => { + throw new Error("bad"); + }, + }, + }); + expectJSON(result).toDeepEqual([ + { + data: { hero: { id: "1" } }, + pending: [{ id: "0", path: ["hero"] }], + hasNext: true, + }, + { + incremental: [ + { + data: { name: null }, + id: "0", + errors: [ + { + message: "bad", + locations: [{ line: 9, column: 9 }], + path: ["hero", "name"], + }, + ], + }, + ], + completed: [{ id: "0" }], + hasNext: false, + }, + ]); + }); + it("Handles non-nullable errors thrown in deferred fragments", async () => { + const document = parse(` + query HeroNameQuery { + hero { + id + ...NameFragment @defer + } + } + fragment NameFragment on Hero { + nonNullName + } + `); + const result = await run(document, { + hero: { + ...hero, + nonNullName: () => null, + }, + }); + expectJSON(result).toDeepEqual([ + { + data: { hero: { id: "1" } }, + pending: [{ id: "0", path: ["hero"] }], + hasNext: true, + }, + { + completed: [ + { + id: "0", + errors: [ + { + message: + "Cannot return null for non-nullable field Hero.nonNullName.", + locations: [{ line: 9, column: 9 }], + path: ["hero", "nonNullName"], + }, + ], + }, + ], + hasNext: false, + }, + ]); + }); + it("Handles non-nullable errors thrown outside deferred fragments", async () => { + const document = parse(` + query HeroNameQuery { + hero { + nonNullName + ...NameFragment @defer + } + } + fragment NameFragment on Hero { + id + } + `); + const result = await run(document, { + hero: { + ...hero, + nonNullName: () => null, + }, + }); + expectJSON(result).toDeepEqual({ + errors: [ + { + message: + "Cannot return null for non-nullable field Hero.nonNullName.", + locations: [ + { + line: 4, + column: 11, + }, + ], + path: ["hero", "nonNullName"], + }, + ], + data: { + hero: null, + }, + }); + }); + it("Handles async non-nullable errors thrown in deferred fragments", async () => { + const document = parse(` + query HeroNameQuery { + hero { + id + ...NameFragment @defer + } + } + fragment NameFragment on Hero { + nonNullName + } + `); + const result = await run(document, { + hero: { + ...hero, + nonNullName: () => Promise.resolve(null), + }, + }); + expectJSON(result).toDeepEqual([ + { + data: { hero: { id: "1" } }, + pending: [{ id: "0", path: ["hero"] }], + hasNext: true, + }, + { + completed: [ + { + id: "0", + errors: [ + { + message: + "Cannot return null for non-nullable field Hero.nonNullName.", + locations: [{ line: 9, column: 9 }], + path: ["hero", "nonNullName"], + }, + ], + }, + ], + hasNext: false, + }, + ]); + }); + it("Returns payloads in correct order", async () => { + const document = parse(` + query HeroNameQuery { + hero { + id + ...NameFragment @defer + } + } + fragment NameFragment on Hero { + name + friends { + ...NestedFragment @defer + } + } + fragment NestedFragment on Friend { + name + } + `); + const result = await run(document, { + hero: { + ...hero, + name: async () => { + await resolveOnNextTick(); + return "slow"; + }, + }, + }); + expectJSON(result).toDeepEqual([ + { + data: { + hero: { id: "1" }, + }, + pending: [{ id: "0", path: ["hero"] }], + hasNext: true, + }, + { + pending: [ + { id: "1", path: ["hero", "friends", 0] }, + { id: "2", path: ["hero", "friends", 1] }, + { id: "3", path: ["hero", "friends", 2] }, + ], + incremental: [ + { + data: { name: "slow", friends: [{}, {}, {}] }, + id: "0", + }, + { data: { name: "Han" }, id: "1" }, + { data: { name: "Leia" }, id: "2" }, + { data: { name: "C-3PO" }, id: "3" }, + ], + completed: [{ id: "0" }, { id: "1" }, { id: "2" }, { id: "3" }], + hasNext: false, + }, + ]); + }); + it("Returns payloads from synchronous data in correct order", async () => { + const document = parse(` + query HeroNameQuery { + hero { + id + ...NameFragment @defer + } + } + fragment NameFragment on Hero { + name + friends { + ...NestedFragment @defer + } + } + fragment NestedFragment on Friend { + name + } + `); + const result = await run(document); + expectJSON(result).toDeepEqual([ + { + data: { + hero: { id: "1" }, + }, + pending: [{ id: "0", path: ["hero"] }], + hasNext: true, + }, + { + pending: [ + { id: "1", path: ["hero", "friends", 0] }, + { id: "2", path: ["hero", "friends", 1] }, + { id: "3", path: ["hero", "friends", 2] }, + ], + incremental: [ + { + data: { + name: "Luke", + friends: [{}, {}, {}], + }, + id: "0", + }, + { data: { name: "Han" }, id: "1" }, + { data: { name: "Leia" }, id: "2" }, + { data: { name: "C-3PO" }, id: "3" }, + ], + completed: [{ id: "0" }, { id: "1" }, { id: "2" }, { id: "3" }], + hasNext: false, + }, + ]); + }); + + it("Filters deferred payloads when a list item returned by an async iterable is nulled", async () => { + const document = parse(` + query { + hero { + friends { + nonNullName + ...NameFragment @defer + } + } + } + fragment NameFragment on Friend { + name + } + `); + const result = await run(document, { + hero: { + ...hero, + async *friends() { + yield await Promise.resolve({ + ...friends[0], + nonNullName: () => Promise.resolve(null), + }); + }, + }, + }); + expectJSON(result).toDeepEqual({ + data: { + hero: { + friends: [null], + }, + }, + errors: [ + { + message: + "Cannot return null for non-nullable field Friend.nonNullName.", + locations: [{ line: 5, column: 11 }], + path: ["hero", "friends", 0, "nonNullName"], + }, + ], + }); + }); + + it("original execute function throws error if anything is deferred and everything else is sync", () => { + const doc = ` + query Deferred { + ... @defer { hero { id } } + } + `; + expect(() => + execute({ + schema, + document: parse(doc), + rootValue: {}, + }) + ).to.throw( + "Executing this GraphQL operation would unexpectedly produce multiple payloads (due to @defer or @stream directive)" + ); + }); + + it("original execute function resolves to error if anything is deferred and something else is async", async () => { + const doc = ` + query Deferred { + hero { name } + ... @defer { hero { id } } + } + `; + await expectPromise( + execute({ + schema, + document: parse(doc), + rootValue: { + hero: { + ...hero, + name: async () => { + await resolveOnNextTick(); + return "slow"; + }, + }, + }, + }) + ).toRejectWith( + "Executing this GraphQL operation would unexpectedly produce multiple payloads (due to @defer or @stream directive)" + ); + }); +}); From 66d6c9be5dac12ab0e183757f6c1e88018a75ed7 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 4 Sep 2025 00:01:08 -0600 Subject: [PATCH 03/97] Update the remaining tests to work with the handler --- .../__tests__/graphql17Alpha9.test.ts | 2785 ++++++++--------- 1 file changed, 1246 insertions(+), 1539 deletions(-) diff --git a/src/incremental/handlers/__tests__/graphql17Alpha9.test.ts b/src/incremental/handlers/__tests__/graphql17Alpha9.test.ts index 4764884965e..fe79c212075 100644 --- a/src/incremental/handlers/__tests__/graphql17Alpha9.test.ts +++ b/src/incremental/handlers/__tests__/graphql17Alpha9.test.ts @@ -15,7 +15,13 @@ import { GraphQLString, } from "graphql-17-alpha9"; -import { gql } from "@apollo/client"; +import { ApolloLink, gql, Observable } from "@apollo/client"; + +import { + GraphQL17Alpha9Handler, + hasIncrementalChunks, + // eslint-disable-next-line local-rules/no-relative-imports +} from "../graphql17Alpha9.js"; // This is the test setup of the `graphql-js` v17.0.0-alpha.9 release: // https://github.com/graphql/graphql-js/blob/3283f8adf52e77a47f148ff2f30185c8d11ff0f0/src/execution/__tests__/defer-test.ts @@ -133,6 +139,27 @@ const query = new GraphQLObjectType({ const schema = new GraphQLSchema({ query }); +function resolveOnNextTick(): Promise { + return Promise.resolve(undefined); +} + +type PromiseOrValue = Promise | T; + +function promiseWithResolvers(): { + promise: Promise; + resolve: (value: T | PromiseOrValue) => void; + reject: (reason?: any) => void; +} { + // these are assigned synchronously within the Promise constructor + let resolve!: (value: T | PromiseOrValue) => void; + let reject!: (reason?: any) => void; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + return { promise, resolve, reject }; +} + async function* run( document: DocumentNode, rootValue: unknown = { hero }, @@ -163,6 +190,17 @@ async function* run( } } +const schemaLink = new ApolloLink((operation) => { + return new Observable((observer) => { + void (async () => { + for await (const chunk of run(operation.query)) { + observer.next(chunk); + } + observer.complete(); + })(); + }); +}); + describe("graphql-js test cases", () => { // These test cases mirror defer tests of the `graphql-js` v17.0.0-alpha.9 release: // https://github.com/graphql/graphql-js/blob/3283f8adf52e77a47f148ff2f30185c8d11ff0f0/src/execution/__tests__/defer-test.ts @@ -179,34 +217,48 @@ describe("graphql-js test cases", () => { name } `; + + const handler = new GraphQL17Alpha9Handler(); + const request = handler.startRequest({ query }); + const incoming = run(query); - expectJSON(incoming).toDeepEqual([ - { + { + const { value: chunk, done } = await incoming.next(); + + assert(!done); + expect(handler.isIncrementalResult(chunk)).toBe(true); + expect(hasIncrementalChunks(chunk)).toBe(false); + expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { hero: { id: "1", }, }, - pending: [{ id: "0", path: ["hero"] }], - hasNext: true, - }, - { - incremental: [ - { - data: { - name: "Luke", - }, - id: "0", + }); + expect(request.hasNext).toBe(true); + } + + { + const { value: chunk, done } = await incoming.next(); + + assert(!done); + expect(handler.isIncrementalResult(chunk)).toBe(true); + expect(hasIncrementalChunks(chunk)).toBe(true); + expect(request.handle(undefined, chunk)).toStrictEqualTyped({ + data: { + hero: { + id: "1", + name: "Luke", }, - ], - completed: [{ id: "0" }], - hasNext: false, - }, - ]); + }, + }); + expect(request.hasNext).toBe(false); + } }); + it("Can disable defer using if argument", async () => { - const document = parse(` + const query = gql` query HeroNameQuery { hero { id @@ -216,163 +268,31 @@ describe("graphql-js test cases", () => { fragment NameFragment on Hero { name } - `); - const result = await run(document); + `; + const handler = new GraphQL17Alpha9Handler(); + const incoming = run(query); - expectJSON(result).toDeepEqual({ - data: { - hero: { - id: "1", - name: "Luke", - }, - }, - }); + const { value: chunk } = await incoming.next(); + + assert(chunk); + expect(handler.isIncrementalResult(chunk)).toBe(false); + expect(hasIncrementalChunks(chunk)).toBe(false); }); - it("Does not disable defer with null if argument", async () => { - const document = parse(` - query HeroNameQuery($shouldDefer: Boolean) { - hero { - id - ...NameFragment @defer(if: $shouldDefer) - } - } - fragment NameFragment on Hero { - name - } - `); - const result = await run(document); - expectJSON(result).toDeepEqual([ - { - data: { hero: { id: "1" } }, - pending: [{ id: "0", path: ["hero"] }], - hasNext: true, - }, - { - incremental: [ - { - data: { name: "Luke" }, - id: "0", - }, - ], - completed: [{ id: "0" }], - hasNext: false, - }, - ]); + + it.skip("Does not disable defer with null if argument", async () => { + // test is not interesting from a client perspective }); - it("Does not execute deferred fragments early when not specified", async () => { - const document = parse(` - query HeroNameQuery { - hero { - id - ...NameFragment @defer - } - } - fragment NameFragment on Hero { - name - } - `); - const order: Array = []; - const result = await run(document, { - hero: { - ...hero, - id: async () => { - await resolveOnNextTick(); - await resolveOnNextTick(); - order.push("slow-id"); - return hero.id; - }, - name: () => { - order.push("fast-name"); - return hero.name; - }, - }, - }); - expectJSON(result).toDeepEqual([ - { - data: { - hero: { - id: "1", - }, - }, - pending: [{ id: "0", path: ["hero"] }], - hasNext: true, - }, - { - incremental: [ - { - data: { - name: "Luke", - }, - id: "0", - }, - ], - completed: [{ id: "0" }], - hasNext: false, - }, - ]); - expect(order).to.deep.equal(["slow-id", "fast-name"]); + it.skip("Does not execute deferred fragments early when not specified", async () => { + // test is not interesting from a client perspective }); - it("Does execute deferred fragments early when specified", async () => { - const document = parse(` - query HeroNameQuery { - hero { - id - ...NameFragment @defer - } - } - fragment NameFragment on Hero { - name - } - `); - const order: Array = []; - const result = await run( - document, - { - hero: { - ...hero, - id: async () => { - await resolveOnNextTick(); - await resolveOnNextTick(); - order.push("slow-id"); - return hero.id; - }, - name: () => { - order.push("fast-name"); - return hero.name; - }, - }, - }, - true - ); - expectJSON(result).toDeepEqual([ - { - data: { - hero: { - id: "1", - }, - }, - pending: [{ id: "0", path: ["hero"] }], - hasNext: true, - }, - { - incremental: [ - { - data: { - name: "Luke", - }, - id: "0", - }, - ], - completed: [{ id: "0" }], - hasNext: false, - }, - ]); - expect(order).to.deep.equal(["fast-name", "slow-id"]); + it.skip("Does execute deferred fragments early when specified", async () => { + // test is not interesting from a client perspective }); + it("Can defer fragments on the top level Query field", async () => { - const document = parse(` + const query = gql` query HeroNameQuery { ...QueryFragment @defer(label: "DeferQuery") } @@ -381,33 +301,44 @@ describe("graphql-js test cases", () => { id } } - `); - const result = await run(document); + `; - expectJSON(result).toDeepEqual([ - { + const handler = new GraphQL17Alpha9Handler(); + const request = handler.startRequest({ query }); + + const incoming = run(query); + + { + const { value: chunk, done } = await incoming.next(); + + assert(!done); + expect(handler.isIncrementalResult(chunk)).toBe(true); + expect(hasIncrementalChunks(chunk)).toBe(false); + expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: {}, - pending: [{ id: "0", path: [], label: "DeferQuery" }], - hasNext: true, - }, - { - incremental: [ - { - data: { - hero: { - id: "1", - }, - }, - id: "0", + }); + expect(request.hasNext).toBe(true); + } + + { + const { value: chunk, done } = await incoming.next(); + + assert(!done); + expect(handler.isIncrementalResult(chunk)).toBe(true); + expect(hasIncrementalChunks(chunk)).toBe(true); + expect(request.handle(undefined, chunk)).toStrictEqualTyped({ + data: { + hero: { + id: "1", }, - ], - completed: [{ id: "0" }], - hasNext: false, - }, - ]); + }, + }); + expect(request.hasNext).toBe(false); + } }); + it("Can defer fragments with errors on the top level Query field", async () => { - const document = parse(` + const query = gql` query HeroNameQuery { ...QueryFragment @defer(label: "DeferQuery") } @@ -416,8 +347,11 @@ describe("graphql-js test cases", () => { name } } - `); - const result = await run(document, { + `; + const handler = new GraphQL17Alpha9Handler(); + const request = handler.startRequest({ query }); + + const incoming = run(query, { hero: { ...hero, name: () => { @@ -426,37 +360,44 @@ describe("graphql-js test cases", () => { }, }); - expectJSON(result).toDeepEqual([ - { + { + const { value: chunk, done } = await incoming.next(); + + assert(!done); + expect(handler.isIncrementalResult(chunk)).toBe(true); + expect(hasIncrementalChunks(chunk)).toBe(false); + expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: {}, - pending: [{ id: "0", path: [], label: "DeferQuery" }], - hasNext: true, - }, - { - incremental: [ + }); + expect(request.hasNext).toBe(true); + } + + { + const { value: chunk, done } = await incoming.next(); + + assert(!done); + expect(handler.isIncrementalResult(chunk)).toBe(true); + expect(hasIncrementalChunks(chunk)).toBe(true); + expect(request.handle(undefined, chunk)).toStrictEqualTyped({ + data: { + hero: { + name: null, + }, + }, + errors: [ { - data: { - hero: { - name: null, - }, - }, - errors: [ - { - message: "bad", - locations: [{ line: 7, column: 11 }], - path: ["hero", "name"], - }, - ], - id: "0", + message: "bad", + locations: [{ line: 7, column: 11 }], + path: ["hero", "name"], }, ], - completed: [{ id: "0" }], - hasNext: false, - }, - ]); + }); + expect(request.hasNext).toBe(false); + } }); + it("Can defer a fragment within an already deferred fragment", async () => { - const document = parse(` + const query = gql` query HeroNameQuery { hero { ...TopFragment @defer(label: "DeferTop") @@ -471,83 +412,55 @@ describe("graphql-js test cases", () => { name } } - `); - const result = await run(document); + `; - expectJSON(result).toDeepEqual([ - { + const handler = new GraphQL17Alpha9Handler(); + const request = handler.startRequest({ query }); + + const incoming = run(query); + + { + const { value: chunk, done } = await incoming.next(); + + assert(!done); + expect(handler.isIncrementalResult(chunk)).toBe(true); + expect(hasIncrementalChunks(chunk)).toBe(false); + expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { hero: {}, }, - pending: [{ id: "0", path: ["hero"], label: "DeferTop" }], - hasNext: true, - }, - { - pending: [{ id: "1", path: ["hero"], label: "DeferNested" }], - incremental: [ - { - data: { - id: "1", - }, - id: "0", - }, - { - data: { - friends: [{ name: "Han" }, { name: "Leia" }, { name: "C-3PO" }], - }, + }); + expect(request.hasNext).toBe(true); + } + + { + const { value: chunk, done } = await incoming.next(); + + assert(!done); + expect(handler.isIncrementalResult(chunk)).toBe(true); + expect(hasIncrementalChunks(chunk)).toBe(true); + expect(request.handle(undefined, chunk)).toStrictEqualTyped({ + data: { + hero: { id: "1", + friends: [{ name: "Han" }, { name: "Leia" }, { name: "C-3PO" }], }, - ], - completed: [{ id: "0" }, { id: "1" }], - hasNext: false, - }, - ]); - }); - it("Can defer a fragment that is also not deferred, deferred fragment is first", async () => { - const document = parse(` - query HeroNameQuery { - hero { - ...TopFragment @defer(label: "DeferTop") - ...TopFragment - } - } - fragment TopFragment on Hero { - name - } - `); - const result = await run(document); - expectJSON(result).toDeepEqual({ - data: { - hero: { - name: "Luke", }, - }, - }); + }); + expect(request.hasNext).toBe(false); + } }); - it("Can defer a fragment that is also not deferred, non-deferred fragment is first", async () => { - const document = parse(` - query HeroNameQuery { - hero { - ...TopFragment - ...TopFragment @defer(label: "DeferTop") - } - } - fragment TopFragment on Hero { - name - } - `); - const result = await run(document); - expectJSON(result).toDeepEqual({ - data: { - hero: { - name: "Luke", - }, - }, - }); + + it.skip("Can defer a fragment that is also not deferred, deferred fragment is first", async () => { + // from the client perspective, a regular graphql query + }); + + it.skip("Can defer a fragment that is also not deferred, non-deferred fragment is first", async () => { + // from the client perspective, a regular graphql query }); it("Can defer an inline fragment", async () => { - const document = parse(` + const query = gql` query HeroNameQuery { hero { id @@ -556,46 +469,52 @@ describe("graphql-js test cases", () => { } } } - `); - const result = await run(document); + `; + const handler = new GraphQL17Alpha9Handler(); + const request = handler.startRequest({ query }); - expectJSON(result).toDeepEqual([ - { - data: { hero: { id: "1" } }, - pending: [{ id: "0", path: ["hero"], label: "InlineDeferred" }], - hasNext: true, - }, - { - incremental: [{ data: { name: "Luke" }, id: "0" }], - completed: [{ id: "0" }], - hasNext: false, - }, - ]); + const incoming = run(query); + + { + const { value: chunk, done } = await incoming.next(); + + assert(!done); + expect(handler.isIncrementalResult(chunk)).toBe(true); + expect(hasIncrementalChunks(chunk)).toBe(false); + expect(request.handle(undefined, chunk)).toStrictEqualTyped({ + data: { + hero: { + id: "1", + }, + }, + }); + expect(request.hasNext).toBe(true); + } + + { + const { value: chunk, done } = await incoming.next(); + + assert(!done); + expect(handler.isIncrementalResult(chunk)).toBe(true); + expect(hasIncrementalChunks(chunk)).toBe(true); + expect(request.handle(undefined, chunk)).toStrictEqualTyped({ + data: { + hero: { + id: "1", + name: "Luke", + }, + }, + }); + expect(request.hasNext).toBe(false); + } }); - it("Does not emit empty defer fragments", async () => { - const document = parse(` - query HeroNameQuery { - hero { - ... @defer { - name @skip(if: true) - } - } - } - fragment TopFragment on Hero { - name - } - `); - const result = await run(document); - expectJSON(result).toDeepEqual({ - data: { - hero: {}, - }, - }); + it.skip("Does not emit empty defer fragments", async () => { + // from the client perspective, a regular query }); it("Emits children of empty defer fragments", async () => { - const document = parse(` + const query = gql` query HeroNameQuery { hero { ... @defer { @@ -605,26 +524,46 @@ describe("graphql-js test cases", () => { } } } - `); - const result = await run(document); - expectJSON(result).toDeepEqual([ - { + `; + + const handler = new GraphQL17Alpha9Handler(); + const request = handler.startRequest({ query }); + + const incoming = run(query); + + { + const { value: chunk, done } = await incoming.next(); + + assert(!done); + expect(handler.isIncrementalResult(chunk)).toBe(true); + expect(hasIncrementalChunks(chunk)).toBe(false); + expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { hero: {}, }, - pending: [{ id: "0", path: ["hero"] }], - hasNext: true, - }, - { - incremental: [{ data: { name: "Luke" }, id: "0" }], - completed: [{ id: "0" }], - hasNext: false, - }, - ]); - }); + }); + expect(request.hasNext).toBe(true); + } + + { + const { value: chunk, done } = await incoming.next(); + + assert(!done); + expect(handler.isIncrementalResult(chunk)).toBe(true); + expect(hasIncrementalChunks(chunk)).toBe(true); + expect(request.handle(undefined, chunk)).toStrictEqualTyped({ + data: { + hero: { + name: "Luke", + }, + }, + }); + expect(request.hasNext).toBe(false); + } + }); it("Can separately emit defer fragments with different labels with varying fields", async () => { - const document = parse(` + const query = gql` query HeroNameQuery { hero { ... @defer(label: "DeferID") { @@ -635,42 +574,47 @@ describe("graphql-js test cases", () => { } } } - `); - const result = await run(document); - expectJSON(result).toDeepEqual([ - { + `; + + const handler = new GraphQL17Alpha9Handler(); + const request = handler.startRequest({ query }); + + const incoming = run(query); + + { + const { value: chunk, done } = await incoming.next(); + + assert(!done); + expect(handler.isIncrementalResult(chunk)).toBe(true); + expect(hasIncrementalChunks(chunk)).toBe(false); + expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { hero: {}, }, - pending: [ - { id: "0", path: ["hero"], label: "DeferID" }, - { id: "1", path: ["hero"], label: "DeferName" }, - ], - hasNext: true, - }, - { - incremental: [ - { - data: { - id: "1", - }, - id: "0", - }, - { - data: { - name: "Luke", - }, + }); + expect(request.hasNext).toBe(true); + } + + { + const { value: chunk, done } = await incoming.next(); + + assert(!done); + expect(handler.isIncrementalResult(chunk)).toBe(true); + expect(hasIncrementalChunks(chunk)).toBe(true); + expect(request.handle(undefined, chunk)).toStrictEqualTyped({ + data: { + hero: { id: "1", + name: "Luke", }, - ], - completed: [{ id: "0" }, { id: "1" }], - hasNext: false, - }, - ]); + }, + }); + expect(request.hasNext).toBe(false); + } }); it("Separately emits defer fragments with different labels with varying subfields", async () => { - const document = parse(` + const query = gql` query HeroNameQuery { ... @defer(label: "DeferID") { hero { @@ -683,95 +627,49 @@ describe("graphql-js test cases", () => { } } } - `); - const result = await run(document); - expectJSON(result).toDeepEqual([ - { + `; + + const handler = new GraphQL17Alpha9Handler(); + const request = handler.startRequest({ query }); + + const incoming = run(query); + + { + const { value: chunk, done } = await incoming.next(); + + assert(!done); + expect(handler.isIncrementalResult(chunk)).toBe(true); + expect(hasIncrementalChunks(chunk)).toBe(false); + expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: {}, - pending: [ - { id: "0", path: [], label: "DeferID" }, - { id: "1", path: [], label: "DeferName" }, - ], - hasNext: true, - }, - { - incremental: [ - { - data: { hero: {} }, - id: "0", - }, - { - data: { id: "1" }, - id: "0", - subPath: ["hero"], - }, - { - data: { name: "Luke" }, + }); + expect(request.hasNext).toBe(true); + } + + { + const { value: chunk, done } = await incoming.next(); + + assert(!done); + expect(handler.isIncrementalResult(chunk)).toBe(true); + expect(hasIncrementalChunks(chunk)).toBe(true); + expect(request.handle(undefined, chunk)).toStrictEqualTyped({ + data: { + hero: { id: "1", - subPath: ["hero"], + name: "Luke", }, - ], - completed: [{ id: "0" }, { id: "1" }], - hasNext: false, - }, - ]); + }, + }); + expect(request.hasNext).toBe(false); + } }); - it("Separately emits defer fragments with different labels with varying subfields that return promises", async () => { - const document = parse(` - query HeroNameQuery { - ... @defer(label: "DeferID") { - hero { - id - } - } - ... @defer(label: "DeferName") { - hero { - name - } - } - } - `); - const result = await run(document, { - hero: { - id: () => Promise.resolve("1"), - name: () => Promise.resolve("Luke"), - }, - }); - expectJSON(result).toDeepEqual([ - { - data: {}, - pending: [ - { id: "0", path: [], label: "DeferID" }, - { id: "1", path: [], label: "DeferName" }, - ], - hasNext: true, - }, - { - incremental: [ - { - data: { hero: {} }, - id: "0", - }, - { - data: { id: "1" }, - id: "0", - subPath: ["hero"], - }, - { - data: { name: "Luke" }, - id: "1", - subPath: ["hero"], - }, - ], - completed: [{ id: "0" }, { id: "1" }], - hasNext: false, - }, - ]); + it.skip("Separately emits defer fragments with different labels with varying subfields that return promises", async () => { + // from the client perspective, a repeat of the last one }); it("Separately emits defer fragments with varying subfields of same priorities but different level of defers", async () => { - const document = parse(` + const query = gql` query HeroNameQuery { hero { ... @defer(label: "DeferID") { @@ -784,43 +682,47 @@ describe("graphql-js test cases", () => { } } } - `); - const result = await run(document); - expectJSON(result).toDeepEqual([ - { + `; + + const handler = new GraphQL17Alpha9Handler(); + const request = handler.startRequest({ query }); + + const incoming = run(query); + + { + const { value: chunk, done } = await incoming.next(); + + assert(!done); + expect(handler.isIncrementalResult(chunk)).toBe(true); + expect(hasIncrementalChunks(chunk)).toBe(false); + expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { hero: {}, }, - pending: [ - { id: "0", path: ["hero"], label: "DeferID" }, - { id: "1", path: [], label: "DeferName" }, - ], - hasNext: true, - }, - { - incremental: [ - { - data: { - id: "1", - }, - id: "0", - }, - { - data: { - name: "Luke", - }, + }); + expect(request.hasNext).toBe(true); + } + + { + const { value: chunk, done } = await incoming.next(); + + assert(!done); + expect(handler.isIncrementalResult(chunk)).toBe(true); + expect(hasIncrementalChunks(chunk)).toBe(true); + expect(request.handle(undefined, chunk)).toStrictEqualTyped({ + data: { + hero: { id: "1", - subPath: ["hero"], + name: "Luke", }, - ], - completed: [{ id: "0" }, { id: "1" }], - hasNext: false, - }, - ]); + }, + }); + expect(request.hasNext).toBe(false); + } }); it("Separately emits nested defer fragments with varying subfields of same priorities but different level of defers", async () => { - const document = parse(` + const query = gql` query HeroNameQuery { ... @defer(label: "DeferName") { hero { @@ -831,40 +733,44 @@ describe("graphql-js test cases", () => { } } } - `); - const result = await run(document); - expectJSON(result).toDeepEqual([ - { + `; + const handler = new GraphQL17Alpha9Handler(); + const request = handler.startRequest({ query }); + + const incoming = run(query); + + { + const { value: chunk, done } = await incoming.next(); + + assert(!done); + expect(handler.isIncrementalResult(chunk)).toBe(true); + expect(hasIncrementalChunks(chunk)).toBe(false); + expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: {}, - pending: [{ id: "0", path: [], label: "DeferName" }], - hasNext: true, - }, - { - pending: [{ id: "1", path: ["hero"], label: "DeferID" }], - incremental: [ - { - data: { - hero: { - name: "Luke", - }, - }, - id: "0", - }, - { - data: { - id: "1", - }, + }); + expect(request.hasNext).toBe(true); + } + + { + const { value: chunk, done } = await incoming.next(); + + assert(!done); + expect(handler.isIncrementalResult(chunk)).toBe(true); + expect(hasIncrementalChunks(chunk)).toBe(true); + expect(request.handle(undefined, chunk)).toStrictEqualTyped({ + data: { + hero: { id: "1", + name: "Luke", }, - ], - completed: [{ id: "0" }, { id: "1" }], - hasNext: false, - }, - ]); + }, + }); + expect(request.hasNext).toBe(false); + } }); it("Initiates deferred grouped field sets only if they have been released as pending", async () => { - const document = parse(` + const query = gql` query { ... @defer { a { @@ -886,113 +792,82 @@ describe("graphql-js test cases", () => { } } } - `); + `; const { promise: slowFieldPromise, resolve: resolveSlowField } = promiseWithResolvers(); - let cResolverCalled = false; - let eResolverCalled = false; - const executeResult = experimentalExecuteIncrementally({ - schema, - document, - rootValue: { - a: { - someField: slowFieldPromise, - b: { - c: () => { - cResolverCalled = true; - return { d: "d" }; - }, - e: () => { - eResolverCalled = true; - return { f: "f" }; - }, + const handler = new GraphQL17Alpha9Handler(); + const request = handler.startRequest({ query }); + + const incoming = run(query, { + a: { + someField: slowFieldPromise, + b: { + c: () => { + return { d: "d" }; + }, + e: () => { + return { f: "f" }; }, }, }, - enableEarlyExecution: false, }); - assert("initialResult" in executeResult); - - const result1 = executeResult.initialResult; - expectJSON(result1).toDeepEqual({ - data: {}, - pending: [ - { id: "0", path: [] }, - { id: "1", path: [] }, - ], - hasNext: true, - }); + { + const { value: chunk, done } = await incoming.next(); - const iterator = executeResult.subsequentResults[Symbol.asyncIterator](); + assert(!done); + expect(handler.isIncrementalResult(chunk)).toBe(true); + expect(hasIncrementalChunks(chunk)).toBe(false); + expect(request.handle(undefined, chunk)).toStrictEqualTyped({ + data: {}, + }); + expect(request.hasNext).toBe(true); + } - expect(cResolverCalled).to.equal(false); - expect(eResolverCalled).to.equal(false); + { + const { value: chunk, done } = await incoming.next(); - const result2 = await iterator.next(); - expectJSON(result2).toDeepEqual({ - value: { - pending: [{ id: "2", path: ["a"] }], - incremental: [ - { - data: { a: {} }, - id: "0", - }, - { - data: { b: {} }, - id: "2", - }, - { - data: { c: { d: "d" } }, - id: "2", - subPath: ["b"], + assert(!done); + expect(handler.isIncrementalResult(chunk)).toBe(true); + expect(hasIncrementalChunks(chunk)).toBe(true); + expect(request.handle(undefined, chunk)).toStrictEqualTyped({ + data: { + a: { + b: { + c: { d: "d" }, + }, }, - ], - completed: [{ id: "0" }, { id: "2" }], - hasNext: true, - }, - done: false, - }); - - expect(cResolverCalled).to.equal(true); - expect(eResolverCalled).to.equal(false); + }, + }); + expect(request.hasNext).toBe(true); + } resolveSlowField("someField"); - const result3 = await iterator.next(); - expectJSON(result3).toDeepEqual({ - value: { - pending: [{ id: "3", path: ["a"] }], - incremental: [ - { - data: { someField: "someField" }, - id: "1", - subPath: ["a"], - }, - { - data: { e: { f: "f" } }, - id: "3", - subPath: ["b"], - }, - ], - completed: [{ id: "1" }, { id: "3" }], - hasNext: false, - }, - done: false, - }); - - expect(eResolverCalled).to.equal(true); + { + const { value: chunk, done } = await incoming.next(); - const result4 = await iterator.next(); - expectJSON(result4).toDeepEqual({ - value: undefined, - done: true, - }); + assert(!done); + expect(handler.isIncrementalResult(chunk)).toBe(true); + expect(hasIncrementalChunks(chunk)).toBe(true); + expect(request.handle(undefined, chunk)).toStrictEqualTyped({ + data: { + a: { + b: { + c: { d: "d" }, + e: { f: "f" }, + }, + someField: "someField", + }, + }, + }); + expect(request.hasNext).toBe(false); + } }); it("Initiates unique deferred grouped field sets after those that are common to sibling defers", async () => { - const document = parse(` + const query = gql` query { ... @defer { a { @@ -1014,103 +889,77 @@ describe("graphql-js test cases", () => { } } } - `); + `; const { promise: cPromise, resolve: resolveC } = promiseWithResolvers(); - let cResolverCalled = false; - let eResolverCalled = false; - const executeResult = experimentalExecuteIncrementally({ - schema, - document, - rootValue: { - a: { - b: { - c: async () => { - cResolverCalled = true; - await cPromise; - return { d: "d" }; - }, - e: () => { - eResolverCalled = true; - return { f: "f" }; - }, + const handler = new GraphQL17Alpha9Handler(); + const request = handler.startRequest({ query }); + + const incoming = run(query, { + a: { + b: { + c: async () => { + await cPromise; + return { d: "d" }; + }, + e: () => { + return { f: "f" }; }, }, }, - enableEarlyExecution: false, }); - assert("initialResult" in executeResult); - - const result1 = executeResult.initialResult; - expectJSON(result1).toDeepEqual({ - data: {}, - pending: [ - { id: "0", path: [] }, - { id: "1", path: [] }, - ], - hasNext: true, - }); + { + const { value: chunk, done } = await incoming.next(); - const iterator = executeResult.subsequentResults[Symbol.asyncIterator](); + assert(!done); + expect(handler.isIncrementalResult(chunk)).toBe(true); + expect(hasIncrementalChunks(chunk)).toBe(false); + expect(request.handle(undefined, chunk)).toStrictEqualTyped({ + data: {}, + }); + expect(request.hasNext).toBe(true); + } - expect(cResolverCalled).to.equal(false); - expect(eResolverCalled).to.equal(false); + { + const { value: chunk, done } = await incoming.next(); - const result2 = await iterator.next(); - expectJSON(result2).toDeepEqual({ - value: { - pending: [ - { id: "2", path: ["a"] }, - { id: "3", path: ["a"] }, - ], - incremental: [ - { - data: { a: {} }, - id: "0", - }, - ], - completed: [{ id: "0" }, { id: "1" }], - hasNext: true, - }, - done: false, - }); + assert(!done); + expect(handler.isIncrementalResult(chunk)).toBe(true); + expect(hasIncrementalChunks(chunk)).toBe(true); + expect(request.handle(undefined, chunk)).toStrictEqualTyped({ + data: { + a: {}, + }, + }); + expect(request.hasNext).toBe(true); + } resolveC(); - expect(cResolverCalled).to.equal(true); - expect(eResolverCalled).to.equal(false); + { + const { value: chunk, done } = await incoming.next(); - const result3 = await iterator.next(); - expectJSON(result3).toDeepEqual({ - value: { - incremental: [ - { - data: { b: { c: { d: "d" } } }, - id: "2", - }, - { - data: { e: { f: "f" } }, - id: "3", - subPath: ["b"], + assert(!done); + expect(handler.isIncrementalResult(chunk)).toBe(true); + expect(hasIncrementalChunks(chunk)).toBe(true); + expect(request.handle(undefined, chunk)).toStrictEqualTyped({ + data: { + a: { + b: { + c: { d: "d" }, + e: { f: "f" }, + }, }, - ], - completed: [{ id: "2" }, { id: "3" }], - hasNext: false, - }, - done: false, - }); - - const result4 = await iterator.next(); - expectJSON(result4).toDeepEqual({ - value: undefined, - done: true, - }); + }, + }); + expect(request.hasNext).toBe(false); + } }); it("Can deduplicate multiple defers on the same object", async () => { - const document = parse(` + const query = gql` query { hero { friends { @@ -1134,33 +983,52 @@ describe("graphql-js test cases", () => { id name } - `); - const result = await run(document); + `; - expectJSON(result).toDeepEqual([ - { - data: { hero: { friends: [{}, {}, {}] } }, - pending: [ - { id: "0", path: ["hero", "friends", 0] }, - { id: "1", path: ["hero", "friends", 1] }, - { id: "2", path: ["hero", "friends", 2] }, - ], - hasNext: true, - }, - { - incremental: [ - { data: { id: "2", name: "Han" }, id: "0" }, - { data: { id: "3", name: "Leia" }, id: "1" }, - { data: { id: "4", name: "C-3PO" }, id: "2" }, - ], - completed: [{ id: "0" }, { id: "1" }, { id: "2" }], - hasNext: false, - }, - ]); + const handler = new GraphQL17Alpha9Handler(); + const request = handler.startRequest({ query }); + + const incoming = run(query); + + { + const { value: chunk, done } = await incoming.next(); + + assert(!done); + expect(handler.isIncrementalResult(chunk)).toBe(true); + expect(hasIncrementalChunks(chunk)).toBe(false); + expect(request.handle(undefined, chunk)).toStrictEqualTyped({ + data: { + hero: { + friends: [{}, {}, {}], + }, + }, + }); + expect(request.hasNext).toBe(true); + } + + { + const { value: chunk, done } = await incoming.next(); + + assert(!done); + expect(handler.isIncrementalResult(chunk)).toBe(true); + expect(hasIncrementalChunks(chunk)).toBe(true); + expect(request.handle(undefined, chunk)).toStrictEqualTyped({ + data: { + hero: { + friends: [ + { id: "2", name: "Han" }, + { id: "3", name: "Leia" }, + { id: "4", name: "C-3PO" }, + ], + }, + }, + }); + expect(request.hasNext).toBe(false); + } }); it("Deduplicates fields present in the initial payload", async () => { - const document = parse(` + const query = gql` query { hero { nestedObject { @@ -1187,15 +1055,24 @@ describe("graphql-js test cases", () => { } } } - `); - const result = await run(document, { + `; + const handler = new GraphQL17Alpha9Handler(); + const request = handler.startRequest({ query }); + + const incoming = run(query, { hero: { nestedObject: { deeperObject: { foo: "foo", bar: "bar" } }, anotherNestedObject: { deeperObject: { foo: "foo" } }, }, }); - expectJSON(result).toDeepEqual([ - { + + { + const { value: chunk, done } = await incoming.next(); + + assert(!done); + expect(handler.isIncrementalResult(chunk)).toBe(true); + expect(hasIncrementalChunks(chunk)).toBe(false); + expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { hero: { nestedObject: { @@ -1210,25 +1087,39 @@ describe("graphql-js test cases", () => { }, }, }, - pending: [{ id: "0", path: ["hero"] }], - hasNext: true, - }, - { - incremental: [ - { - data: { bar: "bar" }, - id: "0", - subPath: ["nestedObject", "deeperObject"], - }, - ], - completed: [{ id: "0" }], - hasNext: false, - }, - ]); + }); + expect(request.hasNext).toBe(true); + } + + { + const { value: chunk, done } = await incoming.next(); + + assert(!done); + expect(handler.isIncrementalResult(chunk)).toBe(true); + expect(hasIncrementalChunks(chunk)).toBe(true); + expect(request.handle(undefined, chunk)).toStrictEqualTyped({ + data: { + hero: { + nestedObject: { + deeperObject: { + foo: "foo", + bar: "bar", + }, + }, + anotherNestedObject: { + deeperObject: { + foo: "foo", + }, + }, + }, + }, + }); + expect(request.hasNext).toBe(false); + } }); it("Deduplicates fields present in a parent defer payload", async () => { - const document = parse(` + const query = gql` query { hero { ... @defer { @@ -1244,44 +1135,52 @@ describe("graphql-js test cases", () => { } } } - `); - const result = await run(document, { + `; + const handler = new GraphQL17Alpha9Handler(); + const request = handler.startRequest({ query }); + + const incoming = run(query, { hero: { nestedObject: { deeperObject: { foo: "foo", bar: "bar" } } }, }); - expectJSON(result).toDeepEqual([ - { + + { + const { value: chunk, done } = await incoming.next(); + + assert(!done); + expect(handler.isIncrementalResult(chunk)).toBe(true); + expect(hasIncrementalChunks(chunk)).toBe(false); + expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { hero: {}, }, - pending: [{ id: "0", path: ["hero"] }], - hasNext: true, - }, - { - pending: [{ id: "1", path: ["hero", "nestedObject", "deeperObject"] }], - incremental: [ - { - data: { - nestedObject: { - deeperObject: { foo: "foo" }, + }); + expect(request.hasNext).toBe(true); + } + + { + const { value: chunk, done } = await incoming.next(); + + assert(!done); + expect(handler.isIncrementalResult(chunk)).toBe(true); + expect(hasIncrementalChunks(chunk)).toBe(true); + expect(request.handle(undefined, chunk)).toStrictEqualTyped({ + data: { + hero: { + nestedObject: { + deeperObject: { + foo: "foo", + bar: "bar", }, }, - id: "0", - }, - { - data: { - bar: "bar", - }, - id: "1", }, - ], - completed: [{ id: "0" }, { id: "1" }], - hasNext: false, - }, - ]); + }, + }); + expect(request.hasNext).toBe(false); + } }); it("Deduplicates fields with deferred fragments at multiple levels", async () => { - const document = parse(` + const query = gql` query { hero { nestedObject { @@ -1312,16 +1211,26 @@ describe("graphql-js test cases", () => { } } } - `); - const result = await run(document, { + `; + + const handler = new GraphQL17Alpha9Handler(); + const request = handler.startRequest({ query }); + + const incoming = run(query, { hero: { nestedObject: { deeperObject: { foo: "foo", bar: "bar", baz: "baz", bak: "bak" }, }, }, }); - expectJSON(result).toDeepEqual([ - { + + { + const { value: chunk, done } = await incoming.next(); + + assert(!done); + expect(handler.isIncrementalResult(chunk)).toBe(true); + expect(hasIncrementalChunks(chunk)).toBe(false); + expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { hero: { nestedObject: { @@ -1331,38 +1240,36 @@ describe("graphql-js test cases", () => { }, }, }, - pending: [{ id: "0", path: ["hero"] }], - hasNext: true, - }, - { - pending: [ - { id: "1", path: ["hero", "nestedObject"] }, - { id: "2", path: ["hero", "nestedObject", "deeperObject"] }, - ], - incremental: [ - { - data: { bar: "bar" }, - id: "0", - subPath: ["nestedObject", "deeperObject"], - }, - { - data: { baz: "baz" }, - id: "1", - subPath: ["deeperObject"], - }, - { - data: { bak: "bak" }, - id: "2", + }); + expect(request.hasNext).toBe(true); + } + + { + const { value: chunk, done } = await incoming.next(); + + assert(!done); + expect(handler.isIncrementalResult(chunk)).toBe(true); + expect(hasIncrementalChunks(chunk)).toBe(true); + expect(request.handle(undefined, chunk)).toStrictEqualTyped({ + data: { + hero: { + nestedObject: { + deeperObject: { + foo: "foo", + bar: "bar", + baz: "baz", + bak: "bak", + }, + }, }, - ], - completed: [{ id: "0" }, { id: "1" }, { id: "2" }], - hasNext: false, - }, - ]); + }, + }); + expect(request.hasNext).toBe(false); + } }); it("Deduplicates multiple fields from deferred fragments from different branches occurring at the same level", async () => { - const document = parse(` + const query = gql` query { hero { nestedObject { @@ -1384,12 +1291,22 @@ describe("graphql-js test cases", () => { } } } - `); - const result = await run(document, { + `; + + const handler = new GraphQL17Alpha9Handler(); + const request = handler.startRequest({ query }); + + const incoming = run(query, { hero: { nestedObject: { deeperObject: { foo: "foo", bar: "bar" } } }, }); - expectJSON(result).toDeepEqual([ - { + + { + const { value: chunk, done } = await incoming.next(); + + assert(!done); + expect(handler.isIncrementalResult(chunk)).toBe(true); + expect(hasIncrementalChunks(chunk)).toBe(false); + expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { hero: { nestedObject: { @@ -1397,35 +1314,34 @@ describe("graphql-js test cases", () => { }, }, }, - pending: [ - { id: "0", path: ["hero", "nestedObject", "deeperObject"] }, - { id: "1", path: ["hero", "nestedObject", "deeperObject"] }, - ], - hasNext: true, - }, - { - incremental: [ - { - data: { - foo: "foo", - }, - id: "0", - }, - { - data: { - bar: "bar", + }); + expect(request.hasNext).toBe(true); + } + + { + const { value: chunk, done } = await incoming.next(); + + assert(!done); + expect(handler.isIncrementalResult(chunk)).toBe(true); + expect(hasIncrementalChunks(chunk)).toBe(true); + expect(request.handle(undefined, chunk)).toStrictEqualTyped({ + data: { + hero: { + nestedObject: { + deeperObject: { + foo: "foo", + bar: "bar", + }, }, - id: "1", }, - ], - completed: [{ id: "0" }, { id: "1" }], - hasNext: false, - }, - ]); + }, + }); + expect(request.hasNext).toBe(false); + } }); it("Deduplicate fields with deferred fragments in different branches at multiple non-overlapping levels", async () => { - const document = parse(` + const query = gql` query { a { b { @@ -1452,8 +1368,12 @@ describe("graphql-js test cases", () => { } } } - `); - const result = await run(document, { + `; + + const handler = new GraphQL17Alpha9Handler(); + const request = handler.startRequest({ query }); + + const incoming = run(query, { a: { b: { c: { d: "d" }, @@ -1462,42 +1382,50 @@ describe("graphql-js test cases", () => { }, g: { h: "h" }, }); - expectJSON(result).toDeepEqual([ - { + + { + const { value: chunk, done } = await incoming.next(); + + assert(!done); + expect(handler.isIncrementalResult(chunk)).toBe(true); + expect(hasIncrementalChunks(chunk)).toBe(false); + expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { a: { b: { - c: { - d: "d", - }, + c: { d: "d" }, }, }, }, - pending: [ - { id: "0", path: ["a", "b"] }, - { id: "1", path: [] }, - ], - hasNext: true, - }, - { - incremental: [ - { - data: { e: { f: "f" } }, - id: "0", + }); + expect(request.hasNext).toBe(true); + } + + { + const { value: chunk, done } = await incoming.next(); + + assert(!done); + expect(handler.isIncrementalResult(chunk)).toBe(true); + expect(hasIncrementalChunks(chunk)).toBe(true); + expect(request.handle(undefined, chunk)).toStrictEqualTyped({ + data: { + a: { + b: { + c: { d: "d" }, + e: { f: "f" }, + }, }, - { - data: { g: { h: "h" } }, - id: "1", + g: { + h: "h", }, - ], - completed: [{ id: "0" }, { id: "1" }], - hasNext: false, - }, - ]); + }, + }); + expect(request.hasNext).toBe(false); + } }); it("Correctly bundles varying subfields into incremental data records unique by defer combination, ignoring fields in a fragment masked by a parent defer", async () => { - const document = parse(` + const query = gql` query HeroNameQuery { ... @defer { hero { @@ -1514,45 +1442,45 @@ describe("graphql-js test cases", () => { } } } - `); - const result = await run(document); - expectJSON(result).toDeepEqual([ - { + `; + const handler = new GraphQL17Alpha9Handler(); + const request = handler.startRequest({ query }); + + const incoming = run(query); + + { + const { value: chunk, done } = await incoming.next(); + + assert(!done); + expect(handler.isIncrementalResult(chunk)).toBe(true); + expect(hasIncrementalChunks(chunk)).toBe(false); + expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: {}, - pending: [ - { id: "0", path: [] }, - { id: "1", path: [] }, - ], - hasNext: true, - }, - { - incremental: [ - { - data: { hero: {} }, - id: "0", - }, - { - data: { id: "1" }, - id: "0", - subPath: ["hero"], - }, - { - data: { - name: "Luke", - shouldBeWithNameDespiteAdditionalDefer: "Luke", - }, + }); + expect(request.hasNext).toBe(true); + } + + { + const { value: chunk, done } = await incoming.next(); + + assert(!done); + expect(handler.isIncrementalResult(chunk)).toBe(true); + expect(hasIncrementalChunks(chunk)).toBe(true); + expect(request.handle(undefined, chunk)).toStrictEqualTyped({ + data: { + hero: { id: "1", - subPath: ["hero"], + name: "Luke", + shouldBeWithNameDespiteAdditionalDefer: "Luke", }, - ], - completed: [{ id: "0" }, { id: "1" }], - hasNext: false, - }, - ]); + }, + }); + expect(request.hasNext).toBe(false); + } }); it("Nulls cross defer boundaries, null first", async () => { - const document = parse(` + const query = gql` query { ... @defer { a { @@ -1574,54 +1502,57 @@ describe("graphql-js test cases", () => { } } } - `); - const result = await run(document, { + `; + const handler = new GraphQL17Alpha9Handler(); + const request = handler.startRequest({ query }); + + const incoming = run(query, { a: { b: { c: { d: "d" } }, someField: "someField" }, }); - expectJSON(result).toDeepEqual([ - { + + { + const { value: chunk, done } = await incoming.next(); + + assert(!done); + expect(handler.isIncrementalResult(chunk)).toBe(true); + expect(hasIncrementalChunks(chunk)).toBe(false); + expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { a: {}, }, - pending: [ - { id: "0", path: [] }, - { id: "1", path: ["a"] }, - ], - hasNext: true, - }, - { - incremental: [ - { - data: { b: { c: {} } }, - id: "1", - }, - { - data: { d: "d" }, - id: "1", - subPath: ["b", "c"], + }); + expect(request.hasNext).toBe(true); + } + + { + const { value: chunk, done } = await incoming.next(); + + assert(!done); + expect(handler.isIncrementalResult(chunk)).toBe(true); + expect(hasIncrementalChunks(chunk)).toBe(true); + expect(request.handle(undefined, chunk)).toStrictEqualTyped({ + data: { + a: { + b: { + c: { d: "d" }, + }, }, - ], - completed: [ + }, + errors: [ { - id: "0", - errors: [ - { - message: - "Cannot return null for non-nullable field c.nonNullErrorField.", - locations: [{ line: 8, column: 17 }], - path: ["a", "b", "c", "nonNullErrorField"], - }, - ], + message: + "Cannot return null for non-nullable field c.nonNullErrorField.", + locations: [{ line: 8, column: 17 }], + path: ["a", "b", "c", "nonNullErrorField"], }, - { id: "1" }, ], - hasNext: false, - }, - ]); + }); + expect(request.hasNext).toBe(false); + } }); it("Nulls cross defer boundaries, value first", async () => { - const document = parse(` + const query = gql` query { ... @defer { a { @@ -1643,57 +1574,60 @@ describe("graphql-js test cases", () => { } } } - `); - const result = await run(document, { + `; + const handler = new GraphQL17Alpha9Handler(); + const request = handler.startRequest({ query }); + + const incoming = run(query, { a: { b: { c: { d: "d" }, nonNullErrorFIeld: null }, someField: "someField", }, }); - expectJSON(result).toDeepEqual([ - { + + { + const { value: chunk, done } = await incoming.next(); + + assert(!done); + expect(handler.isIncrementalResult(chunk)).toBe(true); + expect(hasIncrementalChunks(chunk)).toBe(false); + expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { a: {}, }, - pending: [ - { id: "0", path: [] }, - { id: "1", path: ["a"] }, - ], - hasNext: true, - }, - { - incremental: [ - { - data: { b: { c: {} } }, - id: "1", - }, - { - data: { d: "d" }, - id: "0", - subPath: ["a", "b", "c"], + }); + expect(request.hasNext).toBe(true); + } + + { + const { value: chunk, done } = await incoming.next(); + + assert(!done); + expect(handler.isIncrementalResult(chunk)).toBe(true); + expect(hasIncrementalChunks(chunk)).toBe(true); + expect(request.handle(undefined, chunk)).toStrictEqualTyped({ + data: { + a: { + b: { + c: { d: "d" }, + }, }, - ], - completed: [ - { id: "0" }, + }, + errors: [ { - id: "1", - errors: [ - { - message: - "Cannot return null for non-nullable field c.nonNullErrorField.", - locations: [{ line: 17, column: 17 }], - path: ["a", "b", "c", "nonNullErrorField"], - }, - ], + message: + "Cannot return null for non-nullable field c.nonNullErrorField.", + locations: [{ line: 17, column: 17 }], + path: ["a", "b", "c", "nonNullErrorField"], }, ], - hasNext: false, - }, - ]); + }); + expect(request.hasNext).toBe(false); + } }); it("Handles multiple erroring deferred grouped field sets", async () => { - const document = parse(` + const query = gql` query { ... @defer { a { @@ -1714,53 +1648,57 @@ describe("graphql-js test cases", () => { } } } - `); - const result = await run(document, { + `; + const handler = new GraphQL17Alpha9Handler(); + const request = handler.startRequest({ query }); + + const incoming = run(query, { a: { b: { c: { nonNullErrorField: null } }, }, }); - expectJSON(result).toDeepEqual([ - { + + { + const { value: chunk, done } = await incoming.next(); + + assert(!done); + expect(handler.isIncrementalResult(chunk)).toBe(true); + expect(hasIncrementalChunks(chunk)).toBe(false); + expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: {}, - pending: [ - { id: "0", path: [] }, - { id: "1", path: [] }, - ], - hasNext: true, - }, - { - completed: [ + }); + expect(request.hasNext).toBe(true); + } + + { + const { value: chunk, done } = await incoming.next(); + + assert(!done); + expect(handler.isIncrementalResult(chunk)).toBe(true); + expect(hasIncrementalChunks(chunk)).toBe(true); + expect(request.handle(undefined, chunk)).toStrictEqualTyped({ + data: {}, + errors: [ { - id: "0", - errors: [ - { - message: - "Cannot return null for non-nullable field c.nonNullErrorField.", - locations: [{ line: 7, column: 17 }], - path: ["a", "b", "c", "someError"], - }, - ], + message: + "Cannot return null for non-nullable field c.nonNullErrorField.", + locations: [{ line: 7, column: 17 }], + path: ["a", "b", "c", "someError"], }, { - id: "1", - errors: [ - { - message: - "Cannot return null for non-nullable field c.nonNullErrorField.", - locations: [{ line: 16, column: 17 }], - path: ["a", "b", "c", "anotherError"], - }, - ], + message: + "Cannot return null for non-nullable field c.nonNullErrorField.", + locations: [{ line: 16, column: 17 }], + path: ["a", "b", "c", "anotherError"], }, ], - hasNext: false, - }, - ]); + }); + expect(request.hasNext).toBe(false); + } }); it("Handles multiple erroring deferred grouped field sets for the same fragment", async () => { - const document = parse(` + const query = gql` query { ... @defer { a { @@ -1787,59 +1725,58 @@ describe("graphql-js test cases", () => { } } } - `); - const result = await run(document, { + `; + const handler = new GraphQL17Alpha9Handler(); + const request = handler.startRequest({ query }); + + const incoming = run(query, { a: { b: { c: { d: "d", nonNullErrorField: null } }, }, }); - expectJSON(result).toDeepEqual([ - { + + { + const { value: chunk, done } = await incoming.next(); + + assert(!done); + expect(handler.isIncrementalResult(chunk)).toBe(true); + expect(hasIncrementalChunks(chunk)).toBe(false); + expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: {}, - pending: [ - { id: "0", path: [] }, - { id: "1", path: [] }, - ], - hasNext: true, - }, - { - incremental: [ - { - data: { a: { b: { someC: {}, anotherC: {} } } }, - id: "0", - }, - { - data: { d: "d" }, - id: "0", - subPath: ["a", "b", "someC"], - }, - { - data: { d: "d" }, - id: "0", - subPath: ["a", "b", "anotherC"], + }); + expect(request.hasNext).toBe(true); + } + + { + const { value: chunk, done } = await incoming.next(); + + assert(!done); + expect(handler.isIncrementalResult(chunk)).toBe(true); + expect(hasIncrementalChunks(chunk)).toBe(true); + expect(request.handle(undefined, chunk)).toStrictEqualTyped({ + data: { + a: { + b: { + someC: { d: "d" }, + anotherC: { d: "d" }, + }, }, - ], - completed: [ + }, + errors: [ { - id: "1", - errors: [ - { - message: - "Cannot return null for non-nullable field c.nonNullErrorField.", - locations: [{ line: 19, column: 17 }], - path: ["a", "b", "someC", "someError"], - }, - ], + message: + "Cannot return null for non-nullable field c.nonNullErrorField.", + locations: [{ line: 19, column: 17 }], + path: ["a", "b", "someC", "someError"], }, - { id: "0" }, ], - hasNext: false, - }, - ]); + }); + expect(request.hasNext).toBe(false); + } }); it("filters a payload with a null that cannot be merged", async () => { - const document = parse(` + const query = gql` query { ... @defer { a { @@ -1861,9 +1798,12 @@ describe("graphql-js test cases", () => { } } } - `); - const result = await run( - document, + `; + const handler = new GraphQL17Alpha9Handler(); + const request = handler.startRequest({ query }); + + const incoming = run( + query, { a: { b: { @@ -1880,91 +1820,54 @@ describe("graphql-js test cases", () => { }, true ); - expectJSON(result).toDeepEqual([ - { + + { + const { value: chunk, done } = await incoming.next(); + + assert(!done); + expect(handler.isIncrementalResult(chunk)).toBe(true); + expect(hasIncrementalChunks(chunk)).toBe(false); + expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { a: {}, }, - pending: [ - { id: "0", path: [] }, - { id: "1", path: ["a"] }, - ], - hasNext: true, - }, - { - incremental: [ - { - data: { b: { c: {} } }, - id: "1", - }, - { - data: { d: "d" }, - id: "1", - subPath: ["b", "c"], + }); + expect(request.hasNext).toBe(true); + } + + { + const { value: chunk, done } = await incoming.next(); + + assert(!done); + expect(handler.isIncrementalResult(chunk)).toBe(true); + expect(hasIncrementalChunks(chunk)).toBe(true); + expect(request.handle(undefined, chunk)).toStrictEqualTyped({ + data: { + a: { + b: { + c: { d: "d" }, + }, }, - ], - completed: [{ id: "1" }], - hasNext: true, - }, - { - completed: [ + }, + errors: [ { - id: "0", - errors: [ - { - message: - "Cannot return null for non-nullable field c.nonNullErrorField.", - locations: [{ line: 8, column: 17 }], - path: ["a", "b", "c", "nonNullErrorField"], - }, - ], + message: + "Cannot return null for non-nullable field c.nonNullErrorField.", + locations: [{ line: 8, column: 17 }], + path: ["a", "b", "c", "nonNullErrorField"], }, ], - hasNext: false, - }, - ]); + }); + expect(request.hasNext).toBe(false); + } }); - it("Cancels deferred fields when initial result exhibits null bubbling", async () => { - const document = parse(` - query { - hero { - nonNullName - } - ... @defer { - hero { - name - } - } - } - `); - const result = await run( - document, - { - hero: { - ...hero, - nonNullName: () => null, - }, - }, - true - ); - expectJSON(result).toDeepEqual({ - data: { - hero: null, - }, - errors: [ - { - message: - "Cannot return null for non-nullable field Hero.nonNullName.", - locations: [{ line: 4, column: 11 }], - path: ["hero", "nonNullName"], - }, - ], - }); + it.skip("Cancels deferred fields when initial result exhibits null bubbling", async () => { + // from the client perspective, a regular graphql query }); it("Cancels deferred fields when deferred result exhibits null bubbling", async () => { - const document = parse(` + const query = gql` query { ... @defer { hero { @@ -1973,9 +1876,13 @@ describe("graphql-js test cases", () => { } } } - `); - const result = await run( - document, + `; + + const handler = new GraphQL17Alpha9Handler(); + const request = handler.startRequest({ query }); + + const incoming = run( + query, { hero: { ...hero, @@ -1984,119 +1891,56 @@ describe("graphql-js test cases", () => { }, true ); - expectJSON(result).toDeepEqual([ - { + + { + const { value: chunk, done } = await incoming.next(); + + assert(!done); + expect(handler.isIncrementalResult(chunk)).toBe(true); + expect(hasIncrementalChunks(chunk)).toBe(false); + expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: {}, - pending: [{ id: "0", path: [] }], - hasNext: true, - }, - { - incremental: [ + }); + expect(request.hasNext).toBe(true); + } + + { + const { value: chunk, done } = await incoming.next(); + + assert(!done); + expect(handler.isIncrementalResult(chunk)).toBe(true); + expect(hasIncrementalChunks(chunk)).toBe(true); + expect(request.handle(undefined, chunk)).toStrictEqualTyped({ + data: { + hero: null, + }, + errors: [ { - data: { - hero: null, - }, - errors: [ - { - message: - "Cannot return null for non-nullable field Hero.nonNullName.", - locations: [{ line: 5, column: 13 }], - path: ["hero", "nonNullName"], - }, - ], - id: "0", + message: + "Cannot return null for non-nullable field Hero.nonNullName.", + locations: [{ line: 5, column: 13 }], + path: ["hero", "nonNullName"], }, ], - completed: [{ id: "0" }], - hasNext: false, - }, - ]); + }); + expect(request.hasNext).toBe(false); + } }); - it("Deduplicates list fields", async () => { - const document = parse(` - query { - hero { - friends { - name - } - ... @defer { - friends { - name - } - } - } - } - `); - const result = await run(document); - expectJSON(result).toDeepEqual({ - data: { - hero: { - friends: [{ name: "Han" }, { name: "Leia" }, { name: "C-3PO" }], - }, - }, - }); + it.skip("Deduplicates list fields", async () => { + // from the client perspective, a regular query }); - it("Deduplicates async iterable list fields", async () => { - const document = parse(` - query { - hero { - friends { - name - } - ... @defer { - friends { - name - } - } - } - } - `); - const result = await run(document, { - hero: { - ...hero, - friends: async function* resolve() { - yield await Promise.resolve(friends[0]); - }, - }, - }); - expectJSON(result).toDeepEqual({ - data: { hero: { friends: [{ name: "Han" }] } }, - }); + it.skip("Deduplicates async iterable list fields", async () => { + // from the client perspective, a regular query }); - it("Deduplicates empty async iterable list fields", async () => { - const document = parse(` - query { - hero { - friends { - name - } - ... @defer { - friends { - name - } - } - } - } - `); - const result = await run(document, { - hero: { - ...hero, - - friends: async function* resolve() { - await resolveOnNextTick(); - }, - }, - }); - expectJSON(result).toDeepEqual({ - data: { hero: { friends: [] } }, - }); + it.skip("Deduplicates empty async iterable list fields", async () => { + // from the client perspective, a regular query }); it("Does not deduplicate list fields with non-overlapping fields", async () => { - const document = parse(` + const query = gql` query { hero { friends { @@ -2109,121 +1953,63 @@ describe("graphql-js test cases", () => { } } } - `); - const result = await run(document); - expectJSON(result).toDeepEqual([ - { + `; + const handler = new GraphQL17Alpha9Handler(); + const request = handler.startRequest({ query }); + + const incoming = run(query); + + { + const { value: chunk, done } = await incoming.next(); + + assert(!done); + expect(handler.isIncrementalResult(chunk)).toBe(true); + expect(hasIncrementalChunks(chunk)).toBe(false); + expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { hero: { friends: [{ name: "Han" }, { name: "Leia" }, { name: "C-3PO" }], }, }, - pending: [{ id: "0", path: ["hero"] }], - hasNext: true, - }, - { - incremental: [ - { - data: { id: "2" }, - id: "0", - subPath: ["friends", 0], - }, - { - data: { id: "3" }, - id: "0", - subPath: ["friends", 1], - }, - { - data: { id: "4" }, - id: "0", - subPath: ["friends", 2], + }); + expect(request.hasNext).toBe(true); + } + + { + const { value: chunk, done } = await incoming.next(); + + assert(!done); + expect(handler.isIncrementalResult(chunk)).toBe(true); + expect(hasIncrementalChunks(chunk)).toBe(true); + expect(request.handle(undefined, chunk)).toStrictEqualTyped({ + data: { + hero: { + friends: [ + { id: "2", name: "Han" }, + { id: "3", name: "Leia" }, + { id: "4", name: "C-3PO" }, + ], }, - ], - completed: [{ id: "0" }], - hasNext: false, - }, - ]); + }, + }); + expect(request.hasNext).toBe(false); + } }); - it("Deduplicates list fields that return empty lists", async () => { - const document = parse(` - query { - hero { - friends { - name - } - ... @defer { - friends { - name - } - } - } - } - `); - const result = await run(document, { - hero: { - ...hero, - friends: () => [], - }, - }); - expectJSON(result).toDeepEqual({ - data: { hero: { friends: [] } }, - }); + it.skip("Deduplicates list fields that return empty lists", async () => { + // from the client perspective, a regular query }); - it("Deduplicates null object fields", async () => { - const document = parse(` - query { - hero { - nestedObject { - name - } - ... @defer { - nestedObject { - name - } - } - } - } - `); - const result = await run(document, { - hero: { - ...hero, - nestedObject: () => null, - }, - }); - expectJSON(result).toDeepEqual({ - data: { hero: { nestedObject: null } }, - }); + it.skip("Deduplicates null object fields", async () => { + // from the client perspective, a regular query }); - it("Deduplicates promise object fields", async () => { - const document = parse(` - query { - hero { - nestedObject { - name - } - ... @defer { - nestedObject { - name - } - } - } - } - `); - const result = await run(document, { - hero: { - nestedObject: () => Promise.resolve({ name: "foo" }), - }, - }); - expectJSON(result).toDeepEqual({ - data: { hero: { nestedObject: { name: "foo" } } }, - }); + it.skip("Deduplicates promise object fields", async () => { + // from the client perspective, a regular query }); it("Handles errors thrown in deferred fragments", async () => { - const document = parse(` + const query = gql` query HeroNameQuery { hero { id @@ -2233,8 +2019,12 @@ describe("graphql-js test cases", () => { fragment NameFragment on Hero { name } - `); - const result = await run(document, { + `; + + const handler = new GraphQL17Alpha9Handler(); + const request = handler.startRequest({ query }); + + const incoming = run(query, { hero: { ...hero, name: () => { @@ -2242,33 +2032,50 @@ describe("graphql-js test cases", () => { }, }, }); - expectJSON(result).toDeepEqual([ - { - data: { hero: { id: "1" } }, - pending: [{ id: "0", path: ["hero"] }], - hasNext: true, - }, - { - incremental: [ + + { + const { value: chunk, done } = await incoming.next(); + + assert(!done); + expect(handler.isIncrementalResult(chunk)).toBe(true); + expect(hasIncrementalChunks(chunk)).toBe(false); + expect(request.handle(undefined, chunk)).toStrictEqualTyped({ + data: { + hero: { + id: "1", + }, + }, + }); + expect(request.hasNext).toBe(true); + } + + { + const { value: chunk, done } = await incoming.next(); + + assert(!done); + expect(handler.isIncrementalResult(chunk)).toBe(true); + expect(hasIncrementalChunks(chunk)).toBe(true); + expect(request.handle(undefined, chunk)).toStrictEqualTyped({ + data: { + hero: { + id: "1", + name: null, + }, + }, + errors: [ { - data: { name: null }, - id: "0", - errors: [ - { - message: "bad", - locations: [{ line: 9, column: 9 }], - path: ["hero", "name"], - }, - ], + message: "bad", + locations: [{ line: 9, column: 9 }], + path: ["hero", "name"], }, ], - completed: [{ id: "0" }], - hasNext: false, - }, - ]); + }); + expect(request.hasNext).toBe(false); + } }); + it("Handles non-nullable errors thrown in deferred fragments", async () => { - const document = parse(` + const query = gql` query HeroNameQuery { hero { id @@ -2278,76 +2085,64 @@ describe("graphql-js test cases", () => { fragment NameFragment on Hero { nonNullName } - `); - const result = await run(document, { + `; + const handler = new GraphQL17Alpha9Handler(); + const request = handler.startRequest({ query }); + + const incoming = run(query, { hero: { ...hero, nonNullName: () => null, }, }); - expectJSON(result).toDeepEqual([ - { - data: { hero: { id: "1" } }, - pending: [{ id: "0", path: ["hero"] }], - hasNext: true, - }, - { - completed: [ + + { + const { value: chunk, done } = await incoming.next(); + + assert(!done); + expect(handler.isIncrementalResult(chunk)).toBe(true); + expect(hasIncrementalChunks(chunk)).toBe(false); + expect(request.handle(undefined, chunk)).toStrictEqualTyped({ + data: { + hero: { + name: "Luke", + }, + }, + }); + expect(request.hasNext).toBe(true); + } + + { + const { value: chunk, done } = await incoming.next(); + + assert(!done); + expect(handler.isIncrementalResult(chunk)).toBe(true); + expect(hasIncrementalChunks(chunk)).toBe(true); + expect(request.handle(undefined, chunk)).toStrictEqualTyped({ + data: { + hero: { + id: "1", + }, + }, + errors: [ { - id: "0", - errors: [ - { - message: - "Cannot return null for non-nullable field Hero.nonNullName.", - locations: [{ line: 9, column: 9 }], - path: ["hero", "nonNullName"], - }, - ], + message: + "Cannot return null for non-nullable field Hero.nonNullName.", + locations: [{ line: 9, column: 9 }], + path: ["hero", "nonNullName"], }, ], - hasNext: false, - }, - ]); + }); + expect(request.hasNext).toBe(false); + } }); - it("Handles non-nullable errors thrown outside deferred fragments", async () => { - const document = parse(` - query HeroNameQuery { - hero { - nonNullName - ...NameFragment @defer - } - } - fragment NameFragment on Hero { - id - } - `); - const result = await run(document, { - hero: { - ...hero, - nonNullName: () => null, - }, - }); - expectJSON(result).toDeepEqual({ - errors: [ - { - message: - "Cannot return null for non-nullable field Hero.nonNullName.", - locations: [ - { - line: 4, - column: 11, - }, - ], - path: ["hero", "nonNullName"], - }, - ], - data: { - hero: null, - }, - }); + + it.skip("Handles non-nullable errors thrown outside deferred fragments", async () => { + // from the client perspective, a regular query }); + it("Handles async non-nullable errors thrown in deferred fragments", async () => { - const document = parse(` + const query = gql` query HeroNameQuery { hero { id @@ -2357,39 +2152,60 @@ describe("graphql-js test cases", () => { fragment NameFragment on Hero { nonNullName } - `); - const result = await run(document, { + `; + const handler = new GraphQL17Alpha9Handler(); + const request = handler.startRequest({ query }); + + const incoming = run(query, { hero: { ...hero, nonNullName: () => Promise.resolve(null), }, }); - expectJSON(result).toDeepEqual([ - { - data: { hero: { id: "1" } }, - pending: [{ id: "0", path: ["hero"] }], - hasNext: true, - }, - { - completed: [ + + { + const { value: chunk, done } = await incoming.next(); + + assert(!done); + expect(handler.isIncrementalResult(chunk)).toBe(true); + expect(hasIncrementalChunks(chunk)).toBe(false); + expect(request.handle(undefined, chunk)).toStrictEqualTyped({ + data: { + hero: { + id: "1", + }, + }, + }); + expect(request.hasNext).toBe(true); + } + + { + const { value: chunk, done } = await incoming.next(); + + assert(!done); + expect(handler.isIncrementalResult(chunk)).toBe(true); + expect(hasIncrementalChunks(chunk)).toBe(true); + expect(request.handle(undefined, chunk)).toStrictEqualTyped({ + data: { + hero: { + id: "1", + }, + }, + errors: [ { - id: "0", - errors: [ - { - message: - "Cannot return null for non-nullable field Hero.nonNullName.", - locations: [{ line: 9, column: 9 }], - path: ["hero", "nonNullName"], - }, - ], + message: + "Cannot return null for non-nullable field Hero.nonNullName.", + locations: [{ line: 9, column: 9 }], + path: ["hero", "nonNullName"], }, ], - hasNext: false, - }, - ]); + }); + expect(request.hasNext).toBe(false); + } }); + it("Returns payloads in correct order", async () => { - const document = parse(` + const query = gql` query HeroNameQuery { hero { id @@ -2405,8 +2221,11 @@ describe("graphql-js test cases", () => { fragment NestedFragment on Friend { name } - `); - const result = await run(document, { + `; + const handler = new GraphQL17Alpha9Handler(); + const request = handler.startRequest({ query }); + + const incoming = run(query, { hero: { ...hero, name: async () => { @@ -2415,167 +2234,55 @@ describe("graphql-js test cases", () => { }, }, }); - expectJSON(result).toDeepEqual([ - { + + { + const { value: chunk, done } = await incoming.next(); + + assert(!done); + expect(handler.isIncrementalResult(chunk)).toBe(true); + expect(hasIncrementalChunks(chunk)).toBe(false); + expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { - hero: { id: "1" }, - }, - pending: [{ id: "0", path: ["hero"] }], - hasNext: true, - }, - { - pending: [ - { id: "1", path: ["hero", "friends", 0] }, - { id: "2", path: ["hero", "friends", 1] }, - { id: "3", path: ["hero", "friends", 2] }, - ], - incremental: [ - { - data: { name: "slow", friends: [{}, {}, {}] }, - id: "0", + hero: { + id: "1", }, - { data: { name: "Han" }, id: "1" }, - { data: { name: "Leia" }, id: "2" }, - { data: { name: "C-3PO" }, id: "3" }, - ], - completed: [{ id: "0" }, { id: "1" }, { id: "2" }, { id: "3" }], - hasNext: false, - }, - ]); - }); - it("Returns payloads from synchronous data in correct order", async () => { - const document = parse(` - query HeroNameQuery { - hero { - id - ...NameFragment @defer - } - } - fragment NameFragment on Hero { - name - friends { - ...NestedFragment @defer - } - } - fragment NestedFragment on Friend { - name + }, + }); + expect(request.hasNext).toBe(true); } - `); - const result = await run(document); - expectJSON(result).toDeepEqual([ - { + + { + const { value: chunk, done } = await incoming.next(); + + assert(!done); + expect(handler.isIncrementalResult(chunk)).toBe(true); + expect(hasIncrementalChunks(chunk)).toBe(true); + expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { - hero: { id: "1" }, - }, - pending: [{ id: "0", path: ["hero"] }], - hasNext: true, - }, - { - pending: [ - { id: "1", path: ["hero", "friends", 0] }, - { id: "2", path: ["hero", "friends", 1] }, - { id: "3", path: ["hero", "friends", 2] }, - ], - incremental: [ - { - data: { - name: "Luke", - friends: [{}, {}, {}], - }, - id: "0", + hero: { + id: "1", + name: "slow", + friends: [{ name: "Han" }, { name: "Leia" }, { name: "C-3PO" }], }, - { data: { name: "Han" }, id: "1" }, - { data: { name: "Leia" }, id: "2" }, - { data: { name: "C-3PO" }, id: "3" }, - ], - completed: [{ id: "0" }, { id: "1" }, { id: "2" }, { id: "3" }], - hasNext: false, - }, - ]); + }, + }); + expect(request.hasNext).toBe(false); + } }); - it("Filters deferred payloads when a list item returned by an async iterable is nulled", async () => { - const document = parse(` - query { - hero { - friends { - nonNullName - ...NameFragment @defer - } - } - } - fragment NameFragment on Friend { - name - } - `); - const result = await run(document, { - hero: { - ...hero, - async *friends() { - yield await Promise.resolve({ - ...friends[0], - nonNullName: () => Promise.resolve(null), - }); - }, - }, - }); - expectJSON(result).toDeepEqual({ - data: { - hero: { - friends: [null], - }, - }, - errors: [ - { - message: - "Cannot return null for non-nullable field Friend.nonNullName.", - locations: [{ line: 5, column: 11 }], - path: ["hero", "friends", 0, "nonNullName"], - }, - ], - }); + it.skip("Returns payloads from synchronous data in correct order", async () => { + // from the client perspective, a repeat of the last one }); - it("original execute function throws error if anything is deferred and everything else is sync", () => { - const doc = ` - query Deferred { - ... @defer { hero { id } } - } - `; - expect(() => - execute({ - schema, - document: parse(doc), - rootValue: {}, - }) - ).to.throw( - "Executing this GraphQL operation would unexpectedly produce multiple payloads (due to @defer or @stream directive)" - ); + it.skip("Filters deferred payloads when a list item returned by an async iterable is nulled", async () => { + // from the client perspective, a regular query }); - it("original execute function resolves to error if anything is deferred and something else is async", async () => { - const doc = ` - query Deferred { - hero { name } - ... @defer { hero { id } } - } - `; - await expectPromise( - execute({ - schema, - document: parse(doc), - rootValue: { - hero: { - ...hero, - name: async () => { - await resolveOnNextTick(); - return "slow"; - }, - }, - }, - }) - ).toRejectWith( - "Executing this GraphQL operation would unexpectedly produce multiple payloads (due to @defer or @stream directive)" - ); + it.skip("original execute function throws error if anything is deferred and everything else is sync", () => { + // not relevant for the client + }); + + it.skip("original execute function resolves to error if anything is deferred and something else is async", async () => { + // not relevant for the client }); }); From 12a710c8e5a839e24f82629fa2431cd678a75524 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 4 Sep 2025 00:04:28 -0600 Subject: [PATCH 04/97] Add additional tests --- .../__tests__/graphql17Alpha9.test.ts | 461 +++++++++++++++++- 1 file changed, 460 insertions(+), 1 deletion(-) diff --git a/src/incremental/handlers/__tests__/graphql17Alpha9.test.ts b/src/incremental/handlers/__tests__/graphql17Alpha9.test.ts index fe79c212075..94411e5b51b 100644 --- a/src/incremental/handlers/__tests__/graphql17Alpha9.test.ts +++ b/src/incremental/handlers/__tests__/graphql17Alpha9.test.ts @@ -15,7 +15,20 @@ import { GraphQLString, } from "graphql-17-alpha9"; -import { ApolloLink, gql, Observable } from "@apollo/client"; +import { + ApolloClient, + ApolloLink, + CombinedGraphQLErrors, + gql, + InMemoryCache, + NetworkStatus, + Observable, +} from "@apollo/client"; +import { + markAsStreaming, + mockDeferStream, + ObservableStream, +} from "@apollo/client/testing/internal"; import { GraphQL17Alpha9Handler, @@ -2286,3 +2299,449 @@ describe("graphql-js test cases", () => { // not relevant for the client }); }); + +test("GraphQL17Alpha9Handler can be used with `ApolloClient`", async () => { + const client = new ApolloClient({ + link: schemaLink, + cache: new InMemoryCache(), + incrementalHandler: new GraphQL17Alpha9Handler(), + }); + + const query = gql` + query HeroNameQuery { + hero { + id + ... @defer { + name + } + } + } + `; + + const observableStream = new ObservableStream(client.watchQuery({ query })); + + await expect(observableStream).toEmitTypedValue({ + loading: true, + data: undefined, + dataState: "empty", + networkStatus: NetworkStatus.loading, + partial: true, + }); + + await expect(observableStream).toEmitTypedValue({ + loading: true, + data: markAsStreaming({ + hero: { + __typename: "Hero", + id: "1", + }, + }), + dataState: "streaming", + networkStatus: NetworkStatus.streaming, + partial: true, + }); + + await expect(observableStream).toEmitTypedValue({ + loading: false, + data: { + hero: { + __typename: "Hero", + id: "1", + name: "Luke", + }, + }, + dataState: "complete", + networkStatus: NetworkStatus.ready, + partial: false, + }); +}); + +test("merges cache updates that happen concurrently", async () => { + const stream = mockDeferStream(); + const client = new ApolloClient({ + link: stream.httpLink, + cache: new InMemoryCache(), + incrementalHandler: new GraphQL17Alpha9Handler(), + }); + + const query = gql` + query HeroNameQuery { + hero { + id + job + ... @defer { + name + } + } + } + `; + + const observableStream = new ObservableStream(client.watchQuery({ query })); + + await expect(observableStream).toEmitTypedValue({ + loading: true, + data: undefined, + dataState: "empty", + networkStatus: NetworkStatus.loading, + partial: true, + }); + + stream.enqueueInitialChunk({ + data: { + hero: { + __typename: "Hero", + id: "1", + job: "Farmer", + }, + }, + hasNext: true, + }); + + await expect(observableStream).toEmitTypedValue({ + loading: true, + data: markAsStreaming({ + hero: { + __typename: "Hero", + id: "1", + job: "Farmer", + }, + }), + dataState: "streaming", + networkStatus: NetworkStatus.streaming, + partial: true, + }); + + client.cache.writeFragment({ + id: "Hero:1", + fragment: gql` + fragment HeroJob on Hero { + job + } + `, + data: { + job: "Jedi", + }, + }); + + stream.enqueueSubsequentChunk({ + incremental: [ + { + data: { + name: "Luke", + }, + path: ["hero"], + }, + ], + hasNext: false, + }); + + await expect(observableStream).toEmitTypedValue({ + loading: false, + data: { + hero: { + __typename: "Hero", + id: "1", + job: "Jedi", // updated from cache + name: "Luke", + }, + }, + dataState: "complete", + networkStatus: NetworkStatus.ready, + partial: false, + }); +}); + +test("returns error on initial result", async () => { + const client = new ApolloClient({ + link: schemaLink, + cache: new InMemoryCache(), + incrementalHandler: new GraphQL17Alpha9Handler(), + }); + + const query = gql` + query HeroNameQuery { + hero { + id + ... @defer { + name + } + errorField + } + } + `; + + const observableStream = new ObservableStream( + client.watchQuery({ query, errorPolicy: "all" }) + ); + + await expect(observableStream).toEmitTypedValue({ + loading: true, + data: undefined, + dataState: "empty", + networkStatus: NetworkStatus.loading, + partial: true, + }); + + await expect(observableStream).toEmitTypedValue({ + loading: true, + data: markAsStreaming({ + hero: { + __typename: "Hero", + id: "1", + errorField: null, + }, + }), + error: new CombinedGraphQLErrors({ + data: { + hero: { + __typename: "Hero", + id: "1", + errorField: null, + }, + }, + errors: [ + { + message: "bad", + path: ["hero", "errorField"], + }, + ], + }), + dataState: "streaming", + networkStatus: NetworkStatus.streaming, + partial: true, + }); + + await expect(observableStream).toEmitTypedValue({ + loading: false, + data: { + hero: { + __typename: "Hero", + id: "1", + errorField: null, + name: "Luke", + }, + }, + error: new CombinedGraphQLErrors({ + data: { + hero: { + __typename: "Hero", + id: "1", + errorField: null, + name: "Luke", + }, + }, + errors: [ + { + message: "bad", + path: ["hero", "errorField"], + }, + ], + }), + dataState: "complete", + networkStatus: NetworkStatus.error, + partial: false, + }); + + await expect(observableStream).not.toEmitAnything(); +}); + +test("stream that returns an error but continues to stream", async () => { + const client = new ApolloClient({ + link: schemaLink, + cache: new InMemoryCache(), + incrementalHandler: new GraphQL17Alpha9Handler(), + }); + + const query = gql` + query HeroNameQuery { + hero { + id + ... @defer { + errorField + } + ... @defer { + slowField + } + } + } + `; + + const observableStream = new ObservableStream( + client.watchQuery({ query, errorPolicy: "all" }) + ); + + await expect(observableStream).toEmitTypedValue({ + loading: true, + data: undefined, + dataState: "empty", + networkStatus: NetworkStatus.loading, + partial: true, + }); + + await expect(observableStream).toEmitTypedValue({ + loading: true, + data: markAsStreaming({ + hero: { + __typename: "Hero", + id: "1", + }, + }), + dataState: "streaming", + networkStatus: NetworkStatus.streaming, + partial: true, + }); + + await expect(observableStream).toEmitTypedValue({ + loading: true, + data: markAsStreaming({ + hero: { + __typename: "Hero", + id: "1", + errorField: null, + }, + }), + error: new CombinedGraphQLErrors({ + data: { + hero: { + __typename: "Hero", + id: "1", + errorField: null, + }, + }, + errors: [ + { + message: "bad", + path: ["hero", "errorField"], + }, + ], + }), + dataState: "streaming", + networkStatus: NetworkStatus.streaming, + partial: true, + }); + + await expect(observableStream).toEmitTypedValue({ + loading: false, + data: { + hero: { + __typename: "Hero", + id: "1", + errorField: null, + slowField: "slow", + }, + }, + error: new CombinedGraphQLErrors({ + data: { + hero: { + __typename: "Hero", + id: "1", + errorField: null, + slowField: "slow", + }, + }, + errors: [ + { + message: "bad", + path: ["hero", "errorField"], + }, + ], + }), + dataState: "complete", + networkStatus: NetworkStatus.error, + partial: false, + }); +}); + +test("handles final chunk of { hasNext: false } correctly in usage with Apollo Client", async () => { + const stream = mockDeferStream(); + const client = new ApolloClient({ + link: stream.httpLink, + cache: new InMemoryCache(), + incrementalHandler: new GraphQL17Alpha9Handler(), + }); + + const query = gql` + query ProductsQuery { + allProducts { + id + nonNullErrorField + } + } + `; + + const observableStream = new ObservableStream( + client.watchQuery({ query, errorPolicy: "all" }) + ); + stream.enqueueInitialChunk({ + data: { + allProducts: [null, null, null], + }, + errors: [ + { + message: + "Cannot return null for non-nullable field Product.nonNullErrorField.", + }, + { + message: + "Cannot return null for non-nullable field Product.nonNullErrorField.", + }, + { + message: + "Cannot return null for non-nullable field Product.nonNullErrorField.", + }, + ], + hasNext: true, + }); + + stream.enqueueSubsequentChunk({ + hasNext: false, + }); + + await expect(observableStream).toEmitTypedValue({ + loading: true, + data: undefined, + dataState: "empty", + networkStatus: NetworkStatus.loading, + partial: true, + }); + + await expect(observableStream).toEmitTypedValue({ + loading: true, + data: markAsStreaming({ + allProducts: [null, null, null], + }), + error: new CombinedGraphQLErrors({ + data: { + allProducts: [null, null, null], + }, + errors: [ + { + message: + "Cannot return null for non-nullable field Product.nonNullErrorField.", + }, + { + message: + "Cannot return null for non-nullable field Product.nonNullErrorField.", + }, + { + message: + "Cannot return null for non-nullable field Product.nonNullErrorField.", + }, + ], + }), + dataState: "streaming", + networkStatus: NetworkStatus.streaming, + partial: true, + }); + + await expect(observableStream).toEmitSimilarValue({ + expected: (previous) => ({ + ...previous, + dataState: "complete", + loading: false, + networkStatus: NetworkStatus.error, + partial: false, + }), + }); + await expect(observableStream).not.toEmitAnything(); +}); From 225edff77571c39f0b80770d6a1837021c6e2dc0 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 4 Sep 2025 00:06:01 -0600 Subject: [PATCH 05/97] Create stub of GraphQL17Alpha9Handler --- src/incremental/handlers/graphql17Alpha9.ts | 53 +++++++++++++++++++++ src/incremental/index.ts | 1 + 2 files changed, 54 insertions(+) create mode 100644 src/incremental/handlers/graphql17Alpha9.ts diff --git a/src/incremental/handlers/graphql17Alpha9.ts b/src/incremental/handlers/graphql17Alpha9.ts new file mode 100644 index 00000000000..fd9a0bcd9d1 --- /dev/null +++ b/src/incremental/handlers/graphql17Alpha9.ts @@ -0,0 +1,53 @@ +import type { DocumentNode, GraphQLFormattedError } from "graphql"; + +import type { ApolloLink } from "@apollo/client"; +import type { HKT } from "@apollo/client/utilities"; +import { isNonEmptyArray } from "@apollo/client/utilities/internal"; + +import type { Incremental } from "../types.js"; + +export declare namespace GraphQL17Alpha9Handler { + interface GraphQL17Alpha9Result extends HKT { + arg1: unknown; // TData + arg2: unknown; // TExtensions + return: GraphQL17Alpha9Handler.Chunk>; + } + export interface TypeOverrides { + AdditionalApolloLinkResultTypes: GraphQL17Alpha9Result; + } + + export type InitialResult> = {}; + export type SubsequentResult> = {}; + + export type Chunk> = + | InitialResult + | SubsequentResult; +} + +export class GraphQL17Alpha9Handler> + implements Incremental.Handler> +{ + isIncrementalResult: ( + result: ApolloLink.Result + ) => result is GraphQL17Alpha9Handler.Chunk; + + prepareRequest: (request: ApolloLink.Request) => ApolloLink.Request; + + extractErrors: ( + result: ApolloLink.Result + ) => readonly GraphQLFormattedError[] | undefined | void; + + startRequest: >(request: { + query: DocumentNode; + }) => Incremental.IncrementalRequest< + GraphQL17Alpha9Handler.Chunk, + TData + >; +} + +// only exported for use in tests +export function hasIncrementalChunks( + result: Record +): result is Required { + return isNonEmptyArray(result.incremental); +} diff --git a/src/incremental/index.ts b/src/incremental/index.ts index c340efe8574..334b0dcc826 100644 --- a/src/incremental/index.ts +++ b/src/incremental/index.ts @@ -4,3 +4,4 @@ export { Defer20220824Handler, Defer20220824Handler as GraphQL17Alpha2Handler, } from "./handlers/defer20220824.js"; +export { GraphQL17Alpha9Handler } from "./handlers/graphql17Alpha9.js"; From fbbd08e51f5c98cd7be8eaed7070eb686bd7c45c Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 4 Sep 2025 00:18:51 -0600 Subject: [PATCH 06/97] Add chunk types for alpha9 --- src/incremental/handlers/graphql17Alpha9.ts | 49 ++++++++++++++++++++- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/src/incremental/handlers/graphql17Alpha9.ts b/src/incremental/handlers/graphql17Alpha9.ts index fd9a0bcd9d1..b0ce9b2bfae 100644 --- a/src/incremental/handlers/graphql17Alpha9.ts +++ b/src/incremental/handlers/graphql17Alpha9.ts @@ -12,12 +12,57 @@ export declare namespace GraphQL17Alpha9Handler { arg2: unknown; // TExtensions return: GraphQL17Alpha9Handler.Chunk>; } + export interface TypeOverrides { AdditionalApolloLinkResultTypes: GraphQL17Alpha9Result; } - export type InitialResult> = {}; - export type SubsequentResult> = {}; + export type InitialResult> = { + data: TData; + pending: ReadonlyArray; + hasNext: boolean; + extensions?: Record; + }; + + export type SubsequentResult> = { + hasNext: boolean; + pending?: ReadonlyArray; + incremental?: ReadonlyArray>; + completed?: ReadonlyArray; + extensions?: Record; + }; + + export interface PendingResult { + id: string; + path: Incremental.Path; + label?: string; + } + + export interface CompletedResult { + path: Incremental.Path; + label?: string; + errors?: ReadonlyArray; + } + + export interface IncrementalDeferResult> { + errors?: ReadonlyArray; + data: TData; + id: string; + subPath?: ReadonlyArray; + extensions?: Record; + } + + export interface IncrementalStreamResult> { + errors?: ReadonlyArray; + items: TData; + id: string; + subPath?: ReadonlyArray; + extensions?: Record; + } + + export type IncrementalResult> = + | IncrementalDeferResult + | IncrementalStreamResult; export type Chunk> = | InitialResult From 1626cf6a5a71c1b0af1dac764491d42f1945d4d6 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 4 Sep 2025 00:30:15 -0600 Subject: [PATCH 07/97] Add stub implementations for abstract methods --- src/incremental/handlers/graphql17Alpha9.ts | 66 +++++++++++++++------ 1 file changed, 49 insertions(+), 17 deletions(-) diff --git a/src/incremental/handlers/graphql17Alpha9.ts b/src/incremental/handlers/graphql17Alpha9.ts index b0ce9b2bfae..648b43886e9 100644 --- a/src/incremental/handlers/graphql17Alpha9.ts +++ b/src/incremental/handlers/graphql17Alpha9.ts @@ -1,8 +1,15 @@ -import type { DocumentNode, GraphQLFormattedError } from "graphql"; - -import type { ApolloLink } from "@apollo/client"; -import type { HKT } from "@apollo/client/utilities"; -import { isNonEmptyArray } from "@apollo/client/utilities/internal"; +import type { + DocumentNode, + FormattedExecutionResult, + GraphQLFormattedError, +} from "graphql"; + +import type { ApolloLink } from "@apollo/client/link"; +import type { DeepPartial, HKT } from "@apollo/client/utilities"; +import { + hasDirectives, + isNonEmptyArray, +} from "@apollo/client/utilities/internal"; import type { Incremental } from "../types.js"; @@ -69,25 +76,50 @@ export declare namespace GraphQL17Alpha9Handler { | SubsequentResult; } +class IncrementalRequest> + implements + Incremental.IncrementalRequest, TData> +{ + hasNext = true; + + private data: any = {}; + + handle( + cacheData: TData | DeepPartial | null | undefined = this.data, + chunk: GraphQL17Alpha9Handler.Chunk + ): FormattedExecutionResult { + return { data: null }; + } +} + export class GraphQL17Alpha9Handler> - implements Incremental.Handler> + implements Incremental.Handler> { - isIncrementalResult: ( + isIncrementalResult( result: ApolloLink.Result - ) => result is GraphQL17Alpha9Handler.Chunk; + ): result is GraphQL17Alpha9Handler.Chunk { + return "hasNext" in result; + } - prepareRequest: (request: ApolloLink.Request) => ApolloLink.Request; + prepareRequest(request: ApolloLink.Request): ApolloLink.Request { + if (hasDirectives(["defer"], request.query)) { + const context = request.context ?? {}; + const http = (context.http ??= {}); + http.accept = ["multipart/mixed", ...(http.accept || [])]; - extractErrors: ( - result: ApolloLink.Result - ) => readonly GraphQLFormattedError[] | undefined | void; + request.context = context; + } + + return request; + } + + extractErrors(result: ApolloLink.Result) {} - startRequest: >(request: { + startRequest>(_: { query: DocumentNode; - }) => Incremental.IncrementalRequest< - GraphQL17Alpha9Handler.Chunk, - TData - >; + }) { + return new IncrementalRequest(); + } } // only exported for use in tests From 2681c698e9bc92451d92ebb30c6dc6f889b49eca Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 4 Sep 2025 11:04:03 -0600 Subject: [PATCH 08/97] Update types to be more compatible with graphql-js --- src/incremental/handlers/graphql17Alpha9.ts | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/incremental/handlers/graphql17Alpha9.ts b/src/incremental/handlers/graphql17Alpha9.ts index 648b43886e9..e791a4cf3d2 100644 --- a/src/incremental/handlers/graphql17Alpha9.ts +++ b/src/incremental/handlers/graphql17Alpha9.ts @@ -26,12 +26,13 @@ export declare namespace GraphQL17Alpha9Handler { export type InitialResult> = { data: TData; + errors?: ReadonlyArray; pending: ReadonlyArray; hasNext: boolean; extensions?: Record; }; - export type SubsequentResult> = { + export type SubsequentResult = { hasNext: boolean; pending?: ReadonlyArray; incremental?: ReadonlyArray>; @@ -55,7 +56,7 @@ export declare namespace GraphQL17Alpha9Handler { errors?: ReadonlyArray; data: TData; id: string; - subPath?: ReadonlyArray; + subPath?: Incremental.Path; extensions?: Record; } @@ -63,20 +64,18 @@ export declare namespace GraphQL17Alpha9Handler { errors?: ReadonlyArray; items: TData; id: string; - subPath?: ReadonlyArray; + subPath?: Incremental.Path; extensions?: Record; } - export type IncrementalResult> = + export type IncrementalResult = | IncrementalDeferResult | IncrementalStreamResult; - export type Chunk> = - | InitialResult - | SubsequentResult; + export type Chunk = InitialResult | SubsequentResult; } -class IncrementalRequest> +class IncrementalRequest implements Incremental.IncrementalRequest, TData> { @@ -115,9 +114,7 @@ export class GraphQL17Alpha9Handler> extractErrors(result: ApolloLink.Result) {} - startRequest>(_: { - query: DocumentNode; - }) { + startRequest(_: { query: DocumentNode }) { return new IncrementalRequest(); } } From 6cb1c20d13c0b49f64ebea0df3eedf770d585659 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 4 Sep 2025 11:20:18 -0600 Subject: [PATCH 09/97] Fix type of completed result --- src/incremental/handlers/graphql17Alpha9.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/incremental/handlers/graphql17Alpha9.ts b/src/incremental/handlers/graphql17Alpha9.ts index e791a4cf3d2..7515f8a79ac 100644 --- a/src/incremental/handlers/graphql17Alpha9.ts +++ b/src/incremental/handlers/graphql17Alpha9.ts @@ -47,8 +47,7 @@ export declare namespace GraphQL17Alpha9Handler { } export interface CompletedResult { - path: Incremental.Path; - label?: string; + id: string; errors?: ReadonlyArray; } From ba85d93787544630ba91fa304723b0b71f8e2a53 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 4 Sep 2025 11:29:03 -0600 Subject: [PATCH 10/97] Add initial implementation of merging --- src/incremental/handlers/graphql17Alpha9.ts | 74 ++++++++++++++++++++- 1 file changed, 72 insertions(+), 2 deletions(-) diff --git a/src/incremental/handlers/graphql17Alpha9.ts b/src/incremental/handlers/graphql17Alpha9.ts index 7515f8a79ac..0d20a5652f0 100644 --- a/src/incremental/handlers/graphql17Alpha9.ts +++ b/src/incremental/handlers/graphql17Alpha9.ts @@ -6,10 +6,12 @@ import type { import type { ApolloLink } from "@apollo/client/link"; import type { DeepPartial, HKT } from "@apollo/client/utilities"; +import { DeepMerger } from "@apollo/client/utilities/internal"; import { hasDirectives, isNonEmptyArray, } from "@apollo/client/utilities/internal"; +import { invariant } from "@apollo/client/utilities/invariant"; import type { Incremental } from "../types.js"; @@ -81,16 +83,84 @@ class IncrementalRequest hasNext = true; private data: any = {}; + private errors: GraphQLFormattedError[] = []; + private extensions: Record = {}; + private pending: GraphQL17Alpha9Handler.PendingResult[] = []; + private merger = new DeepMerger(); handle( cacheData: TData | DeepPartial | null | undefined = this.data, chunk: GraphQL17Alpha9Handler.Chunk ): FormattedExecutionResult { - return { data: null }; + this.hasNext = chunk.hasNext; + this.data = cacheData; + + if (chunk.pending) { + this.pending.push(...chunk.pending); + } + + this.mergeIn(chunk); + + if (hasIncrementalChunks(chunk)) { + for (const incremental of chunk.incremental) { + // TODO: Implement support for `@stream`. For now we will skip handling + // streamed responses + if ("items" in incremental) { + continue; + } + + const pending = this.pending.find(({ id }) => incremental.id === id); + invariant( + pending, + "Could not find pending chunk for incremental value. Please file an issue because this is a bug in Apollo Client." + ); + + let { data } = incremental; + const { path } = pending; + + for (let i = path.length - 1; i >= 0; i--) { + const key = path[i]; + const parent: Record = + typeof key === "number" ? [] : {}; + parent[key] = incremental.data; + data = parent as typeof data; + } + + this.mergeIn({ + data: data as TData, + extensions: incremental.extensions, + }); + + for (const completed of chunk.completed) { + const index = this.pending.findIndex(({ id }) => id === completed.id); + this.pending.splice(index, 1); + } + } + } + + const result: FormattedExecutionResult = { data: this.data }; + + if (isNonEmptyArray(this.errors)) { + result.errors = this.errors; + } + + if (Object.keys(this.extensions).length > 0) { + result.extensions = this.extensions; + } + + return result; + } + + private mergeIn(normalized: FormattedExecutionResult) { + if (normalized.data !== undefined) { + this.data = this.merger.merge(this.data, normalized.data); + } + + Object.assign(this.extensions, normalized.extensions); } } -export class GraphQL17Alpha9Handler> +export class GraphQL17Alpha9Handler implements Incremental.Handler> { isIncrementalResult( From 6d8c443767505c5ffb3a6f73a23c159807e4c0b9 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 4 Sep 2025 11:36:45 -0600 Subject: [PATCH 11/97] Add patch for types in v17-alpha9 --- patches/graphql-17-alpha9+17.0.0-alpha.9.patch | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 patches/graphql-17-alpha9+17.0.0-alpha.9.patch diff --git a/patches/graphql-17-alpha9+17.0.0-alpha.9.patch b/patches/graphql-17-alpha9+17.0.0-alpha.9.patch new file mode 100644 index 00000000000..591af1a11f4 --- /dev/null +++ b/patches/graphql-17-alpha9+17.0.0-alpha.9.patch @@ -0,0 +1,16 @@ +diff --git a/node_modules/graphql-17-alpha9/execution/types.d.ts b/node_modules/graphql-17-alpha9/execution/types.d.ts +index 48ef2e9..6ef2ab3 100644 +--- a/node_modules/graphql-17-alpha9/execution/types.d.ts ++++ b/node_modules/graphql-17-alpha9/execution/types.d.ts +@@ -95,9 +95,8 @@ export interface CompletedResult { + errors?: ReadonlyArray; + } + export interface FormattedCompletedResult { +- path: ReadonlyArray; +- label?: string; +- errors?: ReadonlyArray; ++ id: string; ++ errors?: ReadonlyArray; + } + export declare function isPendingExecutionGroup(incrementalDataRecord: IncrementalDataRecord): incrementalDataRecord is PendingExecutionGroup; + export type CompletedExecutionGroup = SuccessfulExecutionGroup | FailedExecutionGroup; From 9cd60c446da9c96b0eae577bf382f1c177377156 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 4 Sep 2025 11:37:25 -0600 Subject: [PATCH 12/97] Add return type for async generator for run function --- src/incremental/handlers/__tests__/graphql17Alpha9.test.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/incremental/handlers/__tests__/graphql17Alpha9.test.ts b/src/incremental/handlers/__tests__/graphql17Alpha9.test.ts index 94411e5b51b..1519bbdede7 100644 --- a/src/incremental/handlers/__tests__/graphql17Alpha9.test.ts +++ b/src/incremental/handlers/__tests__/graphql17Alpha9.test.ts @@ -2,6 +2,7 @@ import assert from "node:assert"; import type { DocumentNode, + FormattedExecutionResult, FormattedInitialIncrementalExecutionResult, FormattedSubsequentIncrementalExecutionResult, } from "graphql-17-alpha9"; @@ -175,11 +176,12 @@ function promiseWithResolvers(): { async function* run( document: DocumentNode, - rootValue: unknown = { hero }, + rootValue: Record = { hero }, enableEarlyExecution = false ): AsyncGenerator< | FormattedInitialIncrementalExecutionResult - | FormattedSubsequentIncrementalExecutionResult + | FormattedSubsequentIncrementalExecutionResult, + FormattedExecutionResult | void > { const result = await experimentalExecuteIncrementally({ schema, From b039ee4e627300a9b7fa5ab64486b91292829971 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 4 Sep 2025 11:39:01 -0600 Subject: [PATCH 13/97] Add errors when merging --- src/incremental/handlers/graphql17Alpha9.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/incremental/handlers/graphql17Alpha9.ts b/src/incremental/handlers/graphql17Alpha9.ts index 0d20a5652f0..87081bf96a9 100644 --- a/src/incremental/handlers/graphql17Alpha9.ts +++ b/src/incremental/handlers/graphql17Alpha9.ts @@ -156,6 +156,10 @@ class IncrementalRequest this.data = this.merger.merge(this.data, normalized.data); } + if (normalized.errors) { + this.errors.push(...normalized.errors); + } + Object.assign(this.extensions, normalized.extensions); } } From 74cb0a436788ce7490168d4afae37fd205430127 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 4 Sep 2025 11:46:00 -0600 Subject: [PATCH 14/97] Ensure errors are merged from incremental results --- src/incremental/handlers/graphql17Alpha9.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/incremental/handlers/graphql17Alpha9.ts b/src/incremental/handlers/graphql17Alpha9.ts index 87081bf96a9..1a7dff2d29b 100644 --- a/src/incremental/handlers/graphql17Alpha9.ts +++ b/src/incremental/handlers/graphql17Alpha9.ts @@ -129,6 +129,7 @@ class IncrementalRequest this.mergeIn({ data: data as TData, extensions: incremental.extensions, + errors: incremental.errors, }); for (const completed of chunk.completed) { From c17707fed2902d6d270786d0c404bb87d7a79ec0 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 4 Sep 2025 11:48:41 -0600 Subject: [PATCH 15/97] Iterate completed after merging all incremental items --- src/incremental/handlers/graphql17Alpha9.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/incremental/handlers/graphql17Alpha9.ts b/src/incremental/handlers/graphql17Alpha9.ts index 1a7dff2d29b..4ca5115b8ca 100644 --- a/src/incremental/handlers/graphql17Alpha9.ts +++ b/src/incremental/handlers/graphql17Alpha9.ts @@ -131,11 +131,13 @@ class IncrementalRequest extensions: incremental.extensions, errors: incremental.errors, }); + } + } - for (const completed of chunk.completed) { - const index = this.pending.findIndex(({ id }) => id === completed.id); - this.pending.splice(index, 1); - } + if ("completed" in chunk && chunk.completed) { + for (const completed of chunk.completed) { + const index = this.pending.findIndex(({ id }) => id === completed.id); + this.pending.splice(index, 1); } } From e7e11b7e3b5827b6b6ac6f073b9845679c48ef80 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 4 Sep 2025 11:49:19 -0600 Subject: [PATCH 16/97] Remove locations from error --- src/incremental/handlers/__tests__/graphql17Alpha9.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/incremental/handlers/__tests__/graphql17Alpha9.test.ts b/src/incremental/handlers/__tests__/graphql17Alpha9.test.ts index 1519bbdede7..b5200f503ba 100644 --- a/src/incremental/handlers/__tests__/graphql17Alpha9.test.ts +++ b/src/incremental/handlers/__tests__/graphql17Alpha9.test.ts @@ -402,7 +402,6 @@ describe("graphql-js test cases", () => { errors: [ { message: "bad", - locations: [{ line: 7, column: 11 }], path: ["hero", "name"], }, ], From 21042b935938c555adfb7834102c631ba51962a5 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 4 Sep 2025 12:21:47 -0600 Subject: [PATCH 17/97] Fix incorrect data merged in --- src/incremental/handlers/graphql17Alpha9.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/incremental/handlers/graphql17Alpha9.ts b/src/incremental/handlers/graphql17Alpha9.ts index 4ca5115b8ca..c8dd5cdcd72 100644 --- a/src/incremental/handlers/graphql17Alpha9.ts +++ b/src/incremental/handlers/graphql17Alpha9.ts @@ -122,7 +122,7 @@ class IncrementalRequest const key = path[i]; const parent: Record = typeof key === "number" ? [] : {}; - parent[key] = incremental.data; + parent[key] = data; data = parent as typeof data; } From 53bd6ce826ba067d84dc8b8b03a8e60efeb23cff Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 4 Sep 2025 12:25:12 -0600 Subject: [PATCH 18/97] Merge errors in completed array --- src/incremental/handlers/graphql17Alpha9.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/incremental/handlers/graphql17Alpha9.ts b/src/incremental/handlers/graphql17Alpha9.ts index c8dd5cdcd72..97500f27f01 100644 --- a/src/incremental/handlers/graphql17Alpha9.ts +++ b/src/incremental/handlers/graphql17Alpha9.ts @@ -138,6 +138,10 @@ class IncrementalRequest for (const completed of chunk.completed) { const index = this.pending.findIndex(({ id }) => id === completed.id); this.pending.splice(index, 1); + + if (completed.errors) { + this.errors.push(...completed.errors); + } } } From 978228af44e654b39bd29fd2cf17f8d5920d02e9 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 4 Sep 2025 12:26:08 -0600 Subject: [PATCH 19/97] Remove locations in error tests --- .../handlers/__tests__/graphql17Alpha9.test.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/incremental/handlers/__tests__/graphql17Alpha9.test.ts b/src/incremental/handlers/__tests__/graphql17Alpha9.test.ts index b5200f503ba..8df4f57e63b 100644 --- a/src/incremental/handlers/__tests__/graphql17Alpha9.test.ts +++ b/src/incremental/handlers/__tests__/graphql17Alpha9.test.ts @@ -1556,7 +1556,6 @@ describe("graphql-js test cases", () => { { message: "Cannot return null for non-nullable field c.nonNullErrorField.", - locations: [{ line: 8, column: 17 }], path: ["a", "b", "c", "nonNullErrorField"], }, ], @@ -1631,7 +1630,6 @@ describe("graphql-js test cases", () => { { message: "Cannot return null for non-nullable field c.nonNullErrorField.", - locations: [{ line: 17, column: 17 }], path: ["a", "b", "c", "nonNullErrorField"], }, ], @@ -1696,13 +1694,11 @@ describe("graphql-js test cases", () => { { message: "Cannot return null for non-nullable field c.nonNullErrorField.", - locations: [{ line: 7, column: 17 }], path: ["a", "b", "c", "someError"], }, { message: "Cannot return null for non-nullable field c.nonNullErrorField.", - locations: [{ line: 16, column: 17 }], path: ["a", "b", "c", "anotherError"], }, ], @@ -1780,7 +1776,6 @@ describe("graphql-js test cases", () => { { message: "Cannot return null for non-nullable field c.nonNullErrorField.", - locations: [{ line: 19, column: 17 }], path: ["a", "b", "someC", "someError"], }, ], @@ -1867,7 +1862,6 @@ describe("graphql-js test cases", () => { { message: "Cannot return null for non-nullable field c.nonNullErrorField.", - locations: [{ line: 8, column: 17 }], path: ["a", "b", "c", "nonNullErrorField"], }, ], @@ -1932,7 +1926,6 @@ describe("graphql-js test cases", () => { { message: "Cannot return null for non-nullable field Hero.nonNullName.", - locations: [{ line: 5, column: 13 }], path: ["hero", "nonNullName"], }, ], @@ -2079,7 +2072,6 @@ describe("graphql-js test cases", () => { errors: [ { message: "bad", - locations: [{ line: 9, column: 9 }], path: ["hero", "name"], }, ], @@ -2142,7 +2134,6 @@ describe("graphql-js test cases", () => { { message: "Cannot return null for non-nullable field Hero.nonNullName.", - locations: [{ line: 9, column: 9 }], path: ["hero", "nonNullName"], }, ], @@ -2209,7 +2200,6 @@ describe("graphql-js test cases", () => { { message: "Cannot return null for non-nullable field Hero.nonNullName.", - locations: [{ line: 9, column: 9 }], path: ["hero", "nonNullName"], }, ], From 706bda0d0821165b996cfc738630fdbd74d9760b Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 4 Sep 2025 12:29:06 -0600 Subject: [PATCH 20/97] Fix incorrect assertion --- src/incremental/handlers/__tests__/graphql17Alpha9.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/incremental/handlers/__tests__/graphql17Alpha9.test.ts b/src/incremental/handlers/__tests__/graphql17Alpha9.test.ts index 8df4f57e63b..2df5e73789c 100644 --- a/src/incremental/handlers/__tests__/graphql17Alpha9.test.ts +++ b/src/incremental/handlers/__tests__/graphql17Alpha9.test.ts @@ -1687,7 +1687,7 @@ describe("graphql-js test cases", () => { assert(!done); expect(handler.isIncrementalResult(chunk)).toBe(true); - expect(hasIncrementalChunks(chunk)).toBe(true); + expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: {}, errors: [ From e6731f844d925e5b2a1a8ce54c36290c3c4d8a88 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 4 Sep 2025 12:33:40 -0600 Subject: [PATCH 21/97] Fix missing assertion in test --- .../handlers/__tests__/graphql17Alpha9.test.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/incremental/handlers/__tests__/graphql17Alpha9.test.ts b/src/incremental/handlers/__tests__/graphql17Alpha9.test.ts index 2df5e73789c..7cbbb743a0f 100644 --- a/src/incremental/handlers/__tests__/graphql17Alpha9.test.ts +++ b/src/incremental/handlers/__tests__/graphql17Alpha9.test.ts @@ -1850,6 +1850,24 @@ describe("graphql-js test cases", () => { assert(!done); expect(handler.isIncrementalResult(chunk)).toBe(true); expect(hasIncrementalChunks(chunk)).toBe(true); + expect(request.handle(undefined, chunk)).toStrictEqualTyped({ + data: { + a: { + b: { + c: { d: "d" }, + }, + }, + }, + }); + expect(request.hasNext).toBe(true); + } + + { + const { value: chunk, done } = await incoming.next(); + + assert(!done); + expect(handler.isIncrementalResult(chunk)).toBe(true); + expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { a: { From dba62d4d1822cd49e01a24666d006e21a90bd113 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 4 Sep 2025 12:36:16 -0600 Subject: [PATCH 22/97] Fix incorrect assertion --- src/incremental/handlers/__tests__/graphql17Alpha9.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/incremental/handlers/__tests__/graphql17Alpha9.test.ts b/src/incremental/handlers/__tests__/graphql17Alpha9.test.ts index 7cbbb743a0f..3e89dbe6169 100644 --- a/src/incremental/handlers/__tests__/graphql17Alpha9.test.ts +++ b/src/incremental/handlers/__tests__/graphql17Alpha9.test.ts @@ -2129,7 +2129,7 @@ describe("graphql-js test cases", () => { expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { hero: { - name: "Luke", + id: "1", }, }, }); @@ -2141,7 +2141,7 @@ describe("graphql-js test cases", () => { assert(!done); expect(handler.isIncrementalResult(chunk)).toBe(true); - expect(hasIncrementalChunks(chunk)).toBe(true); + expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { hero: { @@ -2207,7 +2207,7 @@ describe("graphql-js test cases", () => { assert(!done); expect(handler.isIncrementalResult(chunk)).toBe(true); - expect(hasIncrementalChunks(chunk)).toBe(true); + expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { hero: { From ee5cb45ccb0062f77f52f0599405fa8e4b5565f7 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 4 Sep 2025 12:48:47 -0600 Subject: [PATCH 23/97] Update test to use actual schema fields --- .../__tests__/graphql17Alpha9.test.ts | 53 +++++++++++-------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/src/incremental/handlers/__tests__/graphql17Alpha9.test.ts b/src/incremental/handlers/__tests__/graphql17Alpha9.test.ts index 3e89dbe6169..40e73dddca2 100644 --- a/src/incremental/handlers/__tests__/graphql17Alpha9.test.ts +++ b/src/incremental/handlers/__tests__/graphql17Alpha9.test.ts @@ -205,16 +205,18 @@ async function* run( } } -const schemaLink = new ApolloLink((operation) => { - return new Observable((observer) => { - void (async () => { - for await (const chunk of run(operation.query)) { - observer.next(chunk); - } - observer.complete(); - })(); +function createSchemaLink(rootValue?: Record) { + return new ApolloLink((operation) => { + return new Observable((observer) => { + void (async () => { + for await (const chunk of run(operation.query, rootValue)) { + observer.next(chunk); + } + observer.complete(); + })(); + }); }); -}); +} describe("graphql-js test cases", () => { // These test cases mirror defer tests of the `graphql-js` v17.0.0-alpha.9 release: @@ -2556,7 +2558,16 @@ test("returns error on initial result", async () => { test("stream that returns an error but continues to stream", async () => { const client = new ApolloClient({ - link: schemaLink, + link: createSchemaLink({ + hero: { + ...hero, + nonNullName: null, + name: async () => { + await wait(100); + return "slow"; + }, + }, + }), cache: new InMemoryCache(), incrementalHandler: new GraphQL17Alpha9Handler(), }); @@ -2566,10 +2577,10 @@ test("stream that returns an error but continues to stream", async () => { hero { id ... @defer { - errorField + nonNullName } ... @defer { - slowField + name } } } @@ -2606,7 +2617,6 @@ test("stream that returns an error but continues to stream", async () => { hero: { __typename: "Hero", id: "1", - errorField: null, }, }), error: new CombinedGraphQLErrors({ @@ -2614,13 +2624,13 @@ test("stream that returns an error but continues to stream", async () => { hero: { __typename: "Hero", id: "1", - errorField: null, }, }, errors: [ { - message: "bad", - path: ["hero", "errorField"], + message: + "Cannot return null for non-nullable field Hero.nonNullName.", + path: ["hero", "nonNullName"], }, ], }), @@ -2635,8 +2645,7 @@ test("stream that returns an error but continues to stream", async () => { hero: { __typename: "Hero", id: "1", - errorField: null, - slowField: "slow", + name: "slow", }, }, error: new CombinedGraphQLErrors({ @@ -2644,14 +2653,14 @@ test("stream that returns an error but continues to stream", async () => { hero: { __typename: "Hero", id: "1", - errorField: null, - slowField: "slow", + name: "slow", }, }, errors: [ { - message: "bad", - path: ["hero", "errorField"], + message: + "Cannot return null for non-nullable field Hero.nonNullName.", + path: ["hero", "nonNullName"], }, ], }), From 33ba203cf43ad96686de750859ba6d20b35096bb Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 4 Sep 2025 13:10:10 -0600 Subject: [PATCH 24/97] Yield a regular result instead of return --- .../__tests__/graphql17Alpha9.test.ts | 141 +++++++++--------- 1 file changed, 72 insertions(+), 69 deletions(-) diff --git a/src/incremental/handlers/__tests__/graphql17Alpha9.test.ts b/src/incremental/handlers/__tests__/graphql17Alpha9.test.ts index 40e73dddca2..5af5cde789a 100644 --- a/src/incremental/handlers/__tests__/graphql17Alpha9.test.ts +++ b/src/incremental/handlers/__tests__/graphql17Alpha9.test.ts @@ -29,6 +29,7 @@ import { markAsStreaming, mockDeferStream, ObservableStream, + wait, } from "@apollo/client/testing/internal"; import { @@ -180,8 +181,9 @@ async function* run( enableEarlyExecution = false ): AsyncGenerator< | FormattedInitialIncrementalExecutionResult - | FormattedSubsequentIncrementalExecutionResult, - FormattedExecutionResult | void + | FormattedSubsequentIncrementalExecutionResult + | FormattedExecutionResult, + void > { const result = await experimentalExecuteIncrementally({ schema, @@ -201,7 +203,7 @@ async function* run( ) as FormattedSubsequentIncrementalExecutionResult; } } else { - return result; + yield result; } } @@ -244,7 +246,7 @@ describe("graphql-js test cases", () => { const { value: chunk, done } = await incoming.next(); assert(!done); - expect(handler.isIncrementalResult(chunk)).toBe(true); + assert(handler.isIncrementalResult(chunk)); expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { @@ -260,7 +262,7 @@ describe("graphql-js test cases", () => { const { value: chunk, done } = await incoming.next(); assert(!done); - expect(handler.isIncrementalResult(chunk)).toBe(true); + assert(handler.isIncrementalResult(chunk)); expect(hasIncrementalChunks(chunk)).toBe(true); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { @@ -329,7 +331,7 @@ describe("graphql-js test cases", () => { const { value: chunk, done } = await incoming.next(); assert(!done); - expect(handler.isIncrementalResult(chunk)).toBe(true); + assert(handler.isIncrementalResult(chunk)); expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: {}, @@ -341,7 +343,7 @@ describe("graphql-js test cases", () => { const { value: chunk, done } = await incoming.next(); assert(!done); - expect(handler.isIncrementalResult(chunk)).toBe(true); + assert(handler.isIncrementalResult(chunk)); expect(hasIncrementalChunks(chunk)).toBe(true); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { @@ -381,7 +383,7 @@ describe("graphql-js test cases", () => { const { value: chunk, done } = await incoming.next(); assert(!done); - expect(handler.isIncrementalResult(chunk)).toBe(true); + assert(handler.isIncrementalResult(chunk)); expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: {}, @@ -393,7 +395,7 @@ describe("graphql-js test cases", () => { const { value: chunk, done } = await incoming.next(); assert(!done); - expect(handler.isIncrementalResult(chunk)).toBe(true); + assert(handler.isIncrementalResult(chunk)); expect(hasIncrementalChunks(chunk)).toBe(true); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { @@ -439,7 +441,7 @@ describe("graphql-js test cases", () => { const { value: chunk, done } = await incoming.next(); assert(!done); - expect(handler.isIncrementalResult(chunk)).toBe(true); + assert(handler.isIncrementalResult(chunk)); expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { @@ -453,7 +455,7 @@ describe("graphql-js test cases", () => { const { value: chunk, done } = await incoming.next(); assert(!done); - expect(handler.isIncrementalResult(chunk)).toBe(true); + assert(handler.isIncrementalResult(chunk)); expect(hasIncrementalChunks(chunk)).toBe(true); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { @@ -495,7 +497,7 @@ describe("graphql-js test cases", () => { const { value: chunk, done } = await incoming.next(); assert(!done); - expect(handler.isIncrementalResult(chunk)).toBe(true); + assert(handler.isIncrementalResult(chunk)); expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { @@ -511,7 +513,7 @@ describe("graphql-js test cases", () => { const { value: chunk, done } = await incoming.next(); assert(!done); - expect(handler.isIncrementalResult(chunk)).toBe(true); + assert(handler.isIncrementalResult(chunk)); expect(hasIncrementalChunks(chunk)).toBe(true); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { @@ -551,7 +553,7 @@ describe("graphql-js test cases", () => { const { value: chunk, done } = await incoming.next(); assert(!done); - expect(handler.isIncrementalResult(chunk)).toBe(true); + assert(handler.isIncrementalResult(chunk)); expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { @@ -565,7 +567,7 @@ describe("graphql-js test cases", () => { const { value: chunk, done } = await incoming.next(); assert(!done); - expect(handler.isIncrementalResult(chunk)).toBe(true); + assert(handler.isIncrementalResult(chunk)); expect(hasIncrementalChunks(chunk)).toBe(true); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { @@ -601,7 +603,7 @@ describe("graphql-js test cases", () => { const { value: chunk, done } = await incoming.next(); assert(!done); - expect(handler.isIncrementalResult(chunk)).toBe(true); + assert(handler.isIncrementalResult(chunk)); expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { @@ -615,7 +617,7 @@ describe("graphql-js test cases", () => { const { value: chunk, done } = await incoming.next(); assert(!done); - expect(handler.isIncrementalResult(chunk)).toBe(true); + assert(handler.isIncrementalResult(chunk)); expect(hasIncrementalChunks(chunk)).toBe(true); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { @@ -654,7 +656,7 @@ describe("graphql-js test cases", () => { const { value: chunk, done } = await incoming.next(); assert(!done); - expect(handler.isIncrementalResult(chunk)).toBe(true); + assert(handler.isIncrementalResult(chunk)); expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: {}, @@ -666,7 +668,7 @@ describe("graphql-js test cases", () => { const { value: chunk, done } = await incoming.next(); assert(!done); - expect(handler.isIncrementalResult(chunk)).toBe(true); + assert(handler.isIncrementalResult(chunk)); expect(hasIncrementalChunks(chunk)).toBe(true); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { @@ -709,7 +711,7 @@ describe("graphql-js test cases", () => { const { value: chunk, done } = await incoming.next(); assert(!done); - expect(handler.isIncrementalResult(chunk)).toBe(true); + assert(handler.isIncrementalResult(chunk)); expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { @@ -723,7 +725,7 @@ describe("graphql-js test cases", () => { const { value: chunk, done } = await incoming.next(); assert(!done); - expect(handler.isIncrementalResult(chunk)).toBe(true); + assert(handler.isIncrementalResult(chunk)); expect(hasIncrementalChunks(chunk)).toBe(true); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { @@ -759,7 +761,7 @@ describe("graphql-js test cases", () => { const { value: chunk, done } = await incoming.next(); assert(!done); - expect(handler.isIncrementalResult(chunk)).toBe(true); + assert(handler.isIncrementalResult(chunk)); expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: {}, @@ -771,7 +773,7 @@ describe("graphql-js test cases", () => { const { value: chunk, done } = await incoming.next(); assert(!done); - expect(handler.isIncrementalResult(chunk)).toBe(true); + assert(handler.isIncrementalResult(chunk)); expect(hasIncrementalChunks(chunk)).toBe(true); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { @@ -833,7 +835,7 @@ describe("graphql-js test cases", () => { const { value: chunk, done } = await incoming.next(); assert(!done); - expect(handler.isIncrementalResult(chunk)).toBe(true); + assert(handler.isIncrementalResult(chunk)); expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: {}, @@ -845,7 +847,7 @@ describe("graphql-js test cases", () => { const { value: chunk, done } = await incoming.next(); assert(!done); - expect(handler.isIncrementalResult(chunk)).toBe(true); + assert(handler.isIncrementalResult(chunk)); expect(hasIncrementalChunks(chunk)).toBe(true); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { @@ -865,7 +867,7 @@ describe("graphql-js test cases", () => { const { value: chunk, done } = await incoming.next(); assert(!done); - expect(handler.isIncrementalResult(chunk)).toBe(true); + assert(handler.isIncrementalResult(chunk)); expect(hasIncrementalChunks(chunk)).toBe(true); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { @@ -930,7 +932,7 @@ describe("graphql-js test cases", () => { const { value: chunk, done } = await incoming.next(); assert(!done); - expect(handler.isIncrementalResult(chunk)).toBe(true); + assert(handler.isIncrementalResult(chunk)); expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: {}, @@ -942,7 +944,7 @@ describe("graphql-js test cases", () => { const { value: chunk, done } = await incoming.next(); assert(!done); - expect(handler.isIncrementalResult(chunk)).toBe(true); + assert(handler.isIncrementalResult(chunk)); expect(hasIncrementalChunks(chunk)).toBe(true); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { @@ -958,7 +960,7 @@ describe("graphql-js test cases", () => { const { value: chunk, done } = await incoming.next(); assert(!done); - expect(handler.isIncrementalResult(chunk)).toBe(true); + assert(handler.isIncrementalResult(chunk)); expect(hasIncrementalChunks(chunk)).toBe(true); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { @@ -1010,7 +1012,7 @@ describe("graphql-js test cases", () => { const { value: chunk, done } = await incoming.next(); assert(!done); - expect(handler.isIncrementalResult(chunk)).toBe(true); + assert(handler.isIncrementalResult(chunk)); expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { @@ -1026,7 +1028,7 @@ describe("graphql-js test cases", () => { const { value: chunk, done } = await incoming.next(); assert(!done); - expect(handler.isIncrementalResult(chunk)).toBe(true); + assert(handler.isIncrementalResult(chunk)); expect(hasIncrementalChunks(chunk)).toBe(true); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { @@ -1086,7 +1088,7 @@ describe("graphql-js test cases", () => { const { value: chunk, done } = await incoming.next(); assert(!done); - expect(handler.isIncrementalResult(chunk)).toBe(true); + assert(handler.isIncrementalResult(chunk)); expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { @@ -1111,7 +1113,7 @@ describe("graphql-js test cases", () => { const { value: chunk, done } = await incoming.next(); assert(!done); - expect(handler.isIncrementalResult(chunk)).toBe(true); + assert(handler.isIncrementalResult(chunk)); expect(hasIncrementalChunks(chunk)).toBe(true); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { @@ -1163,7 +1165,7 @@ describe("graphql-js test cases", () => { const { value: chunk, done } = await incoming.next(); assert(!done); - expect(handler.isIncrementalResult(chunk)).toBe(true); + assert(handler.isIncrementalResult(chunk)); expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { @@ -1177,7 +1179,7 @@ describe("graphql-js test cases", () => { const { value: chunk, done } = await incoming.next(); assert(!done); - expect(handler.isIncrementalResult(chunk)).toBe(true); + assert(handler.isIncrementalResult(chunk)); expect(hasIncrementalChunks(chunk)).toBe(true); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { @@ -1244,7 +1246,7 @@ describe("graphql-js test cases", () => { const { value: chunk, done } = await incoming.next(); assert(!done); - expect(handler.isIncrementalResult(chunk)).toBe(true); + assert(handler.isIncrementalResult(chunk)); expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { @@ -1264,7 +1266,7 @@ describe("graphql-js test cases", () => { const { value: chunk, done } = await incoming.next(); assert(!done); - expect(handler.isIncrementalResult(chunk)).toBe(true); + assert(handler.isIncrementalResult(chunk)); expect(hasIncrementalChunks(chunk)).toBe(true); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { @@ -1320,7 +1322,7 @@ describe("graphql-js test cases", () => { const { value: chunk, done } = await incoming.next(); assert(!done); - expect(handler.isIncrementalResult(chunk)).toBe(true); + assert(handler.isIncrementalResult(chunk)); expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { @@ -1338,7 +1340,7 @@ describe("graphql-js test cases", () => { const { value: chunk, done } = await incoming.next(); assert(!done); - expect(handler.isIncrementalResult(chunk)).toBe(true); + assert(handler.isIncrementalResult(chunk)); expect(hasIncrementalChunks(chunk)).toBe(true); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { @@ -1403,7 +1405,7 @@ describe("graphql-js test cases", () => { const { value: chunk, done } = await incoming.next(); assert(!done); - expect(handler.isIncrementalResult(chunk)).toBe(true); + assert(handler.isIncrementalResult(chunk)); expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { @@ -1421,7 +1423,7 @@ describe("graphql-js test cases", () => { const { value: chunk, done } = await incoming.next(); assert(!done); - expect(handler.isIncrementalResult(chunk)).toBe(true); + assert(handler.isIncrementalResult(chunk)); expect(hasIncrementalChunks(chunk)).toBe(true); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { @@ -1468,7 +1470,7 @@ describe("graphql-js test cases", () => { const { value: chunk, done } = await incoming.next(); assert(!done); - expect(handler.isIncrementalResult(chunk)).toBe(true); + assert(handler.isIncrementalResult(chunk)); expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: {}, @@ -1480,7 +1482,7 @@ describe("graphql-js test cases", () => { const { value: chunk, done } = await incoming.next(); assert(!done); - expect(handler.isIncrementalResult(chunk)).toBe(true); + assert(handler.isIncrementalResult(chunk)); expect(hasIncrementalChunks(chunk)).toBe(true); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { @@ -1530,7 +1532,7 @@ describe("graphql-js test cases", () => { const { value: chunk, done } = await incoming.next(); assert(!done); - expect(handler.isIncrementalResult(chunk)).toBe(true); + assert(handler.isIncrementalResult(chunk)); expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { @@ -1544,7 +1546,7 @@ describe("graphql-js test cases", () => { const { value: chunk, done } = await incoming.next(); assert(!done); - expect(handler.isIncrementalResult(chunk)).toBe(true); + assert(handler.isIncrementalResult(chunk)); expect(hasIncrementalChunks(chunk)).toBe(true); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { @@ -1604,7 +1606,7 @@ describe("graphql-js test cases", () => { const { value: chunk, done } = await incoming.next(); assert(!done); - expect(handler.isIncrementalResult(chunk)).toBe(true); + assert(handler.isIncrementalResult(chunk)); expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { @@ -1618,7 +1620,7 @@ describe("graphql-js test cases", () => { const { value: chunk, done } = await incoming.next(); assert(!done); - expect(handler.isIncrementalResult(chunk)).toBe(true); + assert(handler.isIncrementalResult(chunk)); expect(hasIncrementalChunks(chunk)).toBe(true); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { @@ -1676,7 +1678,7 @@ describe("graphql-js test cases", () => { const { value: chunk, done } = await incoming.next(); assert(!done); - expect(handler.isIncrementalResult(chunk)).toBe(true); + assert(handler.isIncrementalResult(chunk)); expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: {}, @@ -1688,7 +1690,7 @@ describe("graphql-js test cases", () => { const { value: chunk, done } = await incoming.next(); assert(!done); - expect(handler.isIncrementalResult(chunk)).toBe(true); + assert(handler.isIncrementalResult(chunk)); expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: {}, @@ -1751,7 +1753,7 @@ describe("graphql-js test cases", () => { const { value: chunk, done } = await incoming.next(); assert(!done); - expect(handler.isIncrementalResult(chunk)).toBe(true); + assert(handler.isIncrementalResult(chunk)); expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: {}, @@ -1763,7 +1765,7 @@ describe("graphql-js test cases", () => { const { value: chunk, done } = await incoming.next(); assert(!done); - expect(handler.isIncrementalResult(chunk)).toBe(true); + assert(handler.isIncrementalResult(chunk)); expect(hasIncrementalChunks(chunk)).toBe(true); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { @@ -1836,7 +1838,7 @@ describe("graphql-js test cases", () => { const { value: chunk, done } = await incoming.next(); assert(!done); - expect(handler.isIncrementalResult(chunk)).toBe(true); + assert(handler.isIncrementalResult(chunk)); expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { @@ -1850,7 +1852,7 @@ describe("graphql-js test cases", () => { const { value: chunk, done } = await incoming.next(); assert(!done); - expect(handler.isIncrementalResult(chunk)).toBe(true); + assert(handler.isIncrementalResult(chunk)); expect(hasIncrementalChunks(chunk)).toBe(true); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { @@ -1868,7 +1870,7 @@ describe("graphql-js test cases", () => { const { value: chunk, done } = await incoming.next(); assert(!done); - expect(handler.isIncrementalResult(chunk)).toBe(true); + assert(handler.isIncrementalResult(chunk)); expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { @@ -1924,7 +1926,7 @@ describe("graphql-js test cases", () => { const { value: chunk, done } = await incoming.next(); assert(!done); - expect(handler.isIncrementalResult(chunk)).toBe(true); + assert(handler.isIncrementalResult(chunk)); expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: {}, @@ -1936,7 +1938,7 @@ describe("graphql-js test cases", () => { const { value: chunk, done } = await incoming.next(); assert(!done); - expect(handler.isIncrementalResult(chunk)).toBe(true); + assert(handler.isIncrementalResult(chunk)); expect(hasIncrementalChunks(chunk)).toBe(true); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { @@ -1990,7 +1992,7 @@ describe("graphql-js test cases", () => { const { value: chunk, done } = await incoming.next(); assert(!done); - expect(handler.isIncrementalResult(chunk)).toBe(true); + assert(handler.isIncrementalResult(chunk)); expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { @@ -2006,7 +2008,7 @@ describe("graphql-js test cases", () => { const { value: chunk, done } = await incoming.next(); assert(!done); - expect(handler.isIncrementalResult(chunk)).toBe(true); + assert(handler.isIncrementalResult(chunk)); expect(hasIncrementalChunks(chunk)).toBe(true); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { @@ -2064,7 +2066,7 @@ describe("graphql-js test cases", () => { const { value: chunk, done } = await incoming.next(); assert(!done); - expect(handler.isIncrementalResult(chunk)).toBe(true); + assert(handler.isIncrementalResult(chunk)); expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { @@ -2080,7 +2082,7 @@ describe("graphql-js test cases", () => { const { value: chunk, done } = await incoming.next(); assert(!done); - expect(handler.isIncrementalResult(chunk)).toBe(true); + assert(handler.isIncrementalResult(chunk)); expect(hasIncrementalChunks(chunk)).toBe(true); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { @@ -2126,7 +2128,7 @@ describe("graphql-js test cases", () => { const { value: chunk, done } = await incoming.next(); assert(!done); - expect(handler.isIncrementalResult(chunk)).toBe(true); + assert(handler.isIncrementalResult(chunk)); expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { @@ -2142,7 +2144,7 @@ describe("graphql-js test cases", () => { const { value: chunk, done } = await incoming.next(); assert(!done); - expect(handler.isIncrementalResult(chunk)).toBe(true); + assert(handler.isIncrementalResult(chunk)); expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { @@ -2192,7 +2194,7 @@ describe("graphql-js test cases", () => { const { value: chunk, done } = await incoming.next(); assert(!done); - expect(handler.isIncrementalResult(chunk)).toBe(true); + assert(handler.isIncrementalResult(chunk)); expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { @@ -2208,7 +2210,7 @@ describe("graphql-js test cases", () => { const { value: chunk, done } = await incoming.next(); assert(!done); - expect(handler.isIncrementalResult(chunk)).toBe(true); + assert(handler.isIncrementalResult(chunk)); expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { @@ -2263,7 +2265,7 @@ describe("graphql-js test cases", () => { const { value: chunk, done } = await incoming.next(); assert(!done); - expect(handler.isIncrementalResult(chunk)).toBe(true); + assert(handler.isIncrementalResult(chunk)); expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { @@ -2279,7 +2281,7 @@ describe("graphql-js test cases", () => { const { value: chunk, done } = await incoming.next(); assert(!done); - expect(handler.isIncrementalResult(chunk)).toBe(true); + assert(handler.isIncrementalResult(chunk)); expect(hasIncrementalChunks(chunk)).toBe(true); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { @@ -2313,7 +2315,7 @@ describe("graphql-js test cases", () => { test("GraphQL17Alpha9Handler can be used with `ApolloClient`", async () => { const client = new ApolloClient({ - link: schemaLink, + link: createSchemaLink(), cache: new InMemoryCache(), incrementalHandler: new GraphQL17Alpha9Handler(), }); @@ -2367,7 +2369,8 @@ test("GraphQL17Alpha9Handler can be used with `ApolloClient`", async () => { }); }); -test("merges cache updates that happen concurrently", async () => { +// TODO: Add test helpers for new format +test.failing("merges cache updates that happen concurrently", async () => { const stream = mockDeferStream(); const client = new ApolloClient({ link: stream.httpLink, @@ -2464,7 +2467,7 @@ test("merges cache updates that happen concurrently", async () => { test("returns error on initial result", async () => { const client = new ApolloClient({ - link: schemaLink, + link: createSchemaLink(), cache: new InMemoryCache(), incrementalHandler: new GraphQL17Alpha9Handler(), }); From 11823bd9c7911dd10076d2bdf227245829986d9e Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 4 Sep 2025 13:18:49 -0600 Subject: [PATCH 25/97] Ensure errors are serialized from helper --- src/incremental/handlers/__tests__/graphql17Alpha9.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/incremental/handlers/__tests__/graphql17Alpha9.test.ts b/src/incremental/handlers/__tests__/graphql17Alpha9.test.ts index 5af5cde789a..65e66d1138f 100644 --- a/src/incremental/handlers/__tests__/graphql17Alpha9.test.ts +++ b/src/incremental/handlers/__tests__/graphql17Alpha9.test.ts @@ -203,7 +203,7 @@ async function* run( ) as FormattedSubsequentIncrementalExecutionResult; } } else { - yield result; + yield JSON.parse(JSON.stringify(result)) as FormattedExecutionResult; } } From 40213fa97d650b16daf3e8f33872016012906270 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 4 Sep 2025 13:21:28 -0600 Subject: [PATCH 26/97] Update test to match incremental behavior --- .../__tests__/graphql17Alpha9.test.ts | 57 ++++--------------- 1 file changed, 12 insertions(+), 45 deletions(-) diff --git a/src/incremental/handlers/__tests__/graphql17Alpha9.test.ts b/src/incremental/handlers/__tests__/graphql17Alpha9.test.ts index 65e66d1138f..6dcd5467ec9 100644 --- a/src/incremental/handlers/__tests__/graphql17Alpha9.test.ts +++ b/src/incremental/handlers/__tests__/graphql17Alpha9.test.ts @@ -2467,7 +2467,12 @@ test.failing("merges cache updates that happen concurrently", async () => { test("returns error on initial result", async () => { const client = new ApolloClient({ - link: createSchemaLink(), + link: createSchemaLink({ + hero: { + ...hero, + nonNullName: null, + }, + }), cache: new InMemoryCache(), incrementalHandler: new GraphQL17Alpha9Handler(), }); @@ -2479,7 +2484,7 @@ test("returns error on initial result", async () => { ... @defer { name } - errorField + nonNullName } } `; @@ -2496,58 +2501,20 @@ test("returns error on initial result", async () => { partial: true, }); - await expect(observableStream).toEmitTypedValue({ - loading: true, - data: markAsStreaming({ - hero: { - __typename: "Hero", - id: "1", - errorField: null, - }, - }), - error: new CombinedGraphQLErrors({ - data: { - hero: { - __typename: "Hero", - id: "1", - errorField: null, - }, - }, - errors: [ - { - message: "bad", - path: ["hero", "errorField"], - }, - ], - }), - dataState: "streaming", - networkStatus: NetworkStatus.streaming, - partial: true, - }); - await expect(observableStream).toEmitTypedValue({ loading: false, data: { - hero: { - __typename: "Hero", - id: "1", - errorField: null, - name: "Luke", - }, + hero: null, }, error: new CombinedGraphQLErrors({ data: { - hero: { - __typename: "Hero", - id: "1", - errorField: null, - name: "Luke", - }, + hero: null, }, errors: [ { - message: "bad", - path: ["hero", "errorField"], + message: + "Cannot return null for non-nullable field Hero.nonNullName.", + path: ["hero", "nonNullName"], }, ], }), From 6e6ff31f2fbe58b7438e35a70d0917cd251393fd Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 4 Sep 2025 13:24:16 -0600 Subject: [PATCH 27/97] Update how path is calculated --- src/incremental/handlers/graphql17Alpha9.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/incremental/handlers/graphql17Alpha9.ts b/src/incremental/handlers/graphql17Alpha9.ts index 97500f27f01..91a50582f48 100644 --- a/src/incremental/handlers/graphql17Alpha9.ts +++ b/src/incremental/handlers/graphql17Alpha9.ts @@ -116,7 +116,7 @@ class IncrementalRequest ); let { data } = incremental; - const { path } = pending; + const path = pending.path.concat(incremental.subPath ?? []); for (let i = path.length - 1; i >= 0; i--) { const key = path[i]; From fa0b6878c2a0cb793a7a32ab015154f1b6366f80 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 4 Sep 2025 13:25:59 -0600 Subject: [PATCH 28/97] Temp skip test --- src/incremental/handlers/__tests__/graphql17Alpha9.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/incremental/handlers/__tests__/graphql17Alpha9.test.ts b/src/incremental/handlers/__tests__/graphql17Alpha9.test.ts index 6dcd5467ec9..fcf37cdf4bb 100644 --- a/src/incremental/handlers/__tests__/graphql17Alpha9.test.ts +++ b/src/incremental/handlers/__tests__/graphql17Alpha9.test.ts @@ -2640,7 +2640,8 @@ test("stream that returns an error but continues to stream", async () => { }); }); -test("handles final chunk of { hasNext: false } correctly in usage with Apollo Client", async () => { +// TODO: Update to use test utils with updated types +test.skip("handles final chunk of { hasNext: false } correctly in usage with Apollo Client", async () => { const stream = mockDeferStream(); const client = new ApolloClient({ link: stream.httpLink, From ec9792f9e4ac142816bd2d600f7b695ab16ebe98 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 4 Sep 2025 13:26:58 -0600 Subject: [PATCH 29/97] Move test to /defer.test.ts --- .../{graphql17Alpha9.test.ts => graphql17Alpha9/defer.test.ts} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/incremental/handlers/__tests__/{graphql17Alpha9.test.ts => graphql17Alpha9/defer.test.ts} (99%) diff --git a/src/incremental/handlers/__tests__/graphql17Alpha9.test.ts b/src/incremental/handlers/__tests__/graphql17Alpha9/defer.test.ts similarity index 99% rename from src/incremental/handlers/__tests__/graphql17Alpha9.test.ts rename to src/incremental/handlers/__tests__/graphql17Alpha9/defer.test.ts index fcf37cdf4bb..888ace8cea2 100644 --- a/src/incremental/handlers/__tests__/graphql17Alpha9.test.ts +++ b/src/incremental/handlers/__tests__/graphql17Alpha9/defer.test.ts @@ -36,7 +36,7 @@ import { GraphQL17Alpha9Handler, hasIncrementalChunks, // eslint-disable-next-line local-rules/no-relative-imports -} from "../graphql17Alpha9.js"; +} from "../../graphql17Alpha9.js"; // This is the test setup of the `graphql-js` v17.0.0-alpha.9 release: // https://github.com/graphql/graphql-js/blob/3283f8adf52e77a47f148ff2f30185c8d11ff0f0/src/execution/__tests__/defer-test.ts From 8d61c1d37f9759ceb050944d040d304b356e5475 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 4 Sep 2025 13:39:05 -0600 Subject: [PATCH 30/97] Update extractErrors --- src/incremental/handlers/graphql17Alpha9.ts | 23 ++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/incremental/handlers/graphql17Alpha9.ts b/src/incremental/handlers/graphql17Alpha9.ts index 91a50582f48..5a2df6a0580 100644 --- a/src/incremental/handlers/graphql17Alpha9.ts +++ b/src/incremental/handlers/graphql17Alpha9.ts @@ -192,7 +192,28 @@ export class GraphQL17Alpha9Handler return request; } - extractErrors(result: ApolloLink.Result) {} + extractErrors(result: ApolloLink.Result) { + const acc: GraphQLFormattedError[] = []; + const push = ({ + errors, + }: { + errors?: ReadonlyArray; + }) => { + if (errors) { + acc.push(...errors); + } + }; + + push(result); + + if (this.isIncrementalResult(result)) { + push(new IncrementalRequest().handle(undefined, result)); + } + + if (acc.length) { + return acc; + } + } startRequest(_: { query: DocumentNode }) { return new IncrementalRequest(); From 2610442af45cc0c43d3cb247d416cb7d393b761f Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 4 Sep 2025 13:43:30 -0600 Subject: [PATCH 31/97] Rename merge --- src/incremental/handlers/graphql17Alpha9.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/incremental/handlers/graphql17Alpha9.ts b/src/incremental/handlers/graphql17Alpha9.ts index 5a2df6a0580..5e1e73cbc88 100644 --- a/src/incremental/handlers/graphql17Alpha9.ts +++ b/src/incremental/handlers/graphql17Alpha9.ts @@ -99,7 +99,7 @@ class IncrementalRequest this.pending.push(...chunk.pending); } - this.mergeIn(chunk); + this.merge(chunk); if (hasIncrementalChunks(chunk)) { for (const incremental of chunk.incremental) { @@ -126,7 +126,7 @@ class IncrementalRequest data = parent as typeof data; } - this.mergeIn({ + this.merge({ data: data as TData, extensions: incremental.extensions, errors: incremental.errors, @@ -158,7 +158,7 @@ class IncrementalRequest return result; } - private mergeIn(normalized: FormattedExecutionResult) { + private merge(normalized: FormattedExecutionResult) { if (normalized.data !== undefined) { this.data = this.merger.merge(this.data, normalized.data); } From dbf5b7f80946242acef9a2734363a9c56592b202 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 4 Sep 2025 13:45:17 -0600 Subject: [PATCH 32/97] Make types mirror defer implementation --- src/incremental/handlers/graphql17Alpha9.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/incremental/handlers/graphql17Alpha9.ts b/src/incremental/handlers/graphql17Alpha9.ts index 5e1e73cbc88..c8934c97fab 100644 --- a/src/incremental/handlers/graphql17Alpha9.ts +++ b/src/incremental/handlers/graphql17Alpha9.ts @@ -171,12 +171,14 @@ class IncrementalRequest } } -export class GraphQL17Alpha9Handler +export class GraphQL17Alpha9Handler implements Incremental.Handler> { isIncrementalResult( result: ApolloLink.Result - ): result is GraphQL17Alpha9Handler.Chunk { + ): result is + | GraphQL17Alpha9Handler.InitialResult + | GraphQL17Alpha9Handler.SubsequentResult { return "hasNext" in result; } From 723c163a11106fc31ecdf5ee892f191cdc065493 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 4 Sep 2025 13:46:01 -0600 Subject: [PATCH 33/97] Update exports snapshot --- src/__tests__/__snapshots__/exports.ts.snap | 1 + 1 file changed, 1 insertion(+) diff --git a/src/__tests__/__snapshots__/exports.ts.snap b/src/__tests__/__snapshots__/exports.ts.snap index c7343506bff..6505b8a0721 100644 --- a/src/__tests__/__snapshots__/exports.ts.snap +++ b/src/__tests__/__snapshots__/exports.ts.snap @@ -145,6 +145,7 @@ exports[`exports of public entry points @apollo/client/incremental 1`] = ` Array [ "Defer20220824Handler", "GraphQL17Alpha2Handler", + "GraphQL17Alpha9Handler", "NotImplementedHandler", ] `; From a12de86ff025c34946c5e7fc896cb42499f7c048 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 4 Sep 2025 16:22:27 -0600 Subject: [PATCH 34/97] Split out helper for mocking an incremental stream --- src/testing/internal/incremental/utils.ts | 103 ++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 src/testing/internal/incremental/utils.ts diff --git a/src/testing/internal/incremental/utils.ts b/src/testing/internal/incremental/utils.ts new file mode 100644 index 00000000000..2d6be2627ac --- /dev/null +++ b/src/testing/internal/incremental/utils.ts @@ -0,0 +1,103 @@ +import { + ReadableStream as NodeReadableStream, + TextEncoderStream, + TransformStream, +} from "node:stream/web"; + +import { HttpLink } from "@apollo/client/link/http"; + +const hasNextSymbol = Symbol("hasNext"); + +export function mockIncrementalStream({ + responseHeaders, +}: { + responseHeaders: Headers; +}) { + type Payload = Chunks & { [hasNextSymbol]: boolean }; + const CLOSE = Symbol(); + let streamController: ReadableStreamDefaultController | null = null; + let sentInitialChunk = false; + + const queue: Array = []; + + function processQueue() { + if (!streamController) { + throw new Error("Cannot process queue without stream controller"); + } + + let chunk; + while ((chunk = queue.shift())) { + if (chunk === CLOSE) { + streamController.close(); + } else { + streamController.enqueue(chunk); + } + } + } + + function createStream() { + return new NodeReadableStream({ + start(c) { + streamController = c; + processQueue(); + }, + }) + .pipeThrough( + new TransformStream({ + transform: (chunk, controller) => { + controller.enqueue( + (!sentInitialChunk ? "\r\n---\r\n" : "") + + "content-type: application/json; charset=utf-8\r\n\r\n" + + JSON.stringify(chunk) + + (chunk[hasNextSymbol] ? "\r\n---\r\n" : "\r\n-----\r\n") + ); + sentInitialChunk = true; + }, + }) + ) + .pipeThrough(new TextEncoderStream()); + } + + const httpLink = new HttpLink({ + fetch(input, init) { + return Promise.resolve( + new Response( + createStream() satisfies NodeReadableStream as ReadableStream, + { + status: 200, + headers: responseHeaders, + } + ) + ); + }, + }); + + function queueNext(event: Payload | typeof CLOSE) { + queue.push(event); + + if (streamController) { + processQueue(); + } + } + + function close() { + queueNext(CLOSE); + + streamController = null; + sentInitialChunk = false; + } + + function enqueue(chunk: Chunks, hasNext: boolean) { + queueNext({ ...chunk, [hasNextSymbol]: hasNext }); + + if (!hasNext) { + close(); + } + } + + return { + httpLink, + enqueue, + close, + }; +} From df5b353058011163a56e6635d99988d3af31e512 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 4 Sep 2025 16:23:43 -0600 Subject: [PATCH 35/97] Rename helper --- src/testing/internal/incremental/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/testing/internal/incremental/utils.ts b/src/testing/internal/incremental/utils.ts index 2d6be2627ac..70b9bae52d2 100644 --- a/src/testing/internal/incremental/utils.ts +++ b/src/testing/internal/incremental/utils.ts @@ -8,7 +8,7 @@ import { HttpLink } from "@apollo/client/link/http"; const hasNextSymbol = Symbol("hasNext"); -export function mockIncrementalStream({ +export function mockMultipartStream({ responseHeaders, }: { responseHeaders: Headers; From f4a4a9f40c5d3a93b2baf766c2e48bbff71c4ad9 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 4 Sep 2025 16:24:59 -0600 Subject: [PATCH 36/97] Use updated helper in incremental utils --- src/testing/internal/incremental.ts | 107 +--------------------------- 1 file changed, 3 insertions(+), 104 deletions(-) diff --git a/src/testing/internal/incremental.ts b/src/testing/internal/incremental.ts index a457b2189ff..8e88299028c 100644 --- a/src/testing/internal/incremental.ts +++ b/src/testing/internal/incremental.ts @@ -1,9 +1,3 @@ -import { - ReadableStream as NodeReadableStream, - TextEncoderStream, - TransformStream, -} from "node:stream/web"; - import type { FormattedInitialIncrementalExecutionResult, FormattedSubsequentIncrementalExecutionResult, @@ -11,109 +5,14 @@ import type { } from "graphql-17-alpha2"; import type { ApolloPayloadResult } from "@apollo/client"; -import { HttpLink } from "@apollo/client/link/http"; - -const hasNextSymbol = Symbol("hasNext"); - -function mockIncrementalStream({ - responseHeaders, -}: { - responseHeaders: Headers; -}) { - type Payload = Chunks & { [hasNextSymbol]: boolean }; - const CLOSE = Symbol(); - let streamController: ReadableStreamDefaultController | null = null; - let sentInitialChunk = false; - - const queue: Array = []; - - function processQueue() { - if (!streamController) { - throw new Error("Cannot process queue without stream controller"); - } - - let chunk; - while ((chunk = queue.shift())) { - if (chunk === CLOSE) { - streamController.close(); - } else { - streamController.enqueue(chunk); - } - } - } - - function createStream() { - return new NodeReadableStream({ - start(c) { - streamController = c; - processQueue(); - }, - }) - .pipeThrough( - new TransformStream({ - transform: (chunk, controller) => { - controller.enqueue( - (!sentInitialChunk ? "\r\n---\r\n" : "") + - "content-type: application/json; charset=utf-8\r\n\r\n" + - JSON.stringify(chunk) + - (chunk[hasNextSymbol] ? "\r\n---\r\n" : "\r\n-----\r\n") - ); - sentInitialChunk = true; - }, - }) - ) - .pipeThrough(new TextEncoderStream()); - } - - const httpLink = new HttpLink({ - fetch(input, init) { - return Promise.resolve( - new Response( - createStream() satisfies NodeReadableStream as ReadableStream, - { - status: 200, - headers: responseHeaders, - } - ) - ); - }, - }); - function queueNext(event: Payload | typeof CLOSE) { - queue.push(event); - - if (streamController) { - processQueue(); - } - } - - function close() { - queueNext(CLOSE); - - streamController = null; - sentInitialChunk = false; - } - - function enqueue(chunk: Chunks, hasNext: boolean) { - queueNext({ ...chunk, [hasNextSymbol]: hasNext }); - - if (!hasNext) { - close(); - } - } - - return { - httpLink, - enqueue, - close, - }; -} +import { mockMultipartStream } from "./incremental/utils.js"; export function mockDeferStream< TData = Record, TExtensions = Record, >() { - const { httpLink, enqueue } = mockIncrementalStream< + const { httpLink, enqueue } = mockMultipartStream< | FormattedInitialIncrementalExecutionResult | FormattedSubsequentIncrementalExecutionResult >({ @@ -153,7 +52,7 @@ export function mockMultipartSubscriptionStream< TData = Record, TExtensions = Record, >() { - const { httpLink, enqueue } = mockIncrementalStream< + const { httpLink, enqueue } = mockMultipartStream< ApolloPayloadResult >({ responseHeaders: new Headers({ From ba1f15e1dc082dd45b844143e2445583059d3bb3 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 4 Sep 2025 16:25:48 -0600 Subject: [PATCH 37/97] Move mockDeferStream to own file --- src/testing/internal/incremental.ts | 48 ++----------------- .../internal/incremental/defer20220824.ts | 47 ++++++++++++++++++ 2 files changed, 50 insertions(+), 45 deletions(-) create mode 100644 src/testing/internal/incremental/defer20220824.ts diff --git a/src/testing/internal/incremental.ts b/src/testing/internal/incremental.ts index 8e88299028c..d2303c2ab3b 100644 --- a/src/testing/internal/incremental.ts +++ b/src/testing/internal/incremental.ts @@ -1,52 +1,10 @@ -import type { - FormattedInitialIncrementalExecutionResult, - FormattedSubsequentIncrementalExecutionResult, - GraphQLFormattedError, -} from "graphql-17-alpha2"; - import type { ApolloPayloadResult } from "@apollo/client"; +import { mockDefer20220824 } from "./incremental/defer20220824.js"; import { mockMultipartStream } from "./incremental/utils.js"; -export function mockDeferStream< - TData = Record, - TExtensions = Record, ->() { - const { httpLink, enqueue } = mockMultipartStream< - | FormattedInitialIncrementalExecutionResult - | FormattedSubsequentIncrementalExecutionResult - >({ - responseHeaders: new Headers({ - "Content-Type": 'multipart/mixed; boundary="-"; deferSpec=20220824', - }), - }); - return { - httpLink, - enqueueInitialChunk( - chunk: FormattedInitialIncrementalExecutionResult - ) { - enqueue(chunk, chunk.hasNext); - }, - enqueueSubsequentChunk( - chunk: FormattedSubsequentIncrementalExecutionResult - ) { - enqueue(chunk, chunk.hasNext); - }, - enqueueErrorChunk(errors: GraphQLFormattedError[]) { - enqueue( - { - hasNext: true, - incremental: [ - { - errors, - }, - ], - }, - true - ); - }, - }; -} +// TODO: Update to new name +export { mockDefer20220824 as mockDeferStream }; export function mockMultipartSubscriptionStream< TData = Record, diff --git a/src/testing/internal/incremental/defer20220824.ts b/src/testing/internal/incremental/defer20220824.ts new file mode 100644 index 00000000000..67afe6636d7 --- /dev/null +++ b/src/testing/internal/incremental/defer20220824.ts @@ -0,0 +1,47 @@ +import type { + FormattedInitialIncrementalExecutionResult, + FormattedSubsequentIncrementalExecutionResult, + GraphQLFormattedError, +} from "graphql-17-alpha2"; + +import { mockMultipartStream } from "./utils.js"; + +export function mockDefer20220824< + TData = Record, + TExtensions = Record, +>() { + const { httpLink, enqueue } = mockMultipartStream< + | FormattedInitialIncrementalExecutionResult + | FormattedSubsequentIncrementalExecutionResult + >({ + responseHeaders: new Headers({ + "Content-Type": 'multipart/mixed; boundary="-"; deferSpec=20220824', + }), + }); + return { + httpLink, + enqueueInitialChunk( + chunk: FormattedInitialIncrementalExecutionResult + ) { + enqueue(chunk, chunk.hasNext); + }, + enqueueSubsequentChunk( + chunk: FormattedSubsequentIncrementalExecutionResult + ) { + enqueue(chunk, chunk.hasNext); + }, + enqueueErrorChunk(errors: GraphQLFormattedError[]) { + enqueue( + { + hasNext: true, + incremental: [ + { + errors, + }, + ], + }, + true + ); + }, + }; +} From 12e8ddbb4a1da25dda7b03b7c34d42847658bd2d Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 4 Sep 2025 16:28:04 -0600 Subject: [PATCH 38/97] Rename mockDeferStream to mockDefer20220824 everywhere --- src/__tests__/__snapshots__/exports.ts.snap | 2 +- src/__tests__/fetchMore.ts | 4 ++-- src/core/__tests__/ApolloClient/general.test.ts | 4 ++-- src/incremental/handlers/__tests__/defer20220824.test.ts | 6 +++--- .../handlers/__tests__/graphql17Alpha9/defer.test.ts | 6 +++--- src/link/error/__tests__/index.ts | 4 ++-- src/testing/internal/incremental.ts | 4 +--- src/testing/internal/index.ts | 2 +- 8 files changed, 15 insertions(+), 17 deletions(-) diff --git a/src/__tests__/__snapshots__/exports.ts.snap b/src/__tests__/__snapshots__/exports.ts.snap index 6505b8a0721..0e3fd3552c3 100644 --- a/src/__tests__/__snapshots__/exports.ts.snap +++ b/src/__tests__/__snapshots__/exports.ts.snap @@ -363,7 +363,7 @@ Array [ "enableFakeTimers", "executeWithDefaultContext", "markAsStreaming", - "mockDeferStream", + "mockDefer20220824", "mockMultipartSubscriptionStream", "renderAsync", "renderHookAsync", diff --git a/src/__tests__/fetchMore.ts b/src/__tests__/fetchMore.ts index 73fdf2af688..7c863397dc1 100644 --- a/src/__tests__/fetchMore.ts +++ b/src/__tests__/fetchMore.ts @@ -19,7 +19,7 @@ import { Defer20220824Handler } from "@apollo/client/incremental"; import { MockLink, MockSubscriptionLink } from "@apollo/client/testing"; import { markAsStreaming, - mockDeferStream, + mockDefer20220824, ObservableStream, setupPaginatedCase, } from "@apollo/client/testing/internal"; @@ -2478,7 +2478,7 @@ test("uses updateQuery to update the result of the query with no-cache queries", }); test("calling `fetchMore` on an ObservableQuery that hasn't finished deferring yet will not put it into completed state", async () => { - const defer = mockDeferStream(); + const defer = mockDefer20220824(); const baseLink = new MockSubscriptionLink(); const client = new ApolloClient({ diff --git a/src/core/__tests__/ApolloClient/general.test.ts b/src/core/__tests__/ApolloClient/general.test.ts index eccc8b5245d..49ecefaa437 100644 --- a/src/core/__tests__/ApolloClient/general.test.ts +++ b/src/core/__tests__/ApolloClient/general.test.ts @@ -14,7 +14,7 @@ import { ApolloLink } from "@apollo/client/link"; import { ClientAwarenessLink } from "@apollo/client/link/client-awareness"; import { MockLink } from "@apollo/client/testing"; import { - mockDeferStream, + mockDefer20220824, ObservableStream, spyOnConsole, wait, @@ -7567,7 +7567,7 @@ describe("ApolloClient", () => { const outgoingRequestSpy = jest.fn(((operation, forward) => forward(operation)) satisfies ApolloLink.RequestHandler); - const defer = mockDeferStream(); + const defer = mockDefer20220824(); const client = new ApolloClient({ cache: new InMemoryCache({}), link: new ApolloLink(outgoingRequestSpy).concat(defer.httpLink), diff --git a/src/incremental/handlers/__tests__/defer20220824.test.ts b/src/incremental/handlers/__tests__/defer20220824.test.ts index f5795710d6b..e412199e2a6 100644 --- a/src/incremental/handlers/__tests__/defer20220824.test.ts +++ b/src/incremental/handlers/__tests__/defer20220824.test.ts @@ -28,7 +28,7 @@ import { import { Defer20220824Handler } from "@apollo/client/incremental"; import { markAsStreaming, - mockDeferStream, + mockDefer20220824, ObservableStream, } from "@apollo/client/testing/internal"; @@ -683,7 +683,7 @@ test("Defer20220824Handler can be used with `ApolloClient`", async () => { }); test("merges cache updates that happen concurrently", async () => { - const stream = mockDeferStream(); + const stream = mockDefer20220824(); const client = new ApolloClient({ link: stream.httpLink, cache: new InMemoryCache(), @@ -979,7 +979,7 @@ test("stream that returns an error but continues to stream", async () => { }); test("handles final chunk of { hasNext: false } correctly in usage with Apollo Client", async () => { - const stream = mockDeferStream(); + const stream = mockDefer20220824(); const client = new ApolloClient({ link: stream.httpLink, cache: new InMemoryCache(), diff --git a/src/incremental/handlers/__tests__/graphql17Alpha9/defer.test.ts b/src/incremental/handlers/__tests__/graphql17Alpha9/defer.test.ts index 888ace8cea2..fdf145ed68e 100644 --- a/src/incremental/handlers/__tests__/graphql17Alpha9/defer.test.ts +++ b/src/incremental/handlers/__tests__/graphql17Alpha9/defer.test.ts @@ -27,7 +27,7 @@ import { } from "@apollo/client"; import { markAsStreaming, - mockDeferStream, + mockDefer20220824, ObservableStream, wait, } from "@apollo/client/testing/internal"; @@ -2371,7 +2371,7 @@ test("GraphQL17Alpha9Handler can be used with `ApolloClient`", async () => { // TODO: Add test helpers for new format test.failing("merges cache updates that happen concurrently", async () => { - const stream = mockDeferStream(); + const stream = mockDefer20220824(); const client = new ApolloClient({ link: stream.httpLink, cache: new InMemoryCache(), @@ -2642,7 +2642,7 @@ test("stream that returns an error but continues to stream", async () => { // TODO: Update to use test utils with updated types test.skip("handles final chunk of { hasNext: false } correctly in usage with Apollo Client", async () => { - const stream = mockDeferStream(); + const stream = mockDefer20220824(); const client = new ApolloClient({ link: stream.httpLink, cache: new InMemoryCache(), diff --git a/src/link/error/__tests__/index.ts b/src/link/error/__tests__/index.ts index 50e814e811e..92928c77746 100644 --- a/src/link/error/__tests__/index.ts +++ b/src/link/error/__tests__/index.ts @@ -13,7 +13,7 @@ import { ApolloLink } from "@apollo/client/link"; import { ErrorLink } from "@apollo/client/link/error"; import { executeWithDefaultContext as execute, - mockDeferStream, + mockDefer20220824, mockMultipartSubscriptionStream, ObservableStream, wait, @@ -214,7 +214,7 @@ describe("error handling", () => { const errorLink = new ErrorLink(callback); const { httpLink, enqueueInitialChunk, enqueueErrorChunk } = - mockDeferStream(); + mockDefer20220824(); const link = errorLink.concat(httpLink); const stream = new ObservableStream(execute(link, { query })); diff --git a/src/testing/internal/incremental.ts b/src/testing/internal/incremental.ts index d2303c2ab3b..6d6cc2c45a7 100644 --- a/src/testing/internal/incremental.ts +++ b/src/testing/internal/incremental.ts @@ -1,10 +1,8 @@ import type { ApolloPayloadResult } from "@apollo/client"; -import { mockDefer20220824 } from "./incremental/defer20220824.js"; import { mockMultipartStream } from "./incremental/utils.js"; -// TODO: Update to new name -export { mockDefer20220824 as mockDeferStream }; +export { mockDefer20220824 } from "./incremental/defer20220824.js"; export function mockMultipartSubscriptionStream< TData = Record, diff --git a/src/testing/internal/index.ts b/src/testing/internal/index.ts index 1ebe8234c9c..b7fb5594279 100644 --- a/src/testing/internal/index.ts +++ b/src/testing/internal/index.ts @@ -26,7 +26,7 @@ export { actAsync } from "./rtl/actAsync.js"; export { renderAsync } from "./rtl/renderAsync.js"; export { renderHookAsync } from "./rtl/renderHookAsync.js"; export { - mockDeferStream, + mockDefer20220824, mockMultipartSubscriptionStream, } from "./incremental.js"; export { resetApolloContext } from "./resetApolloContext.js"; From 3ed6eb46068a43b183f523279bfdb4ecba62148f Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 4 Sep 2025 16:29:45 -0600 Subject: [PATCH 39/97] Rename file --- src/testing/internal/incremental.ts | 2 +- .../incremental/{defer20220824.ts => mockDefer20220824.ts} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename src/testing/internal/incremental/{defer20220824.ts => mockDefer20220824.ts} (100%) diff --git a/src/testing/internal/incremental.ts b/src/testing/internal/incremental.ts index 6d6cc2c45a7..0ce2389b5ae 100644 --- a/src/testing/internal/incremental.ts +++ b/src/testing/internal/incremental.ts @@ -2,7 +2,7 @@ import type { ApolloPayloadResult } from "@apollo/client"; import { mockMultipartStream } from "./incremental/utils.js"; -export { mockDefer20220824 } from "./incremental/defer20220824.js"; +export { mockDefer20220824 } from "./incremental/mockDefer20220824.js"; export function mockMultipartSubscriptionStream< TData = Record, diff --git a/src/testing/internal/incremental/defer20220824.ts b/src/testing/internal/incremental/mockDefer20220824.ts similarity index 100% rename from src/testing/internal/incremental/defer20220824.ts rename to src/testing/internal/incremental/mockDefer20220824.ts From b35c6e06087551bf1d691b3eb0f5e77fe391cf09 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 4 Sep 2025 16:33:41 -0600 Subject: [PATCH 40/97] Add helper to mock newer defer implementation --- src/testing/internal/incremental.ts | 1 + .../mockDeferStreamGraphql17Alpha9.ts | 33 +++++++++++++++++++ src/testing/internal/index.ts | 1 + 3 files changed, 35 insertions(+) create mode 100644 src/testing/internal/incremental/mockDeferStreamGraphql17Alpha9.ts diff --git a/src/testing/internal/incremental.ts b/src/testing/internal/incremental.ts index 0ce2389b5ae..e921f060ff3 100644 --- a/src/testing/internal/incremental.ts +++ b/src/testing/internal/incremental.ts @@ -3,6 +3,7 @@ import type { ApolloPayloadResult } from "@apollo/client"; import { mockMultipartStream } from "./incremental/utils.js"; export { mockDefer20220824 } from "./incremental/mockDefer20220824.js"; +export { mockDeferStreamGraphQL17Alpha9 } from "./incremental/mockDeferStreamGraphql17Alpha9.js"; export function mockMultipartSubscriptionStream< TData = Record, diff --git a/src/testing/internal/incremental/mockDeferStreamGraphql17Alpha9.ts b/src/testing/internal/incremental/mockDeferStreamGraphql17Alpha9.ts new file mode 100644 index 00000000000..9532b1b57eb --- /dev/null +++ b/src/testing/internal/incremental/mockDeferStreamGraphql17Alpha9.ts @@ -0,0 +1,33 @@ +import type { + FormattedInitialIncrementalExecutionResult, + FormattedSubsequentIncrementalExecutionResult, +} from "graphql-17-alpha9"; + +import { mockMultipartStream } from "./utils.js"; + +export function mockDeferStreamGraphQL17Alpha9< + TData = Record, + TExtensions = Record, +>() { + const { httpLink, enqueue } = mockMultipartStream< + | FormattedInitialIncrementalExecutionResult + | FormattedSubsequentIncrementalExecutionResult + >({ + responseHeaders: new Headers({ + "Content-Type": 'multipart/mixed; boundary="-"', + }), + }); + return { + httpLink, + enqueueInitialChunk( + chunk: FormattedInitialIncrementalExecutionResult + ) { + enqueue(chunk, chunk.hasNext); + }, + enqueueSubsequentChunk( + chunk: FormattedSubsequentIncrementalExecutionResult + ) { + enqueue(chunk, chunk.hasNext); + }, + }; +} diff --git a/src/testing/internal/index.ts b/src/testing/internal/index.ts index b7fb5594279..9d2905875e0 100644 --- a/src/testing/internal/index.ts +++ b/src/testing/internal/index.ts @@ -27,6 +27,7 @@ export { renderAsync } from "./rtl/renderAsync.js"; export { renderHookAsync } from "./rtl/renderHookAsync.js"; export { mockDefer20220824, + mockDeferStreamGraphQL17Alpha9, mockMultipartSubscriptionStream, } from "./incremental.js"; export { resetApolloContext } from "./resetApolloContext.js"; From 260d2962383d7d3a0adcd44e2f0c0b45debbe6e5 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 4 Sep 2025 16:35:28 -0600 Subject: [PATCH 41/97] Rename folder to multipart --- .../mockDefer20220824.ts | 0 .../mockDeferStreamGraphql17Alpha9.ts | 0 .../mockMultipartSubscriptionStream.ts | 36 +++++++++++++++++++ .../{incremental => multipart}/utils.ts | 0 4 files changed, 36 insertions(+) rename src/testing/internal/{incremental => multipart}/mockDefer20220824.ts (100%) rename src/testing/internal/{incremental => multipart}/mockDeferStreamGraphql17Alpha9.ts (100%) create mode 100644 src/testing/internal/multipart/mockMultipartSubscriptionStream.ts rename src/testing/internal/{incremental => multipart}/utils.ts (100%) diff --git a/src/testing/internal/incremental/mockDefer20220824.ts b/src/testing/internal/multipart/mockDefer20220824.ts similarity index 100% rename from src/testing/internal/incremental/mockDefer20220824.ts rename to src/testing/internal/multipart/mockDefer20220824.ts diff --git a/src/testing/internal/incremental/mockDeferStreamGraphql17Alpha9.ts b/src/testing/internal/multipart/mockDeferStreamGraphql17Alpha9.ts similarity index 100% rename from src/testing/internal/incremental/mockDeferStreamGraphql17Alpha9.ts rename to src/testing/internal/multipart/mockDeferStreamGraphql17Alpha9.ts diff --git a/src/testing/internal/multipart/mockMultipartSubscriptionStream.ts b/src/testing/internal/multipart/mockMultipartSubscriptionStream.ts new file mode 100644 index 00000000000..73e29c1a9cc --- /dev/null +++ b/src/testing/internal/multipart/mockMultipartSubscriptionStream.ts @@ -0,0 +1,36 @@ +import type { ApolloPayloadResult } from "@apollo/client"; + +import { mockMultipartStream } from "./utils.js"; + +export function mockMultipartSubscriptionStream< + TData = Record, + TExtensions = Record, +>() { + const { httpLink, enqueue } = mockMultipartStream< + ApolloPayloadResult + >({ + responseHeaders: new Headers({ + "Content-Type": "multipart/mixed", + }), + }); + + enqueueHeartbeat(); + + function enqueueHeartbeat() { + enqueue({} as any, true); + } + + return { + httpLink, + enqueueHeartbeat, + enqueuePayloadResult( + payload: ApolloPayloadResult["payload"], + hasNext = true + ) { + enqueue({ payload }, hasNext); + }, + enqueueProtocolErrors(errors: ApolloPayloadResult["errors"]) { + enqueue({ payload: null, errors }, false); + }, + }; +} diff --git a/src/testing/internal/incremental/utils.ts b/src/testing/internal/multipart/utils.ts similarity index 100% rename from src/testing/internal/incremental/utils.ts rename to src/testing/internal/multipart/utils.ts From dc0d1fdc86f86572d30ad5149cff49487db65443 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 4 Sep 2025 16:35:37 -0600 Subject: [PATCH 42/97] Move mulipart subscription mock to own file --- src/testing/internal/incremental.ts | 42 +++-------------------------- 1 file changed, 3 insertions(+), 39 deletions(-) diff --git a/src/testing/internal/incremental.ts b/src/testing/internal/incremental.ts index e921f060ff3..d5371143ab2 100644 --- a/src/testing/internal/incremental.ts +++ b/src/testing/internal/incremental.ts @@ -1,39 +1,3 @@ -import type { ApolloPayloadResult } from "@apollo/client"; - -import { mockMultipartStream } from "./incremental/utils.js"; - -export { mockDefer20220824 } from "./incremental/mockDefer20220824.js"; -export { mockDeferStreamGraphQL17Alpha9 } from "./incremental/mockDeferStreamGraphql17Alpha9.js"; - -export function mockMultipartSubscriptionStream< - TData = Record, - TExtensions = Record, ->() { - const { httpLink, enqueue } = mockMultipartStream< - ApolloPayloadResult - >({ - responseHeaders: new Headers({ - "Content-Type": "multipart/mixed", - }), - }); - - enqueueHeartbeat(); - - function enqueueHeartbeat() { - enqueue({} as any, true); - } - - return { - httpLink, - enqueueHeartbeat, - enqueuePayloadResult( - payload: ApolloPayloadResult["payload"], - hasNext = true - ) { - enqueue({ payload }, hasNext); - }, - enqueueProtocolErrors(errors: ApolloPayloadResult["errors"]) { - enqueue({ payload: null, errors }, false); - }, - }; -} +export { mockDefer20220824 } from "./multipart/mockDefer20220824.js"; +export { mockDeferStreamGraphQL17Alpha9 } from "./multipart/mockDeferStreamGraphql17Alpha9.js"; +export { mockMultipartSubscriptionStream } from "./multipart/mockMultipartSubscriptionStream.js"; From 7b51170eb9b1d41649c974947ed81a951120573f Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 4 Sep 2025 16:36:33 -0600 Subject: [PATCH 43/97] Import directly to avoid another barrel file --- src/testing/internal/incremental.ts | 3 --- src/testing/internal/index.ts | 8 +++----- 2 files changed, 3 insertions(+), 8 deletions(-) delete mode 100644 src/testing/internal/incremental.ts diff --git a/src/testing/internal/incremental.ts b/src/testing/internal/incremental.ts deleted file mode 100644 index d5371143ab2..00000000000 --- a/src/testing/internal/incremental.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { mockDefer20220824 } from "./multipart/mockDefer20220824.js"; -export { mockDeferStreamGraphQL17Alpha9 } from "./multipart/mockDeferStreamGraphql17Alpha9.js"; -export { mockMultipartSubscriptionStream } from "./multipart/mockMultipartSubscriptionStream.js"; diff --git a/src/testing/internal/index.ts b/src/testing/internal/index.ts index 9d2905875e0..37fad789108 100644 --- a/src/testing/internal/index.ts +++ b/src/testing/internal/index.ts @@ -25,11 +25,9 @@ export { createClientWrapper, createMockWrapper } from "./renderHelpers.js"; export { actAsync } from "./rtl/actAsync.js"; export { renderAsync } from "./rtl/renderAsync.js"; export { renderHookAsync } from "./rtl/renderHookAsync.js"; -export { - mockDefer20220824, - mockDeferStreamGraphQL17Alpha9, - mockMultipartSubscriptionStream, -} from "./incremental.js"; +export { mockDefer20220824 } from "./multipart/mockDefer20220824.js"; +export { mockDeferStreamGraphQL17Alpha9 } from "./multipart/mockDeferStreamGraphql17Alpha9.js"; +export { mockMultipartSubscriptionStream } from "./multipart/mockMultipartSubscriptionStream.js"; export { resetApolloContext } from "./resetApolloContext.js"; export { createOperationWithDefaultContext, From 005aa272432304354a73cbf17c396ab6e5405657 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 4 Sep 2025 16:49:39 -0600 Subject: [PATCH 44/97] Format test files with typescript parser --- .prettierrc | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.prettierrc b/.prettierrc index 8a0e9b37b39..5e21b9169ee 100644 --- a/.prettierrc +++ b/.prettierrc @@ -17,6 +17,12 @@ "parser": "typescript-with-jsdoc" } }, + { + "files": ["**/__tests__/**/*.ts", "**/__tests__/**/*.tsx"], + "options": { + "parser": "typescript" + } + }, { "files": ["*.mdx"], "options": { From 4e36d7ce411d7ac3261ca5275015944a0365651b Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 4 Sep 2025 16:49:59 -0600 Subject: [PATCH 45/97] Update exports snapshot --- src/__tests__/__snapshots__/exports.ts.snap | 1 + 1 file changed, 1 insertion(+) diff --git a/src/__tests__/__snapshots__/exports.ts.snap b/src/__tests__/__snapshots__/exports.ts.snap index 0e3fd3552c3..9207e712cdd 100644 --- a/src/__tests__/__snapshots__/exports.ts.snap +++ b/src/__tests__/__snapshots__/exports.ts.snap @@ -364,6 +364,7 @@ Array [ "executeWithDefaultContext", "markAsStreaming", "mockDefer20220824", + "mockDeferStreamGraphQL17Alpha9", "mockMultipartSubscriptionStream", "renderAsync", "renderHookAsync", From 8b9b3141090636b29e91265a4677b495b79e3ab7 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 4 Sep 2025 16:50:15 -0600 Subject: [PATCH 46/97] Move useQuery defer tests to own file --- src/react/hooks/__tests__/useQuery.test.tsx | 1222 ---------------- .../__tests__/useQuery/defer20220824.test.tsx | 1234 +++++++++++++++++ 2 files changed, 1234 insertions(+), 1222 deletions(-) create mode 100644 src/react/hooks/__tests__/useQuery/defer20220824.test.tsx diff --git a/src/react/hooks/__tests__/useQuery.test.tsx b/src/react/hooks/__tests__/useQuery.test.tsx index 2d544bb11ac..5845bac6001 100644 --- a/src/react/hooks/__tests__/useQuery.test.tsx +++ b/src/react/hooks/__tests__/useQuery.test.tsx @@ -34,7 +34,6 @@ import { NetworkStatus, } from "@apollo/client"; import { InMemoryCache } from "@apollo/client/cache"; -import { Defer20220824Handler } from "@apollo/client/incremental"; import { ApolloLink } from "@apollo/client/link"; import { LocalState } from "@apollo/client/local-state"; import type { Unmasked } from "@apollo/client/masking"; @@ -53,7 +52,6 @@ import type { } from "@apollo/client/testing/internal"; import { enableFakeTimers, - markAsStreaming, setupPaginatedCase, setupSimpleCase, setupVariablesCase, @@ -10191,1226 +10189,6 @@ describe("useQuery Hook", () => { }); }); - describe("defer", () => { - it("should handle deferred queries", async () => { - const query = gql` - { - greeting { - message - ... on Greeting @defer { - recipient { - name - } - } - } - } - `; - - const link = new MockSubscriptionLink(); - - const client = new ApolloClient({ - link, - cache: new InMemoryCache(), - incrementalHandler: new Defer20220824Handler(), - }); - - using _disabledAct = disableActEnvironment(); - const { takeSnapshot } = await renderHookToSnapshotStream( - () => useQuery(query), - { - wrapper: ({ children }) => ( - {children} - ), - } - ); - - await expect(takeSnapshot()).resolves.toStrictEqualTyped({ - data: undefined, - dataState: "empty", - loading: true, - networkStatus: NetworkStatus.loading, - previousData: undefined, - variables: {}, - }); - - setTimeout(() => { - link.simulateResult({ - result: { - data: { - greeting: { - message: "Hello world", - __typename: "Greeting", - }, - }, - hasNext: true, - }, - }); - }); - - await expect(takeSnapshot()).resolves.toStrictEqualTyped({ - data: markAsStreaming({ - greeting: { - message: "Hello world", - __typename: "Greeting", - }, - }), - dataState: "streaming", - loading: true, - networkStatus: NetworkStatus.streaming, - previousData: undefined, - variables: {}, - }); - - setTimeout(() => { - link.simulateResult( - { - result: { - incremental: [ - { - data: { - recipient: { - name: "Alice", - __typename: "Person", - }, - __typename: "Greeting", - }, - path: ["greeting"], - }, - ], - hasNext: false, - }, - }, - true - ); - }); - - await expect(takeSnapshot()).resolves.toStrictEqualTyped({ - data: { - greeting: { - message: "Hello world", - __typename: "Greeting", - recipient: { - name: "Alice", - __typename: "Person", - }, - }, - }, - dataState: "complete", - loading: false, - networkStatus: NetworkStatus.ready, - previousData: { - greeting: { - message: "Hello world", - __typename: "Greeting", - }, - }, - variables: {}, - }); - - await expect(takeSnapshot).not.toRerender(); - }); - - it("should handle deferred queries in lists", async () => { - const query = gql` - { - greetings { - message - ... on Greeting @defer { - recipient { - name - } - } - } - } - `; - - const link = new MockSubscriptionLink(); - - const client = new ApolloClient({ - link, - cache: new InMemoryCache(), - incrementalHandler: new Defer20220824Handler(), - }); - - using _disabledAct = disableActEnvironment(); - const { takeSnapshot } = await renderHookToSnapshotStream( - () => useQuery(query), - { - wrapper: ({ children }) => ( - {children} - ), - } - ); - - await expect(takeSnapshot()).resolves.toStrictEqualTyped({ - data: undefined, - dataState: "empty", - loading: true, - networkStatus: NetworkStatus.loading, - previousData: undefined, - variables: {}, - }); - - setTimeout(() => { - link.simulateResult({ - result: { - data: { - greetings: [ - { message: "Hello world", __typename: "Greeting" }, - { message: "Hello again", __typename: "Greeting" }, - ], - }, - hasNext: true, - }, - }); - }); - - await expect(takeSnapshot()).resolves.toStrictEqualTyped({ - data: markAsStreaming({ - greetings: [ - { message: "Hello world", __typename: "Greeting" }, - { message: "Hello again", __typename: "Greeting" }, - ], - }), - dataState: "streaming", - loading: true, - networkStatus: NetworkStatus.streaming, - previousData: undefined, - variables: {}, - }); - - setTimeout(() => { - link.simulateResult({ - result: { - incremental: [ - { - data: { - recipient: { - name: "Alice", - __typename: "Person", - }, - __typename: "Greeting", - }, - path: ["greetings", 0], - }, - ], - hasNext: true, - }, - }); - }); - - await expect(takeSnapshot()).resolves.toStrictEqualTyped({ - data: markAsStreaming({ - greetings: [ - { - message: "Hello world", - __typename: "Greeting", - recipient: { name: "Alice", __typename: "Person" }, - }, - { message: "Hello again", __typename: "Greeting" }, - ], - }), - dataState: "streaming", - loading: true, - networkStatus: NetworkStatus.streaming, - previousData: { - greetings: [ - { message: "Hello world", __typename: "Greeting" }, - { message: "Hello again", __typename: "Greeting" }, - ], - }, - variables: {}, - }); - - setTimeout(() => { - link.simulateResult( - { - result: { - incremental: [ - { - data: { - recipient: { - name: "Bob", - __typename: "Person", - }, - __typename: "Greeting", - }, - path: ["greetings", 1], - }, - ], - hasNext: false, - }, - }, - true - ); - }); - - await expect(takeSnapshot()).resolves.toStrictEqualTyped({ - data: { - greetings: [ - { - message: "Hello world", - __typename: "Greeting", - recipient: { name: "Alice", __typename: "Person" }, - }, - { - message: "Hello again", - __typename: "Greeting", - recipient: { name: "Bob", __typename: "Person" }, - }, - ], - }, - dataState: "complete", - loading: false, - networkStatus: NetworkStatus.ready, - previousData: { - greetings: [ - { - message: "Hello world", - __typename: "Greeting", - recipient: { name: "Alice", __typename: "Person" }, - }, - { message: "Hello again", __typename: "Greeting" }, - ], - }, - variables: {}, - }); - - await expect(takeSnapshot).not.toRerender(); - }); - - it("should handle deferred queries in lists, merging arrays", async () => { - const query = gql` - query DeferVariation { - allProducts { - delivery { - ...MyFragment @defer - } - sku - id - } - } - fragment MyFragment on DeliveryEstimates { - estimatedDelivery - fastestDelivery - } - `; - - const link = new MockSubscriptionLink(); - - const client = new ApolloClient({ - link, - cache: new InMemoryCache(), - incrementalHandler: new Defer20220824Handler(), - }); - - using _disabledAct = disableActEnvironment(); - const { takeSnapshot } = await renderHookToSnapshotStream( - () => useQuery(query), - { - wrapper: ({ children }) => ( - {children} - ), - } - ); - - await expect(takeSnapshot()).resolves.toStrictEqualTyped({ - data: undefined, - dataState: "empty", - loading: true, - networkStatus: NetworkStatus.loading, - previousData: undefined, - variables: {}, - }); - - setTimeout(() => { - link.simulateResult({ - result: { - data: { - allProducts: [ - { - __typename: "Product", - delivery: { - __typename: "DeliveryEstimates", - }, - id: "apollo-federation", - sku: "federation", - }, - { - __typename: "Product", - delivery: { - __typename: "DeliveryEstimates", - }, - id: "apollo-studio", - sku: "studio", - }, - ], - }, - hasNext: true, - }, - }); - }); - - await expect(takeSnapshot()).resolves.toStrictEqualTyped({ - data: markAsStreaming({ - allProducts: [ - { - __typename: "Product", - delivery: { - __typename: "DeliveryEstimates", - }, - id: "apollo-federation", - sku: "federation", - }, - { - __typename: "Product", - delivery: { - __typename: "DeliveryEstimates", - }, - id: "apollo-studio", - sku: "studio", - }, - ], - }), - dataState: "streaming", - loading: true, - networkStatus: NetworkStatus.streaming, - previousData: undefined, - variables: {}, - }); - - setTimeout(() => { - link.simulateResult({ - result: { - hasNext: true, - incremental: [ - { - data: { - __typename: "DeliveryEstimates", - estimatedDelivery: "6/25/2021", - fastestDelivery: "6/24/2021", - }, - path: ["allProducts", 0, "delivery"], - }, - { - data: { - __typename: "DeliveryEstimates", - estimatedDelivery: "6/25/2021", - fastestDelivery: "6/24/2021", - }, - path: ["allProducts", 1, "delivery"], - }, - ], - }, - }); - }); - - await expect(takeSnapshot()).resolves.toStrictEqualTyped({ - data: markAsStreaming({ - allProducts: [ - { - __typename: "Product", - delivery: { - __typename: "DeliveryEstimates", - estimatedDelivery: "6/25/2021", - fastestDelivery: "6/24/2021", - }, - id: "apollo-federation", - sku: "federation", - }, - { - __typename: "Product", - delivery: { - __typename: "DeliveryEstimates", - estimatedDelivery: "6/25/2021", - fastestDelivery: "6/24/2021", - }, - id: "apollo-studio", - sku: "studio", - }, - ], - }), - dataState: "streaming", - loading: true, - networkStatus: NetworkStatus.streaming, - previousData: { - allProducts: [ - { - __typename: "Product", - delivery: { - __typename: "DeliveryEstimates", - }, - id: "apollo-federation", - sku: "federation", - }, - { - __typename: "Product", - delivery: { - __typename: "DeliveryEstimates", - }, - id: "apollo-studio", - sku: "studio", - }, - ], - }, - variables: {}, - }); - }); - - it("should handle deferred queries with fetch policy no-cache", async () => { - const query = gql` - { - greeting { - message - ... on Greeting @defer { - recipient { - name - } - } - } - } - `; - - const link = new MockSubscriptionLink(); - - const client = new ApolloClient({ - link, - cache: new InMemoryCache(), - incrementalHandler: new Defer20220824Handler(), - }); - - using _disabledAct = disableActEnvironment(); - const { takeSnapshot } = await renderHookToSnapshotStream( - () => useQuery(query, { fetchPolicy: "no-cache" }), - { - wrapper: ({ children }) => ( - {children} - ), - } - ); - - await expect(takeSnapshot()).resolves.toStrictEqualTyped({ - data: undefined, - dataState: "empty", - loading: true, - networkStatus: NetworkStatus.loading, - previousData: undefined, - variables: {}, - }); - - setTimeout(() => { - link.simulateResult({ - result: { - data: { - greeting: { - message: "Hello world", - __typename: "Greeting", - }, - }, - hasNext: true, - }, - }); - }); - - await expect(takeSnapshot()).resolves.toStrictEqualTyped({ - data: markAsStreaming({ - greeting: { - message: "Hello world", - __typename: "Greeting", - }, - }), - dataState: "streaming", - loading: true, - networkStatus: NetworkStatus.streaming, - previousData: undefined, - variables: {}, - }); - - setTimeout(() => { - link.simulateResult( - { - result: { - incremental: [ - { - data: { - recipient: { - name: "Alice", - __typename: "Person", - }, - __typename: "Greeting", - }, - path: ["greeting"], - }, - ], - hasNext: false, - }, - }, - true - ); - }); - - await expect(takeSnapshot()).resolves.toStrictEqualTyped({ - data: { - greeting: { - message: "Hello world", - __typename: "Greeting", - recipient: { - name: "Alice", - __typename: "Person", - }, - }, - }, - dataState: "complete", - loading: false, - networkStatus: NetworkStatus.ready, - previousData: { - greeting: { - message: "Hello world", - __typename: "Greeting", - }, - }, - variables: {}, - }); - - await expect(takeSnapshot).not.toRerender(); - }); - - it("should handle deferred queries with errors returned on the incremental batched result", async () => { - const query = gql` - query { - hero { - name - heroFriends { - id - name - ... @defer { - homeWorld - } - } - } - } - `; - - const link = new MockSubscriptionLink(); - - const client = new ApolloClient({ - link, - cache: new InMemoryCache(), - incrementalHandler: new Defer20220824Handler(), - }); - - using _disabledAct = disableActEnvironment(); - const { takeSnapshot } = await renderHookToSnapshotStream( - () => useQuery(query), - { - wrapper: ({ children }) => ( - {children} - ), - } - ); - - await expect(takeSnapshot()).resolves.toStrictEqualTyped({ - data: undefined, - dataState: "empty", - loading: true, - networkStatus: NetworkStatus.loading, - previousData: undefined, - variables: {}, - }); - - setTimeout(() => { - link.simulateResult({ - result: { - data: { - hero: { - name: "R2-D2", - heroFriends: [ - { - id: "1000", - name: "Luke Skywalker", - }, - { - id: "1003", - name: "Leia Organa", - }, - ], - }, - }, - hasNext: true, - }, - }); - }); - - await expect(takeSnapshot()).resolves.toStrictEqualTyped({ - data: markAsStreaming({ - hero: { - heroFriends: [ - { - id: "1000", - name: "Luke Skywalker", - }, - { - id: "1003", - name: "Leia Organa", - }, - ], - name: "R2-D2", - }, - }), - dataState: "streaming", - loading: true, - networkStatus: NetworkStatus.streaming, - previousData: undefined, - variables: {}, - }); - - setTimeout(() => { - link.simulateResult( - { - result: { - incremental: [ - { - path: ["hero", "heroFriends", 0], - errors: [ - { - message: - "homeWorld for character with ID 1000 could not be fetched.", - path: ["hero", "heroFriends", 0, "homeWorld"], - }, - ], - data: { - homeWorld: null, - }, - }, - { - path: ["hero", "heroFriends", 1], - data: { - homeWorld: "Alderaan", - }, - }, - ], - hasNext: false, - }, - }, - true - ); - }); - - await expect(takeSnapshot()).resolves.toStrictEqualTyped({ - data: { - hero: { - heroFriends: [ - { - id: "1000", - name: "Luke Skywalker", - }, - { - id: "1003", - name: "Leia Organa", - }, - ], - name: "R2-D2", - }, - }, - dataState: "complete", - error: new CombinedGraphQLErrors({ - data: { - hero: { - heroFriends: [ - { - id: "1000", - name: "Luke Skywalker", - homeWorld: null, - }, - { - id: "1003", - name: "Leia Organa", - homeWorld: "Alderaan", - }, - ], - name: "R2-D2", - }, - }, - errors: [ - { - message: - "homeWorld for character with ID 1000 could not be fetched.", - path: ["hero", "heroFriends", 0, "homeWorld"], - }, - ], - }), - loading: false, - networkStatus: NetworkStatus.error, - previousData: undefined, - variables: {}, - }); - - await expect(takeSnapshot).not.toRerender(); - }); - - it('should handle deferred queries with errors returned on the incremental batched result and errorPolicy "all"', async () => { - const query = gql` - query { - hero { - name - heroFriends { - id - name - ... @defer { - homeWorld - } - } - } - } - `; - - const link = new MockSubscriptionLink(); - - const client = new ApolloClient({ - link, - cache: new InMemoryCache(), - incrementalHandler: new Defer20220824Handler(), - }); - - using _disabledAct = disableActEnvironment(); - const { takeSnapshot } = await renderHookToSnapshotStream( - () => useQuery(query, { errorPolicy: "all" }), - { - wrapper: ({ children }) => ( - {children} - ), - } - ); - - await expect(takeSnapshot()).resolves.toStrictEqualTyped({ - data: undefined, - dataState: "empty", - loading: true, - networkStatus: NetworkStatus.loading, - previousData: undefined, - variables: {}, - }); - - setTimeout(() => { - link.simulateResult({ - result: { - data: { - hero: { - name: "R2-D2", - heroFriends: [ - { - id: "1000", - name: "Luke Skywalker", - }, - { - id: "1003", - name: "Leia Organa", - }, - ], - }, - }, - hasNext: true, - }, - }); - }); - - await expect(takeSnapshot()).resolves.toStrictEqualTyped({ - data: markAsStreaming({ - hero: { - name: "R2-D2", - heroFriends: [ - { - id: "1000", - name: "Luke Skywalker", - }, - { - id: "1003", - name: "Leia Organa", - }, - ], - }, - }), - dataState: "streaming", - loading: true, - networkStatus: NetworkStatus.streaming, - previousData: undefined, - variables: {}, - }); - - setTimeout(() => { - link.simulateResult( - { - result: { - incremental: [ - { - path: ["hero", "heroFriends", 0], - errors: [ - new GraphQLError( - "homeWorld for character with ID 1000 could not be fetched.", - { path: ["hero", "heroFriends", 0, "homeWorld"] } - ), - ], - data: { - homeWorld: null, - }, - extensions: { - thing1: "foo", - thing2: "bar", - }, - }, - { - path: ["hero", "heroFriends", 1], - data: { - homeWorld: "Alderaan", - }, - extensions: { - thing1: "foo", - thing2: "bar", - }, - }, - ], - hasNext: false, - }, - }, - true - ); - }); - - await expect(takeSnapshot()).resolves.toStrictEqualTyped({ - data: { - hero: { - heroFriends: [ - { - // the only difference with the previous test - // is that homeWorld is populated since errorPolicy: all - // populates both partial data and error.graphQLErrors - homeWorld: null, - id: "1000", - name: "Luke Skywalker", - }, - { - // homeWorld is populated due to errorPolicy: all - homeWorld: "Alderaan", - id: "1003", - name: "Leia Organa", - }, - ], - name: "R2-D2", - }, - }, - dataState: "complete", - error: new CombinedGraphQLErrors({ - data: { - hero: { - heroFriends: [ - { homeWorld: null, id: "1000", name: "Luke Skywalker" }, - { homeWorld: "Alderaan", id: "1003", name: "Leia Organa" }, - ], - name: "R2-D2", - }, - }, - errors: [ - { - message: - "homeWorld for character with ID 1000 could not be fetched.", - path: ["hero", "heroFriends", 0, "homeWorld"], - }, - ], - extensions: { - thing1: "foo", - thing2: "bar", - }, - }), - loading: false, - networkStatus: NetworkStatus.error, - previousData: { - hero: { - heroFriends: [ - { - id: "1000", - name: "Luke Skywalker", - }, - { - id: "1003", - name: "Leia Organa", - }, - ], - name: "R2-D2", - }, - }, - variables: {}, - }); - - await expect(takeSnapshot).not.toRerender(); - }); - - it('returns eventually consistent data from deferred queries with data in the cache while using a "cache-and-network" fetch policy', async () => { - const query = gql` - query { - greeting { - message - ... on Greeting @defer { - recipient { - name - } - } - } - } - `; - - const link = new MockSubscriptionLink(); - const cache = new InMemoryCache(); - const client = new ApolloClient({ - cache, - link, - incrementalHandler: new Defer20220824Handler(), - }); - - cache.writeQuery({ - query, - data: { - greeting: { - __typename: "Greeting", - message: "Hello cached", - recipient: { __typename: "Person", name: "Cached Alice" }, - }, - }, - }); - - using _disabledAct = disableActEnvironment(); - const { takeSnapshot } = await renderHookToSnapshotStream( - () => useQuery(query, { fetchPolicy: "cache-and-network" }), - { - wrapper: ({ children }) => ( - {children} - ), - } - ); - - await expect(takeSnapshot()).resolves.toStrictEqualTyped({ - data: { - greeting: { - __typename: "Greeting", - message: "Hello cached", - recipient: { __typename: "Person", name: "Cached Alice" }, - }, - }, - dataState: "complete", - loading: true, - networkStatus: NetworkStatus.loading, - previousData: undefined, - variables: {}, - }); - - link.simulateResult({ - result: { - data: { - greeting: { __typename: "Greeting", message: "Hello world" }, - }, - hasNext: true, - }, - }); - - await expect(takeSnapshot()).resolves.toStrictEqualTyped({ - data: markAsStreaming({ - greeting: { - __typename: "Greeting", - message: "Hello world", - recipient: { __typename: "Person", name: "Cached Alice" }, - }, - }), - dataState: "streaming", - loading: true, - networkStatus: NetworkStatus.streaming, - previousData: { - greeting: { - __typename: "Greeting", - message: "Hello cached", - recipient: { __typename: "Person", name: "Cached Alice" }, - }, - }, - variables: {}, - }); - - link.simulateResult( - { - result: { - incremental: [ - { - data: { - recipient: { name: "Alice", __typename: "Person" }, - __typename: "Greeting", - }, - path: ["greeting"], - }, - ], - hasNext: false, - }, - }, - true - ); - - await expect(takeSnapshot()).resolves.toStrictEqualTyped({ - data: { - greeting: { - __typename: "Greeting", - message: "Hello world", - recipient: { __typename: "Person", name: "Alice" }, - }, - }, - dataState: "complete", - loading: false, - networkStatus: NetworkStatus.ready, - previousData: { - greeting: { - __typename: "Greeting", - message: "Hello world", - recipient: { __typename: "Person", name: "Cached Alice" }, - }, - }, - variables: {}, - }); - - await expect(takeSnapshot).not.toRerender(); - }); - - it('returns eventually consistent data from deferred queries with partial data in the cache and using a "cache-first" fetch policy with `returnPartialData`', async () => { - const query = gql` - query { - greeting { - message - ... on Greeting @defer { - recipient { - name - } - } - } - } - `; - - const cache = new InMemoryCache(); - const link = new MockSubscriptionLink(); - const client = new ApolloClient({ - cache, - link, - incrementalHandler: new Defer20220824Handler(), - }); - - // We know we are writing partial data to the cache so suppress the console - // warning. - { - using _consoleSpy = spyOnConsole("error"); - cache.writeQuery({ - query, - data: { - greeting: { - __typename: "Greeting", - recipient: { __typename: "Person", name: "Cached Alice" }, - }, - }, - }); - } - - using _disabledAct = disableActEnvironment(); - const { takeSnapshot } = await renderHookToSnapshotStream( - () => - useQuery(query, { - fetchPolicy: "cache-first", - returnPartialData: true, - }), - { - wrapper: ({ children }) => ( - {children} - ), - } - ); - - await expect(takeSnapshot()).resolves.toStrictEqualTyped({ - data: { - greeting: { - __typename: "Greeting", - recipient: { __typename: "Person", name: "Cached Alice" }, - }, - }, - dataState: "partial", - loading: true, - networkStatus: NetworkStatus.loading, - previousData: undefined, - variables: {}, - }); - - link.simulateResult({ - result: { - data: { - greeting: { message: "Hello world", __typename: "Greeting" }, - }, - hasNext: true, - }, - }); - - await expect(takeSnapshot()).resolves.toStrictEqualTyped({ - data: markAsStreaming({ - greeting: { - __typename: "Greeting", - message: "Hello world", - recipient: { __typename: "Person", name: "Cached Alice" }, - }, - }), - dataState: "streaming", - loading: true, - networkStatus: NetworkStatus.streaming, - previousData: { - greeting: { - __typename: "Greeting", - recipient: { __typename: "Person", name: "Cached Alice" }, - }, - }, - variables: {}, - }); - - link.simulateResult( - { - result: { - incremental: [ - { - data: { - __typename: "Greeting", - recipient: { name: "Alice", __typename: "Person" }, - }, - path: ["greeting"], - }, - ], - hasNext: false, - }, - }, - true - ); - - await expect(takeSnapshot()).resolves.toStrictEqualTyped({ - data: { - greeting: { - __typename: "Greeting", - message: "Hello world", - recipient: { __typename: "Person", name: "Alice" }, - }, - }, - dataState: "complete", - loading: false, - networkStatus: NetworkStatus.ready, - previousData: { - greeting: { - __typename: "Greeting", - message: "Hello world", - recipient: { __typename: "Person", name: "Cached Alice" }, - }, - }, - variables: {}, - }); - - await expect(takeSnapshot).not.toRerender(); - }); - }); - describe("interaction with `prioritizeCacheValues`", () => { const cacheData = { something: "foo" }; const emptyData = undefined; diff --git a/src/react/hooks/__tests__/useQuery/defer20220824.test.tsx b/src/react/hooks/__tests__/useQuery/defer20220824.test.tsx new file mode 100644 index 00000000000..9e8229409bd --- /dev/null +++ b/src/react/hooks/__tests__/useQuery/defer20220824.test.tsx @@ -0,0 +1,1234 @@ +import { + disableActEnvironment, + renderHookToSnapshotStream, +} from "@testing-library/react-render-stream"; +import React from "react"; + +import { + ApolloClient, + CombinedGraphQLErrors, + gql, + InMemoryCache, + NetworkStatus, +} from "@apollo/client"; +import { Defer20220824Handler } from "@apollo/client/incremental"; +import { ApolloProvider, useQuery } from "@apollo/client/react"; +import { MockSubscriptionLink } from "@apollo/client/testing"; +import { markAsStreaming, spyOnConsole } from "@apollo/client/testing/internal"; + +test("should handle deferred queries", async () => { + const query = gql` + query { + greeting { + message + ... on Greeting @defer { + recipient { + name + } + } + } + } + `; + + const link = new MockSubscriptionLink(); + + const client = new ApolloClient({ + link, + cache: new InMemoryCache(), + incrementalHandler: new Defer20220824Handler(), + }); + + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( + () => useQuery(query), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + await expect(takeSnapshot()).resolves.toStrictEqualTyped({ + data: undefined, + dataState: "empty", + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); + + setTimeout(() => { + link.simulateResult({ + result: { + data: { + greeting: { + message: "Hello world", + __typename: "Greeting", + }, + }, + hasNext: true, + }, + }); + }); + + await expect(takeSnapshot()).resolves.toStrictEqualTyped({ + data: markAsStreaming({ + greeting: { + message: "Hello world", + __typename: "Greeting", + }, + }), + dataState: "streaming", + loading: true, + networkStatus: NetworkStatus.streaming, + previousData: undefined, + variables: {}, + }); + + setTimeout(() => { + link.simulateResult( + { + result: { + incremental: [ + { + data: { + recipient: { + name: "Alice", + __typename: "Person", + }, + __typename: "Greeting", + }, + path: ["greeting"], + }, + ], + hasNext: false, + }, + }, + true + ); + }); + + await expect(takeSnapshot()).resolves.toStrictEqualTyped({ + data: { + greeting: { + message: "Hello world", + __typename: "Greeting", + recipient: { + name: "Alice", + __typename: "Person", + }, + }, + }, + dataState: "complete", + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { + greeting: { + message: "Hello world", + __typename: "Greeting", + }, + }, + variables: {}, + }); + + await expect(takeSnapshot).not.toRerender(); +}); + +test("should handle deferred queries in lists", async () => { + const query = gql` + { + greetings { + message + ... on Greeting @defer { + recipient { + name + } + } + } + } + `; + + const link = new MockSubscriptionLink(); + + const client = new ApolloClient({ + link, + cache: new InMemoryCache(), + incrementalHandler: new Defer20220824Handler(), + }); + + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( + () => useQuery(query), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + await expect(takeSnapshot()).resolves.toStrictEqualTyped({ + data: undefined, + dataState: "empty", + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); + + setTimeout(() => { + link.simulateResult({ + result: { + data: { + greetings: [ + { message: "Hello world", __typename: "Greeting" }, + { message: "Hello again", __typename: "Greeting" }, + ], + }, + hasNext: true, + }, + }); + }); + + await expect(takeSnapshot()).resolves.toStrictEqualTyped({ + data: markAsStreaming({ + greetings: [ + { message: "Hello world", __typename: "Greeting" }, + { message: "Hello again", __typename: "Greeting" }, + ], + }), + dataState: "streaming", + loading: true, + networkStatus: NetworkStatus.streaming, + previousData: undefined, + variables: {}, + }); + + setTimeout(() => { + link.simulateResult({ + result: { + incremental: [ + { + data: { + recipient: { + name: "Alice", + __typename: "Person", + }, + __typename: "Greeting", + }, + path: ["greetings", 0], + }, + ], + hasNext: true, + }, + }); + }); + + await expect(takeSnapshot()).resolves.toStrictEqualTyped({ + data: markAsStreaming({ + greetings: [ + { + message: "Hello world", + __typename: "Greeting", + recipient: { name: "Alice", __typename: "Person" }, + }, + { message: "Hello again", __typename: "Greeting" }, + ], + }), + dataState: "streaming", + loading: true, + networkStatus: NetworkStatus.streaming, + previousData: { + greetings: [ + { message: "Hello world", __typename: "Greeting" }, + { message: "Hello again", __typename: "Greeting" }, + ], + }, + variables: {}, + }); + + setTimeout(() => { + link.simulateResult( + { + result: { + incremental: [ + { + data: { + recipient: { + name: "Bob", + __typename: "Person", + }, + __typename: "Greeting", + }, + path: ["greetings", 1], + }, + ], + hasNext: false, + }, + }, + true + ); + }); + + await expect(takeSnapshot()).resolves.toStrictEqualTyped({ + data: { + greetings: [ + { + message: "Hello world", + __typename: "Greeting", + recipient: { name: "Alice", __typename: "Person" }, + }, + { + message: "Hello again", + __typename: "Greeting", + recipient: { name: "Bob", __typename: "Person" }, + }, + ], + }, + dataState: "complete", + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { + greetings: [ + { + message: "Hello world", + __typename: "Greeting", + recipient: { name: "Alice", __typename: "Person" }, + }, + { message: "Hello again", __typename: "Greeting" }, + ], + }, + variables: {}, + }); + + await expect(takeSnapshot).not.toRerender(); +}); + +test("should handle deferred queries in lists, merging arrays", async () => { + const query = gql` + query DeferVariation { + allProducts { + delivery { + ...MyFragment @defer + } + sku + id + } + } + fragment MyFragment on DeliveryEstimates { + estimatedDelivery + fastestDelivery + } + `; + + const link = new MockSubscriptionLink(); + + const client = new ApolloClient({ + link, + cache: new InMemoryCache(), + incrementalHandler: new Defer20220824Handler(), + }); + + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( + () => useQuery(query), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + await expect(takeSnapshot()).resolves.toStrictEqualTyped({ + data: undefined, + dataState: "empty", + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); + + setTimeout(() => { + link.simulateResult({ + result: { + data: { + allProducts: [ + { + __typename: "Product", + delivery: { + __typename: "DeliveryEstimates", + }, + id: "apollo-federation", + sku: "federation", + }, + { + __typename: "Product", + delivery: { + __typename: "DeliveryEstimates", + }, + id: "apollo-studio", + sku: "studio", + }, + ], + }, + hasNext: true, + }, + }); + }); + + await expect(takeSnapshot()).resolves.toStrictEqualTyped({ + data: markAsStreaming({ + allProducts: [ + { + __typename: "Product", + delivery: { + __typename: "DeliveryEstimates", + }, + id: "apollo-federation", + sku: "federation", + }, + { + __typename: "Product", + delivery: { + __typename: "DeliveryEstimates", + }, + id: "apollo-studio", + sku: "studio", + }, + ], + }), + dataState: "streaming", + loading: true, + networkStatus: NetworkStatus.streaming, + previousData: undefined, + variables: {}, + }); + + setTimeout(() => { + link.simulateResult({ + result: { + hasNext: true, + incremental: [ + { + data: { + __typename: "DeliveryEstimates", + estimatedDelivery: "6/25/2021", + fastestDelivery: "6/24/2021", + }, + path: ["allProducts", 0, "delivery"], + }, + { + data: { + __typename: "DeliveryEstimates", + estimatedDelivery: "6/25/2021", + fastestDelivery: "6/24/2021", + }, + path: ["allProducts", 1, "delivery"], + }, + ], + }, + }); + }); + + await expect(takeSnapshot()).resolves.toStrictEqualTyped({ + data: markAsStreaming({ + allProducts: [ + { + __typename: "Product", + delivery: { + __typename: "DeliveryEstimates", + estimatedDelivery: "6/25/2021", + fastestDelivery: "6/24/2021", + }, + id: "apollo-federation", + sku: "federation", + }, + { + __typename: "Product", + delivery: { + __typename: "DeliveryEstimates", + estimatedDelivery: "6/25/2021", + fastestDelivery: "6/24/2021", + }, + id: "apollo-studio", + sku: "studio", + }, + ], + }), + dataState: "streaming", + loading: true, + networkStatus: NetworkStatus.streaming, + previousData: { + allProducts: [ + { + __typename: "Product", + delivery: { + __typename: "DeliveryEstimates", + }, + id: "apollo-federation", + sku: "federation", + }, + { + __typename: "Product", + delivery: { + __typename: "DeliveryEstimates", + }, + id: "apollo-studio", + sku: "studio", + }, + ], + }, + variables: {}, + }); +}); + +test("should handle deferred queries with fetch policy no-cache", async () => { + const query = gql` + { + greeting { + message + ... on Greeting @defer { + recipient { + name + } + } + } + } + `; + + const link = new MockSubscriptionLink(); + + const client = new ApolloClient({ + link, + cache: new InMemoryCache(), + incrementalHandler: new Defer20220824Handler(), + }); + + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( + () => useQuery(query, { fetchPolicy: "no-cache" }), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + await expect(takeSnapshot()).resolves.toStrictEqualTyped({ + data: undefined, + dataState: "empty", + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); + + setTimeout(() => { + link.simulateResult({ + result: { + data: { + greeting: { + message: "Hello world", + __typename: "Greeting", + }, + }, + hasNext: true, + }, + }); + }); + + await expect(takeSnapshot()).resolves.toStrictEqualTyped({ + data: markAsStreaming({ + greeting: { + message: "Hello world", + __typename: "Greeting", + }, + }), + dataState: "streaming", + loading: true, + networkStatus: NetworkStatus.streaming, + previousData: undefined, + variables: {}, + }); + + setTimeout(() => { + link.simulateResult( + { + result: { + incremental: [ + { + data: { + recipient: { + name: "Alice", + __typename: "Person", + }, + __typename: "Greeting", + }, + path: ["greeting"], + }, + ], + hasNext: false, + }, + }, + true + ); + }); + + await expect(takeSnapshot()).resolves.toStrictEqualTyped({ + data: { + greeting: { + message: "Hello world", + __typename: "Greeting", + recipient: { + name: "Alice", + __typename: "Person", + }, + }, + }, + dataState: "complete", + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { + greeting: { + message: "Hello world", + __typename: "Greeting", + }, + }, + variables: {}, + }); + + await expect(takeSnapshot).not.toRerender(); +}); + +test("should handle deferred queries with errors returned on the incremental batched result", async () => { + const query = gql` + query { + hero { + name + heroFriends { + id + name + ... @defer { + homeWorld + } + } + } + } + `; + + const link = new MockSubscriptionLink(); + + const client = new ApolloClient({ + link, + cache: new InMemoryCache(), + incrementalHandler: new Defer20220824Handler(), + }); + + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( + () => useQuery(query), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + await expect(takeSnapshot()).resolves.toStrictEqualTyped({ + data: undefined, + dataState: "empty", + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); + + setTimeout(() => { + link.simulateResult({ + result: { + data: { + hero: { + name: "R2-D2", + heroFriends: [ + { + id: "1000", + name: "Luke Skywalker", + }, + { + id: "1003", + name: "Leia Organa", + }, + ], + }, + }, + hasNext: true, + }, + }); + }); + + await expect(takeSnapshot()).resolves.toStrictEqualTyped({ + data: markAsStreaming({ + hero: { + heroFriends: [ + { + id: "1000", + name: "Luke Skywalker", + }, + { + id: "1003", + name: "Leia Organa", + }, + ], + name: "R2-D2", + }, + }), + dataState: "streaming", + loading: true, + networkStatus: NetworkStatus.streaming, + previousData: undefined, + variables: {}, + }); + + setTimeout(() => { + link.simulateResult( + { + result: { + incremental: [ + { + path: ["hero", "heroFriends", 0], + errors: [ + { + message: + "homeWorld for character with ID 1000 could not be fetched.", + path: ["hero", "heroFriends", 0, "homeWorld"], + }, + ], + data: { + homeWorld: null, + }, + }, + { + path: ["hero", "heroFriends", 1], + data: { + homeWorld: "Alderaan", + }, + }, + ], + hasNext: false, + }, + }, + true + ); + }); + + await expect(takeSnapshot()).resolves.toStrictEqualTyped({ + data: { + hero: { + heroFriends: [ + { + id: "1000", + name: "Luke Skywalker", + }, + { + id: "1003", + name: "Leia Organa", + }, + ], + name: "R2-D2", + }, + }, + dataState: "complete", + error: new CombinedGraphQLErrors({ + data: { + hero: { + heroFriends: [ + { + id: "1000", + name: "Luke Skywalker", + homeWorld: null, + }, + { + id: "1003", + name: "Leia Organa", + homeWorld: "Alderaan", + }, + ], + name: "R2-D2", + }, + }, + errors: [ + { + message: "homeWorld for character with ID 1000 could not be fetched.", + path: ["hero", "heroFriends", 0, "homeWorld"], + }, + ], + }), + loading: false, + networkStatus: NetworkStatus.error, + previousData: undefined, + variables: {}, + }); + + await expect(takeSnapshot).not.toRerender(); +}); + +it('should handle deferred queries with errors returned on the incremental batched result and errorPolicy "all"', async () => { + const query = gql` + query { + hero { + name + heroFriends { + id + name + ... @defer { + homeWorld + } + } + } + } + `; + + const link = new MockSubscriptionLink(); + + const client = new ApolloClient({ + link, + cache: new InMemoryCache(), + incrementalHandler: new Defer20220824Handler(), + }); + + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( + () => useQuery(query, { errorPolicy: "all" }), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + await expect(takeSnapshot()).resolves.toStrictEqualTyped({ + data: undefined, + dataState: "empty", + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); + + setTimeout(() => { + link.simulateResult({ + result: { + data: { + hero: { + name: "R2-D2", + heroFriends: [ + { + id: "1000", + name: "Luke Skywalker", + }, + { + id: "1003", + name: "Leia Organa", + }, + ], + }, + }, + hasNext: true, + }, + }); + }); + + await expect(takeSnapshot()).resolves.toStrictEqualTyped({ + data: markAsStreaming({ + hero: { + name: "R2-D2", + heroFriends: [ + { + id: "1000", + name: "Luke Skywalker", + }, + { + id: "1003", + name: "Leia Organa", + }, + ], + }, + }), + dataState: "streaming", + loading: true, + networkStatus: NetworkStatus.streaming, + previousData: undefined, + variables: {}, + }); + + setTimeout(() => { + link.simulateResult( + { + result: { + incremental: [ + { + path: ["hero", "heroFriends", 0], + errors: [ + { + message: + "homeWorld for character with ID 1000 could not be fetched.", + path: ["hero", "heroFriends", 0, "homeWorld"], + }, + ], + data: { + homeWorld: null, + }, + extensions: { + thing1: "foo", + thing2: "bar", + }, + }, + { + path: ["hero", "heroFriends", 1], + data: { + homeWorld: "Alderaan", + }, + extensions: { + thing1: "foo", + thing2: "bar", + }, + }, + ], + hasNext: false, + }, + }, + true + ); + }); + + await expect(takeSnapshot()).resolves.toStrictEqualTyped({ + data: { + hero: { + heroFriends: [ + { + // the only difference with the previous test + // is that homeWorld is populated since errorPolicy: all + // populates both partial data and error.graphQLErrors + homeWorld: null, + id: "1000", + name: "Luke Skywalker", + }, + { + // homeWorld is populated due to errorPolicy: all + homeWorld: "Alderaan", + id: "1003", + name: "Leia Organa", + }, + ], + name: "R2-D2", + }, + }, + dataState: "complete", + error: new CombinedGraphQLErrors({ + data: { + hero: { + heroFriends: [ + { homeWorld: null, id: "1000", name: "Luke Skywalker" }, + { homeWorld: "Alderaan", id: "1003", name: "Leia Organa" }, + ], + name: "R2-D2", + }, + }, + errors: [ + { + message: "homeWorld for character with ID 1000 could not be fetched.", + path: ["hero", "heroFriends", 0, "homeWorld"], + }, + ], + extensions: { + thing1: "foo", + thing2: "bar", + }, + }), + loading: false, + networkStatus: NetworkStatus.error, + previousData: { + hero: { + heroFriends: [ + { + id: "1000", + name: "Luke Skywalker", + }, + { + id: "1003", + name: "Leia Organa", + }, + ], + name: "R2-D2", + }, + }, + variables: {}, + }); + + await expect(takeSnapshot).not.toRerender(); +}); + +it('returns eventually consistent data from deferred queries with data in the cache while using a "cache-and-network" fetch policy', async () => { + const query = gql` + query { + greeting { + message + ... on Greeting @defer { + recipient { + name + } + } + } + } + `; + + const link = new MockSubscriptionLink(); + const cache = new InMemoryCache(); + const client = new ApolloClient({ + cache, + link, + incrementalHandler: new Defer20220824Handler(), + }); + + cache.writeQuery({ + query, + data: { + greeting: { + __typename: "Greeting", + message: "Hello cached", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + }); + + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( + () => useQuery(query, { fetchPolicy: "cache-and-network" }), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + await expect(takeSnapshot()).resolves.toStrictEqualTyped({ + data: { + greeting: { + __typename: "Greeting", + message: "Hello cached", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + dataState: "complete", + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); + + link.simulateResult({ + result: { + data: { + greeting: { __typename: "Greeting", message: "Hello world" }, + }, + hasNext: true, + }, + }); + + await expect(takeSnapshot()).resolves.toStrictEqualTyped({ + data: markAsStreaming({ + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }), + dataState: "streaming", + loading: true, + networkStatus: NetworkStatus.streaming, + previousData: { + greeting: { + __typename: "Greeting", + message: "Hello cached", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + variables: {}, + }); + + link.simulateResult( + { + result: { + incremental: [ + { + data: { + recipient: { name: "Alice", __typename: "Person" }, + __typename: "Greeting", + }, + path: ["greeting"], + }, + ], + hasNext: false, + }, + }, + true + ); + + await expect(takeSnapshot()).resolves.toStrictEqualTyped({ + data: { + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Alice" }, + }, + }, + dataState: "complete", + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + variables: {}, + }); + + await expect(takeSnapshot).not.toRerender(); +}); + +it('returns eventually consistent data from deferred queries with partial data in the cache and using a "cache-first" fetch policy with `returnPartialData`', async () => { + const query = gql` + query { + greeting { + message + ... on Greeting @defer { + recipient { + name + } + } + } + } + `; + + const cache = new InMemoryCache(); + const link = new MockSubscriptionLink(); + const client = new ApolloClient({ + cache, + link, + incrementalHandler: new Defer20220824Handler(), + }); + + // We know we are writing partial data to the cache so suppress the console + // warning. + { + using _consoleSpy = spyOnConsole("error"); + cache.writeQuery({ + query, + data: { + greeting: { + __typename: "Greeting", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + }); + } + + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( + () => + useQuery(query, { + fetchPolicy: "cache-first", + returnPartialData: true, + }), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + await expect(takeSnapshot()).resolves.toStrictEqualTyped({ + data: { + greeting: { + __typename: "Greeting", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + dataState: "partial", + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); + + link.simulateResult({ + result: { + data: { + greeting: { message: "Hello world", __typename: "Greeting" }, + }, + hasNext: true, + }, + }); + + await expect(takeSnapshot()).resolves.toStrictEqualTyped({ + data: markAsStreaming({ + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }), + dataState: "streaming", + loading: true, + networkStatus: NetworkStatus.streaming, + previousData: { + greeting: { + __typename: "Greeting", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + variables: {}, + }); + + link.simulateResult( + { + result: { + incremental: [ + { + data: { + __typename: "Greeting", + recipient: { name: "Alice", __typename: "Person" }, + }, + path: ["greeting"], + }, + ], + hasNext: false, + }, + }, + true + ); + + await expect(takeSnapshot()).resolves.toStrictEqualTyped({ + data: { + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Alice" }, + }, + }, + dataState: "complete", + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + variables: {}, + }); + + await expect(takeSnapshot).not.toRerender(); +}); From 66986d4d38046c966c1abbdbde6e21f0481d55d5 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 4 Sep 2025 16:57:11 -0600 Subject: [PATCH 47/97] Replace MockSubscriptionLink with mock helper --- .../__tests__/useQuery/defer20220824.test.tsx | 558 ++++++++---------- 1 file changed, 244 insertions(+), 314 deletions(-) diff --git a/src/react/hooks/__tests__/useQuery/defer20220824.test.tsx b/src/react/hooks/__tests__/useQuery/defer20220824.test.tsx index 9e8229409bd..79cb0309731 100644 --- a/src/react/hooks/__tests__/useQuery/defer20220824.test.tsx +++ b/src/react/hooks/__tests__/useQuery/defer20220824.test.tsx @@ -13,8 +13,11 @@ import { } from "@apollo/client"; import { Defer20220824Handler } from "@apollo/client/incremental"; import { ApolloProvider, useQuery } from "@apollo/client/react"; -import { MockSubscriptionLink } from "@apollo/client/testing"; -import { markAsStreaming, spyOnConsole } from "@apollo/client/testing/internal"; +import { + markAsStreaming, + mockDefer20220824, + spyOnConsole, +} from "@apollo/client/testing/internal"; test("should handle deferred queries", async () => { const query = gql` @@ -30,10 +33,11 @@ test("should handle deferred queries", async () => { } `; - const link = new MockSubscriptionLink(); + const { httpLink, enqueueInitialChunk, enqueueSubsequentChunk } = + mockDefer20220824(); const client = new ApolloClient({ - link, + link: httpLink, cache: new InMemoryCache(), incrementalHandler: new Defer20220824Handler(), }); @@ -57,18 +61,14 @@ test("should handle deferred queries", async () => { variables: {}, }); - setTimeout(() => { - link.simulateResult({ - result: { - data: { - greeting: { - message: "Hello world", - __typename: "Greeting", - }, - }, - hasNext: true, + enqueueInitialChunk({ + data: { + greeting: { + message: "Hello world", + __typename: "Greeting", }, - }); + }, + hasNext: true, }); await expect(takeSnapshot()).resolves.toStrictEqualTyped({ @@ -85,27 +85,20 @@ test("should handle deferred queries", async () => { variables: {}, }); - setTimeout(() => { - link.simulateResult( + enqueueSubsequentChunk({ + incremental: [ { - result: { - incremental: [ - { - data: { - recipient: { - name: "Alice", - __typename: "Person", - }, - __typename: "Greeting", - }, - path: ["greeting"], - }, - ], - hasNext: false, + data: { + recipient: { + name: "Alice", + __typename: "Person", + }, + __typename: "Greeting", }, + path: ["greeting"], }, - true - ); + ], + hasNext: false, }); await expect(takeSnapshot()).resolves.toStrictEqualTyped({ @@ -148,10 +141,11 @@ test("should handle deferred queries in lists", async () => { } `; - const link = new MockSubscriptionLink(); + const { httpLink, enqueueInitialChunk, enqueueSubsequentChunk } = + mockDefer20220824(); const client = new ApolloClient({ - link, + link: httpLink, cache: new InMemoryCache(), incrementalHandler: new Defer20220824Handler(), }); @@ -175,18 +169,14 @@ test("should handle deferred queries in lists", async () => { variables: {}, }); - setTimeout(() => { - link.simulateResult({ - result: { - data: { - greetings: [ - { message: "Hello world", __typename: "Greeting" }, - { message: "Hello again", __typename: "Greeting" }, - ], - }, - hasNext: true, - }, - }); + enqueueInitialChunk({ + data: { + greetings: [ + { message: "Hello world", __typename: "Greeting" }, + { message: "Hello again", __typename: "Greeting" }, + ], + }, + hasNext: true, }); await expect(takeSnapshot()).resolves.toStrictEqualTyped({ @@ -203,24 +193,20 @@ test("should handle deferred queries in lists", async () => { variables: {}, }); - setTimeout(() => { - link.simulateResult({ - result: { - incremental: [ - { - data: { - recipient: { - name: "Alice", - __typename: "Person", - }, - __typename: "Greeting", - }, - path: ["greetings", 0], + enqueueSubsequentChunk({ + incremental: [ + { + data: { + recipient: { + name: "Alice", + __typename: "Person", }, - ], - hasNext: true, + __typename: "Greeting", + }, + path: ["greetings", 0], }, - }); + ], + hasNext: true, }); await expect(takeSnapshot()).resolves.toStrictEqualTyped({ @@ -246,27 +232,20 @@ test("should handle deferred queries in lists", async () => { variables: {}, }); - setTimeout(() => { - link.simulateResult( + enqueueSubsequentChunk({ + incremental: [ { - result: { - incremental: [ - { - data: { - recipient: { - name: "Bob", - __typename: "Person", - }, - __typename: "Greeting", - }, - path: ["greetings", 1], - }, - ], - hasNext: false, + data: { + recipient: { + name: "Bob", + __typename: "Person", + }, + __typename: "Greeting", }, + path: ["greetings", 1], }, - true - ); + ], + hasNext: false, }); await expect(takeSnapshot()).resolves.toStrictEqualTyped({ @@ -320,10 +299,11 @@ test("should handle deferred queries in lists, merging arrays", async () => { } `; - const link = new MockSubscriptionLink(); + const { httpLink, enqueueInitialChunk, enqueueSubsequentChunk } = + mockDefer20220824(); const client = new ApolloClient({ - link, + link: httpLink, cache: new InMemoryCache(), incrementalHandler: new Defer20220824Handler(), }); @@ -347,32 +327,28 @@ test("should handle deferred queries in lists, merging arrays", async () => { variables: {}, }); - setTimeout(() => { - link.simulateResult({ - result: { - data: { - allProducts: [ - { - __typename: "Product", - delivery: { - __typename: "DeliveryEstimates", - }, - id: "apollo-federation", - sku: "federation", - }, - { - __typename: "Product", - delivery: { - __typename: "DeliveryEstimates", - }, - id: "apollo-studio", - sku: "studio", - }, - ], + enqueueInitialChunk({ + data: { + allProducts: [ + { + __typename: "Product", + delivery: { + __typename: "DeliveryEstimates", + }, + id: "apollo-federation", + sku: "federation", }, - hasNext: true, - }, - }); + { + __typename: "Product", + delivery: { + __typename: "DeliveryEstimates", + }, + id: "apollo-studio", + sku: "studio", + }, + ], + }, + hasNext: true, }); await expect(takeSnapshot()).resolves.toStrictEqualTyped({ @@ -403,30 +379,26 @@ test("should handle deferred queries in lists, merging arrays", async () => { variables: {}, }); - setTimeout(() => { - link.simulateResult({ - result: { - hasNext: true, - incremental: [ - { - data: { - __typename: "DeliveryEstimates", - estimatedDelivery: "6/25/2021", - fastestDelivery: "6/24/2021", - }, - path: ["allProducts", 0, "delivery"], - }, - { - data: { - __typename: "DeliveryEstimates", - estimatedDelivery: "6/25/2021", - fastestDelivery: "6/24/2021", - }, - path: ["allProducts", 1, "delivery"], - }, - ], + enqueueSubsequentChunk({ + hasNext: true, + incremental: [ + { + data: { + __typename: "DeliveryEstimates", + estimatedDelivery: "6/25/2021", + fastestDelivery: "6/24/2021", + }, + path: ["allProducts", 0, "delivery"], }, - }); + { + data: { + __typename: "DeliveryEstimates", + estimatedDelivery: "6/25/2021", + fastestDelivery: "6/24/2021", + }, + path: ["allProducts", 1, "delivery"], + }, + ], }); await expect(takeSnapshot()).resolves.toStrictEqualTyped({ @@ -495,10 +467,11 @@ test("should handle deferred queries with fetch policy no-cache", async () => { } `; - const link = new MockSubscriptionLink(); + const { httpLink, enqueueInitialChunk, enqueueSubsequentChunk } = + mockDefer20220824(); const client = new ApolloClient({ - link, + link: httpLink, cache: new InMemoryCache(), incrementalHandler: new Defer20220824Handler(), }); @@ -522,18 +495,14 @@ test("should handle deferred queries with fetch policy no-cache", async () => { variables: {}, }); - setTimeout(() => { - link.simulateResult({ - result: { - data: { - greeting: { - message: "Hello world", - __typename: "Greeting", - }, - }, - hasNext: true, + enqueueInitialChunk({ + data: { + greeting: { + message: "Hello world", + __typename: "Greeting", }, - }); + }, + hasNext: true, }); await expect(takeSnapshot()).resolves.toStrictEqualTyped({ @@ -550,27 +519,20 @@ test("should handle deferred queries with fetch policy no-cache", async () => { variables: {}, }); - setTimeout(() => { - link.simulateResult( + enqueueSubsequentChunk({ + incremental: [ { - result: { - incremental: [ - { - data: { - recipient: { - name: "Alice", - __typename: "Person", - }, - __typename: "Greeting", - }, - path: ["greeting"], - }, - ], - hasNext: false, + data: { + recipient: { + name: "Alice", + __typename: "Person", + }, + __typename: "Greeting", }, + path: ["greeting"], }, - true - ); + ], + hasNext: false, }); await expect(takeSnapshot()).resolves.toStrictEqualTyped({ @@ -615,10 +577,11 @@ test("should handle deferred queries with errors returned on the incremental bat } `; - const link = new MockSubscriptionLink(); + const { httpLink, enqueueInitialChunk, enqueueSubsequentChunk } = + mockDefer20220824(); const client = new ApolloClient({ - link, + link: httpLink, cache: new InMemoryCache(), incrementalHandler: new Defer20220824Handler(), }); @@ -642,27 +605,23 @@ test("should handle deferred queries with errors returned on the incremental bat variables: {}, }); - setTimeout(() => { - link.simulateResult({ - result: { - data: { - hero: { - name: "R2-D2", - heroFriends: [ - { - id: "1000", - name: "Luke Skywalker", - }, - { - id: "1003", - name: "Leia Organa", - }, - ], + enqueueInitialChunk({ + data: { + hero: { + name: "R2-D2", + heroFriends: [ + { + id: "1000", + name: "Luke Skywalker", }, - }, - hasNext: true, + { + id: "1003", + name: "Leia Organa", + }, + ], }, - }); + }, + hasNext: true, }); await expect(takeSnapshot()).resolves.toStrictEqualTyped({ @@ -688,36 +647,29 @@ test("should handle deferred queries with errors returned on the incremental bat variables: {}, }); - setTimeout(() => { - link.simulateResult( + enqueueSubsequentChunk({ + incremental: [ { - result: { - incremental: [ - { - path: ["hero", "heroFriends", 0], - errors: [ - { - message: - "homeWorld for character with ID 1000 could not be fetched.", - path: ["hero", "heroFriends", 0, "homeWorld"], - }, - ], - data: { - homeWorld: null, - }, - }, - { - path: ["hero", "heroFriends", 1], - data: { - homeWorld: "Alderaan", - }, - }, - ], - hasNext: false, + path: ["hero", "heroFriends", 0], + errors: [ + { + message: + "homeWorld for character with ID 1000 could not be fetched.", + path: ["hero", "heroFriends", 0, "homeWorld"], + }, + ], + data: { + homeWorld: null, + }, + }, + { + path: ["hero", "heroFriends", 1], + data: { + homeWorld: "Alderaan", }, }, - true - ); + ], + hasNext: false, }); await expect(takeSnapshot()).resolves.toStrictEqualTyped({ @@ -787,10 +739,11 @@ it('should handle deferred queries with errors returned on the incremental batch } `; - const link = new MockSubscriptionLink(); + const { httpLink, enqueueInitialChunk, enqueueSubsequentChunk } = + mockDefer20220824(); const client = new ApolloClient({ - link, + link: httpLink, cache: new InMemoryCache(), incrementalHandler: new Defer20220824Handler(), }); @@ -814,27 +767,23 @@ it('should handle deferred queries with errors returned on the incremental batch variables: {}, }); - setTimeout(() => { - link.simulateResult({ - result: { - data: { - hero: { - name: "R2-D2", - heroFriends: [ - { - id: "1000", - name: "Luke Skywalker", - }, - { - id: "1003", - name: "Leia Organa", - }, - ], + enqueueInitialChunk({ + data: { + hero: { + name: "R2-D2", + heroFriends: [ + { + id: "1000", + name: "Luke Skywalker", }, - }, - hasNext: true, + { + id: "1003", + name: "Leia Organa", + }, + ], }, - }); + }, + hasNext: true, }); await expect(takeSnapshot()).resolves.toStrictEqualTyped({ @@ -860,44 +809,37 @@ it('should handle deferred queries with errors returned on the incremental batch variables: {}, }); - setTimeout(() => { - link.simulateResult( + enqueueSubsequentChunk({ + incremental: [ { - result: { - incremental: [ - { - path: ["hero", "heroFriends", 0], - errors: [ - { - message: - "homeWorld for character with ID 1000 could not be fetched.", - path: ["hero", "heroFriends", 0, "homeWorld"], - }, - ], - data: { - homeWorld: null, - }, - extensions: { - thing1: "foo", - thing2: "bar", - }, - }, - { - path: ["hero", "heroFriends", 1], - data: { - homeWorld: "Alderaan", - }, - extensions: { - thing1: "foo", - thing2: "bar", - }, - }, - ], - hasNext: false, + path: ["hero", "heroFriends", 0], + errors: [ + { + message: + "homeWorld for character with ID 1000 could not be fetched.", + path: ["hero", "heroFriends", 0, "homeWorld"], + }, + ], + data: { + homeWorld: null, + }, + extensions: { + thing1: "foo", + thing2: "bar", + }, + }, + { + path: ["hero", "heroFriends", 1], + data: { + homeWorld: "Alderaan", + }, + extensions: { + thing1: "foo", + thing2: "bar", }, }, - true - ); + ], + hasNext: false, }); await expect(takeSnapshot()).resolves.toStrictEqualTyped({ @@ -981,11 +923,12 @@ it('returns eventually consistent data from deferred queries with data in the ca } `; - const link = new MockSubscriptionLink(); + const { httpLink, enqueueInitialChunk, enqueueSubsequentChunk } = + mockDefer20220824(); const cache = new InMemoryCache(); const client = new ApolloClient({ cache, - link, + link: httpLink, incrementalHandler: new Defer20220824Handler(), }); @@ -1025,13 +968,11 @@ it('returns eventually consistent data from deferred queries with data in the ca variables: {}, }); - link.simulateResult({ - result: { - data: { - greeting: { __typename: "Greeting", message: "Hello world" }, - }, - hasNext: true, + enqueueInitialChunk({ + data: { + greeting: { __typename: "Greeting", message: "Hello world" }, }, + hasNext: true, }); await expect(takeSnapshot()).resolves.toStrictEqualTyped({ @@ -1055,23 +996,18 @@ it('returns eventually consistent data from deferred queries with data in the ca variables: {}, }); - link.simulateResult( - { - result: { - incremental: [ - { - data: { - recipient: { name: "Alice", __typename: "Person" }, - __typename: "Greeting", - }, - path: ["greeting"], - }, - ], - hasNext: false, + enqueueSubsequentChunk({ + incremental: [ + { + data: { + recipient: { name: "Alice", __typename: "Person" }, + __typename: "Greeting", + }, + path: ["greeting"], }, - }, - true - ); + ], + hasNext: false, + }); await expect(takeSnapshot()).resolves.toStrictEqualTyped({ data: { @@ -1112,10 +1048,11 @@ it('returns eventually consistent data from deferred queries with partial data i `; const cache = new InMemoryCache(); - const link = new MockSubscriptionLink(); + const { httpLink, enqueueInitialChunk, enqueueSubsequentChunk } = + mockDefer20220824(); const client = new ApolloClient({ cache, - link, + link: httpLink, incrementalHandler: new Defer20220824Handler(), }); @@ -1162,13 +1099,11 @@ it('returns eventually consistent data from deferred queries with partial data i variables: {}, }); - link.simulateResult({ - result: { - data: { - greeting: { message: "Hello world", __typename: "Greeting" }, - }, - hasNext: true, + enqueueInitialChunk({ + data: { + greeting: { message: "Hello world", __typename: "Greeting" }, }, + hasNext: true, }); await expect(takeSnapshot()).resolves.toStrictEqualTyped({ @@ -1191,23 +1126,18 @@ it('returns eventually consistent data from deferred queries with partial data i variables: {}, }); - link.simulateResult( - { - result: { - incremental: [ - { - data: { - __typename: "Greeting", - recipient: { name: "Alice", __typename: "Person" }, - }, - path: ["greeting"], - }, - ], - hasNext: false, + enqueueSubsequentChunk({ + incremental: [ + { + data: { + __typename: "Greeting", + recipient: { name: "Alice", __typename: "Person" }, + }, + path: ["greeting"], }, - }, - true - ); + ], + hasNext: false, + }); await expect(takeSnapshot()).resolves.toStrictEqualTyped({ data: { From 0612a52e5e7b2c71b893dbbe5e9ab4b72315c677 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 4 Sep 2025 18:07:10 -0600 Subject: [PATCH 48/97] Add tests for useQuery with the new defer spec --- .../useQuery/deferGraphQL17Alpha2.test.tsx | 1154 +++++++++++++++++ 1 file changed, 1154 insertions(+) create mode 100644 src/react/hooks/__tests__/useQuery/deferGraphQL17Alpha2.test.tsx diff --git a/src/react/hooks/__tests__/useQuery/deferGraphQL17Alpha2.test.tsx b/src/react/hooks/__tests__/useQuery/deferGraphQL17Alpha2.test.tsx new file mode 100644 index 00000000000..218f774691a --- /dev/null +++ b/src/react/hooks/__tests__/useQuery/deferGraphQL17Alpha2.test.tsx @@ -0,0 +1,1154 @@ +import { + disableActEnvironment, + renderHookToSnapshotStream, +} from "@testing-library/react-render-stream"; +import React from "react"; + +import { + ApolloClient, + CombinedGraphQLErrors, + gql, + InMemoryCache, + NetworkStatus, +} from "@apollo/client"; +import { GraphQL17Alpha9Handler } from "@apollo/client/incremental"; +import { ApolloProvider, useQuery } from "@apollo/client/react"; +import { + markAsStreaming, + mockDeferStreamGraphQL17Alpha9, + spyOnConsole, +} from "@apollo/client/testing/internal"; + +test("should handle deferred queries", async () => { + const query = gql` + query { + greeting { + message + ... on Greeting @defer { + recipient { + name + } + } + } + } + `; + + const { httpLink, enqueueInitialChunk, enqueueSubsequentChunk } = + mockDeferStreamGraphQL17Alpha9(); + + const client = new ApolloClient({ + link: httpLink, + cache: new InMemoryCache(), + incrementalHandler: new GraphQL17Alpha9Handler(), + }); + + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( + () => useQuery(query), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + await expect(takeSnapshot()).resolves.toStrictEqualTyped({ + data: undefined, + dataState: "empty", + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); + + enqueueInitialChunk({ + data: { + greeting: { + message: "Hello world", + __typename: "Greeting", + }, + }, + pending: [{ id: "0", path: ["greeting"] }], + hasNext: true, + }); + + await expect(takeSnapshot()).resolves.toStrictEqualTyped({ + data: markAsStreaming({ + greeting: { + message: "Hello world", + __typename: "Greeting", + }, + }), + dataState: "streaming", + loading: true, + networkStatus: NetworkStatus.streaming, + previousData: undefined, + variables: {}, + }); + + enqueueSubsequentChunk({ + incremental: [ + { + data: { + recipient: { + name: "Alice", + __typename: "Person", + }, + __typename: "Greeting", + }, + id: "0", + }, + ], + completed: [{ id: "0" }], + hasNext: false, + }); + + await expect(takeSnapshot()).resolves.toStrictEqualTyped({ + data: { + greeting: { + message: "Hello world", + __typename: "Greeting", + recipient: { + name: "Alice", + __typename: "Person", + }, + }, + }, + dataState: "complete", + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { + greeting: { + message: "Hello world", + __typename: "Greeting", + }, + }, + variables: {}, + }); + + await expect(takeSnapshot).not.toRerender(); +}); + +test("should handle deferred queries in lists", async () => { + const query = gql` + { + greetings { + message + ... on Greeting @defer { + recipient { + name + } + } + } + } + `; + + const { httpLink, enqueueInitialChunk, enqueueSubsequentChunk } = + mockDeferStreamGraphQL17Alpha9(); + + const client = new ApolloClient({ + link: httpLink, + cache: new InMemoryCache(), + incrementalHandler: new GraphQL17Alpha9Handler(), + }); + + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( + () => useQuery(query), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + await expect(takeSnapshot()).resolves.toStrictEqualTyped({ + data: undefined, + dataState: "empty", + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); + + enqueueInitialChunk({ + data: { + greetings: [ + { message: "Hello world", __typename: "Greeting" }, + { message: "Hello again", __typename: "Greeting" }, + ], + }, + pending: [ + { id: "0", path: ["greetings", 0] }, + { id: "1", path: ["greetings", 1] }, + ], + hasNext: true, + }); + + await expect(takeSnapshot()).resolves.toStrictEqualTyped({ + data: markAsStreaming({ + greetings: [ + { message: "Hello world", __typename: "Greeting" }, + { message: "Hello again", __typename: "Greeting" }, + ], + }), + dataState: "streaming", + loading: true, + networkStatus: NetworkStatus.streaming, + previousData: undefined, + variables: {}, + }); + + enqueueSubsequentChunk({ + incremental: [ + { + data: { + recipient: { + name: "Alice", + __typename: "Person", + }, + __typename: "Greeting", + }, + id: "0", + }, + { + data: { + recipient: { + name: "Bob", + __typename: "Person", + }, + __typename: "Greeting", + }, + id: "1", + }, + ], + completed: [{ id: "0" }, { id: "1" }], + hasNext: false, + }); + + await expect(takeSnapshot()).resolves.toStrictEqualTyped({ + data: { + greetings: [ + { + message: "Hello world", + __typename: "Greeting", + recipient: { name: "Alice", __typename: "Person" }, + }, + { + message: "Hello again", + __typename: "Greeting", + recipient: { name: "Bob", __typename: "Person" }, + }, + ], + }, + dataState: "complete", + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { + greetings: [ + { message: "Hello world", __typename: "Greeting" }, + { message: "Hello again", __typename: "Greeting" }, + ], + }, + variables: {}, + }); + + await expect(takeSnapshot).not.toRerender(); +}); + +test("should handle deferred queries in lists, merging arrays", async () => { + const query = gql` + query DeferVariation { + allProducts { + delivery { + ...MyFragment @defer + } + sku + id + } + } + fragment MyFragment on DeliveryEstimates { + estimatedDelivery + fastestDelivery + } + `; + + const { httpLink, enqueueInitialChunk, enqueueSubsequentChunk } = + mockDeferStreamGraphQL17Alpha9(); + + const client = new ApolloClient({ + link: httpLink, + cache: new InMemoryCache(), + incrementalHandler: new GraphQL17Alpha9Handler(), + }); + + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( + () => useQuery(query), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + await expect(takeSnapshot()).resolves.toStrictEqualTyped({ + data: undefined, + dataState: "empty", + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); + + enqueueInitialChunk({ + data: { + allProducts: [ + { + __typename: "Product", + delivery: { + __typename: "DeliveryEstimates", + }, + id: "apollo-federation", + sku: "federation", + }, + { + __typename: "Product", + delivery: { + __typename: "DeliveryEstimates", + }, + id: "apollo-studio", + sku: "studio", + }, + ], + }, + pending: [ + { id: "0", path: ["allProducts", 0, "delivery"] }, + { id: "1", path: ["allProducts", 1, "delivery"] }, + ], + hasNext: true, + }); + + await expect(takeSnapshot()).resolves.toStrictEqualTyped({ + data: markAsStreaming({ + allProducts: [ + { + __typename: "Product", + delivery: { + __typename: "DeliveryEstimates", + }, + id: "apollo-federation", + sku: "federation", + }, + { + __typename: "Product", + delivery: { + __typename: "DeliveryEstimates", + }, + id: "apollo-studio", + sku: "studio", + }, + ], + }), + dataState: "streaming", + loading: true, + networkStatus: NetworkStatus.streaming, + previousData: undefined, + variables: {}, + }); + + enqueueSubsequentChunk({ + hasNext: false, + incremental: [ + { + data: { + __typename: "DeliveryEstimates", + estimatedDelivery: "6/25/2021", + fastestDelivery: "6/24/2021", + }, + id: "0", + }, + { + data: { + __typename: "DeliveryEstimates", + estimatedDelivery: "6/25/2021", + fastestDelivery: "6/24/2021", + }, + id: "1", + }, + ], + completed: [{ id: "0" }, { id: "1" }], + }); + + await expect(takeSnapshot()).resolves.toStrictEqualTyped({ + data: markAsStreaming({ + allProducts: [ + { + __typename: "Product", + delivery: { + __typename: "DeliveryEstimates", + estimatedDelivery: "6/25/2021", + fastestDelivery: "6/24/2021", + }, + id: "apollo-federation", + sku: "federation", + }, + { + __typename: "Product", + delivery: { + __typename: "DeliveryEstimates", + estimatedDelivery: "6/25/2021", + fastestDelivery: "6/24/2021", + }, + id: "apollo-studio", + sku: "studio", + }, + ], + }), + dataState: "complete", + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { + allProducts: [ + { + __typename: "Product", + delivery: { + __typename: "DeliveryEstimates", + }, + id: "apollo-federation", + sku: "federation", + }, + { + __typename: "Product", + delivery: { + __typename: "DeliveryEstimates", + }, + id: "apollo-studio", + sku: "studio", + }, + ], + }, + variables: {}, + }); +}); + +test("should handle deferred queries with fetch policy no-cache", async () => { + const query = gql` + { + greeting { + message + ... on Greeting @defer { + recipient { + name + } + } + } + } + `; + + const { httpLink, enqueueInitialChunk, enqueueSubsequentChunk } = + mockDeferStreamGraphQL17Alpha9(); + + const client = new ApolloClient({ + link: httpLink, + cache: new InMemoryCache(), + incrementalHandler: new GraphQL17Alpha9Handler(), + }); + + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( + () => useQuery(query, { fetchPolicy: "no-cache" }), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + await expect(takeSnapshot()).resolves.toStrictEqualTyped({ + data: undefined, + dataState: "empty", + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); + + enqueueInitialChunk({ + data: { + greeting: { + message: "Hello world", + __typename: "Greeting", + }, + }, + pending: [{ id: "0", path: ["greeting"] }], + hasNext: true, + }); + + await expect(takeSnapshot()).resolves.toStrictEqualTyped({ + data: markAsStreaming({ + greeting: { + message: "Hello world", + __typename: "Greeting", + }, + }), + dataState: "streaming", + loading: true, + networkStatus: NetworkStatus.streaming, + previousData: undefined, + variables: {}, + }); + + enqueueSubsequentChunk({ + incremental: [ + { + data: { + recipient: { + name: "Alice", + __typename: "Person", + }, + __typename: "Greeting", + }, + id: "0", + }, + ], + completed: [{ id: "0" }], + hasNext: false, + }); + + await expect(takeSnapshot()).resolves.toStrictEqualTyped({ + data: { + greeting: { + message: "Hello world", + __typename: "Greeting", + recipient: { + name: "Alice", + __typename: "Person", + }, + }, + }, + dataState: "complete", + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); + + await expect(takeSnapshot).not.toRerender(); +}); + +test("should handle deferred queries with errors returned on the incremental batched result", async () => { + const query = gql` + query { + hero { + name + heroFriends { + id + name + ... @defer { + homeWorld + } + } + } + } + `; + + const { httpLink, enqueueInitialChunk, enqueueSubsequentChunk } = + mockDeferStreamGraphQL17Alpha9(); + + const client = new ApolloClient({ + link: httpLink, + cache: new InMemoryCache(), + incrementalHandler: new GraphQL17Alpha9Handler(), + }); + + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( + () => useQuery(query), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + await expect(takeSnapshot()).resolves.toStrictEqualTyped({ + data: undefined, + dataState: "empty", + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); + + enqueueInitialChunk({ + data: { + hero: { + name: "R2-D2", + heroFriends: [ + { + id: "1000", + name: "Luke Skywalker", + }, + { + id: "1003", + name: "Leia Organa", + }, + ], + }, + }, + pending: [ + { id: "0", path: ["hero", "heroFriends", 0] }, + { id: "1", path: ["hero", "heroFriends", 1] }, + ], + hasNext: true, + }); + + await expect(takeSnapshot()).resolves.toStrictEqualTyped({ + data: markAsStreaming({ + hero: { + heroFriends: [ + { + id: "1000", + name: "Luke Skywalker", + }, + { + id: "1003", + name: "Leia Organa", + }, + ], + name: "R2-D2", + }, + }), + dataState: "streaming", + loading: true, + networkStatus: NetworkStatus.streaming, + previousData: undefined, + variables: {}, + }); + + enqueueSubsequentChunk({ + incremental: [ + { + errors: [ + { + message: + "homeWorld for character with ID 1000 could not be fetched.", + path: ["hero", "heroFriends", 0, "homeWorld"], + }, + ], + data: { + homeWorld: null, + }, + id: "0", + }, + { + data: { + homeWorld: "Alderaan", + }, + id: "1", + }, + ], + completed: [{ id: "0" }, { id: "1" }], + hasNext: false, + }); + + await expect(takeSnapshot()).resolves.toStrictEqualTyped({ + data: { + hero: { + heroFriends: [ + { + id: "1000", + name: "Luke Skywalker", + }, + { + id: "1003", + name: "Leia Organa", + }, + ], + name: "R2-D2", + }, + }, + dataState: "complete", + error: new CombinedGraphQLErrors({ + data: { + hero: { + heroFriends: [ + { + id: "1000", + name: "Luke Skywalker", + homeWorld: null, + }, + { + id: "1003", + name: "Leia Organa", + homeWorld: "Alderaan", + }, + ], + name: "R2-D2", + }, + }, + errors: [ + { + message: "homeWorld for character with ID 1000 could not be fetched.", + path: ["hero", "heroFriends", 0, "homeWorld"], + }, + ], + }), + loading: false, + networkStatus: NetworkStatus.error, + previousData: undefined, + variables: {}, + }); + + await expect(takeSnapshot).not.toRerender(); +}); + +test('should handle deferred queries with errors returned on the incremental batched result and errorPolicy "all"', async () => { + const query = gql` + query { + hero { + name + heroFriends { + id + name + ... @defer { + homeWorld + } + } + } + } + `; + + const { httpLink, enqueueInitialChunk, enqueueSubsequentChunk } = + mockDeferStreamGraphQL17Alpha9(); + + const client = new ApolloClient({ + link: httpLink, + cache: new InMemoryCache(), + incrementalHandler: new GraphQL17Alpha9Handler(), + }); + + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( + () => useQuery(query, { errorPolicy: "all" }), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + await expect(takeSnapshot()).resolves.toStrictEqualTyped({ + data: undefined, + dataState: "empty", + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); + + enqueueInitialChunk({ + data: { + hero: { + name: "R2-D2", + heroFriends: [ + { + id: "1000", + name: "Luke Skywalker", + }, + { + id: "1003", + name: "Leia Organa", + }, + ], + }, + }, + pending: [ + { id: "0", path: ["hero", "heroFriends", 0] }, + { id: "1", path: ["hero", "heroFriends", 1] }, + ], + hasNext: true, + }); + + await expect(takeSnapshot()).resolves.toStrictEqualTyped({ + data: markAsStreaming({ + hero: { + name: "R2-D2", + heroFriends: [ + { + id: "1000", + name: "Luke Skywalker", + }, + { + id: "1003", + name: "Leia Organa", + }, + ], + }, + }), + dataState: "streaming", + loading: true, + networkStatus: NetworkStatus.streaming, + previousData: undefined, + variables: {}, + }); + + enqueueSubsequentChunk({ + incremental: [ + { + errors: [ + { + message: + "homeWorld for character with ID 1000 could not be fetched.", + path: ["hero", "heroFriends", 0, "homeWorld"], + }, + ], + data: { + homeWorld: null, + }, + id: "0", + extensions: { + thing1: "foo", + thing2: "bar", + }, + }, + { + data: { + homeWorld: "Alderaan", + }, + id: "1", + extensions: { + thing1: "foo", + thing2: "bar", + }, + }, + ], + completed: [{ id: "0" }, { id: "1" }], + hasNext: false, + }); + + await expect(takeSnapshot()).resolves.toStrictEqualTyped({ + data: { + hero: { + heroFriends: [ + { + // the only difference with the previous test + // is that homeWorld is populated since errorPolicy: all + // populates both partial data and error.graphQLErrors + homeWorld: null, + id: "1000", + name: "Luke Skywalker", + }, + { + // homeWorld is populated due to errorPolicy: all + homeWorld: "Alderaan", + id: "1003", + name: "Leia Organa", + }, + ], + name: "R2-D2", + }, + }, + dataState: "complete", + error: new CombinedGraphQLErrors({ + data: { + hero: { + heroFriends: [ + { homeWorld: null, id: "1000", name: "Luke Skywalker" }, + { homeWorld: "Alderaan", id: "1003", name: "Leia Organa" }, + ], + name: "R2-D2", + }, + }, + errors: [ + { + message: "homeWorld for character with ID 1000 could not be fetched.", + path: ["hero", "heroFriends", 0, "homeWorld"], + }, + ], + extensions: { + thing1: "foo", + thing2: "bar", + }, + }), + loading: false, + networkStatus: NetworkStatus.error, + previousData: { + hero: { + heroFriends: [ + { + id: "1000", + name: "Luke Skywalker", + }, + { + id: "1003", + name: "Leia Organa", + }, + ], + name: "R2-D2", + }, + }, + variables: {}, + }); + + await expect(takeSnapshot).not.toRerender(); +}); + +test('returns eventually consistent data from deferred queries with data in the cache while using a "cache-and-network" fetch policy', async () => { + const query = gql` + query { + greeting { + message + ... on Greeting @defer { + recipient { + name + } + } + } + } + `; + + const { httpLink, enqueueInitialChunk, enqueueSubsequentChunk } = + mockDeferStreamGraphQL17Alpha9(); + const cache = new InMemoryCache(); + const client = new ApolloClient({ + cache, + link: httpLink, + incrementalHandler: new GraphQL17Alpha9Handler(), + }); + + cache.writeQuery({ + query, + data: { + greeting: { + __typename: "Greeting", + message: "Hello cached", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + }); + + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( + () => useQuery(query, { fetchPolicy: "cache-and-network" }), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + await expect(takeSnapshot()).resolves.toStrictEqualTyped({ + data: { + greeting: { + __typename: "Greeting", + message: "Hello cached", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + dataState: "complete", + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); + + enqueueInitialChunk({ + data: { + greeting: { __typename: "Greeting", message: "Hello world" }, + }, + pending: [{ id: "0", path: ["greeting"] }], + hasNext: true, + }); + + await expect(takeSnapshot()).resolves.toStrictEqualTyped({ + data: markAsStreaming({ + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }), + dataState: "streaming", + loading: true, + networkStatus: NetworkStatus.streaming, + previousData: { + greeting: { + __typename: "Greeting", + message: "Hello cached", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + variables: {}, + }); + + enqueueSubsequentChunk({ + incremental: [ + { + data: { + recipient: { name: "Alice", __typename: "Person" }, + __typename: "Greeting", + }, + id: "0", + }, + ], + completed: [{ id: "0" }], + hasNext: false, + }); + + await expect(takeSnapshot()).resolves.toStrictEqualTyped({ + data: { + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Alice" }, + }, + }, + dataState: "complete", + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + variables: {}, + }); + + await expect(takeSnapshot).not.toRerender(); +}); + +test('returns eventually consistent data from deferred queries with partial data in the cache and using a "cache-first" fetch policy with `returnPartialData`', async () => { + const query = gql` + query { + greeting { + message + ... on Greeting @defer { + recipient { + name + } + } + } + } + `; + + const cache = new InMemoryCache(); + const { httpLink, enqueueInitialChunk, enqueueSubsequentChunk } = + mockDeferStreamGraphQL17Alpha9(); + const client = new ApolloClient({ + cache, + link: httpLink, + incrementalHandler: new GraphQL17Alpha9Handler(), + }); + + // We know we are writing partial data to the cache so suppress the console + // warning. + { + using _consoleSpy = spyOnConsole("error"); + cache.writeQuery({ + query, + data: { + greeting: { + __typename: "Greeting", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + }); + } + + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( + () => + useQuery(query, { + fetchPolicy: "cache-first", + returnPartialData: true, + }), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + await expect(takeSnapshot()).resolves.toStrictEqualTyped({ + data: { + greeting: { + __typename: "Greeting", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + dataState: "partial", + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); + + enqueueInitialChunk({ + data: { + greeting: { message: "Hello world", __typename: "Greeting" }, + }, + pending: [{ id: "0", path: ["greeting"] }], + hasNext: true, + }); + + await expect(takeSnapshot()).resolves.toStrictEqualTyped({ + data: markAsStreaming({ + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }), + dataState: "streaming", + loading: true, + networkStatus: NetworkStatus.streaming, + previousData: { + greeting: { + __typename: "Greeting", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + variables: {}, + }); + + enqueueSubsequentChunk({ + incremental: [ + { + data: { + __typename: "Greeting", + recipient: { name: "Alice", __typename: "Person" }, + }, + id: "0", + }, + ], + completed: [{ id: "0" }], + hasNext: false, + }); + + await expect(takeSnapshot()).resolves.toStrictEqualTyped({ + data: { + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Alice" }, + }, + }, + dataState: "complete", + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + variables: {}, + }); + + await expect(takeSnapshot).not.toRerender(); +}); From af769e703d54e58228e3bc0698a8df5a6d3bb009 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 4 Sep 2025 18:40:55 -0600 Subject: [PATCH 49/97] Port first defer test for useSuspenseQuery to own file --- .../hooks/__tests__/useSuspenseQuery.test.tsx | 100 ------------- .../useSuspenseQuery/defer20220824.test.tsx | 137 ++++++++++++++++++ 2 files changed, 137 insertions(+), 100 deletions(-) create mode 100644 src/react/hooks/__tests__/useSuspenseQuery/defer20220824.test.tsx diff --git a/src/react/hooks/__tests__/useSuspenseQuery.test.tsx b/src/react/hooks/__tests__/useSuspenseQuery.test.tsx index 5ba0dce3d1b..1f40289dc4d 100644 --- a/src/react/hooks/__tests__/useSuspenseQuery.test.tsx +++ b/src/react/hooks/__tests__/useSuspenseQuery.test.tsx @@ -7137,106 +7137,6 @@ describe("useSuspenseQuery", () => { expect(client.getObservableQueries().size).toBe(1); }); - it("suspends deferred queries until initial chunk loads then streams in data as it loads", async () => { - const query = gql` - query { - greeting { - message - ... on Greeting @defer { - recipient { - name - } - } - } - } - `; - - const link = new MockSubscriptionLink(); - - const { result, renders } = await renderSuspenseHook( - () => useSuspenseQuery(query), - { link, incrementalHandler: new Defer20220824Handler() } - ); - - expect(renders.suspenseCount).toBe(1); - - link.simulateResult({ - result: { - data: { greeting: { message: "Hello world", __typename: "Greeting" } }, - hasNext: true, - }, - }); - - await waitFor(() => { - expect(result.current).toStrictEqualTyped({ - data: markAsStreaming({ - greeting: { message: "Hello world", __typename: "Greeting" }, - }), - dataState: "streaming", - networkStatus: NetworkStatus.streaming, - error: undefined, - }); - }); - - link.simulateResult( - { - result: { - incremental: [ - { - data: { - recipient: { name: "Alice", __typename: "Person" }, - __typename: "Greeting", - }, - path: ["greeting"], - }, - ], - hasNext: false, - }, - }, - true - ); - - await waitFor(() => { - expect(result.current).toStrictEqualTyped({ - data: { - greeting: { - __typename: "Greeting", - message: "Hello world", - recipient: { __typename: "Person", name: "Alice" }, - }, - }, - dataState: "complete", - networkStatus: NetworkStatus.ready, - error: undefined, - }); - }); - - expect(renders.count).toBe(3 + (IS_REACT_19 ? renders.suspenseCount : 0)); - expect(renders.suspenseCount).toBe(1); - expect(renders.frames).toStrictEqualTyped([ - { - data: markAsStreaming({ - greeting: { message: "Hello world", __typename: "Greeting" }, - }), - dataState: "streaming", - networkStatus: NetworkStatus.streaming, - error: undefined, - }, - { - data: { - greeting: { - __typename: "Greeting", - message: "Hello world", - recipient: { __typename: "Person", name: "Alice" }, - }, - }, - dataState: "complete", - networkStatus: NetworkStatus.ready, - error: undefined, - }, - ]); - }); - it.each([ "cache-first", "network-only", diff --git a/src/react/hooks/__tests__/useSuspenseQuery/defer20220824.test.tsx b/src/react/hooks/__tests__/useSuspenseQuery/defer20220824.test.tsx new file mode 100644 index 00000000000..6c3ae5f41ef --- /dev/null +++ b/src/react/hooks/__tests__/useSuspenseQuery/defer20220824.test.tsx @@ -0,0 +1,137 @@ +import { + createRenderStream, + disableActEnvironment, + useTrackRenders, +} from "@testing-library/react-render-stream"; +import React, { Suspense } from "react"; +import { ErrorBoundary } from "react-error-boundary"; + +import { + ApolloClient, + gql, + InMemoryCache, + NetworkStatus, +} from "@apollo/client"; +import { Defer20220824Handler } from "@apollo/client/incremental"; +import { ApolloProvider, useSuspenseQuery } from "@apollo/client/react"; +import { + markAsStreaming, + mockDefer20220824, +} from "@apollo/client/testing/internal"; + +test("suspends deferred queries until initial chunk loads then streams in data as it loads", async () => { + const query = gql` + query { + greeting { + message + ... on Greeting @defer { + recipient { + name + } + } + } + } + `; + + function Component() { + useTrackRenders(); + + const result = useSuspenseQuery(query); + replaceSnapshot(result); + + return null; + } + + function SuspenseFallback() { + useTrackRenders(); + + return null; + } + + function App() { + return ( + }> + + + ); + } + + const { httpLink, enqueueInitialChunk, enqueueSubsequentChunk } = + mockDefer20220824(); + + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: httpLink, + incrementalHandler: new Defer20220824Handler(), + }); + + using _disabledAct = disableActEnvironment(); + const { takeRender, replaceSnapshot, render } = + createRenderStream< + useSuspenseQuery.Result + >(); + + await render(, { + wrapper: ({ children }) => ( + {children} + ), + }); + + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual([SuspenseFallback]); + } + + enqueueInitialChunk({ + data: { greeting: { message: "Hello world", __typename: "Greeting" } }, + hasNext: true, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual([Component]); + expect(snapshot).toStrictEqualTyped({ + data: markAsStreaming({ + greeting: { message: "Hello world", __typename: "Greeting" }, + }), + dataState: "streaming", + networkStatus: NetworkStatus.streaming, + error: undefined, + }); + } + + enqueueSubsequentChunk({ + incremental: [ + { + data: { + recipient: { name: "Alice", __typename: "Person" }, + __typename: "Greeting", + }, + path: ["greeting"], + }, + ], + hasNext: false, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual([Component]); + expect(snapshot).toStrictEqualTyped({ + data: { + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Alice" }, + }, + }, + dataState: "complete", + networkStatus: NetworkStatus.ready, + error: undefined, + }); + } + + await expect(takeRender).not.toRerender(); +}); From 45bf6ab60209867adc0ff98de5cf3174830b85bf Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 4 Sep 2025 19:24:33 -0600 Subject: [PATCH 50/97] Extract render helper --- .../useSuspenseQuery/defer20220824.test.tsx | 83 +++++++++++-------- 1 file changed, 49 insertions(+), 34 deletions(-) diff --git a/src/react/hooks/__tests__/useSuspenseQuery/defer20220824.test.tsx b/src/react/hooks/__tests__/useSuspenseQuery/defer20220824.test.tsx index 6c3ae5f41ef..08a8824998d 100644 --- a/src/react/hooks/__tests__/useSuspenseQuery/defer20220824.test.tsx +++ b/src/react/hooks/__tests__/useSuspenseQuery/defer20220824.test.tsx @@ -1,11 +1,12 @@ +import type { RenderOptions } from "@testing-library/react"; import { createRenderStream, disableActEnvironment, useTrackRenders, } from "@testing-library/react-render-stream"; import React, { Suspense } from "react"; -import { ErrorBoundary } from "react-error-boundary"; +import type { OperationVariables } from "@apollo/client"; import { ApolloClient, gql, @@ -19,31 +20,21 @@ import { mockDefer20220824, } from "@apollo/client/testing/internal"; -test("suspends deferred queries until initial chunk loads then streams in data as it loads", async () => { - const query = gql` - query { - greeting { - message - ... on Greeting @defer { - recipient { - name - } - } - } - } - `; - - function Component() { - useTrackRenders(); +const IS_REACT_19 = React.version.startsWith("19"); - const result = useSuspenseQuery(query); - replaceSnapshot(result); +async function renderSuspenseHook( + renderHook: () => useSuspenseQuery.Result, + options: Pick +) { + function UseSuspenseQuery() { + useTrackRenders({ name: "useSuspenseQuery" }); + renderStream.replaceSnapshot(renderHook()); return null; } function SuspenseFallback() { - useTrackRenders(); + useTrackRenders({ name: "SuspenseFallback" }); return null; } @@ -51,11 +42,37 @@ test("suspends deferred queries until initial chunk loads then streams in data a function App() { return ( }> - + ); } + const { render, takeRender, ...renderStream } = + createRenderStream>(); + + const utils = await render(, options); + + function rerender() { + return utils.rerender(); + } + + return { takeRender, rerender }; +} + +test("suspends deferred queries until initial chunk loads then streams in data as it loads", async () => { + const query = gql` + query { + greeting { + message + ... on Greeting @defer { + recipient { + name + } + } + } + } + `; + const { httpLink, enqueueInitialChunk, enqueueSubsequentChunk } = mockDefer20220824(); @@ -66,21 +83,19 @@ test("suspends deferred queries until initial chunk loads then streams in data a }); using _disabledAct = disableActEnvironment(); - const { takeRender, replaceSnapshot, render } = - createRenderStream< - useSuspenseQuery.Result - >(); - - await render(, { - wrapper: ({ children }) => ( - {children} - ), - }); + const { takeRender } = await renderSuspenseHook( + () => useSuspenseQuery(query), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); { const { renderedComponents } = await takeRender(); - expect(renderedComponents).toStrictEqual([SuspenseFallback]); + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); } enqueueInitialChunk({ @@ -91,7 +106,7 @@ test("suspends deferred queries until initial chunk loads then streams in data a { const { snapshot, renderedComponents } = await takeRender(); - expect(renderedComponents).toStrictEqual([Component]); + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); expect(snapshot).toStrictEqualTyped({ data: markAsStreaming({ greeting: { message: "Hello world", __typename: "Greeting" }, @@ -118,7 +133,7 @@ test("suspends deferred queries until initial chunk loads then streams in data a { const { snapshot, renderedComponents } = await takeRender(); - expect(renderedComponents).toStrictEqual([Component]); + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); expect(snapshot).toStrictEqualTyped({ data: { greeting: { From ae21e84bfd562883bfda8fa1968cdfe51d8103cc Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 4 Sep 2025 19:34:34 -0600 Subject: [PATCH 51/97] Migrate useSuspenseQuery defer tests to own file with renderStream --- .../hooks/__tests__/useSuspenseQuery.test.tsx | 2763 +---------------- .../useSuspenseQuery/defer20220824.test.tsx | 2408 +++++++++++++- 2 files changed, 2395 insertions(+), 2776 deletions(-) diff --git a/src/react/hooks/__tests__/useSuspenseQuery.test.tsx b/src/react/hooks/__tests__/useSuspenseQuery.test.tsx index 1f40289dc4d..31ea6583555 100644 --- a/src/react/hooks/__tests__/useSuspenseQuery.test.tsx +++ b/src/react/hooks/__tests__/useSuspenseQuery.test.tsx @@ -32,10 +32,7 @@ import { NetworkStatus, } from "@apollo/client"; import type { Incremental } from "@apollo/client/incremental"; -import { - Defer20220824Handler, - NotImplementedHandler, -} from "@apollo/client/incremental"; +import { NotImplementedHandler } from "@apollo/client/incremental"; import type { Unmasked } from "@apollo/client/masking"; import { ApolloProvider, @@ -50,7 +47,6 @@ import type { import { actAsync, createClientWrapper, - markAsStreaming, renderAsync, renderHookAsync, setupPaginatedCase, @@ -7137,2763 +7133,6 @@ describe("useSuspenseQuery", () => { expect(client.getObservableQueries().size).toBe(1); }); - it.each([ - "cache-first", - "network-only", - "no-cache", - "cache-and-network", - ])( - 'suspends deferred queries until initial chunk loads then streams in data as it loads when using a "%s" fetch policy', - async (fetchPolicy) => { - const query = gql` - query { - greeting { - message - ... on Greeting @defer { - recipient { - name - } - } - } - } - `; - - const link = new MockSubscriptionLink(); - - const { result, renders } = await renderSuspenseHook( - () => useSuspenseQuery(query, { fetchPolicy }), - { link, incrementalHandler: new Defer20220824Handler() } - ); - - expect(renders.suspenseCount).toBe(1); - - link.simulateResult({ - result: { - data: { - greeting: { message: "Hello world", __typename: "Greeting" }, - }, - hasNext: true, - }, - }); - - await waitFor(() => { - expect(result.current).toStrictEqualTyped({ - data: markAsStreaming({ - greeting: { message: "Hello world", __typename: "Greeting" }, - }), - dataState: "streaming", - networkStatus: NetworkStatus.streaming, - error: undefined, - }); - }); - - link.simulateResult( - { - result: { - incremental: [ - { - data: { - recipient: { name: "Alice", __typename: "Person" }, - __typename: "Greeting", - }, - path: ["greeting"], - }, - ], - hasNext: false, - }, - }, - true - ); - - await waitFor(() => { - expect(result.current).toStrictEqualTyped({ - data: { - greeting: { - __typename: "Greeting", - message: "Hello world", - recipient: { __typename: "Person", name: "Alice" }, - }, - }, - dataState: "complete", - networkStatus: NetworkStatus.ready, - error: undefined, - }); - }); - - expect(renders.count).toBe(3 + (IS_REACT_19 ? renders.suspenseCount : 0)); - expect(renders.suspenseCount).toBe(1); - expect(renders.frames).toStrictEqualTyped([ - { - data: markAsStreaming({ - greeting: { message: "Hello world", __typename: "Greeting" }, - }), - dataState: "streaming", - networkStatus: NetworkStatus.streaming, - error: undefined, - }, - { - data: { - greeting: { - __typename: "Greeting", - message: "Hello world", - recipient: { __typename: "Person", name: "Alice" }, - }, - }, - dataState: "complete", - networkStatus: NetworkStatus.ready, - error: undefined, - }, - ]); - } - ); - - it('does not suspend deferred queries with data in the cache and using a "cache-first" fetch policy', async () => { - const query = gql` - query { - greeting { - message - ... on Greeting @defer { - recipient { - name - } - } - } - } - `; - - const cache = new InMemoryCache(); - - cache.writeQuery({ - query, - data: { - greeting: { - __typename: "Greeting", - message: "Hello world", - recipient: { __typename: "Person", name: "Alice" }, - }, - }, - }); - - const { result, renders } = await renderSuspenseHook( - () => useSuspenseQuery(query, { fetchPolicy: "cache-first" }), - { cache, incrementalHandler: new Defer20220824Handler() } - ); - - expect(result.current).toStrictEqualTyped({ - data: { - greeting: { - message: "Hello world", - __typename: "Greeting", - recipient: { __typename: "Person", name: "Alice" }, - }, - }, - dataState: "complete", - networkStatus: NetworkStatus.ready, - error: undefined, - }); - - expect(renders.suspenseCount).toBe(0); - expect(renders.frames).toStrictEqualTyped([ - { - data: { - greeting: { - __typename: "Greeting", - message: "Hello world", - recipient: { __typename: "Person", name: "Alice" }, - }, - }, - dataState: "complete", - networkStatus: NetworkStatus.ready, - error: undefined, - }, - ]); - }); - - it('does not suspend deferred queries with partial data in the cache and using a "cache-first" fetch policy with `returnPartialData`', async () => { - const query = gql` - query { - greeting { - message - ... on Greeting @defer { - recipient { - name - } - } - } - } - `; - - const link = new MockSubscriptionLink(); - const cache = new InMemoryCache(); - - // We are intentionally writing partial data to the cache. Supress console - // warnings to avoid unnecessary noise in the test. - { - using _consoleSpy = spyOnConsole("error"); - cache.writeQuery({ - query, - data: { - greeting: { - __typename: "Greeting", - recipient: { __typename: "Person", name: "Cached Alice" }, - }, - }, - }); - } - - const { result, renders } = await renderSuspenseHook( - () => - useSuspenseQuery(query, { - fetchPolicy: "cache-first", - returnPartialData: true, - }), - { cache, link, incrementalHandler: new Defer20220824Handler() } - ); - - expect(result.current).toStrictEqualTyped({ - data: { - greeting: { - __typename: "Greeting", - recipient: { __typename: "Person", name: "Cached Alice" }, - }, - }, - dataState: "partial", - networkStatus: NetworkStatus.loading, - error: undefined, - }); - - link.simulateResult({ - result: { - data: { greeting: { message: "Hello world", __typename: "Greeting" } }, - hasNext: true, - }, - }); - - await waitFor(() => { - expect(result.current).toStrictEqualTyped({ - data: markAsStreaming({ - greeting: { - __typename: "Greeting", - message: "Hello world", - recipient: { __typename: "Person", name: "Cached Alice" }, - }, - }), - dataState: "streaming", - networkStatus: NetworkStatus.streaming, - error: undefined, - }); - }); - - link.simulateResult( - { - result: { - incremental: [ - { - data: { - __typename: "Greeting", - recipient: { name: "Alice", __typename: "Person" }, - }, - path: ["greeting"], - }, - ], - hasNext: false, - }, - }, - true - ); - - await waitFor(() => { - expect(result.current).toStrictEqualTyped({ - data: { - greeting: { - __typename: "Greeting", - message: "Hello world", - recipient: { __typename: "Person", name: "Alice" }, - }, - }, - dataState: "complete", - networkStatus: NetworkStatus.ready, - error: undefined, - }); - }); - - expect(renders.count).toBe(3 + (IS_REACT_19 ? renders.suspenseCount : 0)); - expect(renders.suspenseCount).toBe(0); - expect(renders.frames).toStrictEqualTyped([ - { - data: { - greeting: { - __typename: "Greeting", - recipient: { __typename: "Person", name: "Cached Alice" }, - }, - }, - dataState: "partial", - networkStatus: NetworkStatus.loading, - error: undefined, - }, - { - data: markAsStreaming({ - greeting: { - __typename: "Greeting", - message: "Hello world", - recipient: { __typename: "Person", name: "Cached Alice" }, - }, - }), - dataState: "streaming", - networkStatus: NetworkStatus.streaming, - error: undefined, - }, - { - data: { - greeting: { - __typename: "Greeting", - message: "Hello world", - recipient: { __typename: "Person", name: "Alice" }, - }, - }, - dataState: "complete", - networkStatus: NetworkStatus.ready, - error: undefined, - }, - ]); - }); - - it('does not suspend deferred queries with data in the cache and using a "cache-and-network" fetch policy', async () => { - const query = gql` - query { - greeting { - message - ... on Greeting @defer { - recipient { - name - } - } - } - } - `; - - const link = new MockSubscriptionLink(); - const cache = new InMemoryCache(); - const client = new ApolloClient({ - cache, - link, - incrementalHandler: new Defer20220824Handler(), - }); - - cache.writeQuery({ - query, - data: { - greeting: { - __typename: "Greeting", - message: "Hello cached", - recipient: { __typename: "Person", name: "Cached Alice" }, - }, - }, - }); - - const { result, renders } = await renderSuspenseHook( - () => useSuspenseQuery(query, { fetchPolicy: "cache-and-network" }), - { client } - ); - - expect(result.current).toStrictEqualTyped({ - data: { - greeting: { - message: "Hello cached", - __typename: "Greeting", - recipient: { __typename: "Person", name: "Cached Alice" }, - }, - }, - dataState: "complete", - networkStatus: NetworkStatus.loading, - error: undefined, - }); - - link.simulateResult({ - result: { - data: { greeting: { __typename: "Greeting", message: "Hello world" } }, - hasNext: true, - }, - }); - - await waitFor(() => { - expect(result.current).toStrictEqualTyped({ - data: markAsStreaming({ - greeting: { - __typename: "Greeting", - message: "Hello world", - recipient: { __typename: "Person", name: "Cached Alice" }, - }, - }), - dataState: "streaming", - networkStatus: NetworkStatus.streaming, - error: undefined, - }); - }); - - link.simulateResult( - { - result: { - incremental: [ - { - data: { - recipient: { name: "Alice", __typename: "Person" }, - __typename: "Greeting", - }, - path: ["greeting"], - }, - ], - hasNext: false, - }, - }, - true - ); - - await waitFor(() => { - expect(result.current).toStrictEqualTyped({ - data: { - greeting: { - __typename: "Greeting", - message: "Hello world", - recipient: { __typename: "Person", name: "Alice" }, - }, - }, - dataState: "complete", - networkStatus: NetworkStatus.ready, - error: undefined, - }); - }); - - expect(renders.count).toBe(3 + (IS_REACT_19 ? renders.suspenseCount : 0)); - expect(renders.suspenseCount).toBe(0); - expect(renders.frames).toStrictEqualTyped([ - { - data: { - greeting: { - __typename: "Greeting", - message: "Hello cached", - recipient: { __typename: "Person", name: "Cached Alice" }, - }, - }, - dataState: "complete", - networkStatus: NetworkStatus.loading, - error: undefined, - }, - { - data: markAsStreaming({ - greeting: { - __typename: "Greeting", - message: "Hello world", - recipient: { __typename: "Person", name: "Cached Alice" }, - }, - }), - dataState: "streaming", - networkStatus: NetworkStatus.streaming, - error: undefined, - }, - { - data: { - greeting: { - __typename: "Greeting", - message: "Hello world", - recipient: { __typename: "Person", name: "Alice" }, - }, - }, - dataState: "complete", - networkStatus: NetworkStatus.ready, - error: undefined, - }, - ]); - }); - - it("suspends deferred queries with lists and properly patches results", async () => { - const query = gql` - query { - greetings { - message - ... on Greeting @defer { - recipient { - name - } - } - } - } - `; - - const link = new MockSubscriptionLink(); - - const { result, renders } = await renderSuspenseHook( - () => useSuspenseQuery(query), - { link, incrementalHandler: new Defer20220824Handler() } - ); - - expect(renders.suspenseCount).toBe(1); - - link.simulateResult({ - result: { - data: { - greetings: [ - { __typename: "Greeting", message: "Hello world" }, - { __typename: "Greeting", message: "Hello again" }, - ], - }, - hasNext: true, - }, - }); - - await waitFor(() => { - expect(result.current).toStrictEqualTyped({ - data: markAsStreaming({ - greetings: [ - { __typename: "Greeting", message: "Hello world" }, - { __typename: "Greeting", message: "Hello again" }, - ], - }), - dataState: "streaming", - networkStatus: NetworkStatus.streaming, - error: undefined, - }); - }); - - link.simulateResult({ - result: { - incremental: [ - { - data: { - __typename: "Greeting", - recipient: { __typename: "Person", name: "Alice" }, - }, - path: ["greetings", 0], - }, - ], - hasNext: true, - }, - }); - - await waitFor(() => { - expect(result.current).toStrictEqualTyped({ - data: markAsStreaming({ - greetings: [ - { - __typename: "Greeting", - message: "Hello world", - recipient: { __typename: "Person", name: "Alice" }, - }, - { - __typename: "Greeting", - message: "Hello again", - }, - ], - }), - dataState: "streaming", - networkStatus: NetworkStatus.streaming, - error: undefined, - }); - }); - - link.simulateResult( - { - result: { - incremental: [ - { - data: { - __typename: "Greeting", - recipient: { __typename: "Person", name: "Bob" }, - }, - path: ["greetings", 1], - }, - ], - hasNext: false, - }, - }, - true - ); - - await waitFor(() => { - expect(result.current).toStrictEqualTyped({ - data: { - greetings: [ - { - __typename: "Greeting", - message: "Hello world", - recipient: { __typename: "Person", name: "Alice" }, - }, - { - __typename: "Greeting", - message: "Hello again", - recipient: { __typename: "Person", name: "Bob" }, - }, - ], - }, - dataState: "complete", - networkStatus: NetworkStatus.ready, - error: undefined, - }); - }); - - expect(renders.count).toBe(4 + (IS_REACT_19 ? renders.suspenseCount : 0)); - expect(renders.suspenseCount).toBe(1); - expect(renders.frames).toStrictEqualTyped([ - { - data: markAsStreaming({ - greetings: [ - { __typename: "Greeting", message: "Hello world" }, - { __typename: "Greeting", message: "Hello again" }, - ], - }), - dataState: "streaming", - networkStatus: NetworkStatus.streaming, - error: undefined, - }, - { - data: markAsStreaming({ - greetings: [ - { - __typename: "Greeting", - message: "Hello world", - recipient: { __typename: "Person", name: "Alice" }, - }, - { - __typename: "Greeting", - message: "Hello again", - }, - ], - }), - dataState: "streaming", - networkStatus: NetworkStatus.streaming, - error: undefined, - }, - { - data: { - greetings: [ - { - __typename: "Greeting", - message: "Hello world", - recipient: { __typename: "Person", name: "Alice" }, - }, - { - __typename: "Greeting", - message: "Hello again", - recipient: { __typename: "Person", name: "Bob" }, - }, - ], - }, - dataState: "complete", - networkStatus: NetworkStatus.ready, - error: undefined, - }, - ]); - }); - - it("suspends queries with deferred fragments in lists and properly merges arrays", async () => { - const query = gql` - query DeferVariation { - allProducts { - delivery { - ...MyFragment @defer - } - sku - id - } - } - - fragment MyFragment on DeliveryEstimates { - estimatedDelivery - fastestDelivery - } - `; - - const link = new MockSubscriptionLink(); - - const { result, renders } = await renderSuspenseHook( - () => useSuspenseQuery(query), - { link, incrementalHandler: new Defer20220824Handler() } - ); - - expect(renders.suspenseCount).toBe(1); - - link.simulateResult({ - result: { - data: { - allProducts: [ - { - __typename: "Product", - delivery: { - __typename: "DeliveryEstimates", - }, - id: "apollo-federation", - sku: "federation", - }, - { - __typename: "Product", - delivery: { - __typename: "DeliveryEstimates", - }, - id: "apollo-studio", - sku: "studio", - }, - ], - }, - hasNext: true, - }, - }); - - await waitFor(() => { - expect(result.current).toStrictEqualTyped({ - data: markAsStreaming({ - allProducts: [ - { - __typename: "Product", - delivery: { - __typename: "DeliveryEstimates", - }, - id: "apollo-federation", - sku: "federation", - }, - { - __typename: "Product", - delivery: { - __typename: "DeliveryEstimates", - }, - id: "apollo-studio", - sku: "studio", - }, - ], - }), - dataState: "streaming", - networkStatus: NetworkStatus.streaming, - error: undefined, - }); - }); - - link.simulateResult({ - result: { - hasNext: true, - incremental: [ - { - data: { - __typename: "DeliveryEstimates", - estimatedDelivery: "6/25/2021", - fastestDelivery: "6/24/2021", - }, - path: ["allProducts", 0, "delivery"], - }, - { - data: { - __typename: "DeliveryEstimates", - estimatedDelivery: "6/25/2021", - fastestDelivery: "6/24/2021", - }, - path: ["allProducts", 1, "delivery"], - }, - ], - }, - }); - - await waitFor(() => { - expect(result.current).toStrictEqualTyped({ - data: markAsStreaming({ - allProducts: [ - { - __typename: "Product", - delivery: { - __typename: "DeliveryEstimates", - estimatedDelivery: "6/25/2021", - fastestDelivery: "6/24/2021", - }, - id: "apollo-federation", - sku: "federation", - }, - { - __typename: "Product", - delivery: { - __typename: "DeliveryEstimates", - estimatedDelivery: "6/25/2021", - fastestDelivery: "6/24/2021", - }, - id: "apollo-studio", - sku: "studio", - }, - ], - }), - dataState: "streaming", - networkStatus: NetworkStatus.streaming, - error: undefined, - }); - }); - }); - - it("incrementally rerenders data returned by a `refetch` for a deferred query", async () => { - const query = gql` - query { - greeting { - message - ... @defer { - recipient { - name - } - } - } - } - `; - - const cache = new InMemoryCache(); - const link = new MockSubscriptionLink(); - const client = new ApolloClient({ - link, - cache, - incrementalHandler: new Defer20220824Handler(), - }); - - const { result, renders } = await renderSuspenseHook( - () => useSuspenseQuery(query), - { client } - ); - - link.simulateResult({ - result: { - data: { greeting: { __typename: "Greeting", message: "Hello world" } }, - hasNext: true, - }, - }); - - await waitFor(() => { - expect(result.current).toStrictEqualTyped({ - data: markAsStreaming({ - greeting: { - __typename: "Greeting", - message: "Hello world", - }, - }), - dataState: "streaming", - networkStatus: NetworkStatus.streaming, - error: undefined, - }); - }); - - link.simulateResult( - { - result: { - incremental: [ - { - data: { - recipient: { name: "Alice", __typename: "Person" }, - }, - path: ["greeting"], - }, - ], - hasNext: false, - }, - }, - true - ); - - await waitFor(() => { - expect(result.current).toStrictEqualTyped({ - data: { - greeting: { - __typename: "Greeting", - message: "Hello world", - recipient: { - __typename: "Person", - name: "Alice", - }, - }, - }, - dataState: "complete", - networkStatus: NetworkStatus.ready, - error: undefined, - }); - }); - - let refetchPromise: Promise>; - await actAsync(async () => { - refetchPromise = result.current.refetch(); - }); - - link.simulateResult({ - result: { - data: { - greeting: { - __typename: "Greeting", - message: "Goodbye", - }, - }, - hasNext: true, - }, - }); - - await waitFor(() => { - expect(result.current).toStrictEqualTyped({ - data: markAsStreaming({ - greeting: { - __typename: "Greeting", - message: "Goodbye", - recipient: { - __typename: "Person", - name: "Alice", - }, - }, - }), - dataState: "streaming", - networkStatus: NetworkStatus.streaming, - error: undefined, - }); - }); - - link.simulateResult( - { - result: { - incremental: [ - { - data: { - recipient: { name: "Bob", __typename: "Person" }, - }, - path: ["greeting"], - }, - ], - hasNext: false, - }, - }, - true - ); - - await waitFor(() => { - expect(result.current).toStrictEqualTyped({ - data: { - greeting: { - __typename: "Greeting", - message: "Goodbye", - recipient: { - __typename: "Person", - name: "Bob", - }, - }, - }, - dataState: "complete", - networkStatus: NetworkStatus.ready, - error: undefined, - }); - }); - - await expect(refetchPromise!).resolves.toStrictEqualTyped({ - data: { - greeting: { - __typename: "Greeting", - message: "Goodbye", - recipient: { - __typename: "Person", - name: "Bob", - }, - }, - }, - }); - - expect(renders.count).toBe(6 + (IS_REACT_19 ? renders.suspenseCount : 0)); - expect(renders.suspenseCount).toBe(2); - expect(renders.frames).toStrictEqualTyped([ - { - data: markAsStreaming({ - greeting: { - __typename: "Greeting", - message: "Hello world", - }, - }), - dataState: "streaming", - networkStatus: NetworkStatus.streaming, - error: undefined, - }, - { - data: { - greeting: { - __typename: "Greeting", - message: "Hello world", - recipient: { - __typename: "Person", - name: "Alice", - }, - }, - }, - dataState: "complete", - networkStatus: NetworkStatus.ready, - error: undefined, - }, - { - data: markAsStreaming({ - greeting: { - __typename: "Greeting", - message: "Goodbye", - recipient: { - __typename: "Person", - name: "Alice", - }, - }, - }), - dataState: "streaming", - networkStatus: NetworkStatus.streaming, - error: undefined, - }, - { - data: { - greeting: { - __typename: "Greeting", - message: "Goodbye", - recipient: { - __typename: "Person", - name: "Bob", - }, - }, - }, - dataState: "complete", - networkStatus: NetworkStatus.ready, - error: undefined, - }, - ]); - }); - - it("incrementally renders data returned after skipping a deferred query", async () => { - const query = gql` - query { - greeting { - message - ... @defer { - recipient { - name - } - } - } - } - `; - - const cache = new InMemoryCache(); - const link = new MockSubscriptionLink(); - const client = new ApolloClient({ - link, - cache, - incrementalHandler: new Defer20220824Handler(), - }); - - const { result, rerenderAsync, renders } = await renderSuspenseHook( - ({ skip }) => useSuspenseQuery(query, { skip }), - { client, initialProps: { skip: true } } - ); - - expect(result.current).toStrictEqualTyped({ - data: undefined, - dataState: "empty", - networkStatus: NetworkStatus.ready, - error: undefined, - }); - - await rerenderAsync({ skip: false }); - - expect(renders.suspenseCount).toBe(1); - - link.simulateResult({ - result: { - data: { greeting: { __typename: "Greeting", message: "Hello world" } }, - hasNext: true, - }, - }); - - await waitFor(() => { - expect(result.current).toStrictEqualTyped({ - data: markAsStreaming({ - greeting: { - __typename: "Greeting", - message: "Hello world", - }, - }), - dataState: "streaming", - networkStatus: NetworkStatus.streaming, - error: undefined, - }); - }); - - link.simulateResult( - { - result: { - incremental: [ - { - data: { - recipient: { name: "Alice", __typename: "Person" }, - }, - path: ["greeting"], - }, - ], - hasNext: false, - }, - }, - true - ); - - await waitFor(() => { - expect(result.current).toStrictEqualTyped({ - data: { - greeting: { - __typename: "Greeting", - message: "Hello world", - recipient: { - __typename: "Person", - name: "Alice", - }, - }, - }, - dataState: "complete", - networkStatus: NetworkStatus.ready, - error: undefined, - }); - }); - - expect(renders.count).toBe(4 + (IS_REACT_19 ? renders.suspenseCount : 0)); - expect(renders.suspenseCount).toBe(1); - expect(renders.frames).toStrictEqualTyped([ - { - data: undefined, - dataState: "empty", - networkStatus: NetworkStatus.ready, - error: undefined, - }, - { - data: markAsStreaming({ - greeting: { - __typename: "Greeting", - message: "Hello world", - }, - }), - dataState: "streaming", - networkStatus: NetworkStatus.streaming, - error: undefined, - }, - { - data: { - greeting: { - __typename: "Greeting", - message: "Hello world", - recipient: { - __typename: "Person", - name: "Alice", - }, - }, - }, - dataState: "complete", - networkStatus: NetworkStatus.ready, - error: undefined, - }, - ]); - }); - - // TODO: This test is a bit of a lie. `fetchMore` should incrementally - // rerender when using `@defer` but there is currently a bug in the core - // implementation that prevents updates until the final result is returned. - // This test reflects the behavior as it exists today, but will need - // to be updated once the core bug is fixed. - // - // NOTE: A duplicate it.failng test has been added right below this one with - // the expected behavior added in (i.e. the commented code in this test). Once - // the core bug is fixed, this test can be removed in favor of the other test. - // - // https://github.com/apollographql/apollo-client/issues/11034 - it("rerenders data returned by `fetchMore` for a deferred query", async () => { - const query = gql` - query ($offset: Int) { - greetings(offset: $offset) { - message - ... @defer { - recipient { - name - } - } - } - } - `; - - const cache = new InMemoryCache({ - typePolicies: { - Query: { - fields: { - greetings: offsetLimitPagination(), - }, - }, - }, - }); - const link = new MockSubscriptionLink(); - const client = new ApolloClient({ - link, - cache, - incrementalHandler: new Defer20220824Handler(), - }); - - const { result, renders } = await renderSuspenseHook( - () => useSuspenseQuery(query, { variables: { offset: 0 } }), - { client } - ); - - link.simulateResult({ - result: { - data: { - greetings: [{ __typename: "Greeting", message: "Hello world" }], - }, - hasNext: true, - }, - }); - - await waitFor(() => { - expect(result.current).toStrictEqualTyped({ - data: markAsStreaming({ - greetings: [ - { - __typename: "Greeting", - message: "Hello world", - }, - ], - }), - dataState: "streaming", - networkStatus: NetworkStatus.streaming, - error: undefined, - }); - }); - - link.simulateResult( - { - result: { - incremental: [ - { - data: { - recipient: { name: "Alice", __typename: "Person" }, - }, - path: ["greetings", 0], - }, - ], - hasNext: false, - }, - }, - true - ); - - await waitFor(() => { - expect(result.current).toStrictEqualTyped({ - data: { - greetings: [ - { - __typename: "Greeting", - message: "Hello world", - recipient: { - __typename: "Person", - name: "Alice", - }, - }, - ], - }, - dataState: "complete", - networkStatus: NetworkStatus.ready, - error: undefined, - }); - }); - - let fetchMorePromise: Promise>; - await actAsync(() => { - fetchMorePromise = result.current.fetchMore({ variables: { offset: 1 } }); - }); - - link.simulateResult({ - result: { - data: { - greetings: [ - { - __typename: "Greeting", - message: "Goodbye", - }, - ], - }, - hasNext: true, - }, - }); - - // TODO: Re-enable once the core bug is fixed - // await waitFor(() => { - // expect(result.current).toStrictEqualTyped({ - // data: { - // greetings: [ - // { - // __typename: 'Greeting', - // message: 'Hello world', - // recipient: { - // __typename: 'Person', - // name: 'Alice', - // }, - // }, - // { - // __typename: 'Greeting', - // message: 'Goodbye', - // }, - // ], - // }, - // dataState: "streaming", - // networkStatus: NetworkStatus.streaming, - // error: undefined, - // }); - // }); - - link.simulateResult( - { - result: { - incremental: [ - { - data: { - recipient: { name: "Bob", __typename: "Person" }, - }, - path: ["greetings", 0], - }, - ], - hasNext: false, - }, - }, - true - ); - - await waitFor(() => { - expect(result.current).toStrictEqualTyped({ - data: { - greetings: [ - { - __typename: "Greeting", - message: "Hello world", - recipient: { - __typename: "Person", - name: "Alice", - }, - }, - { - __typename: "Greeting", - message: "Goodbye", - recipient: { - __typename: "Person", - name: "Bob", - }, - }, - ], - }, - dataState: "complete", - networkStatus: NetworkStatus.ready, - error: undefined, - }); - }); - - await expect(fetchMorePromise!).resolves.toStrictEqualTyped({ - data: { - greetings: [ - { - __typename: "Greeting", - message: "Goodbye", - recipient: { - __typename: "Person", - name: "Bob", - }, - }, - ], - }, - }); - - expect(renders.count).toBe(5 + (IS_REACT_19 ? renders.suspenseCount : 0)); - expect(renders.suspenseCount).toBe(2); - expect(renders.frames).toStrictEqualTyped([ - { - data: markAsStreaming({ - greetings: [ - { - __typename: "Greeting", - message: "Hello world", - }, - ], - }), - dataState: "streaming", - networkStatus: NetworkStatus.streaming, - error: undefined, - }, - { - data: { - greetings: [ - { - __typename: "Greeting", - message: "Hello world", - recipient: { - __typename: "Person", - name: "Alice", - }, - }, - ], - }, - dataState: "complete", - networkStatus: NetworkStatus.ready, - error: undefined, - }, - // TODO: Re-enable when the core `fetchMore` bug is fixed - // { - // data: { - // greetings: [ - // { - // __typename: 'Greeting', - // message: 'Hello world', - // recipient: { - // __typename: 'Person', - // name: 'Alice', - // }, - // }, - // { - // __typename: 'Greeting', - // message: 'Goodbye', - // }, - // ], - // }, - // dataState: "streaming", - // networkStatus: NetworkStatus.streaming, - // error: undefined, - // }, - { - data: { - greetings: [ - { - __typename: "Greeting", - message: "Hello world", - recipient: { - __typename: "Person", - name: "Alice", - }, - }, - { - __typename: "Greeting", - message: "Goodbye", - recipient: { - __typename: "Person", - name: "Bob", - }, - }, - ], - }, - dataState: "complete", - networkStatus: NetworkStatus.ready, - error: undefined, - }, - ]); - }); - - // TODO: This is a duplicate of the test above, but with the expected behavior - // added (hence the `it.failing`). Remove the previous test once issue #11034 - // is fixed. - // - // https://github.com/apollographql/apollo-client/issues/11034 - it.failing( - "incrementally rerenders data returned by a `fetchMore` for a deferred query", - async () => { - const query = gql` - query ($offset: Int) { - greetings(offset: $offset) { - message - ... @defer { - recipient { - name - } - } - } - } - `; - - const cache = new InMemoryCache({ - typePolicies: { - Query: { - fields: { - greetings: offsetLimitPagination(), - }, - }, - }, - }); - const link = new MockSubscriptionLink(); - const client = new ApolloClient({ - link, - cache, - incrementalHandler: new Defer20220824Handler(), - }); - - const { result, renders } = await renderSuspenseHook( - () => useSuspenseQuery(query, { variables: { offset: 0 } }), - { client } - ); - - link.simulateResult({ - result: { - data: { - greetings: [{ __typename: "Greeting", message: "Hello world" }], - }, - hasNext: true, - }, - }); - - await waitFor(() => { - expect(result.current).toStrictEqualTyped({ - data: markAsStreaming({ - greetings: [ - { - __typename: "Greeting", - message: "Hello world", - }, - ], - }), - dataState: "streaming", - networkStatus: NetworkStatus.streaming, - error: undefined, - }); - }); - - link.simulateResult( - { - result: { - incremental: [ - { - data: { - recipient: { name: "Alice", __typename: "Person" }, - }, - path: ["greetings", 0], - }, - ], - hasNext: false, - }, - }, - true - ); - - await waitFor(() => { - expect(result.current).toStrictEqualTyped({ - data: { - greetings: [ - { - __typename: "Greeting", - message: "Hello world", - recipient: { - __typename: "Person", - name: "Alice", - }, - }, - ], - }, - dataState: "complete", - networkStatus: NetworkStatus.ready, - error: undefined, - }); - }); - - let fetchMorePromise: Promise>; - await actAsync(() => { - fetchMorePromise = result.current.fetchMore({ - variables: { offset: 1 }, - }); - }); - - link.simulateResult({ - result: { - data: { - greetings: [ - { - __typename: "Greeting", - message: "Goodbye", - }, - ], - }, - hasNext: true, - }, - }); - - await waitFor(() => { - expect(result.current).toStrictEqualTyped({ - data: markAsStreaming({ - greetings: [ - { - __typename: "Greeting", - message: "Hello world", - recipient: { - __typename: "Person", - name: "Alice", - }, - }, - { - __typename: "Greeting", - message: "Goodbye", - }, - ], - }), - dataState: "streaming", - networkStatus: NetworkStatus.streaming, - error: undefined, - }); - }); - - link.simulateResult( - { - result: { - incremental: [ - { - data: { - recipient: { name: "Bob", __typename: "Person" }, - }, - path: ["greetings", 0], - }, - ], - hasNext: false, - }, - }, - true - ); - - await waitFor(() => { - expect(result.current).toStrictEqualTyped({ - data: { - greetings: [ - { - __typename: "Greeting", - message: "Hello world", - recipient: { - __typename: "Person", - name: "Alice", - }, - }, - { - __typename: "Greeting", - message: "Goodbye", - recipient: { - __typename: "Person", - name: "Bob", - }, - }, - ], - }, - dataState: "complete", - networkStatus: NetworkStatus.ready, - error: undefined, - }); - }); - - await expect(fetchMorePromise!).resolves.toEqual({ - data: { - greetings: [ - { - __typename: "Greeting", - message: "Goodbye", - recipient: { - __typename: "Person", - name: "Bob", - }, - }, - ], - }, - loading: false, - networkStatus: NetworkStatus.ready, - error: undefined, - }); - - expect(renders.count).toBe(5 + (IS_REACT_19 ? renders.suspenseCount : 0)); - expect(renders.suspenseCount).toBe(2); - expect(renders.frames).toStrictEqualTyped([ - { - data: markAsStreaming({ - greetings: [ - { - __typename: "Greeting", - message: "Hello world", - }, - ], - }), - dataState: "streaming", - networkStatus: NetworkStatus.streaming, - error: undefined, - }, - { - data: { - greetings: [ - { - __typename: "Greeting", - message: "Hello world", - recipient: { - __typename: "Person", - name: "Alice", - }, - }, - ], - }, - dataState: "complete", - networkStatus: NetworkStatus.ready, - error: undefined, - }, - { - data: markAsStreaming({ - greetings: [ - { - __typename: "Greeting", - message: "Hello world", - recipient: { - __typename: "Person", - name: "Alice", - }, - }, - { - __typename: "Greeting", - message: "Goodbye", - }, - ], - }), - dataState: "streaming", - networkStatus: NetworkStatus.streaming, - error: undefined, - }, - { - data: { - greetings: [ - { - __typename: "Greeting", - message: "Hello world", - recipient: { - __typename: "Person", - name: "Alice", - }, - }, - { - __typename: "Greeting", - message: "Goodbye", - recipient: { - __typename: "Person", - name: "Bob", - }, - }, - ], - }, - dataState: "complete", - networkStatus: NetworkStatus.ready, - error: undefined, - }, - ]); - } - ); - - it("throws network errors returned by deferred queries", async () => { - using _consoleSpy = spyOnConsole("error"); - - const query = gql` - query { - greeting { - message - ... on Greeting @defer { - recipient { - name - } - } - } - } - `; - - const link = new MockSubscriptionLink(); - - const { renders } = await renderSuspenseHook( - () => useSuspenseQuery(query), - { - link, - incrementalHandler: new Defer20220824Handler(), - } - ); - - link.simulateResult({ - error: new Error("Could not fetch"), - }); - - await waitFor(() => expect(renders.errorCount).toBe(1)); - - expect(renders.errors.length).toBe(1); - expect(renders.suspenseCount).toBe(1); - expect(renders.frames).toEqual([]); - - const [error] = renders.errors; - - expect(error).toBeInstanceOf(Error); - expect(error).toEqual(new Error("Could not fetch")); - }); - - it("throws graphql errors returned by deferred queries", async () => { - using _consoleSpy = spyOnConsole("error"); - - const query = gql` - query { - greeting { - message - ... on Greeting @defer { - recipient { - name - } - } - } - } - `; - - const link = new MockSubscriptionLink(); - - const { renders } = await renderSuspenseHook( - () => useSuspenseQuery(query), - { - link, - incrementalHandler: new Defer20220824Handler(), - } - ); - - link.simulateResult({ - result: { - errors: [new GraphQLError("Could not fetch greeting")], - }, - }); - - await waitFor(() => expect(renders.errorCount).toBe(1)); - - expect(renders.errors.length).toBe(1); - expect(renders.suspenseCount).toBe(1); - expect(renders.frames).toEqual([]); - - const [error] = renders.errors; - - expect(error).toBeInstanceOf(CombinedGraphQLErrors); - expect(error).toEqual( - new CombinedGraphQLErrors({ - errors: [{ message: "Could not fetch greeting" }], - }) - ); - }); - - it("throws errors returned by deferred queries that include partial data", async () => { - using _consoleSpy = spyOnConsole("error"); - - const query = gql` - query { - greeting { - message - ... on Greeting @defer { - recipient { - name - } - } - } - } - `; - - const link = new MockSubscriptionLink(); - - const { renders } = await renderSuspenseHook( - () => useSuspenseQuery(query), - { - link, - incrementalHandler: new Defer20220824Handler(), - } - ); - - link.simulateResult({ - result: { - data: { greeting: null }, - errors: [new GraphQLError("Could not fetch greeting")], - }, - }); - - await waitFor(() => expect(renders.errorCount).toBe(1)); - - expect(renders.errors.length).toBe(1); - expect(renders.suspenseCount).toBe(1); - expect(renders.frames).toEqual([]); - - const [error] = renders.errors; - - expect(error).toBeInstanceOf(CombinedGraphQLErrors); - expect(error).toEqual( - new CombinedGraphQLErrors({ - data: { greeting: null }, - errors: [{ message: "Could not fetch greeting" }], - }) - ); - }); - - it("discards partial data and throws errors returned in incremental chunks", async () => { - using _consoleSpy = spyOnConsole("error"); - - const query = gql` - query { - hero { - name - heroFriends { - id - name - ... @defer { - homeWorld - } - } - } - } - `; - - const link = new MockSubscriptionLink(); - - const { result, renders } = await renderSuspenseHook( - () => useSuspenseQuery(query), - { link, incrementalHandler: new Defer20220824Handler() } - ); - - link.simulateResult({ - result: { - data: { - hero: { - name: "R2-D2", - heroFriends: [ - { - id: "1000", - name: "Luke Skywalker", - }, - { - id: "1003", - name: "Leia Organa", - }, - ], - }, - }, - hasNext: true, - }, - }); - - await waitFor(() => { - expect(result.current).toStrictEqualTyped({ - data: markAsStreaming({ - hero: { - heroFriends: [ - { - id: "1000", - name: "Luke Skywalker", - }, - { - id: "1003", - name: "Leia Organa", - }, - ], - name: "R2-D2", - }, - }), - dataState: "streaming", - networkStatus: NetworkStatus.streaming, - error: undefined, - }); - }); - - link.simulateResult( - { - result: { - incremental: [ - { - path: ["hero", "heroFriends", 0], - errors: [ - new GraphQLError( - "homeWorld for character with ID 1000 could not be fetched.", - { path: ["hero", "heroFriends", 0, "homeWorld"] } - ), - ], - data: { - homeWorld: null, - }, - }, - // This chunk is ignored since errorPolicy `none` throws away partial - // data - { - path: ["hero", "heroFriends", 1], - data: { - homeWorld: "Alderaan", - }, - }, - ], - hasNext: false, - }, - }, - true - ); - - await waitFor(() => { - expect(renders.errorCount).toBe(1); - }); - - expect(renders.suspenseCount).toBe(1); - expect(renders.frames).toStrictEqualTyped([ - { - data: markAsStreaming({ - hero: { - heroFriends: [ - { - id: "1000", - name: "Luke Skywalker", - }, - { - id: "1003", - name: "Leia Organa", - }, - ], - name: "R2-D2", - }, - }), - dataState: "streaming", - networkStatus: NetworkStatus.streaming, - error: undefined, - }, - ]); - - const [error] = renders.errors; - - expect(error).toBeInstanceOf(CombinedGraphQLErrors); - expect(error).toEqual( - new CombinedGraphQLErrors({ - data: { - hero: { - heroFriends: [ - { - id: "1000", - name: "Luke Skywalker", - homeWorld: null, - }, - { - id: "1003", - name: "Leia Organa", - homeWorld: "Alderaan", - }, - ], - name: "R2-D2", - }, - }, - errors: [ - { - message: - "homeWorld for character with ID 1000 could not be fetched.", - path: ["hero", "heroFriends", 0, "homeWorld"], - }, - ], - }) - ); - }); - - it("adds partial data and does not throw errors returned in incremental chunks but returns them in `error` property with errorPolicy set to `all`", async () => { - const query = gql` - query { - hero { - name - heroFriends { - id - name - ... @defer { - homeWorld - } - } - } - } - `; - - const link = new MockSubscriptionLink(); - - const { result, renders } = await renderSuspenseHook( - () => useSuspenseQuery(query, { errorPolicy: "all" }), - { link, incrementalHandler: new Defer20220824Handler() } - ); - - link.simulateResult({ - result: { - data: { - hero: { - name: "R2-D2", - heroFriends: [ - { - id: "1000", - name: "Luke Skywalker", - }, - { - id: "1003", - name: "Leia Organa", - }, - ], - }, - }, - hasNext: true, - }, - }); - - await waitFor(() => { - expect(result.current).toStrictEqualTyped({ - data: markAsStreaming({ - hero: { - heroFriends: [ - { - id: "1000", - name: "Luke Skywalker", - }, - { - id: "1003", - name: "Leia Organa", - }, - ], - name: "R2-D2", - }, - }), - dataState: "streaming", - networkStatus: NetworkStatus.streaming, - error: undefined, - }); - }); - - link.simulateResult( - { - result: { - incremental: [ - { - path: ["hero", "heroFriends", 0], - errors: [ - new GraphQLError( - "homeWorld for character with ID 1000 could not be fetched.", - { path: ["hero", "heroFriends", 0, "homeWorld"] } - ), - ], - data: { - homeWorld: null, - }, - }, - // Unlike the default (errorPolicy = `none`), this data will be - // added to the final result - { - path: ["hero", "heroFriends", 1], - data: { - homeWorld: "Alderaan", - }, - }, - ], - hasNext: false, - }, - }, - true - ); - - await waitFor(() => { - expect(result.current).toStrictEqualTyped({ - data: { - hero: { - heroFriends: [ - { - id: "1000", - name: "Luke Skywalker", - homeWorld: null, - }, - { - id: "1003", - name: "Leia Organa", - homeWorld: "Alderaan", - }, - ], - name: "R2-D2", - }, - }, - dataState: "complete", - networkStatus: NetworkStatus.error, - error: new CombinedGraphQLErrors({ - data: { - hero: { - heroFriends: [ - { - id: "1000", - name: "Luke Skywalker", - homeWorld: null, - }, - { - id: "1003", - name: "Leia Organa", - homeWorld: "Alderaan", - }, - ], - name: "R2-D2", - }, - }, - errors: [ - { - message: - "homeWorld for character with ID 1000 could not be fetched.", - path: ["hero", "heroFriends", 0, "homeWorld"], - }, - ], - }), - }); - }); - - expect(renders.count).toBe(3 + (IS_REACT_19 ? renders.suspenseCount : 0)); - expect(renders.suspenseCount).toBe(1); - expect(renders.frames).toStrictEqualTyped([ - { - data: markAsStreaming({ - hero: { - heroFriends: [ - { - id: "1000", - name: "Luke Skywalker", - }, - { - id: "1003", - name: "Leia Organa", - }, - ], - name: "R2-D2", - }, - }), - dataState: "streaming", - networkStatus: NetworkStatus.streaming, - error: undefined, - }, - { - data: { - hero: { - heroFriends: [ - { - id: "1000", - name: "Luke Skywalker", - homeWorld: null, - }, - { - id: "1003", - name: "Leia Organa", - homeWorld: "Alderaan", - }, - ], - name: "R2-D2", - }, - }, - dataState: "complete", - networkStatus: NetworkStatus.error, - error: new CombinedGraphQLErrors({ - data: { - hero: { - heroFriends: [ - { - id: "1000", - name: "Luke Skywalker", - homeWorld: null, - }, - { - id: "1003", - name: "Leia Organa", - homeWorld: "Alderaan", - }, - ], - name: "R2-D2", - }, - }, - errors: [ - { - message: - "homeWorld for character with ID 1000 could not be fetched.", - path: ["hero", "heroFriends", 0, "homeWorld"], - }, - ], - }), - }, - ]); - }); - - it("adds partial data and discards errors returned in incremental chunks with errorPolicy set to `ignore`", async () => { - const query = gql` - query { - hero { - name - heroFriends { - id - name - ... @defer { - homeWorld - } - } - } - } - `; - - const link = new MockSubscriptionLink(); - - const { result, renders } = await renderSuspenseHook( - () => useSuspenseQuery(query, { errorPolicy: "ignore" }), - { link, incrementalHandler: new Defer20220824Handler() } - ); - - link.simulateResult({ - result: { - data: { - hero: { - name: "R2-D2", - heroFriends: [ - { - id: "1000", - name: "Luke Skywalker", - }, - { - id: "1003", - name: "Leia Organa", - }, - ], - }, - }, - hasNext: true, - }, - }); - - await waitFor(() => { - expect(result.current).toStrictEqualTyped({ - data: markAsStreaming({ - hero: { - heroFriends: [ - { - id: "1000", - name: "Luke Skywalker", - }, - { - id: "1003", - name: "Leia Organa", - }, - ], - name: "R2-D2", - }, - }), - dataState: "streaming", - networkStatus: NetworkStatus.streaming, - error: undefined, - }); - }); - - link.simulateResult( - { - result: { - incremental: [ - { - path: ["hero", "heroFriends", 0], - errors: [ - new GraphQLError( - "homeWorld for character with ID 1000 could not be fetched.", - { path: ["hero", "heroFriends", 0, "homeWorld"] } - ), - ], - data: { - homeWorld: null, - }, - }, - { - path: ["hero", "heroFriends", 1], - data: { - homeWorld: "Alderaan", - }, - }, - ], - hasNext: false, - }, - }, - true - ); - - await waitFor(() => { - expect(result.current).toStrictEqualTyped({ - data: { - hero: { - heroFriends: [ - { - id: "1000", - name: "Luke Skywalker", - homeWorld: null, - }, - { - id: "1003", - name: "Leia Organa", - homeWorld: "Alderaan", - }, - ], - name: "R2-D2", - }, - }, - dataState: "complete", - networkStatus: NetworkStatus.ready, - error: undefined, - }); - }); - - expect(renders.count).toBe(3 + (IS_REACT_19 ? renders.suspenseCount : 0)); - expect(renders.suspenseCount).toBe(1); - expect(renders.frames).toStrictEqualTyped([ - { - data: markAsStreaming({ - hero: { - heroFriends: [ - { - id: "1000", - name: "Luke Skywalker", - }, - { - id: "1003", - name: "Leia Organa", - }, - ], - name: "R2-D2", - }, - }), - dataState: "streaming", - networkStatus: NetworkStatus.streaming, - error: undefined, - }, - { - data: { - hero: { - heroFriends: [ - { - id: "1000", - name: "Luke Skywalker", - homeWorld: null, - }, - { - id: "1003", - name: "Leia Organa", - homeWorld: "Alderaan", - }, - ], - name: "R2-D2", - }, - }, - dataState: "complete", - networkStatus: NetworkStatus.ready, - error: undefined, - }, - ]); - }); - - it("can refetch and respond to cache updates after encountering an error in an incremental chunk for a deferred query when `errorPolicy` is `all`", async () => { - const query = gql` - query { - hero { - name - heroFriends { - id - name - ... @defer { - homeWorld - } - } - } - } - `; - - const cache = new InMemoryCache(); - const link = new MockSubscriptionLink(); - const client = new ApolloClient({ - link, - cache, - incrementalHandler: new Defer20220824Handler(), - }); - - const { result, renders } = await renderSuspenseHook( - () => useSuspenseQuery(query, { errorPolicy: "all" }), - { client } - ); - - link.simulateResult({ - result: { - data: { - hero: { - name: "R2-D2", - heroFriends: [ - { id: "1000", name: "Luke Skywalker" }, - { id: "1003", name: "Leia Organa" }, - ], - }, - }, - hasNext: true, - }, - }); - - await waitFor(() => { - expect(result.current).toStrictEqualTyped({ - data: markAsStreaming({ - hero: { - heroFriends: [ - { id: "1000", name: "Luke Skywalker" }, - { id: "1003", name: "Leia Organa" }, - ], - name: "R2-D2", - }, - }), - dataState: "streaming", - networkStatus: NetworkStatus.streaming, - error: undefined, - }); - }); - - link.simulateResult( - { - result: { - incremental: [ - { - path: ["hero", "heroFriends", 0], - errors: [ - new GraphQLError( - "homeWorld for character with ID 1000 could not be fetched.", - { path: ["hero", "heroFriends", 0, "homeWorld"] } - ), - ], - data: { - homeWorld: null, - }, - }, - { - path: ["hero", "heroFriends", 1], - data: { - homeWorld: "Alderaan", - }, - }, - ], - hasNext: false, - }, - }, - true - ); - - await waitFor(() => { - expect(result.current).toStrictEqualTyped({ - data: { - hero: { - heroFriends: [ - { id: "1000", name: "Luke Skywalker", homeWorld: null }, - { id: "1003", name: "Leia Organa", homeWorld: "Alderaan" }, - ], - name: "R2-D2", - }, - }, - dataState: "complete", - networkStatus: NetworkStatus.error, - error: new CombinedGraphQLErrors({ - data: { - hero: { - heroFriends: [ - { id: "1000", name: "Luke Skywalker", homeWorld: null }, - { id: "1003", name: "Leia Organa", homeWorld: "Alderaan" }, - ], - name: "R2-D2", - }, - }, - errors: [ - { - message: - "homeWorld for character with ID 1000 could not be fetched.", - path: ["hero", "heroFriends", 0, "homeWorld"], - }, - ], - }), - }); - }); - - let refetchPromise: Promise>; - await actAsync(async () => { - refetchPromise = result.current.refetch(); - }); - - link.simulateResult({ - result: { - data: { - hero: { - name: "R2-D2", - heroFriends: [ - { id: "1000", name: "Luke Skywalker" }, - { id: "1003", name: "Leia Organa" }, - ], - }, - }, - hasNext: true, - }, - }); - - await waitFor(() => { - expect(result.current).toStrictEqualTyped({ - data: markAsStreaming({ - hero: { - heroFriends: [ - { id: "1000", name: "Luke Skywalker", homeWorld: null }, - { id: "1003", name: "Leia Organa", homeWorld: "Alderaan" }, - ], - name: "R2-D2", - }, - }), - dataState: "streaming", - networkStatus: NetworkStatus.streaming, - error: undefined, - }); - }); - - link.simulateResult( - { - result: { - incremental: [ - { - path: ["hero", "heroFriends", 0], - data: { - homeWorld: "Alderaan", - }, - }, - { - path: ["hero", "heroFriends", 1], - data: { - homeWorld: "Alderaan", - }, - }, - ], - hasNext: false, - }, - }, - true - ); - - await waitFor(() => { - expect(result.current).toStrictEqualTyped({ - data: { - hero: { - heroFriends: [ - { id: "1000", name: "Luke Skywalker", homeWorld: "Alderaan" }, - { id: "1003", name: "Leia Organa", homeWorld: "Alderaan" }, - ], - name: "R2-D2", - }, - }, - dataState: "complete", - networkStatus: NetworkStatus.ready, - error: undefined, - }); - }); - - await expect(refetchPromise!).resolves.toStrictEqualTyped({ - data: { - hero: { - heroFriends: [ - { id: "1000", name: "Luke Skywalker", homeWorld: "Alderaan" }, - { id: "1003", name: "Leia Organa", homeWorld: "Alderaan" }, - ], - name: "R2-D2", - }, - }, - }); - - cache.updateQuery({ query }, (data) => ({ - hero: { - ...data.hero, - name: "C3PO", - }, - })); - - await waitFor(() => { - expect(result.current).toStrictEqualTyped({ - data: { - hero: { - heroFriends: [ - { id: "1000", name: "Luke Skywalker", homeWorld: "Alderaan" }, - { id: "1003", name: "Leia Organa", homeWorld: "Alderaan" }, - ], - name: "C3PO", - }, - }, - dataState: "complete", - networkStatus: NetworkStatus.ready, - error: undefined, - }); - }); - - expect(renders.count).toBe(7 + (IS_REACT_19 ? renders.suspenseCount : 0)); - expect(renders.suspenseCount).toBe(2); - expect(renders.frames).toStrictEqualTyped([ - { - data: markAsStreaming({ - hero: { - heroFriends: [ - { id: "1000", name: "Luke Skywalker" }, - { id: "1003", name: "Leia Organa" }, - ], - name: "R2-D2", - }, - }), - dataState: "streaming", - networkStatus: NetworkStatus.streaming, - error: undefined, - }, - { - data: { - hero: { - heroFriends: [ - { id: "1000", name: "Luke Skywalker", homeWorld: null }, - { id: "1003", name: "Leia Organa", homeWorld: "Alderaan" }, - ], - name: "R2-D2", - }, - }, - dataState: "complete", - networkStatus: NetworkStatus.error, - error: new CombinedGraphQLErrors({ - data: { - hero: { - heroFriends: [ - { id: "1000", name: "Luke Skywalker", homeWorld: null }, - { id: "1003", name: "Leia Organa", homeWorld: "Alderaan" }, - ], - name: "R2-D2", - }, - }, - errors: [ - { - message: - "homeWorld for character with ID 1000 could not be fetched.", - path: ["hero", "heroFriends", 0, "homeWorld"], - }, - ], - }), - }, - { - data: markAsStreaming({ - hero: { - heroFriends: [ - { id: "1000", name: "Luke Skywalker", homeWorld: null }, - { id: "1003", name: "Leia Organa", homeWorld: "Alderaan" }, - ], - name: "R2-D2", - }, - }), - dataState: "streaming", - networkStatus: NetworkStatus.streaming, - error: undefined, - }, - { - data: { - hero: { - heroFriends: [ - { id: "1000", name: "Luke Skywalker", homeWorld: "Alderaan" }, - { id: "1003", name: "Leia Organa", homeWorld: "Alderaan" }, - ], - name: "R2-D2", - }, - }, - dataState: "complete", - networkStatus: NetworkStatus.ready, - error: undefined, - }, - { - data: { - hero: { - heroFriends: [ - { id: "1000", name: "Luke Skywalker", homeWorld: "Alderaan" }, - { id: "1003", name: "Leia Organa", homeWorld: "Alderaan" }, - ], - name: "C3PO", - }, - }, - dataState: "complete", - networkStatus: NetworkStatus.ready, - error: undefined, - }, - ]); - }); - it("can subscribe to subscriptions and react to cache updates via `subscribeToMore`", async () => { interface SubscriptionData { greetingUpdated: string; diff --git a/src/react/hooks/__tests__/useSuspenseQuery/defer20220824.test.tsx b/src/react/hooks/__tests__/useSuspenseQuery/defer20220824.test.tsx index 08a8824998d..53e520fa515 100644 --- a/src/react/hooks/__tests__/useSuspenseQuery/defer20220824.test.tsx +++ b/src/react/hooks/__tests__/useSuspenseQuery/defer20220824.test.tsx @@ -5,10 +5,14 @@ import { useTrackRenders, } from "@testing-library/react-render-stream"; import React, { Suspense } from "react"; +import { ErrorBoundary } from "react-error-boundary"; +import { delay, of, throwError } from "rxjs"; -import type { OperationVariables } from "@apollo/client"; +import type { ErrorLike, OperationVariables } from "@apollo/client"; import { ApolloClient, + ApolloLink, + CombinedGraphQLErrors, gql, InMemoryCache, NetworkStatus, @@ -18,17 +22,27 @@ import { ApolloProvider, useSuspenseQuery } from "@apollo/client/react"; import { markAsStreaming, mockDefer20220824, + spyOnConsole, + wait, } from "@apollo/client/testing/internal"; +import { offsetLimitPagination } from "@apollo/client/utilities"; +import { invariant } from "@apollo/client/utilities/invariant"; const IS_REACT_19 = React.version.startsWith("19"); -async function renderSuspenseHook( - renderHook: () => useSuspenseQuery.Result, - options: Pick +async function renderSuspenseHook< + TData, + TVariables extends OperationVariables, + Props = never, +>( + renderHook: ( + props: Props extends never ? undefined : Props + ) => useSuspenseQuery.Result, + options: Pick & { initialProps?: Props } ) { - function UseSuspenseQuery() { + function UseSuspenseQuery({ props }: { props: Props | undefined }) { useTrackRenders({ name: "useSuspenseQuery" }); - renderStream.replaceSnapshot(renderHook()); + replaceSnapshot(renderHook(props as any)); return null; } @@ -39,24 +53,45 @@ async function renderSuspenseHook( return null; } - function App() { + function ErrorFallback() { + useTrackRenders({ name: "ErrorFallback" }); + + return null; + } + + function App({ props }: { props: Props | undefined }) { return ( }> - + replaceSnapshot({ error })} + > + + ); } - const { render, takeRender, ...renderStream } = - createRenderStream>(); + const { render, takeRender, replaceSnapshot, getCurrentRender } = + createRenderStream< + useSuspenseQuery.Result | { error: ErrorLike } + >(); + + const utils = await render(, options); + + function rerender(props: Props) { + return utils.rerender(); + } + + function getCurrentSnapshot() { + const { snapshot } = getCurrentRender(); - const utils = await render(, options); + invariant("data" in snapshot, "Snapshot is not a hook snapshot"); - function rerender() { - return utils.rerender(); + return snapshot; } - return { takeRender, rerender }; + return { getCurrentSnapshot, takeRender, rerender }; } test("suspends deferred queries until initial chunk loads then streams in data as it loads", async () => { @@ -150,3 +185,2348 @@ test("suspends deferred queries until initial chunk loads then streams in data a await expect(takeRender).not.toRerender(); }); + +test.each([ + "cache-first", + "network-only", + "no-cache", + "cache-and-network", +])( + 'suspends deferred queries until initial chunk loads then streams in data as it loads when using a "%s" fetch policy', + async (fetchPolicy) => { + const query = gql` + query { + greeting { + message + ... on Greeting @defer { + recipient { + name + } + } + } + } + `; + + const { httpLink, enqueueInitialChunk, enqueueSubsequentChunk } = + mockDefer20220824(); + + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: httpLink, + incrementalHandler: new Defer20220824Handler(), + }); + + using _disabledAct = disableActEnvironment(); + const { takeRender } = await renderSuspenseHook( + () => useSuspenseQuery(query, { fetchPolicy }), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + } + + enqueueInitialChunk({ + data: { + greeting: { message: "Hello world", __typename: "Greeting" }, + }, + hasNext: true, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: markAsStreaming({ + greeting: { message: "Hello world", __typename: "Greeting" }, + }), + dataState: "streaming", + networkStatus: NetworkStatus.streaming, + error: undefined, + }); + } + + enqueueSubsequentChunk({ + incremental: [ + { + data: { + recipient: { name: "Alice", __typename: "Person" }, + __typename: "Greeting", + }, + path: ["greeting"], + }, + ], + hasNext: false, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: { + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Alice" }, + }, + }, + dataState: "complete", + networkStatus: NetworkStatus.ready, + error: undefined, + }); + } + + await expect(takeRender).not.toRerender(); + } +); + +test('does not suspend deferred queries with data in the cache and using a "cache-first" fetch policy', async () => { + const query = gql` + query { + greeting { + message + ... on Greeting @defer { + recipient { + name + } + } + } + } + `; + + const cache = new InMemoryCache(); + + cache.writeQuery({ + query, + data: { + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Alice" }, + }, + }, + }); + + const client = new ApolloClient({ + cache, + link: ApolloLink.empty(), + incrementalHandler: new Defer20220824Handler(), + }); + + using _disabledAct = disableActEnvironment(); + const { takeRender } = await renderSuspenseHook( + () => useSuspenseQuery(query, { fetchPolicy: "cache-first" }), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: { + greeting: { + message: "Hello world", + __typename: "Greeting", + recipient: { __typename: "Person", name: "Alice" }, + }, + }, + dataState: "complete", + networkStatus: NetworkStatus.ready, + error: undefined, + }); + + await expect(takeRender).not.toRerender(); +}); + +test('does not suspend deferred queries with partial data in the cache and using a "cache-first" fetch policy with `returnPartialData`', async () => { + const query = gql` + query { + greeting { + message + ... on Greeting @defer { + recipient { + name + } + } + } + } + `; + + const { httpLink, enqueueInitialChunk, enqueueSubsequentChunk } = + mockDefer20220824(); + const cache = new InMemoryCache(); + + // We are intentionally writing partial data to the cache. Supress console + // warnings to avoid unnecessary noise in the test. + { + using _consoleSpy = spyOnConsole("error"); + cache.writeQuery({ + query, + data: { + greeting: { + __typename: "Greeting", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + }); + } + + const client = new ApolloClient({ + cache, + link: httpLink, + incrementalHandler: new Defer20220824Handler(), + }); + + using _disabledAct = disableActEnvironment(); + const { takeRender } = await renderSuspenseHook( + () => + useSuspenseQuery(query, { + fetchPolicy: "cache-first", + returnPartialData: true, + }), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: { + greeting: { + __typename: "Greeting", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + dataState: "partial", + networkStatus: NetworkStatus.loading, + error: undefined, + }); + } + + enqueueInitialChunk({ + data: { greeting: { message: "Hello world", __typename: "Greeting" } }, + hasNext: true, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: markAsStreaming({ + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }), + dataState: "streaming", + networkStatus: NetworkStatus.streaming, + error: undefined, + }); + } + + enqueueSubsequentChunk({ + incremental: [ + { + data: { + __typename: "Greeting", + recipient: { name: "Alice", __typename: "Person" }, + }, + path: ["greeting"], + }, + ], + hasNext: false, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: { + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Alice" }, + }, + }, + dataState: "complete", + networkStatus: NetworkStatus.ready, + error: undefined, + }); + } + + await expect(takeRender).not.toRerender(); +}); + +test('does not suspend deferred queries with data in the cache and using a "cache-and-network" fetch policy', async () => { + const query = gql` + query { + greeting { + message + ... on Greeting @defer { + recipient { + name + } + } + } + } + `; + + const { httpLink, enqueueInitialChunk, enqueueSubsequentChunk } = + mockDefer20220824(); + + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: httpLink, + incrementalHandler: new Defer20220824Handler(), + }); + + client.writeQuery({ + query, + data: { + greeting: { + __typename: "Greeting", + message: "Hello cached", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + }); + + using _disabledAct = disableActEnvironment(); + const { takeRender } = await renderSuspenseHook( + () => useSuspenseQuery(query, { fetchPolicy: "cache-and-network" }), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: { + greeting: { + message: "Hello cached", + __typename: "Greeting", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + dataState: "complete", + networkStatus: NetworkStatus.loading, + error: undefined, + }); + } + + enqueueInitialChunk({ + data: { greeting: { __typename: "Greeting", message: "Hello world" } }, + hasNext: true, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: markAsStreaming({ + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }), + dataState: "streaming", + networkStatus: NetworkStatus.streaming, + error: undefined, + }); + } + + enqueueSubsequentChunk({ + incremental: [ + { + data: { + recipient: { name: "Alice", __typename: "Person" }, + __typename: "Greeting", + }, + path: ["greeting"], + }, + ], + hasNext: false, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: { + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Alice" }, + }, + }, + dataState: "complete", + networkStatus: NetworkStatus.ready, + error: undefined, + }); + } + + await expect(takeRender).not.toRerender(); +}); + +test("suspends deferred queries with lists and properly patches results", async () => { + const query = gql` + query { + greetings { + message + ... on Greeting @defer { + recipient { + name + } + } + } + } + `; + + const { httpLink, enqueueInitialChunk, enqueueSubsequentChunk } = + mockDefer20220824(); + + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: httpLink, + incrementalHandler: new Defer20220824Handler(), + }); + + using _disabledAct = disableActEnvironment(); + const { takeRender } = await renderSuspenseHook( + () => useSuspenseQuery(query), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + } + + enqueueInitialChunk({ + data: { + greetings: [ + { __typename: "Greeting", message: "Hello world" }, + { __typename: "Greeting", message: "Hello again" }, + ], + }, + hasNext: true, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: markAsStreaming({ + greetings: [ + { __typename: "Greeting", message: "Hello world" }, + { __typename: "Greeting", message: "Hello again" }, + ], + }), + dataState: "streaming", + networkStatus: NetworkStatus.streaming, + error: undefined, + }); + } + + enqueueSubsequentChunk({ + incremental: [ + { + data: { + __typename: "Greeting", + recipient: { __typename: "Person", name: "Alice" }, + }, + path: ["greetings", 0], + }, + ], + hasNext: true, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: markAsStreaming({ + greetings: [ + { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Alice" }, + }, + { + __typename: "Greeting", + message: "Hello again", + }, + ], + }), + dataState: "streaming", + networkStatus: NetworkStatus.streaming, + error: undefined, + }); + } + + enqueueSubsequentChunk({ + incremental: [ + { + data: { + __typename: "Greeting", + recipient: { __typename: "Person", name: "Bob" }, + }, + path: ["greetings", 1], + }, + ], + hasNext: false, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: { + greetings: [ + { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Alice" }, + }, + { + __typename: "Greeting", + message: "Hello again", + recipient: { __typename: "Person", name: "Bob" }, + }, + ], + }, + dataState: "complete", + networkStatus: NetworkStatus.ready, + error: undefined, + }); + } + + await expect(takeRender).not.toRerender(); +}); + +test("suspends queries with deferred fragments in lists and properly merges arrays", async () => { + const query = gql` + query DeferVariation { + allProducts { + delivery { + ...MyFragment @defer + } + sku + id + } + } + + fragment MyFragment on DeliveryEstimates { + estimatedDelivery + fastestDelivery + } + `; + + const { httpLink, enqueueInitialChunk, enqueueSubsequentChunk } = + mockDefer20220824(); + + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: httpLink, + incrementalHandler: new Defer20220824Handler(), + }); + + using _disabledAct = disableActEnvironment(); + const { takeRender } = await renderSuspenseHook( + () => useSuspenseQuery(query), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + } + + enqueueInitialChunk({ + data: { + allProducts: [ + { + __typename: "Product", + delivery: { + __typename: "DeliveryEstimates", + }, + id: "apollo-federation", + sku: "federation", + }, + { + __typename: "Product", + delivery: { + __typename: "DeliveryEstimates", + }, + id: "apollo-studio", + sku: "studio", + }, + ], + }, + hasNext: true, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: markAsStreaming({ + allProducts: [ + { + __typename: "Product", + delivery: { + __typename: "DeliveryEstimates", + }, + id: "apollo-federation", + sku: "federation", + }, + { + __typename: "Product", + delivery: { + __typename: "DeliveryEstimates", + }, + id: "apollo-studio", + sku: "studio", + }, + ], + }), + dataState: "streaming", + networkStatus: NetworkStatus.streaming, + error: undefined, + }); + } + + enqueueSubsequentChunk({ + hasNext: false, + incremental: [ + { + data: { + __typename: "DeliveryEstimates", + estimatedDelivery: "6/25/2021", + fastestDelivery: "6/24/2021", + }, + path: ["allProducts", 0, "delivery"], + }, + { + data: { + __typename: "DeliveryEstimates", + estimatedDelivery: "6/25/2021", + fastestDelivery: "6/24/2021", + }, + path: ["allProducts", 1, "delivery"], + }, + ], + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: { + allProducts: [ + { + __typename: "Product", + delivery: { + __typename: "DeliveryEstimates", + estimatedDelivery: "6/25/2021", + fastestDelivery: "6/24/2021", + }, + id: "apollo-federation", + sku: "federation", + }, + { + __typename: "Product", + delivery: { + __typename: "DeliveryEstimates", + estimatedDelivery: "6/25/2021", + fastestDelivery: "6/24/2021", + }, + id: "apollo-studio", + sku: "studio", + }, + ], + }, + dataState: "complete", + networkStatus: NetworkStatus.ready, + error: undefined, + }); + } + + await expect(takeRender).not.toRerender(); +}); + +test("incrementally rerenders data returned by a `refetch` for a deferred query", async () => { + const query = gql` + query { + greeting { + message + ... @defer { + recipient { + name + } + } + } + } + `; + + const { httpLink, enqueueInitialChunk, enqueueSubsequentChunk } = + mockDefer20220824(); + + const client = new ApolloClient({ + link: httpLink, + cache: new InMemoryCache(), + incrementalHandler: new Defer20220824Handler(), + }); + + using _disabledAct = disableActEnvironment(); + const { takeRender, getCurrentSnapshot } = await renderSuspenseHook( + () => useSuspenseQuery(query), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + } + + enqueueInitialChunk({ + data: { greeting: { __typename: "Greeting", message: "Hello world" } }, + hasNext: true, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: markAsStreaming({ + greeting: { + __typename: "Greeting", + message: "Hello world", + }, + }), + dataState: "streaming", + networkStatus: NetworkStatus.streaming, + error: undefined, + }); + } + + enqueueSubsequentChunk({ + incremental: [ + { + data: { + recipient: { name: "Alice", __typename: "Person" }, + }, + path: ["greeting"], + }, + ], + hasNext: false, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: { + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { + __typename: "Person", + name: "Alice", + }, + }, + }, + dataState: "complete", + networkStatus: NetworkStatus.ready, + error: undefined, + }); + } + + const refetchPromise = getCurrentSnapshot().refetch(); + + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + } + + enqueueInitialChunk({ + data: { + greeting: { + __typename: "Greeting", + message: "Goodbye", + }, + }, + hasNext: true, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: markAsStreaming({ + greeting: { + __typename: "Greeting", + message: "Goodbye", + recipient: { + __typename: "Person", + name: "Alice", + }, + }, + }), + dataState: "streaming", + networkStatus: NetworkStatus.streaming, + error: undefined, + }); + } + + enqueueSubsequentChunk({ + incremental: [ + { + data: { + recipient: { name: "Bob", __typename: "Person" }, + }, + path: ["greeting"], + }, + ], + hasNext: false, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: { + greeting: { + __typename: "Greeting", + message: "Goodbye", + recipient: { + __typename: "Person", + name: "Bob", + }, + }, + }, + dataState: "complete", + networkStatus: NetworkStatus.ready, + error: undefined, + }); + } + + await expect(refetchPromise).resolves.toStrictEqualTyped({ + data: { + greeting: { + __typename: "Greeting", + message: "Goodbye", + recipient: { + __typename: "Person", + name: "Bob", + }, + }, + }, + }); +}); + +test("incrementally renders data returned after skipping a deferred query", async () => { + const query = gql` + query { + greeting { + message + ... @defer { + recipient { + name + } + } + } + } + `; + + const { httpLink, enqueueInitialChunk, enqueueSubsequentChunk } = + mockDefer20220824(); + + const client = new ApolloClient({ + link: httpLink, + cache: new InMemoryCache(), + incrementalHandler: new Defer20220824Handler(), + }); + + using __disabledAct = disableActEnvironment(); + const { takeRender, rerender } = await renderSuspenseHook( + ({ skip }) => useSuspenseQuery(query, { skip }), + { + initialProps: { skip: true }, + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: undefined, + dataState: "empty", + networkStatus: NetworkStatus.ready, + error: undefined, + }); + } + + await rerender({ skip: false }); + + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + } + + enqueueInitialChunk({ + data: { greeting: { __typename: "Greeting", message: "Hello world" } }, + hasNext: true, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: markAsStreaming({ + greeting: { + __typename: "Greeting", + message: "Hello world", + }, + }), + dataState: "streaming", + networkStatus: NetworkStatus.streaming, + error: undefined, + }); + } + + enqueueSubsequentChunk({ + incremental: [ + { + data: { + recipient: { name: "Alice", __typename: "Person" }, + }, + path: ["greeting"], + }, + ], + hasNext: false, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: { + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { + __typename: "Person", + name: "Alice", + }, + }, + }, + dataState: "complete", + networkStatus: NetworkStatus.ready, + error: undefined, + }); + } + + await expect(takeRender).not.toRerender(); +}); + +// TODO: This test is a bit of a lie. `fetchMore` should incrementally +// rerender when using `@defer` but there is currently a bug in the core +// implementation that prevents updates until the final result is returned. +// This test reflects the behavior as it exists today, but will need +// to be updated once the core bug is fixed. +// +// NOTE: A duplicate it.failng test has been added right below this one with +// the expected behavior added in (i.e. the commented code in this test). Once +// the core bug is fixed, this test can be removed in favor of the other test. +// +// https://github.com/apollographql/apollo-client/issues/11034 +test("rerenders data returned by `fetchMore` for a deferred query", async () => { + const query = gql` + query ($offset: Int) { + greetings(offset: $offset) { + message + ... @defer { + recipient { + name + } + } + } + } + `; + + const { httpLink, enqueueInitialChunk, enqueueSubsequentChunk } = + mockDefer20220824(); + + const cache = new InMemoryCache({ + typePolicies: { + Query: { + fields: { + greetings: offsetLimitPagination(), + }, + }, + }, + }); + + const client = new ApolloClient({ + link: httpLink, + cache, + incrementalHandler: new Defer20220824Handler(), + }); + + using _disabledAct = disableActEnvironment(); + const { takeRender, getCurrentSnapshot } = await renderSuspenseHook( + () => useSuspenseQuery(query, { variables: { offset: 0 } }), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + } + + enqueueInitialChunk({ + data: { + greetings: [{ __typename: "Greeting", message: "Hello world" }], + }, + hasNext: true, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: markAsStreaming({ + greetings: [{ __typename: "Greeting", message: "Hello world" }], + }), + dataState: "streaming", + networkStatus: NetworkStatus.streaming, + error: undefined, + }); + } + + enqueueSubsequentChunk({ + incremental: [ + { + data: { + recipient: { name: "Alice", __typename: "Person" }, + }, + path: ["greetings", 0], + }, + ], + hasNext: false, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: { + greetings: [ + { + __typename: "Greeting", + message: "Hello world", + recipient: { + __typename: "Person", + name: "Alice", + }, + }, + ], + }, + dataState: "complete", + networkStatus: NetworkStatus.ready, + error: undefined, + }); + } + + const fetchMorePromise = getCurrentSnapshot().fetchMore({ + variables: { offset: 1 }, + }); + + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + } + + enqueueInitialChunk({ + data: { + greetings: [ + { + __typename: "Greeting", + message: "Goodbye", + }, + ], + }, + hasNext: true, + }); + + // TODO: Re-enable once the core bug is fixed + // { + // const { snapshot, renderedComponents } = await takeRender(); + // + // expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + // expect(snapshot).toStrictEqualTyped({ + // data: markAsStreaming({ + // greetings: [ + // { + // __typename: "Greeting", + // message: "Hello world", + // recipient: { + // __typename: "Person", + // name: "Alice", + // }, + // }, + // { + // __typename: "Greeting", + // message: "Goodbye", + // }, + // ], + // }), + // dataState: "streaming", + // networkStatus: NetworkStatus.streaming, + // error: undefined, + // }); + // } + + await wait(0); + enqueueSubsequentChunk({ + incremental: [ + { + data: { + recipient: { name: "Bob", __typename: "Person" }, + }, + path: ["greetings", 0], + }, + ], + hasNext: false, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: { + greetings: [ + { + __typename: "Greeting", + message: "Hello world", + recipient: { + __typename: "Person", + name: "Alice", + }, + }, + { + __typename: "Greeting", + message: "Goodbye", + recipient: { + __typename: "Person", + name: "Bob", + }, + }, + ], + }, + dataState: "complete", + networkStatus: NetworkStatus.ready, + error: undefined, + }); + } + + await expect(fetchMorePromise!).resolves.toStrictEqualTyped({ + data: { + greetings: [ + { + __typename: "Greeting", + message: "Goodbye", + recipient: { + __typename: "Person", + name: "Bob", + }, + }, + ], + }, + }); + + await expect(takeRender).not.toRerender(); +}); + +// TODO: This is a duplicate of the test above, but with the expected behavior +// added (hence the `it.failing`). Remove the previous test once issue #11034 +// is fixed. +// +// https://github.com/apollographql/apollo-client/issues/11034 +it.failing( + "incrementally rerenders data returned by a `fetchMore` for a deferred query", + async () => { + const query = gql` + query ($offset: Int) { + greetings(offset: $offset) { + message + ... @defer { + recipient { + name + } + } + } + } + `; + + const { httpLink, enqueueInitialChunk, enqueueSubsequentChunk } = + mockDefer20220824(); + + const cache = new InMemoryCache({ + typePolicies: { + Query: { + fields: { + greetings: offsetLimitPagination(), + }, + }, + }, + }); + + const client = new ApolloClient({ + link: httpLink, + cache, + incrementalHandler: new Defer20220824Handler(), + }); + + using _disabledAct = disableActEnvironment(); + const { takeRender, getCurrentSnapshot } = await renderSuspenseHook( + () => useSuspenseQuery(query, { variables: { offset: 0 } }), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + } + + enqueueInitialChunk({ + data: { + greetings: [{ __typename: "Greeting", message: "Hello world" }], + }, + hasNext: true, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: markAsStreaming({ + greetings: [{ __typename: "Greeting", message: "Hello world" }], + }), + dataState: "streaming", + networkStatus: NetworkStatus.streaming, + error: undefined, + }); + } + + enqueueSubsequentChunk({ + incremental: [ + { + data: { + recipient: { name: "Alice", __typename: "Person" }, + }, + path: ["greetings", 0], + }, + ], + hasNext: false, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: { + greetings: [ + { + __typename: "Greeting", + message: "Hello world", + recipient: { + __typename: "Person", + name: "Alice", + }, + }, + ], + }, + dataState: "complete", + networkStatus: NetworkStatus.ready, + error: undefined, + }); + } + + const fetchMorePromise = getCurrentSnapshot().fetchMore({ + variables: { offset: 1 }, + }); + + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + } + + enqueueInitialChunk({ + data: { + greetings: [ + { + __typename: "Greeting", + message: "Goodbye", + }, + ], + }, + hasNext: true, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: markAsStreaming({ + greetings: [ + { + __typename: "Greeting", + message: "Hello world", + recipient: { + __typename: "Person", + name: "Alice", + }, + }, + { + __typename: "Greeting", + message: "Goodbye", + }, + ], + }), + dataState: "streaming", + networkStatus: NetworkStatus.streaming, + error: undefined, + }); + } + + enqueueSubsequentChunk({ + incremental: [ + { + data: { + recipient: { name: "Bob", __typename: "Person" }, + }, + path: ["greetings", 0], + }, + ], + hasNext: false, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: { + greetings: [ + { + __typename: "Greeting", + message: "Hello world", + recipient: { + __typename: "Person", + name: "Alice", + }, + }, + { + __typename: "Greeting", + message: "Goodbye", + recipient: { + __typename: "Person", + name: "Bob", + }, + }, + ], + }, + dataState: "complete", + networkStatus: NetworkStatus.ready, + error: undefined, + }); + } + + await expect(fetchMorePromise!).resolves.toStrictEqualTyped({ + data: { + greetings: [ + { + __typename: "Greeting", + message: "Goodbye", + recipient: { + __typename: "Person", + name: "Bob", + }, + }, + ], + }, + }); + + await expect(takeRender).not.toRerender(); + } +); + +test("throws network errors returned by deferred queries", async () => { + using _consoleSpy = spyOnConsole("error"); + + const query = gql` + query { + greeting { + message + ... on Greeting @defer { + recipient { + name + } + } + } + } + `; + + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: new ApolloLink(() => { + return throwError(() => new Error("Could not fetch")).pipe(delay(20)); + }), + incrementalHandler: new Defer20220824Handler(), + }); + + using _disabledAct = disableActEnvironment(); + const { takeRender } = await renderSuspenseHook( + () => useSuspenseQuery(query), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + } + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["ErrorFallback"]); + expect(snapshot).toStrictEqualTyped({ + error: new Error("Could not fetch"), + }); + } + + await expect(takeRender).not.toRerender(); +}); + +test("throws graphql errors returned by deferred queries", async () => { + using _consoleSpy = spyOnConsole("error"); + + const query = gql` + query { + greeting { + message + ... on Greeting @defer { + recipient { + name + } + } + } + } + `; + + const { httpLink, enqueueInitialChunk } = mockDefer20220824(); + + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: httpLink, + incrementalHandler: new Defer20220824Handler(), + }); + + using _disabledAct = disableActEnvironment(); + const { takeRender } = await renderSuspenseHook( + () => useSuspenseQuery(query), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + } + + enqueueInitialChunk({ + errors: [{ message: "Could not fetch greeting" }], + hasNext: false, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["ErrorFallback"]); + expect(snapshot).toStrictEqualTyped({ + error: new CombinedGraphQLErrors({ + data: null, + errors: [{ message: "Could not fetch greeting" }], + }), + }); + } + + await expect(takeRender).not.toRerender(); +}); + +test("throws errors returned by deferred queries that include partial data", async () => { + using _consoleSpy = spyOnConsole("error"); + + const query = gql` + query { + greeting { + message + ... on Greeting @defer { + recipient { + name + } + } + } + } + `; + + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: new ApolloLink(() => { + return of({ + data: { greeting: null }, + errors: [{ message: "Could not fetch greeting" }], + }).pipe(delay(20)); + }), + incrementalHandler: new Defer20220824Handler(), + }); + + using _disabledAct = disableActEnvironment(); + const { takeRender } = await renderSuspenseHook( + () => useSuspenseQuery(query), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + } + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["ErrorFallback"]); + expect(snapshot).toStrictEqualTyped({ + error: new CombinedGraphQLErrors({ + data: { greeting: null }, + errors: [{ message: "Could not fetch greeting" }], + }), + }); + } + + await expect(takeRender).not.toRerender(); +}); + +test("discards partial data and throws errors returned in incremental chunks", async () => { + using _consoleSpy = spyOnConsole("error"); + + const query = gql` + query { + hero { + name + heroFriends { + id + name + ... @defer { + homeWorld + } + } + } + } + `; + + const { httpLink, enqueueInitialChunk, enqueueSubsequentChunk } = + mockDefer20220824(); + + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: httpLink, + incrementalHandler: new Defer20220824Handler(), + }); + + using _disabledAct = disableActEnvironment(); + const { takeRender } = await renderSuspenseHook( + () => useSuspenseQuery(query), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + } + + enqueueInitialChunk({ + data: { + hero: { + name: "R2-D2", + heroFriends: [ + { + id: "1000", + name: "Luke Skywalker", + }, + { + id: "1003", + name: "Leia Organa", + }, + ], + }, + }, + hasNext: true, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: markAsStreaming({ + hero: { + heroFriends: [ + { + id: "1000", + name: "Luke Skywalker", + }, + { + id: "1003", + name: "Leia Organa", + }, + ], + name: "R2-D2", + }, + }), + dataState: "streaming", + networkStatus: NetworkStatus.streaming, + error: undefined, + }); + } + + enqueueSubsequentChunk({ + incremental: [ + { + path: ["hero", "heroFriends", 0], + errors: [ + { + message: + "homeWorld for character with ID 1000 could not be fetched.", + path: ["hero", "heroFriends", 0, "homeWorld"], + }, + ], + data: { + homeWorld: null, + }, + }, + // This chunk is ignored since errorPolicy `none` throws away partial + // data + { + path: ["hero", "heroFriends", 1], + data: { + homeWorld: "Alderaan", + }, + }, + ], + hasNext: false, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["ErrorFallback"]); + expect(snapshot).toStrictEqualTyped({ + error: new CombinedGraphQLErrors({ + data: { + hero: { + heroFriends: [ + { + id: "1000", + name: "Luke Skywalker", + homeWorld: null, + }, + { + id: "1003", + name: "Leia Organa", + homeWorld: "Alderaan", + }, + ], + name: "R2-D2", + }, + }, + errors: [ + { + message: + "homeWorld for character with ID 1000 could not be fetched.", + path: ["hero", "heroFriends", 0, "homeWorld"], + }, + ], + }), + }); + } + + await expect(takeRender).not.toRerender(); +}); + +test("adds partial data and does not throw errors returned in incremental chunks but returns them in `error` property with errorPolicy set to `all`", async () => { + const query = gql` + query { + hero { + name + heroFriends { + id + name + ... @defer { + homeWorld + } + } + } + } + `; + + const { httpLink, enqueueInitialChunk, enqueueSubsequentChunk } = + mockDefer20220824(); + + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: httpLink, + incrementalHandler: new Defer20220824Handler(), + }); + + using _disabledAct = disableActEnvironment(); + const { takeRender } = await renderSuspenseHook( + () => useSuspenseQuery(query, { errorPolicy: "all" }), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + } + + enqueueInitialChunk({ + data: { + hero: { + name: "R2-D2", + heroFriends: [ + { + id: "1000", + name: "Luke Skywalker", + }, + { + id: "1003", + name: "Leia Organa", + }, + ], + }, + }, + hasNext: true, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: markAsStreaming({ + hero: { + heroFriends: [ + { + id: "1000", + name: "Luke Skywalker", + }, + { + id: "1003", + name: "Leia Organa", + }, + ], + name: "R2-D2", + }, + }), + dataState: "streaming", + networkStatus: NetworkStatus.streaming, + error: undefined, + }); + } + + enqueueSubsequentChunk({ + incremental: [ + { + path: ["hero", "heroFriends", 0], + errors: [ + { + message: + "homeWorld for character with ID 1000 could not be fetched.", + path: ["hero", "heroFriends", 0, "homeWorld"], + }, + ], + data: { + homeWorld: null, + }, + }, + // Unlike the default (errorPolicy = `none`), this data will be + // added to the final result + { + path: ["hero", "heroFriends", 1], + data: { + homeWorld: "Alderaan", + }, + }, + ], + hasNext: false, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: { + hero: { + heroFriends: [ + { + id: "1000", + name: "Luke Skywalker", + homeWorld: null, + }, + { + id: "1003", + name: "Leia Organa", + homeWorld: "Alderaan", + }, + ], + name: "R2-D2", + }, + }, + dataState: "complete", + networkStatus: NetworkStatus.error, + error: new CombinedGraphQLErrors({ + data: { + hero: { + heroFriends: [ + { + id: "1000", + name: "Luke Skywalker", + homeWorld: null, + }, + { + id: "1003", + name: "Leia Organa", + homeWorld: "Alderaan", + }, + ], + name: "R2-D2", + }, + }, + errors: [ + { + message: + "homeWorld for character with ID 1000 could not be fetched.", + path: ["hero", "heroFriends", 0, "homeWorld"], + }, + ], + }), + }); + } + + await expect(takeRender).not.toRerender(); +}); + +test("adds partial data and discards errors returned in incremental chunks with errorPolicy set to `ignore`", async () => { + const query = gql` + query { + hero { + name + heroFriends { + id + name + ... @defer { + homeWorld + } + } + } + } + `; + + const { httpLink, enqueueInitialChunk, enqueueSubsequentChunk } = + mockDefer20220824(); + + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: httpLink, + incrementalHandler: new Defer20220824Handler(), + }); + + using _disabledAct = disableActEnvironment(); + const { takeRender } = await renderSuspenseHook( + () => useSuspenseQuery(query, { errorPolicy: "ignore" }), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + } + + enqueueInitialChunk({ + data: { + hero: { + name: "R2-D2", + heroFriends: [ + { + id: "1000", + name: "Luke Skywalker", + }, + { + id: "1003", + name: "Leia Organa", + }, + ], + }, + }, + hasNext: true, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: markAsStreaming({ + hero: { + heroFriends: [ + { + id: "1000", + name: "Luke Skywalker", + }, + { + id: "1003", + name: "Leia Organa", + }, + ], + name: "R2-D2", + }, + }), + dataState: "streaming", + networkStatus: NetworkStatus.streaming, + error: undefined, + }); + } + + enqueueSubsequentChunk({ + incremental: [ + { + path: ["hero", "heroFriends", 0], + errors: [ + { + message: + "homeWorld for character with ID 1000 could not be fetched.", + path: ["hero", "heroFriends", 0, "homeWorld"], + }, + ], + data: { + homeWorld: null, + }, + }, + { + path: ["hero", "heroFriends", 1], + data: { + homeWorld: "Alderaan", + }, + }, + ], + hasNext: false, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: { + hero: { + heroFriends: [ + { + id: "1000", + name: "Luke Skywalker", + homeWorld: null, + }, + { + id: "1003", + name: "Leia Organa", + homeWorld: "Alderaan", + }, + ], + name: "R2-D2", + }, + }, + dataState: "complete", + networkStatus: NetworkStatus.ready, + error: undefined, + }); + } + + await expect(takeRender).not.toRerender(); +}); + +test("can refetch and respond to cache updates after encountering an error in an incremental chunk for a deferred query when `errorPolicy` is `all`", async () => { + const query = gql` + query { + hero { + name + heroFriends { + id + name + ... @defer { + homeWorld + } + } + } + } + `; + + const { httpLink, enqueueInitialChunk, enqueueSubsequentChunk } = + mockDefer20220824(); + + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: httpLink, + incrementalHandler: new Defer20220824Handler(), + }); + + using _disabledAct = disableActEnvironment(); + const { takeRender, getCurrentSnapshot } = await renderSuspenseHook( + () => useSuspenseQuery(query, { errorPolicy: "all" }), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + } + + enqueueInitialChunk({ + data: { + hero: { + name: "R2-D2", + heroFriends: [ + { id: "1000", name: "Luke Skywalker" }, + { id: "1003", name: "Leia Organa" }, + ], + }, + }, + hasNext: true, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: markAsStreaming({ + hero: { + heroFriends: [ + { id: "1000", name: "Luke Skywalker" }, + { id: "1003", name: "Leia Organa" }, + ], + name: "R2-D2", + }, + }), + dataState: "streaming", + networkStatus: NetworkStatus.streaming, + error: undefined, + }); + } + + enqueueSubsequentChunk({ + incremental: [ + { + path: ["hero", "heroFriends", 0], + errors: [ + { + message: + "homeWorld for character with ID 1000 could not be fetched.", + path: ["hero", "heroFriends", 0, "homeWorld"], + }, + ], + data: { + homeWorld: null, + }, + }, + { + path: ["hero", "heroFriends", 1], + data: { + homeWorld: "Alderaan", + }, + }, + ], + hasNext: false, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: { + hero: { + heroFriends: [ + { id: "1000", name: "Luke Skywalker", homeWorld: null }, + { id: "1003", name: "Leia Organa", homeWorld: "Alderaan" }, + ], + name: "R2-D2", + }, + }, + dataState: "complete", + networkStatus: NetworkStatus.error, + error: new CombinedGraphQLErrors({ + data: { + hero: { + heroFriends: [ + { id: "1000", name: "Luke Skywalker", homeWorld: null }, + { id: "1003", name: "Leia Organa", homeWorld: "Alderaan" }, + ], + name: "R2-D2", + }, + }, + errors: [ + { + message: + "homeWorld for character with ID 1000 could not be fetched.", + path: ["hero", "heroFriends", 0, "homeWorld"], + }, + ], + }), + }); + } + + const refetchPromise = getCurrentSnapshot().refetch(); + + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + } + + enqueueInitialChunk({ + data: { + hero: { + name: "R2-D2", + heroFriends: [ + { id: "1000", name: "Luke Skywalker" }, + { id: "1003", name: "Leia Organa" }, + ], + }, + }, + hasNext: true, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: markAsStreaming({ + hero: { + heroFriends: [ + { id: "1000", name: "Luke Skywalker", homeWorld: null }, + { id: "1003", name: "Leia Organa", homeWorld: "Alderaan" }, + ], + name: "R2-D2", + }, + }), + dataState: "streaming", + networkStatus: NetworkStatus.streaming, + error: undefined, + }); + } + + enqueueSubsequentChunk({ + incremental: [ + { + path: ["hero", "heroFriends", 0], + data: { + homeWorld: "Alderaan", + }, + }, + { + path: ["hero", "heroFriends", 1], + data: { + homeWorld: "Alderaan", + }, + }, + ], + hasNext: false, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: { + hero: { + heroFriends: [ + { id: "1000", name: "Luke Skywalker", homeWorld: "Alderaan" }, + { id: "1003", name: "Leia Organa", homeWorld: "Alderaan" }, + ], + name: "R2-D2", + }, + }, + dataState: "complete", + networkStatus: NetworkStatus.ready, + error: undefined, + }); + } + + await expect(refetchPromise!).resolves.toStrictEqualTyped({ + data: { + hero: { + heroFriends: [ + { id: "1000", name: "Luke Skywalker", homeWorld: "Alderaan" }, + { id: "1003", name: "Leia Organa", homeWorld: "Alderaan" }, + ], + name: "R2-D2", + }, + }, + }); + + client.cache.updateQuery({ query }, (data) => ({ + hero: { + ...data.hero, + name: "C3PO", + }, + })); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: { + hero: { + heroFriends: [ + { id: "1000", name: "Luke Skywalker", homeWorld: "Alderaan" }, + { id: "1003", name: "Leia Organa", homeWorld: "Alderaan" }, + ], + name: "C3PO", + }, + }, + dataState: "complete", + networkStatus: NetworkStatus.ready, + error: undefined, + }); + } + + await expect(takeRender).not.toRerender(); +}); From fec5d76bc64d6522ce98dbb8e3767e4f842b0599 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 4 Sep 2025 21:51:14 -0600 Subject: [PATCH 52/97] Ignore useSuspenseQuery subfile tests --- config/jest.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/config/jest.config.ts b/config/jest.config.ts index 9e8be6190dc..bf0997ccec5 100644 --- a/config/jest.config.ts +++ b/config/jest.config.ts @@ -49,6 +49,7 @@ const react17TestFileIgnoreList = [ "src/testing/experimental/__tests__/createTestSchema.test.tsx", "src/react/hooks/__tests__/useSuspenseFragment.test.tsx", "src/react/hooks/__tests__/useSuspenseQuery.test.tsx", + "src/react/hooks/__tests__/useSuspenseQuery/*", "src/react/hooks/__tests__/useBackgroundQuery.test.tsx", "src/react/hooks/__tests__/useLoadableQuery.test.tsx", "src/react/hooks/__tests__/useQueryRefHandlers.test.tsx", From 0064d8f4ac95d2fa8b1709cbca1075db79317fe5 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 4 Sep 2025 21:55:10 -0600 Subject: [PATCH 53/97] Remove unneeded heck for React 19 in test --- .../hooks/__tests__/useSuspenseQuery/defer20220824.test.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/react/hooks/__tests__/useSuspenseQuery/defer20220824.test.tsx b/src/react/hooks/__tests__/useSuspenseQuery/defer20220824.test.tsx index 53e520fa515..796380b6724 100644 --- a/src/react/hooks/__tests__/useSuspenseQuery/defer20220824.test.tsx +++ b/src/react/hooks/__tests__/useSuspenseQuery/defer20220824.test.tsx @@ -28,8 +28,6 @@ import { import { offsetLimitPagination } from "@apollo/client/utilities"; import { invariant } from "@apollo/client/utilities/invariant"; -const IS_REACT_19 = React.version.startsWith("19"); - async function renderSuspenseHook< TData, TVariables extends OperationVariables, From 1d61c488a7fc6245c707a6d56cabf9774a3355b0 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 4 Sep 2025 22:18:34 -0600 Subject: [PATCH 54/97] Remove unneeded non-null assertion --- .../hooks/__tests__/useSuspenseQuery/defer20220824.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/react/hooks/__tests__/useSuspenseQuery/defer20220824.test.tsx b/src/react/hooks/__tests__/useSuspenseQuery/defer20220824.test.tsx index 796380b6724..a5710c6025a 100644 --- a/src/react/hooks/__tests__/useSuspenseQuery/defer20220824.test.tsx +++ b/src/react/hooks/__tests__/useSuspenseQuery/defer20220824.test.tsx @@ -1398,7 +1398,7 @@ test("rerenders data returned by `fetchMore` for a deferred query", async () => }); } - await expect(fetchMorePromise!).resolves.toStrictEqualTyped({ + await expect(fetchMorePromise).resolves.toStrictEqualTyped({ data: { greetings: [ { From 74ab3ca44010fe592368a4f43b7a45fb882033ff Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 4 Sep 2025 22:21:14 -0600 Subject: [PATCH 55/97] Rename ErrorFallback to ErrorBoundary --- .../__tests__/useSuspenseQuery/defer20220824.test.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/react/hooks/__tests__/useSuspenseQuery/defer20220824.test.tsx b/src/react/hooks/__tests__/useSuspenseQuery/defer20220824.test.tsx index a5710c6025a..459ddbf4f1c 100644 --- a/src/react/hooks/__tests__/useSuspenseQuery/defer20220824.test.tsx +++ b/src/react/hooks/__tests__/useSuspenseQuery/defer20220824.test.tsx @@ -52,7 +52,7 @@ async function renderSuspenseHook< } function ErrorFallback() { - useTrackRenders({ name: "ErrorFallback" }); + useTrackRenders({ name: "ErrorBoundary" }); return null; } @@ -1682,7 +1682,7 @@ test("throws network errors returned by deferred queries", async () => { { const { snapshot, renderedComponents } = await takeRender(); - expect(renderedComponents).toStrictEqual(["ErrorFallback"]); + expect(renderedComponents).toStrictEqual(["ErrorBoundary"]); expect(snapshot).toStrictEqualTyped({ error: new Error("Could not fetch"), }); @@ -1739,7 +1739,7 @@ test("throws graphql errors returned by deferred queries", async () => { { const { snapshot, renderedComponents } = await takeRender(); - expect(renderedComponents).toStrictEqual(["ErrorFallback"]); + expect(renderedComponents).toStrictEqual(["ErrorBoundary"]); expect(snapshot).toStrictEqualTyped({ error: new CombinedGraphQLErrors({ data: null, @@ -1797,7 +1797,7 @@ test("throws errors returned by deferred queries that include partial data", asy { const { snapshot, renderedComponents } = await takeRender(); - expect(renderedComponents).toStrictEqual(["ErrorFallback"]); + expect(renderedComponents).toStrictEqual(["ErrorBoundary"]); expect(snapshot).toStrictEqualTyped({ error: new CombinedGraphQLErrors({ data: { greeting: null }, @@ -1927,7 +1927,7 @@ test("discards partial data and throws errors returned in incremental chunks", a { const { snapshot, renderedComponents } = await takeRender(); - expect(renderedComponents).toStrictEqual(["ErrorFallback"]); + expect(renderedComponents).toStrictEqual(["ErrorBoundary"]); expect(snapshot).toStrictEqualTyped({ error: new CombinedGraphQLErrors({ data: { From 78266894239aaf8ce97f658256bc18f0580f2835 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 4 Sep 2025 22:27:34 -0600 Subject: [PATCH 56/97] Remove unneeded non-null assertion --- .../hooks/__tests__/useSuspenseQuery/defer20220824.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/react/hooks/__tests__/useSuspenseQuery/defer20220824.test.tsx b/src/react/hooks/__tests__/useSuspenseQuery/defer20220824.test.tsx index 459ddbf4f1c..59e373142ec 100644 --- a/src/react/hooks/__tests__/useSuspenseQuery/defer20220824.test.tsx +++ b/src/react/hooks/__tests__/useSuspenseQuery/defer20220824.test.tsx @@ -2487,7 +2487,7 @@ test("can refetch and respond to cache updates after encountering an error in an }); } - await expect(refetchPromise!).resolves.toStrictEqualTyped({ + await expect(refetchPromise).resolves.toStrictEqualTyped({ data: { hero: { heroFriends: [ From 51e9ff3ae580bac2f520f5a79f8fb49b0e29c9d3 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 4 Sep 2025 22:28:10 -0600 Subject: [PATCH 57/97] Add useSuspenseQuery tets for GraphQL17Alpha9Handler --- .../deferGraphQL17Alpha9.test.tsx | 2588 +++++++++++++++++ 1 file changed, 2588 insertions(+) create mode 100644 src/react/hooks/__tests__/useSuspenseQuery/deferGraphQL17Alpha9.test.tsx diff --git a/src/react/hooks/__tests__/useSuspenseQuery/deferGraphQL17Alpha9.test.tsx b/src/react/hooks/__tests__/useSuspenseQuery/deferGraphQL17Alpha9.test.tsx new file mode 100644 index 00000000000..4ad4e3ceb3f --- /dev/null +++ b/src/react/hooks/__tests__/useSuspenseQuery/deferGraphQL17Alpha9.test.tsx @@ -0,0 +1,2588 @@ +import type { RenderOptions } from "@testing-library/react"; +import { + createRenderStream, + disableActEnvironment, + useTrackRenders, +} from "@testing-library/react-render-stream"; +import React, { Suspense } from "react"; +import { ErrorBoundary } from "react-error-boundary"; +import { delay, of, throwError } from "rxjs"; + +import type { ErrorLike, OperationVariables } from "@apollo/client"; +import { + ApolloClient, + ApolloLink, + CombinedGraphQLErrors, + gql, + InMemoryCache, + NetworkStatus, +} from "@apollo/client"; +import { GraphQL17Alpha9Handler } from "@apollo/client/incremental"; +import { ApolloProvider, useSuspenseQuery } from "@apollo/client/react"; +import { + markAsStreaming, + mockDeferStreamGraphQL17Alpha9, + spyOnConsole, + wait, +} from "@apollo/client/testing/internal"; +import { offsetLimitPagination } from "@apollo/client/utilities"; +import { invariant } from "@apollo/client/utilities/invariant"; + +async function renderSuspenseHook< + TData, + TVariables extends OperationVariables, + Props = never, +>( + renderHook: ( + props: Props extends never ? undefined : Props + ) => useSuspenseQuery.Result, + options: Pick & { initialProps?: Props } +) { + function UseSuspenseQuery({ props }: { props: Props | undefined }) { + useTrackRenders({ name: "useSuspenseQuery" }); + replaceSnapshot(renderHook(props as any)); + + return null; + } + + function SuspenseFallback() { + useTrackRenders({ name: "SuspenseFallback" }); + + return null; + } + + function ErrorFallback() { + useTrackRenders({ name: "ErrorBoundary" }); + + return null; + } + + function App({ props }: { props: Props | undefined }) { + return ( + }> + replaceSnapshot({ error })} + > + + + + ); + } + + const { render, takeRender, replaceSnapshot, getCurrentRender } = + createRenderStream< + useSuspenseQuery.Result | { error: ErrorLike } + >(); + + const utils = await render(, options); + + function rerender(props: Props) { + return utils.rerender(); + } + + function getCurrentSnapshot() { + const { snapshot } = getCurrentRender(); + + invariant("data" in snapshot, "Snapshot is not a hook snapshot"); + + return snapshot; + } + + return { getCurrentSnapshot, takeRender, rerender }; +} + +test("suspends deferred queries until initial chunk loads then streams in data as it loads", async () => { + const query = gql` + query { + greeting { + message + ... on Greeting @defer { + recipient { + name + } + } + } + } + `; + + const { httpLink, enqueueInitialChunk, enqueueSubsequentChunk } = + mockDeferStreamGraphQL17Alpha9(); + + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: httpLink, + incrementalHandler: new GraphQL17Alpha9Handler(), + }); + + using _disabledAct = disableActEnvironment(); + const { takeRender } = await renderSuspenseHook( + () => useSuspenseQuery(query), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + } + + enqueueInitialChunk({ + data: { greeting: { message: "Hello world", __typename: "Greeting" } }, + pending: [{ id: "0", path: ["greeting"] }], + hasNext: true, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: markAsStreaming({ + greeting: { message: "Hello world", __typename: "Greeting" }, + }), + dataState: "streaming", + networkStatus: NetworkStatus.streaming, + error: undefined, + }); + } + + enqueueSubsequentChunk({ + incremental: [ + { + data: { + recipient: { name: "Alice", __typename: "Person" }, + __typename: "Greeting", + }, + id: "0", + }, + ], + completed: [{ id: "0" }], + hasNext: false, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: { + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Alice" }, + }, + }, + dataState: "complete", + networkStatus: NetworkStatus.ready, + error: undefined, + }); + } + + await expect(takeRender).not.toRerender(); +}); + +test.each([ + "cache-first", + "network-only", + "no-cache", + "cache-and-network", +])( + 'suspends deferred queries until initial chunk loads then streams in data as it loads when using a "%s" fetch policy', + async (fetchPolicy) => { + const query = gql` + query { + greeting { + message + ... on Greeting @defer { + recipient { + name + } + } + } + } + `; + + const { httpLink, enqueueInitialChunk, enqueueSubsequentChunk } = + mockDeferStreamGraphQL17Alpha9(); + + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: httpLink, + incrementalHandler: new GraphQL17Alpha9Handler(), + }); + + using _disabledAct = disableActEnvironment(); + const { takeRender } = await renderSuspenseHook( + () => useSuspenseQuery(query, { fetchPolicy }), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + } + + enqueueInitialChunk({ + data: { + greeting: { message: "Hello world", __typename: "Greeting" }, + }, + pending: [{ id: "0", path: ["greeting"] }], + hasNext: true, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: markAsStreaming({ + greeting: { message: "Hello world", __typename: "Greeting" }, + }), + dataState: "streaming", + networkStatus: NetworkStatus.streaming, + error: undefined, + }); + } + + enqueueSubsequentChunk({ + incremental: [ + { + data: { + recipient: { name: "Alice", __typename: "Person" }, + __typename: "Greeting", + }, + id: "0", + }, + ], + completed: [{ id: "0" }], + hasNext: false, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: { + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Alice" }, + }, + }, + dataState: "complete", + networkStatus: NetworkStatus.ready, + error: undefined, + }); + } + + await expect(takeRender).not.toRerender(); + } +); + +test('does not suspend deferred queries with data in the cache and using a "cache-first" fetch policy', async () => { + const query = gql` + query { + greeting { + message + ... on Greeting @defer { + recipient { + name + } + } + } + } + `; + + const cache = new InMemoryCache(); + + cache.writeQuery({ + query, + data: { + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Alice" }, + }, + }, + }); + + const client = new ApolloClient({ + cache, + link: ApolloLink.empty(), + incrementalHandler: new GraphQL17Alpha9Handler(), + }); + + using _disabledAct = disableActEnvironment(); + const { takeRender } = await renderSuspenseHook( + () => useSuspenseQuery(query, { fetchPolicy: "cache-first" }), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: { + greeting: { + message: "Hello world", + __typename: "Greeting", + recipient: { __typename: "Person", name: "Alice" }, + }, + }, + dataState: "complete", + networkStatus: NetworkStatus.ready, + error: undefined, + }); + + await expect(takeRender).not.toRerender(); +}); + +test('does not suspend deferred queries with partial data in the cache and using a "cache-first" fetch policy with `returnPartialData`', async () => { + const query = gql` + query { + greeting { + message + ... on Greeting @defer { + recipient { + name + } + } + } + } + `; + + const { httpLink, enqueueInitialChunk, enqueueSubsequentChunk } = + mockDeferStreamGraphQL17Alpha9(); + const cache = new InMemoryCache(); + + // We are intentionally writing partial data to the cache. Supress console + // warnings to avoid unnecessary noise in the test. + { + using _consoleSpy = spyOnConsole("error"); + cache.writeQuery({ + query, + data: { + greeting: { + __typename: "Greeting", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + }); + } + + const client = new ApolloClient({ + cache, + link: httpLink, + incrementalHandler: new GraphQL17Alpha9Handler(), + }); + + using _disabledAct = disableActEnvironment(); + const { takeRender } = await renderSuspenseHook( + () => + useSuspenseQuery(query, { + fetchPolicy: "cache-first", + returnPartialData: true, + }), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: { + greeting: { + __typename: "Greeting", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + dataState: "partial", + networkStatus: NetworkStatus.loading, + error: undefined, + }); + } + + enqueueInitialChunk({ + data: { greeting: { message: "Hello world", __typename: "Greeting" } }, + pending: [{ id: "0", path: ["greeting"] }], + hasNext: true, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: markAsStreaming({ + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }), + dataState: "streaming", + networkStatus: NetworkStatus.streaming, + error: undefined, + }); + } + + enqueueSubsequentChunk({ + incremental: [ + { + data: { + __typename: "Greeting", + recipient: { name: "Alice", __typename: "Person" }, + }, + id: "0", + }, + ], + completed: [{ id: "0" }], + hasNext: false, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: { + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Alice" }, + }, + }, + dataState: "complete", + networkStatus: NetworkStatus.ready, + error: undefined, + }); + } + + await expect(takeRender).not.toRerender(); +}); + +test('does not suspend deferred queries with data in the cache and using a "cache-and-network" fetch policy', async () => { + const query = gql` + query { + greeting { + message + ... on Greeting @defer { + recipient { + name + } + } + } + } + `; + + const { httpLink, enqueueInitialChunk, enqueueSubsequentChunk } = + mockDeferStreamGraphQL17Alpha9(); + + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: httpLink, + incrementalHandler: new GraphQL17Alpha9Handler(), + }); + + client.writeQuery({ + query, + data: { + greeting: { + __typename: "Greeting", + message: "Hello cached", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + }); + + using _disabledAct = disableActEnvironment(); + const { takeRender } = await renderSuspenseHook( + () => useSuspenseQuery(query, { fetchPolicy: "cache-and-network" }), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: { + greeting: { + message: "Hello cached", + __typename: "Greeting", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + dataState: "complete", + networkStatus: NetworkStatus.loading, + error: undefined, + }); + } + + enqueueInitialChunk({ + data: { greeting: { __typename: "Greeting", message: "Hello world" } }, + pending: [{ id: "0", path: ["greeting"] }], + hasNext: true, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: markAsStreaming({ + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }), + dataState: "streaming", + networkStatus: NetworkStatus.streaming, + error: undefined, + }); + } + + enqueueSubsequentChunk({ + incremental: [ + { + data: { + recipient: { name: "Alice", __typename: "Person" }, + __typename: "Greeting", + }, + id: "0", + }, + ], + completed: [{ id: "0" }], + hasNext: false, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: { + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Alice" }, + }, + }, + dataState: "complete", + networkStatus: NetworkStatus.ready, + error: undefined, + }); + } + + await expect(takeRender).not.toRerender(); +}); + +test("suspends deferred queries with lists and properly patches results", async () => { + const query = gql` + query { + greetings { + message + ... on Greeting @defer { + recipient { + name + } + } + } + } + `; + + const { httpLink, enqueueInitialChunk, enqueueSubsequentChunk } = + mockDeferStreamGraphQL17Alpha9(); + + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: httpLink, + incrementalHandler: new GraphQL17Alpha9Handler(), + }); + + using _disabledAct = disableActEnvironment(); + const { takeRender } = await renderSuspenseHook( + () => useSuspenseQuery(query), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + } + + enqueueInitialChunk({ + data: { + greetings: [ + { __typename: "Greeting", message: "Hello world" }, + { __typename: "Greeting", message: "Hello again" }, + ], + }, + pending: [ + { id: "0", path: ["greetings", 0] }, + { id: "1", path: ["greetings", 1] }, + ], + hasNext: true, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: markAsStreaming({ + greetings: [ + { __typename: "Greeting", message: "Hello world" }, + { __typename: "Greeting", message: "Hello again" }, + ], + }), + dataState: "streaming", + networkStatus: NetworkStatus.streaming, + error: undefined, + }); + } + + enqueueSubsequentChunk({ + incremental: [ + { + data: { + __typename: "Greeting", + recipient: { __typename: "Person", name: "Alice" }, + }, + id: "0", + }, + ], + completed: [{ id: "0" }], + hasNext: true, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: markAsStreaming({ + greetings: [ + { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Alice" }, + }, + { + __typename: "Greeting", + message: "Hello again", + }, + ], + }), + dataState: "streaming", + networkStatus: NetworkStatus.streaming, + error: undefined, + }); + } + + enqueueSubsequentChunk({ + incremental: [ + { + data: { + __typename: "Greeting", + recipient: { __typename: "Person", name: "Bob" }, + }, + id: "1", + }, + ], + completed: [{ id: "1" }], + hasNext: false, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: { + greetings: [ + { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Alice" }, + }, + { + __typename: "Greeting", + message: "Hello again", + recipient: { __typename: "Person", name: "Bob" }, + }, + ], + }, + dataState: "complete", + networkStatus: NetworkStatus.ready, + error: undefined, + }); + } + + await expect(takeRender).not.toRerender(); +}); + +test("suspends queries with deferred fragments in lists and properly merges arrays", async () => { + const query = gql` + query DeferVariation { + allProducts { + delivery { + ...MyFragment @defer + } + sku + id + } + } + + fragment MyFragment on DeliveryEstimates { + estimatedDelivery + fastestDelivery + } + `; + + const { httpLink, enqueueInitialChunk, enqueueSubsequentChunk } = + mockDeferStreamGraphQL17Alpha9(); + + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: httpLink, + incrementalHandler: new GraphQL17Alpha9Handler(), + }); + + using _disabledAct = disableActEnvironment(); + const { takeRender } = await renderSuspenseHook( + () => useSuspenseQuery(query), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + } + + enqueueInitialChunk({ + data: { + allProducts: [ + { + __typename: "Product", + delivery: { + __typename: "DeliveryEstimates", + }, + id: "apollo-federation", + sku: "federation", + }, + { + __typename: "Product", + delivery: { + __typename: "DeliveryEstimates", + }, + id: "apollo-studio", + sku: "studio", + }, + ], + }, + pending: [ + { id: "0", path: ["allProducts", 0, "delivery"] }, + { id: "1", path: ["allProducts", 1, "delivery"] }, + ], + hasNext: true, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: markAsStreaming({ + allProducts: [ + { + __typename: "Product", + delivery: { + __typename: "DeliveryEstimates", + }, + id: "apollo-federation", + sku: "federation", + }, + { + __typename: "Product", + delivery: { + __typename: "DeliveryEstimates", + }, + id: "apollo-studio", + sku: "studio", + }, + ], + }), + dataState: "streaming", + networkStatus: NetworkStatus.streaming, + error: undefined, + }); + } + + enqueueSubsequentChunk({ + hasNext: false, + incremental: [ + { + data: { + __typename: "DeliveryEstimates", + estimatedDelivery: "6/25/2021", + fastestDelivery: "6/24/2021", + }, + id: "0", + }, + { + data: { + __typename: "DeliveryEstimates", + estimatedDelivery: "6/25/2021", + fastestDelivery: "6/24/2021", + }, + id: "1", + }, + ], + completed: [{ id: "0" }, { id: "1" }], + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: { + allProducts: [ + { + __typename: "Product", + delivery: { + __typename: "DeliveryEstimates", + estimatedDelivery: "6/25/2021", + fastestDelivery: "6/24/2021", + }, + id: "apollo-federation", + sku: "federation", + }, + { + __typename: "Product", + delivery: { + __typename: "DeliveryEstimates", + estimatedDelivery: "6/25/2021", + fastestDelivery: "6/24/2021", + }, + id: "apollo-studio", + sku: "studio", + }, + ], + }, + dataState: "complete", + networkStatus: NetworkStatus.ready, + error: undefined, + }); + } + + await expect(takeRender).not.toRerender(); +}); + +test("incrementally rerenders data returned by a `refetch` for a deferred query", async () => { + const query = gql` + query { + greeting { + message + ... @defer { + recipient { + name + } + } + } + } + `; + + const { httpLink, enqueueInitialChunk, enqueueSubsequentChunk } = + mockDeferStreamGraphQL17Alpha9(); + + const client = new ApolloClient({ + link: httpLink, + cache: new InMemoryCache(), + incrementalHandler: new GraphQL17Alpha9Handler(), + }); + + using _disabledAct = disableActEnvironment(); + const { takeRender, getCurrentSnapshot } = await renderSuspenseHook( + () => useSuspenseQuery(query), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + } + + enqueueInitialChunk({ + data: { greeting: { __typename: "Greeting", message: "Hello world" } }, + pending: [{ id: "0", path: ["greeting"] }], + hasNext: true, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: markAsStreaming({ + greeting: { + __typename: "Greeting", + message: "Hello world", + }, + }), + dataState: "streaming", + networkStatus: NetworkStatus.streaming, + error: undefined, + }); + } + + enqueueSubsequentChunk({ + incremental: [ + { + data: { + recipient: { name: "Alice", __typename: "Person" }, + }, + id: "0", + }, + ], + completed: [{ id: "0" }], + hasNext: false, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: { + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { + __typename: "Person", + name: "Alice", + }, + }, + }, + dataState: "complete", + networkStatus: NetworkStatus.ready, + error: undefined, + }); + } + + const refetchPromise = getCurrentSnapshot().refetch(); + + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + } + + enqueueInitialChunk({ + data: { + greeting: { + __typename: "Greeting", + message: "Goodbye", + }, + }, + pending: [{ id: "0", path: ["greeting"] }], + hasNext: true, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: markAsStreaming({ + greeting: { + __typename: "Greeting", + message: "Goodbye", + recipient: { + __typename: "Person", + name: "Alice", + }, + }, + }), + dataState: "streaming", + networkStatus: NetworkStatus.streaming, + error: undefined, + }); + } + + enqueueSubsequentChunk({ + incremental: [ + { + data: { + recipient: { name: "Bob", __typename: "Person" }, + }, + id: "0", + }, + ], + completed: [{ id: "0" }], + hasNext: false, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: { + greeting: { + __typename: "Greeting", + message: "Goodbye", + recipient: { + __typename: "Person", + name: "Bob", + }, + }, + }, + dataState: "complete", + networkStatus: NetworkStatus.ready, + error: undefined, + }); + } + + await expect(refetchPromise).resolves.toStrictEqualTyped({ + data: { + greeting: { + __typename: "Greeting", + message: "Goodbye", + recipient: { + __typename: "Person", + name: "Bob", + }, + }, + }, + }); +}); + +test("incrementally renders data returned after skipping a deferred query", async () => { + const query = gql` + query { + greeting { + message + ... @defer { + recipient { + name + } + } + } + } + `; + + const { httpLink, enqueueInitialChunk, enqueueSubsequentChunk } = + mockDeferStreamGraphQL17Alpha9(); + + const client = new ApolloClient({ + link: httpLink, + cache: new InMemoryCache(), + incrementalHandler: new GraphQL17Alpha9Handler(), + }); + + using __disabledAct = disableActEnvironment(); + const { takeRender, rerender } = await renderSuspenseHook( + ({ skip }) => useSuspenseQuery(query, { skip }), + { + initialProps: { skip: true }, + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: undefined, + dataState: "empty", + networkStatus: NetworkStatus.ready, + error: undefined, + }); + } + + await rerender({ skip: false }); + + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + } + + enqueueInitialChunk({ + data: { greeting: { __typename: "Greeting", message: "Hello world" } }, + pending: [{ id: "0", path: ["greeting"] }], + hasNext: true, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: markAsStreaming({ + greeting: { + __typename: "Greeting", + message: "Hello world", + }, + }), + dataState: "streaming", + networkStatus: NetworkStatus.streaming, + error: undefined, + }); + } + + enqueueSubsequentChunk({ + incremental: [ + { + data: { + recipient: { name: "Alice", __typename: "Person" }, + }, + id: "0", + }, + ], + completed: [{ id: "0" }], + hasNext: false, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: { + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { + __typename: "Person", + name: "Alice", + }, + }, + }, + dataState: "complete", + networkStatus: NetworkStatus.ready, + error: undefined, + }); + } + + await expect(takeRender).not.toRerender(); +}); + +// TODO: This test is a bit of a lie. `fetchMore` should incrementally +// rerender when using `@defer` but there is currently a bug in the core +// implementation that prevents updates until the final result is returned. +// This test reflects the behavior as it exists today, but will need +// to be updated once the core bug is fixed. +// +// NOTE: A duplicate it.failng test has been added right below this one with +// the expected behavior added in (i.e. the commented code in this test). Once +// the core bug is fixed, this test can be removed in favor of the other test. +// +// https://github.com/apollographql/apollo-client/issues/11034 +test("rerenders data returned by `fetchMore` for a deferred query", async () => { + const query = gql` + query ($offset: Int) { + greetings(offset: $offset) { + message + ... @defer { + recipient { + name + } + } + } + } + `; + + const { httpLink, enqueueInitialChunk, enqueueSubsequentChunk } = + mockDeferStreamGraphQL17Alpha9(); + + const cache = new InMemoryCache({ + typePolicies: { + Query: { + fields: { + greetings: offsetLimitPagination(), + }, + }, + }, + }); + + const client = new ApolloClient({ + link: httpLink, + cache, + incrementalHandler: new GraphQL17Alpha9Handler(), + }); + + using _disabledAct = disableActEnvironment(); + const { takeRender, getCurrentSnapshot } = await renderSuspenseHook( + () => useSuspenseQuery(query, { variables: { offset: 0 } }), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + } + + enqueueInitialChunk({ + data: { + greetings: [{ __typename: "Greeting", message: "Hello world" }], + }, + pending: [{ id: "0", path: ["greetings", 0] }], + hasNext: true, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: markAsStreaming({ + greetings: [{ __typename: "Greeting", message: "Hello world" }], + }), + dataState: "streaming", + networkStatus: NetworkStatus.streaming, + error: undefined, + }); + } + + enqueueSubsequentChunk({ + incremental: [ + { + data: { + recipient: { name: "Alice", __typename: "Person" }, + }, + id: "0", + }, + ], + completed: [{ id: "0" }], + hasNext: false, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: { + greetings: [ + { + __typename: "Greeting", + message: "Hello world", + recipient: { + __typename: "Person", + name: "Alice", + }, + }, + ], + }, + dataState: "complete", + networkStatus: NetworkStatus.ready, + error: undefined, + }); + } + + const fetchMorePromise = getCurrentSnapshot().fetchMore({ + variables: { offset: 1 }, + }); + + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + } + + enqueueInitialChunk({ + data: { + greetings: [ + { + __typename: "Greeting", + message: "Goodbye", + }, + ], + }, + pending: [{ id: "0", path: ["greetings", 0] }], + hasNext: true, + }); + + // TODO: Re-enable once the core bug is fixed + // { + // const { snapshot, renderedComponents } = await takeRender(); + // + // expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + // expect(snapshot).toStrictEqualTyped({ + // data: markAsStreaming({ + // greetings: [ + // { + // __typename: "Greeting", + // message: "Hello world", + // recipient: { + // __typename: "Person", + // name: "Alice", + // }, + // }, + // { + // __typename: "Greeting", + // message: "Goodbye", + // }, + // ], + // }), + // dataState: "streaming", + // networkStatus: NetworkStatus.streaming, + // error: undefined, + // }); + // } + + await wait(0); + enqueueSubsequentChunk({ + incremental: [ + { + data: { + recipient: { name: "Bob", __typename: "Person" }, + }, + id: "0", + }, + ], + completed: [{ id: "0" }], + hasNext: false, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: { + greetings: [ + { + __typename: "Greeting", + message: "Hello world", + recipient: { + __typename: "Person", + name: "Alice", + }, + }, + { + __typename: "Greeting", + message: "Goodbye", + recipient: { + __typename: "Person", + name: "Bob", + }, + }, + ], + }, + dataState: "complete", + networkStatus: NetworkStatus.ready, + error: undefined, + }); + } + + await expect(fetchMorePromise).resolves.toStrictEqualTyped({ + data: { + greetings: [ + { + __typename: "Greeting", + message: "Goodbye", + recipient: { + __typename: "Person", + name: "Bob", + }, + }, + ], + }, + }); + + await expect(takeRender).not.toRerender(); +}); + +// TODO: This is a duplicate of the test above, but with the expected behavior +// added (hence the `it.failing`). Remove the previous test once issue #11034 +// is fixed. +// +// https://github.com/apollographql/apollo-client/issues/11034 +it.failing( + "incrementally rerenders data returned by a `fetchMore` for a deferred query", + async () => { + const query = gql` + query ($offset: Int) { + greetings(offset: $offset) { + message + ... @defer { + recipient { + name + } + } + } + } + `; + + const { httpLink, enqueueInitialChunk, enqueueSubsequentChunk } = + mockDeferStreamGraphQL17Alpha9(); + + const cache = new InMemoryCache({ + typePolicies: { + Query: { + fields: { + greetings: offsetLimitPagination(), + }, + }, + }, + }); + + const client = new ApolloClient({ + link: httpLink, + cache, + incrementalHandler: new GraphQL17Alpha9Handler(), + }); + + using _disabledAct = disableActEnvironment(); + const { takeRender, getCurrentSnapshot } = await renderSuspenseHook( + () => useSuspenseQuery(query, { variables: { offset: 0 } }), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + } + + enqueueInitialChunk({ + data: { + greetings: [{ __typename: "Greeting", message: "Hello world" }], + }, + pending: [{ id: "0", path: ["greetings", 0] }], + hasNext: true, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: markAsStreaming({ + greetings: [{ __typename: "Greeting", message: "Hello world" }], + }), + dataState: "streaming", + networkStatus: NetworkStatus.streaming, + error: undefined, + }); + } + + enqueueSubsequentChunk({ + incremental: [ + { + data: { + recipient: { name: "Alice", __typename: "Person" }, + }, + id: "0", + }, + ], + completed: [{ id: "0" }], + hasNext: false, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: { + greetings: [ + { + __typename: "Greeting", + message: "Hello world", + recipient: { + __typename: "Person", + name: "Alice", + }, + }, + ], + }, + dataState: "complete", + networkStatus: NetworkStatus.ready, + error: undefined, + }); + } + + const fetchMorePromise = getCurrentSnapshot().fetchMore({ + variables: { offset: 1 }, + }); + + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + } + + enqueueInitialChunk({ + data: { + greetings: [ + { + __typename: "Greeting", + message: "Goodbye", + }, + ], + }, + pending: [{ id: "0", path: ["greetings", 0] }], + hasNext: true, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: markAsStreaming({ + greetings: [ + { + __typename: "Greeting", + message: "Hello world", + recipient: { + __typename: "Person", + name: "Alice", + }, + }, + { + __typename: "Greeting", + message: "Goodbye", + }, + ], + }), + dataState: "streaming", + networkStatus: NetworkStatus.streaming, + error: undefined, + }); + } + + enqueueSubsequentChunk({ + incremental: [ + { + data: { + recipient: { name: "Bob", __typename: "Person" }, + }, + id: "0", + }, + ], + completed: [{ id: "0" }], + hasNext: false, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: { + greetings: [ + { + __typename: "Greeting", + message: "Hello world", + recipient: { + __typename: "Person", + name: "Alice", + }, + }, + { + __typename: "Greeting", + message: "Goodbye", + recipient: { + __typename: "Person", + name: "Bob", + }, + }, + ], + }, + dataState: "complete", + networkStatus: NetworkStatus.ready, + error: undefined, + }); + } + + await expect(fetchMorePromise).resolves.toStrictEqualTyped({ + data: { + greetings: [ + { + __typename: "Greeting", + message: "Goodbye", + recipient: { + __typename: "Person", + name: "Bob", + }, + }, + ], + }, + }); + + await expect(takeRender).not.toRerender(); + } +); + +test("throws network errors returned by deferred queries", async () => { + using _consoleSpy = spyOnConsole("error"); + + const query = gql` + query { + greeting { + message + ... on Greeting @defer { + recipient { + name + } + } + } + } + `; + + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: new ApolloLink(() => { + return throwError(() => new Error("Could not fetch")).pipe(delay(20)); + }), + incrementalHandler: new GraphQL17Alpha9Handler(), + }); + + using _disabledAct = disableActEnvironment(); + const { takeRender } = await renderSuspenseHook( + () => useSuspenseQuery(query), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + } + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["ErrorBoundary"]); + expect(snapshot).toStrictEqualTyped({ + error: new Error("Could not fetch"), + }); + } + + await expect(takeRender).not.toRerender(); +}); + +test("throws graphql errors returned by deferred queries", async () => { + using _consoleSpy = spyOnConsole("error"); + + const query = gql` + query { + greeting { + message + ... on Greeting @defer { + recipient { + name + } + } + } + } + `; + + const { httpLink, enqueueInitialChunk } = mockDeferStreamGraphQL17Alpha9(); + + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: httpLink, + incrementalHandler: new GraphQL17Alpha9Handler(), + }); + + using _disabledAct = disableActEnvironment(); + const { takeRender } = await renderSuspenseHook( + () => useSuspenseQuery(query), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + } + + enqueueInitialChunk({ + errors: [{ message: "Could not fetch greeting" }], + hasNext: false, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["ErrorBoundary"]); + expect(snapshot).toStrictEqualTyped({ + error: new CombinedGraphQLErrors({ + data: null, + errors: [{ message: "Could not fetch greeting" }], + }), + }); + } + + await expect(takeRender).not.toRerender(); +}); + +test("throws errors returned by deferred queries that include partial data", async () => { + using _consoleSpy = spyOnConsole("error"); + + const query = gql` + query { + greeting { + message + ... on Greeting @defer { + recipient { + name + } + } + } + } + `; + + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: new ApolloLink(() => { + return of({ + data: { greeting: null }, + errors: [{ message: "Could not fetch greeting" }], + }).pipe(delay(20)); + }), + incrementalHandler: new GraphQL17Alpha9Handler(), + }); + + using _disabledAct = disableActEnvironment(); + const { takeRender } = await renderSuspenseHook( + () => useSuspenseQuery(query), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + } + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["ErrorBoundary"]); + expect(snapshot).toStrictEqualTyped({ + error: new CombinedGraphQLErrors({ + data: { greeting: null }, + errors: [{ message: "Could not fetch greeting" }], + }), + }); + } + + await expect(takeRender).not.toRerender(); +}); + +test("discards partial data and throws errors returned in incremental chunks", async () => { + using _consoleSpy = spyOnConsole("error"); + + const query = gql` + query { + hero { + name + heroFriends { + id + name + ... @defer { + homeWorld + } + } + } + } + `; + + const { httpLink, enqueueInitialChunk, enqueueSubsequentChunk } = + mockDeferStreamGraphQL17Alpha9(); + + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: httpLink, + incrementalHandler: new GraphQL17Alpha9Handler(), + }); + + using _disabledAct = disableActEnvironment(); + const { takeRender } = await renderSuspenseHook( + () => useSuspenseQuery(query), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + } + + enqueueInitialChunk({ + data: { + hero: { + name: "R2-D2", + heroFriends: [ + { + id: "1000", + name: "Luke Skywalker", + }, + { + id: "1003", + name: "Leia Organa", + }, + ], + }, + }, + pending: [ + { id: "0", path: ["hero", "heroFriends", 0] }, + { id: "1", path: ["hero", "heroFriends", 1] }, + ], + hasNext: true, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: markAsStreaming({ + hero: { + heroFriends: [ + { + id: "1000", + name: "Luke Skywalker", + }, + { + id: "1003", + name: "Leia Organa", + }, + ], + name: "R2-D2", + }, + }), + dataState: "streaming", + networkStatus: NetworkStatus.streaming, + error: undefined, + }); + } + + enqueueSubsequentChunk({ + incremental: [ + { + id: "0", + errors: [ + { + message: + "homeWorld for character with ID 1000 could not be fetched.", + path: ["hero", "heroFriends", 0, "homeWorld"], + }, + ], + data: { + homeWorld: null, + }, + }, + // This chunk is ignored since errorPolicy `none` throws away partial + // data + { + id: "1", + data: { + homeWorld: "Alderaan", + }, + }, + ], + completed: [{ id: "0" }, { id: "1" }], + hasNext: false, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["ErrorBoundary"]); + expect(snapshot).toStrictEqualTyped({ + error: new CombinedGraphQLErrors({ + data: { + hero: { + heroFriends: [ + { + id: "1000", + name: "Luke Skywalker", + homeWorld: null, + }, + { + id: "1003", + name: "Leia Organa", + homeWorld: "Alderaan", + }, + ], + name: "R2-D2", + }, + }, + errors: [ + { + message: + "homeWorld for character with ID 1000 could not be fetched.", + path: ["hero", "heroFriends", 0, "homeWorld"], + }, + ], + }), + }); + } + + await expect(takeRender).not.toRerender(); +}); + +test("adds partial data and does not throw errors returned in incremental chunks but returns them in `error` property with errorPolicy set to `all`", async () => { + const query = gql` + query { + hero { + name + heroFriends { + id + name + ... @defer { + homeWorld + } + } + } + } + `; + + const { httpLink, enqueueInitialChunk, enqueueSubsequentChunk } = + mockDeferStreamGraphQL17Alpha9(); + + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: httpLink, + incrementalHandler: new GraphQL17Alpha9Handler(), + }); + + using _disabledAct = disableActEnvironment(); + const { takeRender } = await renderSuspenseHook( + () => useSuspenseQuery(query, { errorPolicy: "all" }), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + } + + enqueueInitialChunk({ + data: { + hero: { + name: "R2-D2", + heroFriends: [ + { + id: "1000", + name: "Luke Skywalker", + }, + { + id: "1003", + name: "Leia Organa", + }, + ], + }, + }, + pending: [ + { id: "0", path: ["hero", "heroFriends", 0] }, + { id: "1", path: ["hero", "heroFriends", 1] }, + ], + hasNext: true, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: markAsStreaming({ + hero: { + heroFriends: [ + { + id: "1000", + name: "Luke Skywalker", + }, + { + id: "1003", + name: "Leia Organa", + }, + ], + name: "R2-D2", + }, + }), + dataState: "streaming", + networkStatus: NetworkStatus.streaming, + error: undefined, + }); + } + + enqueueSubsequentChunk({ + incremental: [ + { + id: "0", + errors: [ + { + message: + "homeWorld for character with ID 1000 could not be fetched.", + path: ["hero", "heroFriends", 0, "homeWorld"], + }, + ], + data: { + homeWorld: null, + }, + }, + // Unlike the default (errorPolicy = `none`), this data will be + // added to the final result + { + id: "1", + data: { + homeWorld: "Alderaan", + }, + }, + ], + completed: [{ id: "0" }, { id: "1" }], + hasNext: false, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: { + hero: { + heroFriends: [ + { + id: "1000", + name: "Luke Skywalker", + homeWorld: null, + }, + { + id: "1003", + name: "Leia Organa", + homeWorld: "Alderaan", + }, + ], + name: "R2-D2", + }, + }, + dataState: "complete", + networkStatus: NetworkStatus.error, + error: new CombinedGraphQLErrors({ + data: { + hero: { + heroFriends: [ + { + id: "1000", + name: "Luke Skywalker", + homeWorld: null, + }, + { + id: "1003", + name: "Leia Organa", + homeWorld: "Alderaan", + }, + ], + name: "R2-D2", + }, + }, + errors: [ + { + message: + "homeWorld for character with ID 1000 could not be fetched.", + path: ["hero", "heroFriends", 0, "homeWorld"], + }, + ], + }), + }); + } + + await expect(takeRender).not.toRerender(); +}); + +test("adds partial data and discards errors returned in incremental chunks with errorPolicy set to `ignore`", async () => { + const query = gql` + query { + hero { + name + heroFriends { + id + name + ... @defer { + homeWorld + } + } + } + } + `; + + const { httpLink, enqueueInitialChunk, enqueueSubsequentChunk } = + mockDeferStreamGraphQL17Alpha9(); + + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: httpLink, + incrementalHandler: new GraphQL17Alpha9Handler(), + }); + + using _disabledAct = disableActEnvironment(); + const { takeRender } = await renderSuspenseHook( + () => useSuspenseQuery(query, { errorPolicy: "ignore" }), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + } + + enqueueInitialChunk({ + data: { + hero: { + name: "R2-D2", + heroFriends: [ + { + id: "1000", + name: "Luke Skywalker", + }, + { + id: "1003", + name: "Leia Organa", + }, + ], + }, + }, + pending: [ + { id: "0", path: ["hero", "heroFriends", 0] }, + { id: "1", path: ["hero", "heroFriends", 1] }, + ], + hasNext: true, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: markAsStreaming({ + hero: { + heroFriends: [ + { + id: "1000", + name: "Luke Skywalker", + }, + { + id: "1003", + name: "Leia Organa", + }, + ], + name: "R2-D2", + }, + }), + dataState: "streaming", + networkStatus: NetworkStatus.streaming, + error: undefined, + }); + } + + enqueueSubsequentChunk({ + incremental: [ + { + id: "0", + errors: [ + { + message: + "homeWorld for character with ID 1000 could not be fetched.", + path: ["hero", "heroFriends", 0, "homeWorld"], + }, + ], + data: { + homeWorld: null, + }, + }, + { + id: "1", + data: { + homeWorld: "Alderaan", + }, + }, + ], + completed: [{ id: "0" }, { id: "1" }], + hasNext: false, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: { + hero: { + heroFriends: [ + { + id: "1000", + name: "Luke Skywalker", + homeWorld: null, + }, + { + id: "1003", + name: "Leia Organa", + homeWorld: "Alderaan", + }, + ], + name: "R2-D2", + }, + }, + dataState: "complete", + networkStatus: NetworkStatus.ready, + error: undefined, + }); + } + + await expect(takeRender).not.toRerender(); +}); + +test("can refetch and respond to cache updates after encountering an error in an incremental chunk for a deferred query when `errorPolicy` is `all`", async () => { + const query = gql` + query { + hero { + name + heroFriends { + id + name + ... @defer { + homeWorld + } + } + } + } + `; + + const { httpLink, enqueueInitialChunk, enqueueSubsequentChunk } = + mockDeferStreamGraphQL17Alpha9(); + + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: httpLink, + incrementalHandler: new GraphQL17Alpha9Handler(), + }); + + using _disabledAct = disableActEnvironment(); + const { takeRender, getCurrentSnapshot } = await renderSuspenseHook( + () => useSuspenseQuery(query, { errorPolicy: "all" }), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + } + + enqueueInitialChunk({ + data: { + hero: { + name: "R2-D2", + heroFriends: [ + { id: "1000", name: "Luke Skywalker" }, + { id: "1003", name: "Leia Organa" }, + ], + }, + }, + pending: [ + { id: "0", path: ["hero", "heroFriends", 0] }, + { id: "1", path: ["hero", "heroFriends", 1] }, + ], + hasNext: true, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: markAsStreaming({ + hero: { + heroFriends: [ + { id: "1000", name: "Luke Skywalker" }, + { id: "1003", name: "Leia Organa" }, + ], + name: "R2-D2", + }, + }), + dataState: "streaming", + networkStatus: NetworkStatus.streaming, + error: undefined, + }); + } + + enqueueSubsequentChunk({ + incremental: [ + { + id: "0", + errors: [ + { + message: + "homeWorld for character with ID 1000 could not be fetched.", + path: ["hero", "heroFriends", 0, "homeWorld"], + }, + ], + data: { + homeWorld: null, + }, + }, + { + id: "1", + data: { + homeWorld: "Alderaan", + }, + }, + ], + completed: [{ id: "0" }, { id: "1" }], + hasNext: false, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: { + hero: { + heroFriends: [ + { id: "1000", name: "Luke Skywalker", homeWorld: null }, + { id: "1003", name: "Leia Organa", homeWorld: "Alderaan" }, + ], + name: "R2-D2", + }, + }, + dataState: "complete", + networkStatus: NetworkStatus.error, + error: new CombinedGraphQLErrors({ + data: { + hero: { + heroFriends: [ + { id: "1000", name: "Luke Skywalker", homeWorld: null }, + { id: "1003", name: "Leia Organa", homeWorld: "Alderaan" }, + ], + name: "R2-D2", + }, + }, + errors: [ + { + message: + "homeWorld for character with ID 1000 could not be fetched.", + path: ["hero", "heroFriends", 0, "homeWorld"], + }, + ], + }), + }); + } + + const refetchPromise = getCurrentSnapshot().refetch(); + + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + } + + enqueueInitialChunk({ + data: { + hero: { + name: "R2-D2", + heroFriends: [ + { id: "1000", name: "Luke Skywalker" }, + { id: "1003", name: "Leia Organa" }, + ], + }, + }, + pending: [ + { id: "0", path: ["hero", "heroFriends", 0] }, + { id: "1", path: ["hero", "heroFriends", 1] }, + ], + hasNext: true, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: markAsStreaming({ + hero: { + heroFriends: [ + { id: "1000", name: "Luke Skywalker", homeWorld: null }, + { id: "1003", name: "Leia Organa", homeWorld: "Alderaan" }, + ], + name: "R2-D2", + }, + }), + dataState: "streaming", + networkStatus: NetworkStatus.streaming, + error: undefined, + }); + } + + enqueueSubsequentChunk({ + incremental: [ + { + id: "0", + data: { + homeWorld: "Alderaan", + }, + }, + { + id: "1", + data: { + homeWorld: "Alderaan", + }, + }, + ], + completed: [{ id: "0" }, { id: "1" }], + hasNext: false, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: { + hero: { + heroFriends: [ + { id: "1000", name: "Luke Skywalker", homeWorld: "Alderaan" }, + { id: "1003", name: "Leia Organa", homeWorld: "Alderaan" }, + ], + name: "R2-D2", + }, + }, + dataState: "complete", + networkStatus: NetworkStatus.ready, + error: undefined, + }); + } + + await expect(refetchPromise).resolves.toStrictEqualTyped({ + data: { + hero: { + heroFriends: [ + { id: "1000", name: "Luke Skywalker", homeWorld: "Alderaan" }, + { id: "1003", name: "Leia Organa", homeWorld: "Alderaan" }, + ], + name: "R2-D2", + }, + }, + }); + + client.cache.updateQuery({ query }, (data) => ({ + hero: { + ...data.hero, + name: "C3PO", + }, + })); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useSuspenseQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: { + hero: { + heroFriends: [ + { id: "1000", name: "Luke Skywalker", homeWorld: "Alderaan" }, + { id: "1003", name: "Leia Organa", homeWorld: "Alderaan" }, + ], + name: "C3PO", + }, + }, + dataState: "complete", + networkStatus: NetworkStatus.ready, + error: undefined, + }); + } + + await expect(takeRender).not.toRerender(); +}); From 61736c056a21b61af4af84d962242f64e0f09a21 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 4 Sep 2025 22:36:41 -0600 Subject: [PATCH 58/97] Copy useBackgroundQuery defer tests to own file --- .../__tests__/useBackgroundQuery.test.tsx | 297 -------------- .../useBackgroundQuery/defer20220824.test.tsx | 361 ++++++++++++++++++ 2 files changed, 361 insertions(+), 297 deletions(-) create mode 100644 src/react/hooks/__tests__/useBackgroundQuery/defer20220824.test.tsx diff --git a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx index d0bce5acec0..d1c0db9e893 100644 --- a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx +++ b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx @@ -30,7 +30,6 @@ import { NetworkStatus, } from "@apollo/client"; import { InMemoryCache } from "@apollo/client/cache"; -import { Defer20220824Handler } from "@apollo/client/incremental"; import type { QueryRef } from "@apollo/client/react"; import { ApolloProvider, @@ -1391,150 +1390,6 @@ it("works with startTransition to change variables", async () => { } }); -it('does not suspend deferred queries with data in the cache and using a "cache-and-network" fetch policy', async () => { - interface Data { - greeting: { - __typename: string; - message: string; - recipient: { name: string; __typename: string }; - }; - } - - const query: TypedDocumentNode = gql` - query { - greeting { - message - ... on Greeting @defer { - recipient { - name - } - } - } - } - `; - - const link = new MockSubscriptionLink(); - const cache = new InMemoryCache(); - cache.writeQuery({ - query, - data: { - greeting: { - __typename: "Greeting", - message: "Hello cached", - recipient: { __typename: "Person", name: "Cached Alice" }, - }, - }, - }); - const client = new ApolloClient({ - cache, - link, - incrementalHandler: new Defer20220824Handler(), - }); - - const renderStream = createDefaultProfiler(); - - const { SuspenseFallback, ReadQueryHook } = - createDefaultTrackedComponents(renderStream); - - function App() { - useTrackRenders(); - const [queryRef] = useBackgroundQuery(query, { - fetchPolicy: "cache-and-network", - }); - - return ( - }> - - - ); - } - - using _disabledAct = disableActEnvironment(); - await renderStream.render(, { wrapper: createClientWrapper(client) }); - - { - const { snapshot, renderedComponents } = await renderStream.takeRender(); - - expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); - expect(snapshot.result).toStrictEqualTyped({ - data: { - greeting: { - __typename: "Greeting", - message: "Hello cached", - recipient: { __typename: "Person", name: "Cached Alice" }, - }, - }, - dataState: "complete", - error: undefined, - networkStatus: NetworkStatus.loading, - }); - } - - link.simulateResult({ - result: { - data: { - greeting: { __typename: "Greeting", message: "Hello world" }, - }, - hasNext: true, - }, - }); - - { - const { snapshot, renderedComponents } = await renderStream.takeRender(); - - expect(renderedComponents).toStrictEqual([ReadQueryHook]); - expect(snapshot.result).toStrictEqualTyped({ - data: { - greeting: { - __typename: "Greeting", - message: "Hello world", - recipient: { __typename: "Person", name: "Cached Alice" }, - }, - }, - dataState: "streaming", - error: undefined, - networkStatus: NetworkStatus.streaming, - }); - } - - link.simulateResult( - { - result: { - incremental: [ - { - data: { - recipient: { name: "Alice", __typename: "Person" }, - __typename: "Greeting", - }, - path: ["greeting"], - }, - ], - hasNext: false, - }, - }, - true - ); - - { - const { snapshot, renderedComponents } = await renderStream.takeRender(); - - expect(renderedComponents).toStrictEqual([ReadQueryHook]); - expect(snapshot.result).toStrictEqualTyped({ - data: { - greeting: { - __typename: "Greeting", - message: "Hello world", - recipient: { __typename: "Person", name: "Alice" }, - }, - }, - dataState: "complete", - error: undefined, - networkStatus: NetworkStatus.ready, - }); - } - - await expect(renderStream).not.toRerender({ timeout: 50 }); -}); it("reacts to cache updates", async () => { const { query, mocks } = setupSimpleCase(); @@ -3816,158 +3671,6 @@ it('suspends and does not use partial data when changing variables and using a " await expect(renderStream).not.toRerender({ timeout: 50 }); }); -it('does not suspend deferred queries with partial data in the cache and using a "cache-first" fetch policy with `returnPartialData`', async () => { - interface QueryData { - greeting: { - __typename: string; - message?: string; - recipient?: { - __typename: string; - name: string; - }; - }; - } - - const query: TypedDocumentNode = gql` - query { - greeting { - message - ... on Greeting @defer { - recipient { - name - } - } - } - } - `; - - const link = new MockSubscriptionLink(); - const cache = new InMemoryCache(); - - // We are intentionally writing partial data to the cache. Supress console - // warnings to avoid unnecessary noise in the test. - { - using _consoleSpy = spyOnConsole("error"); - cache.writeQuery({ - query, - data: { - greeting: { - __typename: "Greeting", - recipient: { __typename: "Person", name: "Cached Alice" }, - }, - }, - }); - } - - const client = new ApolloClient({ - link, - cache, - incrementalHandler: new Defer20220824Handler(), - }); - - const renderStream = createDefaultProfiler>(); - const { SuspenseFallback, ReadQueryHook } = - createDefaultTrackedComponents(renderStream); - - function App() { - useTrackRenders(); - const [queryRef] = useBackgroundQuery(query, { - fetchPolicy: "cache-first", - returnPartialData: true, - }); - - return ( - }> - - - ); - } - - using _disabledAct = disableActEnvironment(); - await renderStream.render(, { wrapper: createClientWrapper(client) }); - - { - const { snapshot, renderedComponents } = await renderStream.takeRender(); - - expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); - expect(snapshot.result).toStrictEqualTyped({ - data: { - greeting: { - __typename: "Greeting", - recipient: { __typename: "Person", name: "Cached Alice" }, - }, - }, - dataState: "partial", - error: undefined, - networkStatus: NetworkStatus.loading, - }); - } - - link.simulateResult({ - result: { - data: { - greeting: { message: "Hello world", __typename: "Greeting" }, - }, - hasNext: true, - }, - }); - - { - const { snapshot, renderedComponents } = await renderStream.takeRender(); - - expect(renderedComponents).toStrictEqual([ReadQueryHook]); - expect(snapshot.result).toStrictEqualTyped({ - data: { - greeting: { - __typename: "Greeting", - message: "Hello world", - recipient: { __typename: "Person", name: "Cached Alice" }, - }, - }, - dataState: "streaming", - error: undefined, - networkStatus: NetworkStatus.streaming, - }); - } - - link.simulateResult( - { - result: { - incremental: [ - { - data: { - __typename: "Greeting", - recipient: { name: "Alice", __typename: "Person" }, - }, - path: ["greeting"], - }, - ], - hasNext: false, - }, - }, - true - ); - - { - const { snapshot, renderedComponents } = await renderStream.takeRender(); - - expect(renderedComponents).toStrictEqual([ReadQueryHook]); - expect(snapshot.result).toStrictEqualTyped({ - data: { - greeting: { - __typename: "Greeting", - message: "Hello world", - recipient: { __typename: "Person", name: "Alice" }, - }, - }, - dataState: "complete", - error: undefined, - networkStatus: NetworkStatus.ready, - }); - } - - await expect(renderStream).not.toRerender({ timeout: 50 }); -}); it.each([ "cache-first", diff --git a/src/react/hooks/__tests__/useBackgroundQuery/defer20220824.test.tsx b/src/react/hooks/__tests__/useBackgroundQuery/defer20220824.test.tsx new file mode 100644 index 00000000000..f2f0b875029 --- /dev/null +++ b/src/react/hooks/__tests__/useBackgroundQuery/defer20220824.test.tsx @@ -0,0 +1,361 @@ +import type { RenderStream } from "@testing-library/react-render-stream"; +import { + createRenderStream, + disableActEnvironment, + useTrackRenders, +} from "@testing-library/react-render-stream"; +import React, { Suspense } from "react"; + +import type { DataState, TypedDocumentNode } from "@apollo/client"; +import { ApolloClient, gql, NetworkStatus } from "@apollo/client"; +import { InMemoryCache } from "@apollo/client/cache"; +import { Defer20220824Handler } from "@apollo/client/incremental"; +import type { QueryRef } from "@apollo/client/react"; +import { useBackgroundQuery, useReadQuery } from "@apollo/client/react"; +import { MockSubscriptionLink } from "@apollo/client/testing"; +import { + createClientWrapper, + spyOnConsole, +} from "@apollo/client/testing/internal"; +import type { DeepPartial } from "@apollo/client/utilities"; + +function createDefaultTrackedComponents< + Snapshot extends { + result: useReadQuery.Result | null; + }, + TData = Snapshot["result"] extends useReadQuery.Result | null ? + TData + : unknown, + TStates extends DataState["dataState"] = Snapshot["result"] extends ( + useReadQuery.Result | null + ) ? + TStates + : "complete" | "streaming", +>(renderStream: RenderStream) { + function SuspenseFallback() { + useTrackRenders(); + return
Loading
; + } + + function ReadQueryHook({ + queryRef, + }: { + queryRef: QueryRef; + }) { + useTrackRenders(); + renderStream.mergeSnapshot({ + result: useReadQuery(queryRef), + } as unknown as Partial); + + return null; + } + + return { SuspenseFallback, ReadQueryHook }; +} + +function createDefaultProfiler() { + return createRenderStream({ + initialSnapshot: { + result: null as useReadQuery.Result | null, + }, + }); +} + +test('does not suspend deferred queries with data in the cache and using a "cache-and-network" fetch policy', async () => { + interface Data { + greeting: { + __typename: string; + message: string; + recipient: { name: string; __typename: string }; + }; + } + + const query: TypedDocumentNode = gql` + query { + greeting { + message + ... on Greeting @defer { + recipient { + name + } + } + } + } + `; + + const link = new MockSubscriptionLink(); + const cache = new InMemoryCache(); + cache.writeQuery({ + query, + data: { + greeting: { + __typename: "Greeting", + message: "Hello cached", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + }); + const client = new ApolloClient({ + cache, + link, + incrementalHandler: new Defer20220824Handler(), + }); + + const renderStream = createDefaultProfiler(); + + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(renderStream); + + function App() { + useTrackRenders(); + const [queryRef] = useBackgroundQuery(query, { + fetchPolicy: "cache-and-network", + }); + + return ( + }> + + + ); + } + + using _disabledAct = disableActEnvironment(); + await renderStream.render(, { wrapper: createClientWrapper(client) }); + + { + const { snapshot, renderedComponents } = await renderStream.takeRender(); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot.result).toStrictEqualTyped({ + data: { + greeting: { + __typename: "Greeting", + message: "Hello cached", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + dataState: "complete", + error: undefined, + networkStatus: NetworkStatus.loading, + }); + } + + link.simulateResult({ + result: { + data: { + greeting: { __typename: "Greeting", message: "Hello world" }, + }, + hasNext: true, + }, + }); + + { + const { snapshot, renderedComponents } = await renderStream.takeRender(); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toStrictEqualTyped({ + data: { + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + dataState: "streaming", + error: undefined, + networkStatus: NetworkStatus.streaming, + }); + } + + link.simulateResult( + { + result: { + incremental: [ + { + data: { + recipient: { name: "Alice", __typename: "Person" }, + __typename: "Greeting", + }, + path: ["greeting"], + }, + ], + hasNext: false, + }, + }, + true + ); + + { + const { snapshot, renderedComponents } = await renderStream.takeRender(); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toStrictEqualTyped({ + data: { + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Alice" }, + }, + }, + dataState: "complete", + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await expect(renderStream).not.toRerender({ timeout: 50 }); +}); + +test('does not suspend deferred queries with partial data in the cache and using a "cache-first" fetch policy with `returnPartialData`', async () => { + interface QueryData { + greeting: { + __typename: string; + message?: string; + recipient?: { + __typename: string; + name: string; + }; + }; + } + + const query: TypedDocumentNode = gql` + query { + greeting { + message + ... on Greeting @defer { + recipient { + name + } + } + } + } + `; + + const link = new MockSubscriptionLink(); + const cache = new InMemoryCache(); + + // We are intentionally writing partial data to the cache. Supress console + // warnings to avoid unnecessary noise in the test. + { + using _consoleSpy = spyOnConsole("error"); + cache.writeQuery({ + query, + data: { + greeting: { + __typename: "Greeting", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + }); + } + + const client = new ApolloClient({ + link, + cache, + incrementalHandler: new Defer20220824Handler(), + }); + + const renderStream = createDefaultProfiler>(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(renderStream); + + function App() { + useTrackRenders(); + const [queryRef] = useBackgroundQuery(query, { + fetchPolicy: "cache-first", + returnPartialData: true, + }); + + return ( + }> + + + ); + } + + using _disabledAct = disableActEnvironment(); + await renderStream.render(, { wrapper: createClientWrapper(client) }); + + { + const { snapshot, renderedComponents } = await renderStream.takeRender(); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot.result).toStrictEqualTyped({ + data: { + greeting: { + __typename: "Greeting", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + dataState: "partial", + error: undefined, + networkStatus: NetworkStatus.loading, + }); + } + + link.simulateResult({ + result: { + data: { + greeting: { message: "Hello world", __typename: "Greeting" }, + }, + hasNext: true, + }, + }); + + { + const { snapshot, renderedComponents } = await renderStream.takeRender(); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toStrictEqualTyped({ + data: { + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + dataState: "streaming", + error: undefined, + networkStatus: NetworkStatus.streaming, + }); + } + + link.simulateResult( + { + result: { + incremental: [ + { + data: { + __typename: "Greeting", + recipient: { name: "Alice", __typename: "Person" }, + }, + path: ["greeting"], + }, + ], + hasNext: false, + }, + }, + true + ); + + { + const { snapshot, renderedComponents } = await renderStream.takeRender(); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toStrictEqualTyped({ + data: { + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Alice" }, + }, + }, + dataState: "complete", + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await expect(renderStream).not.toRerender({ timeout: 50 }); +}); + From aa17427a547356bed7dd8fe5888e9e6d78512c64 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 4 Sep 2025 22:40:14 -0600 Subject: [PATCH 59/97] Use the new helpers --- .../useBackgroundQuery/defer20220824.test.tsx | 88 ++++++++----------- 1 file changed, 39 insertions(+), 49 deletions(-) diff --git a/src/react/hooks/__tests__/useBackgroundQuery/defer20220824.test.tsx b/src/react/hooks/__tests__/useBackgroundQuery/defer20220824.test.tsx index f2f0b875029..182866ea9af 100644 --- a/src/react/hooks/__tests__/useBackgroundQuery/defer20220824.test.tsx +++ b/src/react/hooks/__tests__/useBackgroundQuery/defer20220824.test.tsx @@ -12,9 +12,9 @@ import { InMemoryCache } from "@apollo/client/cache"; import { Defer20220824Handler } from "@apollo/client/incremental"; import type { QueryRef } from "@apollo/client/react"; import { useBackgroundQuery, useReadQuery } from "@apollo/client/react"; -import { MockSubscriptionLink } from "@apollo/client/testing"; import { createClientWrapper, + mockDefer20220824, spyOnConsole, } from "@apollo/client/testing/internal"; import type { DeepPartial } from "@apollo/client/utilities"; @@ -83,7 +83,9 @@ test('does not suspend deferred queries with data in the cache and using a "cach } `; - const link = new MockSubscriptionLink(); + const { httpLink, enqueueInitialChunk, enqueueSubsequentChunk } = + mockDefer20220824(); + const cache = new InMemoryCache(); cache.writeQuery({ query, @@ -97,7 +99,7 @@ test('does not suspend deferred queries with data in the cache and using a "cach }); const client = new ApolloClient({ cache, - link, + link: httpLink, incrementalHandler: new Defer20220824Handler(), }); @@ -140,13 +142,11 @@ test('does not suspend deferred queries with data in the cache and using a "cach }); } - link.simulateResult({ - result: { - data: { - greeting: { __typename: "Greeting", message: "Hello world" }, - }, - hasNext: true, + enqueueInitialChunk({ + data: { + greeting: { __typename: "Greeting", message: "Hello world" }, }, + hasNext: true, }); { @@ -167,23 +167,18 @@ test('does not suspend deferred queries with data in the cache and using a "cach }); } - link.simulateResult( - { - result: { - incremental: [ - { - data: { - recipient: { name: "Alice", __typename: "Person" }, - __typename: "Greeting", - }, - path: ["greeting"], - }, - ], - hasNext: false, + enqueueSubsequentChunk({ + incremental: [ + { + data: { + recipient: { name: "Alice", __typename: "Person" }, + __typename: "Greeting", + }, + path: ["greeting"], }, - }, - true - ); + ], + hasNext: false, + }); { const { snapshot, renderedComponents } = await renderStream.takeRender(); @@ -231,7 +226,9 @@ test('does not suspend deferred queries with partial data in the cache and using } `; - const link = new MockSubscriptionLink(); + const { httpLink, enqueueInitialChunk, enqueueSubsequentChunk } = + mockDefer20220824(); + const cache = new InMemoryCache(); // We are intentionally writing partial data to the cache. Supress console @@ -250,7 +247,7 @@ test('does not suspend deferred queries with partial data in the cache and using } const client = new ApolloClient({ - link, + link: httpLink, cache, incrementalHandler: new Defer20220824Handler(), }); @@ -293,13 +290,11 @@ test('does not suspend deferred queries with partial data in the cache and using }); } - link.simulateResult({ - result: { - data: { - greeting: { message: "Hello world", __typename: "Greeting" }, - }, - hasNext: true, + enqueueInitialChunk({ + data: { + greeting: { message: "Hello world", __typename: "Greeting" }, }, + hasNext: true, }); { @@ -320,23 +315,18 @@ test('does not suspend deferred queries with partial data in the cache and using }); } - link.simulateResult( - { - result: { - incremental: [ - { - data: { - __typename: "Greeting", - recipient: { name: "Alice", __typename: "Person" }, - }, - path: ["greeting"], - }, - ], - hasNext: false, + enqueueSubsequentChunk({ + incremental: [ + { + data: { + __typename: "Greeting", + recipient: { name: "Alice", __typename: "Person" }, + }, + path: ["greeting"], }, - }, - true - ); + ], + hasNext: false, + }); { const { snapshot, renderedComponents } = await renderStream.takeRender(); From da0113cf7240a99b0bf88feff42f33b17eb375a5 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 4 Sep 2025 22:59:41 -0600 Subject: [PATCH 60/97] Use a pattern similar to useSuspenseQuery tests in useBackgroundQuery defer tests --- .../useBackgroundQuery/defer20220824.test.tsx | 196 +++++++++--------- 1 file changed, 101 insertions(+), 95 deletions(-) diff --git a/src/react/hooks/__tests__/useBackgroundQuery/defer20220824.test.tsx b/src/react/hooks/__tests__/useBackgroundQuery/defer20220824.test.tsx index 182866ea9af..9ce0c721105 100644 --- a/src/react/hooks/__tests__/useBackgroundQuery/defer20220824.test.tsx +++ b/src/react/hooks/__tests__/useBackgroundQuery/defer20220824.test.tsx @@ -1,12 +1,18 @@ -import type { RenderStream } from "@testing-library/react-render-stream"; +import type { RenderOptions } from "@testing-library/react"; import { createRenderStream, disableActEnvironment, useTrackRenders, } from "@testing-library/react-render-stream"; import React, { Suspense } from "react"; - -import type { DataState, TypedDocumentNode } from "@apollo/client"; +import { ErrorBoundary } from "react-error-boundary"; + +import type { + DataState, + ErrorLike, + OperationVariables, + TypedDocumentNode, +} from "@apollo/client"; import { ApolloClient, gql, NetworkStatus } from "@apollo/client"; import { InMemoryCache } from "@apollo/client/cache"; import { Defer20220824Handler } from "@apollo/client/incremental"; @@ -17,48 +23,69 @@ import { mockDefer20220824, spyOnConsole, } from "@apollo/client/testing/internal"; -import type { DeepPartial } from "@apollo/client/utilities"; - -function createDefaultTrackedComponents< - Snapshot extends { - result: useReadQuery.Result | null; - }, - TData = Snapshot["result"] extends useReadQuery.Result | null ? - TData - : unknown, - TStates extends DataState["dataState"] = Snapshot["result"] extends ( - useReadQuery.Result | null + +async function renderSuspenseHook< + TData, + TVariables extends OperationVariables, + TQueryRef extends QueryRef, + TStates extends DataState["dataState"] = TQueryRef extends ( + QueryRef ) ? - TStates - : "complete" | "streaming", ->(renderStream: RenderStream) { + States + : never, + Props = never, +>( + renderHook: ( + props: Props extends never ? undefined : Props + ) => [TQueryRef, useBackgroundQuery.Result], + options: Pick & { initialProps?: Props } +) { + function UseReadQuery({ queryRef }: { queryRef: QueryRef }) { + useTrackRenders({ name: "useReadQuery" }); + replaceSnapshot(useReadQuery(queryRef) as any); + + return null; + } + function SuspenseFallback() { - useTrackRenders(); - return
Loading
; + useTrackRenders({ name: "SuspenseFallback" }); + + return null; } - function ReadQueryHook({ - queryRef, - }: { - queryRef: QueryRef; - }) { - useTrackRenders(); - renderStream.mergeSnapshot({ - result: useReadQuery(queryRef), - } as unknown as Partial); + function ErrorFallback() { + useTrackRenders({ name: "ErrorBoundary" }); return null; } - return { SuspenseFallback, ReadQueryHook }; -} + function App({ props }: { props: Props | undefined }) { + useTrackRenders({ name: "useBackgroundQuery" }); + const [queryRef] = renderHook(props as any); -function createDefaultProfiler() { - return createRenderStream({ - initialSnapshot: { - result: null as useReadQuery.Result | null, - }, - }); + return ( + }> + replaceSnapshot({ error })} + > + + + + ); + } + + const { render, takeRender, replaceSnapshot } = createRenderStream< + useReadQuery.Result | { error: ErrorLike } + >(); + + const utils = await render(, options); + + function rerender(props: Props) { + return utils.rerender(); + } + + return { takeRender, rerender }; } test('does not suspend deferred queries with data in the cache and using a "cache-and-network" fetch policy', async () => { @@ -103,32 +130,20 @@ test('does not suspend deferred queries with data in the cache and using a "cach incrementalHandler: new Defer20220824Handler(), }); - const renderStream = createDefaultProfiler(); - - const { SuspenseFallback, ReadQueryHook } = - createDefaultTrackedComponents(renderStream); - - function App() { - useTrackRenders(); - const [queryRef] = useBackgroundQuery(query, { - fetchPolicy: "cache-and-network", - }); - - return ( - }> - - - ); - } - using _disabledAct = disableActEnvironment(); - await renderStream.render(, { wrapper: createClientWrapper(client) }); + const { takeRender } = await renderSuspenseHook( + () => useBackgroundQuery(query, { fetchPolicy: "cache-and-network" }), + { wrapper: createClientWrapper(client) } + ); { - const { snapshot, renderedComponents } = await renderStream.takeRender(); + const { snapshot, renderedComponents } = await takeRender(); - expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); - expect(snapshot.result).toStrictEqualTyped({ + expect(renderedComponents).toStrictEqual([ + "useBackgroundQuery", + "useReadQuery", + ]); + expect(snapshot).toStrictEqualTyped({ data: { greeting: { __typename: "Greeting", @@ -150,10 +165,10 @@ test('does not suspend deferred queries with data in the cache and using a "cach }); { - const { snapshot, renderedComponents } = await renderStream.takeRender(); + const { snapshot, renderedComponents } = await takeRender(); - expect(renderedComponents).toStrictEqual([ReadQueryHook]); - expect(snapshot.result).toStrictEqualTyped({ + expect(renderedComponents).toStrictEqual(["useReadQuery"]); + expect(snapshot).toStrictEqualTyped({ data: { greeting: { __typename: "Greeting", @@ -181,10 +196,10 @@ test('does not suspend deferred queries with data in the cache and using a "cach }); { - const { snapshot, renderedComponents } = await renderStream.takeRender(); + const { snapshot, renderedComponents } = await takeRender(); - expect(renderedComponents).toStrictEqual([ReadQueryHook]); - expect(snapshot.result).toStrictEqualTyped({ + expect(renderedComponents).toStrictEqual(["useReadQuery"]); + expect(snapshot).toStrictEqualTyped({ data: { greeting: { __typename: "Greeting", @@ -198,7 +213,7 @@ test('does not suspend deferred queries with data in the cache and using a "cach }); } - await expect(renderStream).not.toRerender({ timeout: 50 }); + await expect(takeRender).not.toRerender(); }); test('does not suspend deferred queries with partial data in the cache and using a "cache-first" fetch policy with `returnPartialData`', async () => { @@ -252,32 +267,24 @@ test('does not suspend deferred queries with partial data in the cache and using incrementalHandler: new Defer20220824Handler(), }); - const renderStream = createDefaultProfiler>(); - const { SuspenseFallback, ReadQueryHook } = - createDefaultTrackedComponents(renderStream); - - function App() { - useTrackRenders(); - const [queryRef] = useBackgroundQuery(query, { - fetchPolicy: "cache-first", - returnPartialData: true, - }); - - return ( - }> - - - ); - } - using _disabledAct = disableActEnvironment(); - await renderStream.render(, { wrapper: createClientWrapper(client) }); + const { takeRender } = await renderSuspenseHook( + () => + useBackgroundQuery(query, { + fetchPolicy: "cache-first", + returnPartialData: true, + }), + { wrapper: createClientWrapper(client) } + ); { - const { snapshot, renderedComponents } = await renderStream.takeRender(); + const { snapshot, renderedComponents } = await takeRender(); - expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); - expect(snapshot.result).toStrictEqualTyped({ + expect(renderedComponents).toStrictEqual([ + "useBackgroundQuery", + "useReadQuery", + ]); + expect(snapshot).toStrictEqualTyped({ data: { greeting: { __typename: "Greeting", @@ -298,10 +305,10 @@ test('does not suspend deferred queries with partial data in the cache and using }); { - const { snapshot, renderedComponents } = await renderStream.takeRender(); + const { snapshot, renderedComponents } = await takeRender(); - expect(renderedComponents).toStrictEqual([ReadQueryHook]); - expect(snapshot.result).toStrictEqualTyped({ + expect(renderedComponents).toStrictEqual(["useReadQuery"]); + expect(snapshot).toStrictEqualTyped({ data: { greeting: { __typename: "Greeting", @@ -329,10 +336,10 @@ test('does not suspend deferred queries with partial data in the cache and using }); { - const { snapshot, renderedComponents } = await renderStream.takeRender(); + const { snapshot, renderedComponents } = await takeRender(); - expect(renderedComponents).toStrictEqual([ReadQueryHook]); - expect(snapshot.result).toStrictEqualTyped({ + expect(renderedComponents).toStrictEqual(["useReadQuery"]); + expect(snapshot).toStrictEqualTyped({ data: { greeting: { __typename: "Greeting", @@ -346,6 +353,5 @@ test('does not suspend deferred queries with partial data in the cache and using }); } - await expect(renderStream).not.toRerender({ timeout: 50 }); + await expect(takeRender).not.toRerender(); }); - From 9ae7fa3b6913295297c2eb456d7816551cc104f9 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 4 Sep 2025 23:01:15 -0600 Subject: [PATCH 61/97] Use createClientWrapper helper in the defer test files --- .../__tests__/useQuery/defer20220824.test.tsx | 35 +++------ .../useQuery/deferGraphQL17Alpha2.test.tsx | 35 +++------ .../useSuspenseQuery/defer20220824.test.tsx | 75 +++++-------------- .../deferGraphQL17Alpha9.test.tsx | 75 +++++-------------- 4 files changed, 60 insertions(+), 160 deletions(-) diff --git a/src/react/hooks/__tests__/useQuery/defer20220824.test.tsx b/src/react/hooks/__tests__/useQuery/defer20220824.test.tsx index 79cb0309731..a43a83dc428 100644 --- a/src/react/hooks/__tests__/useQuery/defer20220824.test.tsx +++ b/src/react/hooks/__tests__/useQuery/defer20220824.test.tsx @@ -12,8 +12,9 @@ import { NetworkStatus, } from "@apollo/client"; import { Defer20220824Handler } from "@apollo/client/incremental"; -import { ApolloProvider, useQuery } from "@apollo/client/react"; +import { useQuery } from "@apollo/client/react"; import { + createClientWrapper, markAsStreaming, mockDefer20220824, spyOnConsole, @@ -46,9 +47,7 @@ test("should handle deferred queries", async () => { const { takeSnapshot } = await renderHookToSnapshotStream( () => useQuery(query), { - wrapper: ({ children }) => ( - {children} - ), + wrapper: createClientWrapper(client), } ); @@ -154,9 +153,7 @@ test("should handle deferred queries in lists", async () => { const { takeSnapshot } = await renderHookToSnapshotStream( () => useQuery(query), { - wrapper: ({ children }) => ( - {children} - ), + wrapper: createClientWrapper(client), } ); @@ -312,9 +309,7 @@ test("should handle deferred queries in lists, merging arrays", async () => { const { takeSnapshot } = await renderHookToSnapshotStream( () => useQuery(query), { - wrapper: ({ children }) => ( - {children} - ), + wrapper: createClientWrapper(client), } ); @@ -480,9 +475,7 @@ test("should handle deferred queries with fetch policy no-cache", async () => { const { takeSnapshot } = await renderHookToSnapshotStream( () => useQuery(query, { fetchPolicy: "no-cache" }), { - wrapper: ({ children }) => ( - {children} - ), + wrapper: createClientWrapper(client), } ); @@ -590,9 +583,7 @@ test("should handle deferred queries with errors returned on the incremental bat const { takeSnapshot } = await renderHookToSnapshotStream( () => useQuery(query), { - wrapper: ({ children }) => ( - {children} - ), + wrapper: createClientWrapper(client), } ); @@ -752,9 +743,7 @@ it('should handle deferred queries with errors returned on the incremental batch const { takeSnapshot } = await renderHookToSnapshotStream( () => useQuery(query, { errorPolicy: "all" }), { - wrapper: ({ children }) => ( - {children} - ), + wrapper: createClientWrapper(client), } ); @@ -947,9 +936,7 @@ it('returns eventually consistent data from deferred queries with data in the ca const { takeSnapshot } = await renderHookToSnapshotStream( () => useQuery(query, { fetchPolicy: "cache-and-network" }), { - wrapper: ({ children }) => ( - {children} - ), + wrapper: createClientWrapper(client), } ); @@ -1079,9 +1066,7 @@ it('returns eventually consistent data from deferred queries with partial data i returnPartialData: true, }), { - wrapper: ({ children }) => ( - {children} - ), + wrapper: createClientWrapper(client), } ); diff --git a/src/react/hooks/__tests__/useQuery/deferGraphQL17Alpha2.test.tsx b/src/react/hooks/__tests__/useQuery/deferGraphQL17Alpha2.test.tsx index 218f774691a..60db1cde900 100644 --- a/src/react/hooks/__tests__/useQuery/deferGraphQL17Alpha2.test.tsx +++ b/src/react/hooks/__tests__/useQuery/deferGraphQL17Alpha2.test.tsx @@ -12,8 +12,9 @@ import { NetworkStatus, } from "@apollo/client"; import { GraphQL17Alpha9Handler } from "@apollo/client/incremental"; -import { ApolloProvider, useQuery } from "@apollo/client/react"; +import { useQuery } from "@apollo/client/react"; import { + createClientWrapper, markAsStreaming, mockDeferStreamGraphQL17Alpha9, spyOnConsole, @@ -46,9 +47,7 @@ test("should handle deferred queries", async () => { const { takeSnapshot } = await renderHookToSnapshotStream( () => useQuery(query), { - wrapper: ({ children }) => ( - {children} - ), + wrapper: createClientWrapper(client), } ); @@ -156,9 +155,7 @@ test("should handle deferred queries in lists", async () => { const { takeSnapshot } = await renderHookToSnapshotStream( () => useQuery(query), { - wrapper: ({ children }) => ( - {children} - ), + wrapper: createClientWrapper(client), } ); @@ -286,9 +283,7 @@ test("should handle deferred queries in lists, merging arrays", async () => { const { takeSnapshot } = await renderHookToSnapshotStream( () => useQuery(query), { - wrapper: ({ children }) => ( - {children} - ), + wrapper: createClientWrapper(client), } ); @@ -459,9 +454,7 @@ test("should handle deferred queries with fetch policy no-cache", async () => { const { takeSnapshot } = await renderHookToSnapshotStream( () => useQuery(query, { fetchPolicy: "no-cache" }), { - wrapper: ({ children }) => ( - {children} - ), + wrapper: createClientWrapper(client), } ); @@ -566,9 +559,7 @@ test("should handle deferred queries with errors returned on the incremental bat const { takeSnapshot } = await renderHookToSnapshotStream( () => useQuery(query), { - wrapper: ({ children }) => ( - {children} - ), + wrapper: createClientWrapper(client), } ); @@ -733,9 +724,7 @@ test('should handle deferred queries with errors returned on the incremental bat const { takeSnapshot } = await renderHookToSnapshotStream( () => useQuery(query, { errorPolicy: "all" }), { - wrapper: ({ children }) => ( - {children} - ), + wrapper: createClientWrapper(client), } ); @@ -933,9 +922,7 @@ test('returns eventually consistent data from deferred queries with data in the const { takeSnapshot } = await renderHookToSnapshotStream( () => useQuery(query, { fetchPolicy: "cache-and-network" }), { - wrapper: ({ children }) => ( - {children} - ), + wrapper: createClientWrapper(client), } ); @@ -1067,9 +1054,7 @@ test('returns eventually consistent data from deferred queries with partial data returnPartialData: true, }), { - wrapper: ({ children }) => ( - {children} - ), + wrapper: createClientWrapper(client), } ); diff --git a/src/react/hooks/__tests__/useSuspenseQuery/defer20220824.test.tsx b/src/react/hooks/__tests__/useSuspenseQuery/defer20220824.test.tsx index 59e373142ec..dcf8e4a32cb 100644 --- a/src/react/hooks/__tests__/useSuspenseQuery/defer20220824.test.tsx +++ b/src/react/hooks/__tests__/useSuspenseQuery/defer20220824.test.tsx @@ -18,8 +18,9 @@ import { NetworkStatus, } from "@apollo/client"; import { Defer20220824Handler } from "@apollo/client/incremental"; -import { ApolloProvider, useSuspenseQuery } from "@apollo/client/react"; +import { useSuspenseQuery } from "@apollo/client/react"; import { + createClientWrapper, markAsStreaming, mockDefer20220824, spyOnConsole, @@ -119,9 +120,7 @@ test("suspends deferred queries until initial chunk loads then streams in data a const { takeRender } = await renderSuspenseHook( () => useSuspenseQuery(query), { - wrapper: ({ children }) => ( - {children} - ), + wrapper: createClientWrapper(client), } ); @@ -218,9 +217,7 @@ test.each([ const { takeRender } = await renderSuspenseHook( () => useSuspenseQuery(query, { fetchPolicy }), { - wrapper: ({ children }) => ( - {children} - ), + wrapper: createClientWrapper(client), } ); @@ -323,9 +320,7 @@ test('does not suspend deferred queries with data in the cache and using a "cach const { takeRender } = await renderSuspenseHook( () => useSuspenseQuery(query, { fetchPolicy: "cache-first" }), { - wrapper: ({ children }) => ( - {children} - ), + wrapper: createClientWrapper(client), } ); @@ -395,9 +390,7 @@ test('does not suspend deferred queries with partial data in the cache and using returnPartialData: true, }), { - wrapper: ({ children }) => ( - {children} - ), + wrapper: createClientWrapper(client), } ); @@ -513,9 +506,7 @@ test('does not suspend deferred queries with data in the cache and using a "cach const { takeRender } = await renderSuspenseHook( () => useSuspenseQuery(query, { fetchPolicy: "cache-and-network" }), { - wrapper: ({ children }) => ( - {children} - ), + wrapper: createClientWrapper(client), } ); @@ -621,9 +612,7 @@ test("suspends deferred queries with lists and properly patches results", async const { takeRender } = await renderSuspenseHook( () => useSuspenseQuery(query), { - wrapper: ({ children }) => ( - {children} - ), + wrapper: createClientWrapper(client), } ); @@ -769,9 +758,7 @@ test("suspends queries with deferred fragments in lists and properly merges arra const { takeRender } = await renderSuspenseHook( () => useSuspenseQuery(query), { - wrapper: ({ children }) => ( - {children} - ), + wrapper: createClientWrapper(client), } ); @@ -923,9 +910,7 @@ test("incrementally rerenders data returned by a `refetch` for a deferred query" const { takeRender, getCurrentSnapshot } = await renderSuspenseHook( () => useSuspenseQuery(query), { - wrapper: ({ children }) => ( - {children} - ), + wrapper: createClientWrapper(client), } ); @@ -1104,9 +1089,7 @@ test("incrementally renders data returned after skipping a deferred query", asyn ({ skip }) => useSuspenseQuery(query, { skip }), { initialProps: { skip: true }, - wrapper: ({ children }) => ( - {children} - ), + wrapper: createClientWrapper(client), } ); @@ -1236,9 +1219,7 @@ test("rerenders data returned by `fetchMore` for a deferred query", async () => const { takeRender, getCurrentSnapshot } = await renderSuspenseHook( () => useSuspenseQuery(query, { variables: { offset: 0 } }), { - wrapper: ({ children }) => ( - {children} - ), + wrapper: createClientWrapper(client), } ); @@ -1460,9 +1441,7 @@ it.failing( const { takeRender, getCurrentSnapshot } = await renderSuspenseHook( () => useSuspenseQuery(query, { variables: { offset: 0 } }), { - wrapper: ({ children }) => ( - {children} - ), + wrapper: createClientWrapper(client), } ); @@ -1667,9 +1646,7 @@ test("throws network errors returned by deferred queries", async () => { const { takeRender } = await renderSuspenseHook( () => useSuspenseQuery(query), { - wrapper: ({ children }) => ( - {children} - ), + wrapper: createClientWrapper(client), } ); @@ -1719,9 +1696,7 @@ test("throws graphql errors returned by deferred queries", async () => { const { takeRender } = await renderSuspenseHook( () => useSuspenseQuery(query), { - wrapper: ({ children }) => ( - {children} - ), + wrapper: createClientWrapper(client), } ); @@ -1782,9 +1757,7 @@ test("throws errors returned by deferred queries that include partial data", asy const { takeRender } = await renderSuspenseHook( () => useSuspenseQuery(query), { - wrapper: ({ children }) => ( - {children} - ), + wrapper: createClientWrapper(client), } ); @@ -1840,9 +1813,7 @@ test("discards partial data and throws errors returned in incremental chunks", a const { takeRender } = await renderSuspenseHook( () => useSuspenseQuery(query), { - wrapper: ({ children }) => ( - {children} - ), + wrapper: createClientWrapper(client), } ); @@ -1990,9 +1961,7 @@ test("adds partial data and does not throw errors returned in incremental chunks const { takeRender } = await renderSuspenseHook( () => useSuspenseQuery(query, { errorPolicy: "all" }), { - wrapper: ({ children }) => ( - {children} - ), + wrapper: createClientWrapper(client), } ); @@ -2159,9 +2128,7 @@ test("adds partial data and discards errors returned in incremental chunks with const { takeRender } = await renderSuspenseHook( () => useSuspenseQuery(query, { errorPolicy: "ignore" }), { - wrapper: ({ children }) => ( - {children} - ), + wrapper: createClientWrapper(client), } ); @@ -2301,9 +2268,7 @@ test("can refetch and respond to cache updates after encountering an error in an const { takeRender, getCurrentSnapshot } = await renderSuspenseHook( () => useSuspenseQuery(query, { errorPolicy: "all" }), { - wrapper: ({ children }) => ( - {children} - ), + wrapper: createClientWrapper(client), } ); diff --git a/src/react/hooks/__tests__/useSuspenseQuery/deferGraphQL17Alpha9.test.tsx b/src/react/hooks/__tests__/useSuspenseQuery/deferGraphQL17Alpha9.test.tsx index 4ad4e3ceb3f..063aa94590c 100644 --- a/src/react/hooks/__tests__/useSuspenseQuery/deferGraphQL17Alpha9.test.tsx +++ b/src/react/hooks/__tests__/useSuspenseQuery/deferGraphQL17Alpha9.test.tsx @@ -18,8 +18,9 @@ import { NetworkStatus, } from "@apollo/client"; import { GraphQL17Alpha9Handler } from "@apollo/client/incremental"; -import { ApolloProvider, useSuspenseQuery } from "@apollo/client/react"; +import { useSuspenseQuery } from "@apollo/client/react"; import { + createClientWrapper, markAsStreaming, mockDeferStreamGraphQL17Alpha9, spyOnConsole, @@ -119,9 +120,7 @@ test("suspends deferred queries until initial chunk loads then streams in data a const { takeRender } = await renderSuspenseHook( () => useSuspenseQuery(query), { - wrapper: ({ children }) => ( - {children} - ), + wrapper: createClientWrapper(client), } ); @@ -220,9 +219,7 @@ test.each([ const { takeRender } = await renderSuspenseHook( () => useSuspenseQuery(query, { fetchPolicy }), { - wrapper: ({ children }) => ( - {children} - ), + wrapper: createClientWrapper(client), } ); @@ -327,9 +324,7 @@ test('does not suspend deferred queries with data in the cache and using a "cach const { takeRender } = await renderSuspenseHook( () => useSuspenseQuery(query, { fetchPolicy: "cache-first" }), { - wrapper: ({ children }) => ( - {children} - ), + wrapper: createClientWrapper(client), } ); @@ -399,9 +394,7 @@ test('does not suspend deferred queries with partial data in the cache and using returnPartialData: true, }), { - wrapper: ({ children }) => ( - {children} - ), + wrapper: createClientWrapper(client), } ); @@ -519,9 +512,7 @@ test('does not suspend deferred queries with data in the cache and using a "cach const { takeRender } = await renderSuspenseHook( () => useSuspenseQuery(query, { fetchPolicy: "cache-and-network" }), { - wrapper: ({ children }) => ( - {children} - ), + wrapper: createClientWrapper(client), } ); @@ -629,9 +620,7 @@ test("suspends deferred queries with lists and properly patches results", async const { takeRender } = await renderSuspenseHook( () => useSuspenseQuery(query), { - wrapper: ({ children }) => ( - {children} - ), + wrapper: createClientWrapper(client), } ); @@ -783,9 +772,7 @@ test("suspends queries with deferred fragments in lists and properly merges arra const { takeRender } = await renderSuspenseHook( () => useSuspenseQuery(query), { - wrapper: ({ children }) => ( - {children} - ), + wrapper: createClientWrapper(client), } ); @@ -942,9 +929,7 @@ test("incrementally rerenders data returned by a `refetch` for a deferred query" const { takeRender, getCurrentSnapshot } = await renderSuspenseHook( () => useSuspenseQuery(query), { - wrapper: ({ children }) => ( - {children} - ), + wrapper: createClientWrapper(client), } ); @@ -1127,9 +1112,7 @@ test("incrementally renders data returned after skipping a deferred query", asyn ({ skip }) => useSuspenseQuery(query, { skip }), { initialProps: { skip: true }, - wrapper: ({ children }) => ( - {children} - ), + wrapper: createClientWrapper(client), } ); @@ -1261,9 +1244,7 @@ test("rerenders data returned by `fetchMore` for a deferred query", async () => const { takeRender, getCurrentSnapshot } = await renderSuspenseHook( () => useSuspenseQuery(query, { variables: { offset: 0 } }), { - wrapper: ({ children }) => ( - {children} - ), + wrapper: createClientWrapper(client), } ); @@ -1489,9 +1470,7 @@ it.failing( const { takeRender, getCurrentSnapshot } = await renderSuspenseHook( () => useSuspenseQuery(query, { variables: { offset: 0 } }), { - wrapper: ({ children }) => ( - {children} - ), + wrapper: createClientWrapper(client), } ); @@ -1700,9 +1679,7 @@ test("throws network errors returned by deferred queries", async () => { const { takeRender } = await renderSuspenseHook( () => useSuspenseQuery(query), { - wrapper: ({ children }) => ( - {children} - ), + wrapper: createClientWrapper(client), } ); @@ -1752,9 +1729,7 @@ test("throws graphql errors returned by deferred queries", async () => { const { takeRender } = await renderSuspenseHook( () => useSuspenseQuery(query), { - wrapper: ({ children }) => ( - {children} - ), + wrapper: createClientWrapper(client), } ); @@ -1815,9 +1790,7 @@ test("throws errors returned by deferred queries that include partial data", asy const { takeRender } = await renderSuspenseHook( () => useSuspenseQuery(query), { - wrapper: ({ children }) => ( - {children} - ), + wrapper: createClientWrapper(client), } ); @@ -1873,9 +1846,7 @@ test("discards partial data and throws errors returned in incremental chunks", a const { takeRender } = await renderSuspenseHook( () => useSuspenseQuery(query), { - wrapper: ({ children }) => ( - {children} - ), + wrapper: createClientWrapper(client), } ); @@ -2028,9 +1999,7 @@ test("adds partial data and does not throw errors returned in incremental chunks const { takeRender } = await renderSuspenseHook( () => useSuspenseQuery(query, { errorPolicy: "all" }), { - wrapper: ({ children }) => ( - {children} - ), + wrapper: createClientWrapper(client), } ); @@ -2202,9 +2171,7 @@ test("adds partial data and discards errors returned in incremental chunks with const { takeRender } = await renderSuspenseHook( () => useSuspenseQuery(query, { errorPolicy: "ignore" }), { - wrapper: ({ children }) => ( - {children} - ), + wrapper: createClientWrapper(client), } ); @@ -2349,9 +2316,7 @@ test("can refetch and respond to cache updates after encountering an error in an const { takeRender, getCurrentSnapshot } = await renderSuspenseHook( () => useSuspenseQuery(query, { errorPolicy: "all" }), { - wrapper: ({ children }) => ( - {children} - ), + wrapper: createClientWrapper(client), } ); From 27e5faf62144f76493f885748869d8d20db3f5b4 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 4 Sep 2025 23:07:44 -0600 Subject: [PATCH 62/97] Add useBackgroundQuery tests for defer with updated spec --- .../deferGraphQL17Alpha9.test.tsx | 361 ++++++++++++++++++ 1 file changed, 361 insertions(+) create mode 100644 src/react/hooks/__tests__/useBackgroundQuery/deferGraphQL17Alpha9.test.tsx diff --git a/src/react/hooks/__tests__/useBackgroundQuery/deferGraphQL17Alpha9.test.tsx b/src/react/hooks/__tests__/useBackgroundQuery/deferGraphQL17Alpha9.test.tsx new file mode 100644 index 00000000000..1efc081c759 --- /dev/null +++ b/src/react/hooks/__tests__/useBackgroundQuery/deferGraphQL17Alpha9.test.tsx @@ -0,0 +1,361 @@ +import type { RenderOptions } from "@testing-library/react"; +import { + createRenderStream, + disableActEnvironment, + useTrackRenders, +} from "@testing-library/react-render-stream"; +import React, { Suspense } from "react"; +import { ErrorBoundary } from "react-error-boundary"; + +import type { + DataState, + ErrorLike, + OperationVariables, + TypedDocumentNode, +} from "@apollo/client"; +import { ApolloClient, gql, NetworkStatus } from "@apollo/client"; +import { InMemoryCache } from "@apollo/client/cache"; +import { GraphQL17Alpha9Handler } from "@apollo/client/incremental"; +import type { QueryRef } from "@apollo/client/react"; +import { useBackgroundQuery, useReadQuery } from "@apollo/client/react"; +import { + createClientWrapper, + mockDeferStreamGraphQL17Alpha9, + spyOnConsole, +} from "@apollo/client/testing/internal"; + +async function renderSuspenseHook< + TData, + TVariables extends OperationVariables, + TQueryRef extends QueryRef, + TStates extends DataState["dataState"] = TQueryRef extends ( + QueryRef + ) ? + States + : never, + Props = never, +>( + renderHook: ( + props: Props extends never ? undefined : Props + ) => [TQueryRef, useBackgroundQuery.Result], + options: Pick & { initialProps?: Props } +) { + function UseReadQuery({ queryRef }: { queryRef: QueryRef }) { + useTrackRenders({ name: "useReadQuery" }); + replaceSnapshot(useReadQuery(queryRef) as any); + + return null; + } + + function SuspenseFallback() { + useTrackRenders({ name: "SuspenseFallback" }); + + return null; + } + + function ErrorFallback() { + useTrackRenders({ name: "ErrorBoundary" }); + + return null; + } + + function App({ props }: { props: Props | undefined }) { + useTrackRenders({ name: "useBackgroundQuery" }); + const [queryRef] = renderHook(props as any); + + return ( + }> + replaceSnapshot({ error })} + > + + + + ); + } + + const { render, takeRender, replaceSnapshot } = createRenderStream< + useReadQuery.Result | { error: ErrorLike } + >(); + + const utils = await render(, options); + + function rerender(props: Props) { + return utils.rerender(); + } + + return { takeRender, rerender }; +} + +test('does not suspend deferred queries with data in the cache and using a "cache-and-network" fetch policy', async () => { + interface Data { + greeting: { + __typename: string; + message: string; + recipient: { name: string; __typename: string }; + }; + } + + const query: TypedDocumentNode = gql` + query { + greeting { + message + ... on Greeting @defer { + recipient { + name + } + } + } + } + `; + + const { httpLink, enqueueInitialChunk, enqueueSubsequentChunk } = + mockDeferStreamGraphQL17Alpha9(); + + const cache = new InMemoryCache(); + cache.writeQuery({ + query, + data: { + greeting: { + __typename: "Greeting", + message: "Hello cached", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + }); + const client = new ApolloClient({ + cache, + link: httpLink, + incrementalHandler: new GraphQL17Alpha9Handler(), + }); + + using _disabledAct = disableActEnvironment(); + const { takeRender } = await renderSuspenseHook( + () => useBackgroundQuery(query, { fetchPolicy: "cache-and-network" }), + { wrapper: createClientWrapper(client) } + ); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual([ + "useBackgroundQuery", + "useReadQuery", + ]); + expect(snapshot).toStrictEqualTyped({ + data: { + greeting: { + __typename: "Greeting", + message: "Hello cached", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + dataState: "complete", + error: undefined, + networkStatus: NetworkStatus.loading, + }); + } + + enqueueInitialChunk({ + data: { + greeting: { __typename: "Greeting", message: "Hello world" }, + }, + pending: [{ id: "0", path: ["greeting"] }], + hasNext: true, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useReadQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: { + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + dataState: "streaming", + error: undefined, + networkStatus: NetworkStatus.streaming, + }); + } + + enqueueSubsequentChunk({ + incremental: [ + { + data: { + recipient: { name: "Alice", __typename: "Person" }, + __typename: "Greeting", + }, + id: "0", + }, + ], + completed: [{ id: "0" }], + hasNext: false, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useReadQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: { + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Alice" }, + }, + }, + dataState: "complete", + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await expect(takeRender).not.toRerender(); +}); + +test('does not suspend deferred queries with partial data in the cache and using a "cache-first" fetch policy with `returnPartialData`', async () => { + interface QueryData { + greeting: { + __typename: string; + message?: string; + recipient?: { + __typename: string; + name: string; + }; + }; + } + + const query: TypedDocumentNode = gql` + query { + greeting { + message + ... on Greeting @defer { + recipient { + name + } + } + } + } + `; + + const { httpLink, enqueueInitialChunk, enqueueSubsequentChunk } = + mockDeferStreamGraphQL17Alpha9(); + + const cache = new InMemoryCache(); + + // We are intentionally writing partial data to the cache. Supress console + // warnings to avoid unnecessary noise in the test. + { + using _consoleSpy = spyOnConsole("error"); + cache.writeQuery({ + query, + data: { + greeting: { + __typename: "Greeting", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + }); + } + + const client = new ApolloClient({ + link: httpLink, + cache, + incrementalHandler: new GraphQL17Alpha9Handler(), + }); + + using _disabledAct = disableActEnvironment(); + const { takeRender } = await renderSuspenseHook( + () => + useBackgroundQuery(query, { + fetchPolicy: "cache-first", + returnPartialData: true, + }), + { wrapper: createClientWrapper(client) } + ); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual([ + "useBackgroundQuery", + "useReadQuery", + ]); + expect(snapshot).toStrictEqualTyped({ + data: { + greeting: { + __typename: "Greeting", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + dataState: "partial", + error: undefined, + networkStatus: NetworkStatus.loading, + }); + } + + enqueueInitialChunk({ + data: { + greeting: { message: "Hello world", __typename: "Greeting" }, + }, + pending: [{ id: "0", path: ["greeting"] }], + hasNext: true, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useReadQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: { + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + dataState: "streaming", + error: undefined, + networkStatus: NetworkStatus.streaming, + }); + } + + enqueueSubsequentChunk({ + incremental: [ + { + data: { + __typename: "Greeting", + recipient: { name: "Alice", __typename: "Person" }, + }, + id: "0", + }, + ], + completed: [{ id: "0" }], + hasNext: false, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useReadQuery"]); + expect(snapshot).toStrictEqualTyped({ + data: { + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Alice" }, + }, + }, + dataState: "complete", + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await expect(takeRender).not.toRerender(); +}); From 7f7f72c4da540af52f9c138cdfbf2c7874694138 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 4 Sep 2025 23:20:21 -0600 Subject: [PATCH 63/97] Move useMutation defer tests to own file --- .../hooks/__tests__/useMutation.test.tsx | 378 +--------------- .../useMutation/defer20220824.test.tsx | 412 ++++++++++++++++++ 2 files changed, 413 insertions(+), 377 deletions(-) create mode 100644 src/react/hooks/__tests__/useMutation/defer20220824.test.tsx diff --git a/src/react/hooks/__tests__/useMutation.test.tsx b/src/react/hooks/__tests__/useMutation.test.tsx index f4e01ba122a..3f925c5c510 100644 --- a/src/react/hooks/__tests__/useMutation.test.tsx +++ b/src/react/hooks/__tests__/useMutation.test.tsx @@ -26,10 +26,9 @@ import { NetworkStatus, } from "@apollo/client"; import { InMemoryCache } from "@apollo/client/cache"; -import { Defer20220824Handler } from "@apollo/client/incremental"; import { BatchHttpLink } from "@apollo/client/link/batch-http"; import { ApolloProvider, useMutation, useQuery } from "@apollo/client/react"; -import { MockLink, MockSubscriptionLink } from "@apollo/client/testing"; +import { MockLink } from "@apollo/client/testing"; import { spyOnConsole, wait } from "@apollo/client/testing/internal"; import { MockedProvider } from "@apollo/client/testing/react"; import type { DeepPartial } from "@apollo/client/utilities"; @@ -3922,381 +3921,6 @@ describe("useMutation Hook", () => { await waitFor(() => screen.findByText("item 3")); }); }); - describe("defer", () => { - const CREATE_TODO_MUTATION_DEFER = gql` - mutation createTodo($description: String!, $priority: String) { - createTodo(description: $description, priority: $priority) { - id - ... @defer { - description - priority - } - } - } - `; - const variables = { - description: "Get milk!", - }; - it("resolves a deferred mutation with the full result", async () => { - using consoleSpies = spyOnConsole("error"); - const link = new MockSubscriptionLink(); - - const client = new ApolloClient({ - link, - cache: new InMemoryCache(), - incrementalHandler: new Defer20220824Handler(), - }); - - using _disabledAct = disableActEnvironment(); - const { takeSnapshot, getCurrentSnapshot } = - await renderHookToSnapshotStream( - () => useMutation(CREATE_TODO_MUTATION_DEFER), - { - wrapper: ({ children }) => ( - {children} - ), - } - ); - - { - const [, mutation] = await takeSnapshot(); - - expect(mutation).toStrictEqualTyped({ - data: undefined, - error: undefined, - loading: false, - called: false, - }); - } - - const [mutate] = getCurrentSnapshot(); - - const promise = mutate({ variables }); - - { - const [, mutation] = await takeSnapshot(); - - expect(mutation).toStrictEqualTyped({ - data: undefined, - error: undefined, - loading: true, - called: true, - }); - } - - setTimeout(() => { - link.simulateResult({ - result: { - data: { - createTodo: { - id: 1, - __typename: "Todo", - }, - }, - hasNext: true, - }, - }); - }); - - await expect(takeSnapshot).not.toRerender(); - - setTimeout(() => { - link.simulateResult( - { - result: { - incremental: [ - { - data: { - description: "Get milk!", - priority: "High", - __typename: "Todo", - }, - path: ["createTodo"], - }, - ], - hasNext: false, - }, - }, - true - ); - }); - - { - const [, mutation] = await takeSnapshot(); - - expect(mutation).toStrictEqualTyped({ - data: { - createTodo: { - id: 1, - description: "Get milk!", - priority: "High", - __typename: "Todo", - }, - }, - error: undefined, - loading: false, - called: true, - }); - } - - await expect(promise).resolves.toStrictEqualTyped({ - data: { - createTodo: { - id: 1, - description: "Get milk!", - priority: "High", - __typename: "Todo", - }, - }, - }); - - expect(consoleSpies.error).not.toHaveBeenCalled(); - }); - - it("resolves with resulting errors and calls onError callback", async () => { - using consoleSpies = spyOnConsole("error"); - const link = new MockSubscriptionLink(); - - const client = new ApolloClient({ - link, - cache: new InMemoryCache(), - incrementalHandler: new Defer20220824Handler(), - }); - - const onError = jest.fn(); - using _disabledAct = disableActEnvironment(); - const { takeSnapshot, getCurrentSnapshot } = - await renderHookToSnapshotStream( - () => useMutation(CREATE_TODO_MUTATION_DEFER, { onError }), - { - wrapper: ({ children }) => ( - {children} - ), - } - ); - - { - const [, result] = await takeSnapshot(); - - expect(result).toStrictEqualTyped({ - data: undefined, - error: undefined, - loading: false, - called: false, - }); - } - - const [createTodo] = getCurrentSnapshot(); - - const promise = createTodo({ variables }); - - { - const [, result] = await takeSnapshot(); - - expect(result).toStrictEqualTyped({ - data: undefined, - error: undefined, - loading: true, - called: true, - }); - } - - link.simulateResult({ - result: { - data: { - createTodo: { - id: 1, - __typename: "Todo", - }, - }, - hasNext: true, - }, - }); - - await expect(takeSnapshot).not.toRerender(); - - link.simulateResult( - { - result: { - incremental: [ - { - data: null, - errors: [{ message: CREATE_TODO_ERROR }], - path: ["createTodo"], - }, - ], - hasNext: false, - }, - }, - true - ); - - await expect(promise).rejects.toThrow( - new CombinedGraphQLErrors({ errors: [{ message: CREATE_TODO_ERROR }] }) - ); - - { - const [, result] = await takeSnapshot(); - - expect(result).toStrictEqualTyped({ - data: undefined, - error: new CombinedGraphQLErrors({ - data: { createTodo: { __typename: "Todo", id: 1 } }, - errors: [{ message: CREATE_TODO_ERROR }], - }), - loading: false, - called: true, - }); - } - - await expect(takeSnapshot).not.toRerender(); - - expect(onError).toHaveBeenCalledTimes(1); - expect(onError).toHaveBeenLastCalledWith( - new CombinedGraphQLErrors({ - data: { createTodo: { __typename: "Todo", id: 1 } }, - errors: [{ message: CREATE_TODO_ERROR }], - }), - expect.anything() - ); - expect(consoleSpies.error).not.toHaveBeenCalled(); - }); - - it("calls the update function with the final merged result data", async () => { - using consoleSpies = spyOnConsole("error"); - const link = new MockSubscriptionLink(); - const update = jest.fn(); - const client = new ApolloClient({ - link, - cache: new InMemoryCache(), - incrementalHandler: new Defer20220824Handler(), - }); - - using _disabledAct = disableActEnvironment(); - const { takeSnapshot, getCurrentSnapshot } = - await renderHookToSnapshotStream( - () => useMutation(CREATE_TODO_MUTATION_DEFER, { update }), - { - wrapper: ({ children }) => ( - {children} - ), - } - ); - - { - const [, result] = await takeSnapshot(); - - expect(result).toStrictEqualTyped({ - data: undefined, - error: undefined, - loading: false, - called: false, - }); - } - - const [createTodo] = getCurrentSnapshot(); - - const promiseReturnedByMutate = createTodo({ variables }); - - { - const [, result] = await takeSnapshot(); - - expect(result).toStrictEqualTyped({ - data: undefined, - error: undefined, - loading: true, - called: true, - }); - } - - link.simulateResult({ - result: { - data: { - createTodo: { - id: 1, - __typename: "Todo", - }, - }, - hasNext: true, - }, - }); - - await expect(takeSnapshot).not.toRerender(); - - link.simulateResult( - { - result: { - incremental: [ - { - data: { - description: "Get milk!", - priority: "High", - __typename: "Todo", - }, - path: ["createTodo"], - }, - ], - hasNext: false, - }, - }, - true - ); - - await expect(promiseReturnedByMutate).resolves.toStrictEqualTyped({ - data: { - createTodo: { - id: 1, - description: "Get milk!", - priority: "High", - __typename: "Todo", - }, - }, - }); - - { - const [, result] = await takeSnapshot(); - - expect(result).toStrictEqualTyped({ - data: { - createTodo: { - id: 1, - description: "Get milk!", - priority: "High", - __typename: "Todo", - }, - }, - error: undefined, - loading: false, - called: true, - }); - } - - await expect(takeSnapshot).not.toRerender(); - - expect(update).toHaveBeenCalledTimes(1); - expect(update).toHaveBeenCalledWith( - // the first item is the cache, which we don't need to make any - // assertions against in this test - expect.anything(), - // second argument is the result - expect.objectContaining({ - data: { - createTodo: { - id: 1, - description: "Get milk!", - priority: "High", - __typename: "Todo", - }, - }, - }), - // third argument is an object containing context and variables - // but we only care about variables here - expect.objectContaining({ variables }) - ); - - expect(consoleSpies.error).not.toHaveBeenCalled(); - }); - }); }); describe("data masking", () => { diff --git a/src/react/hooks/__tests__/useMutation/defer20220824.test.tsx b/src/react/hooks/__tests__/useMutation/defer20220824.test.tsx new file mode 100644 index 00000000000..45b046eefd6 --- /dev/null +++ b/src/react/hooks/__tests__/useMutation/defer20220824.test.tsx @@ -0,0 +1,412 @@ +import { + disableActEnvironment, + renderHookToSnapshotStream, +} from "@testing-library/react-render-stream"; +import { gql } from "graphql-tag"; + +import { ApolloClient, CombinedGraphQLErrors } from "@apollo/client"; +import { InMemoryCache } from "@apollo/client/cache"; +import { Defer20220824Handler } from "@apollo/client/incremental"; +import { useMutation } from "@apollo/client/react"; +import { MockSubscriptionLink } from "@apollo/client/testing"; +import { + createClientWrapper, + spyOnConsole, +} from "@apollo/client/testing/internal"; + +const CREATE_TODO_ERROR = "Failed to create item"; + +test("resolves a deferred mutation with the full result", async () => { + using _ = spyOnConsole("error"); + const mutation = gql` + mutation createTodo($description: String!, $priority: String) { + createTodo(description: $description, priority: $priority) { + id + ... @defer { + description + priority + } + } + } + `; + const variables = { + description: "Get milk!", + }; + + const link = new MockSubscriptionLink(); + + const client = new ApolloClient({ + link, + cache: new InMemoryCache(), + incrementalHandler: new Defer20220824Handler(), + }); + + using _disabledAct = disableActEnvironment(); + const { takeSnapshot, getCurrentSnapshot } = await renderHookToSnapshotStream( + () => useMutation(mutation), + { wrapper: createClientWrapper(client) } + ); + + { + const [, mutation] = await takeSnapshot(); + + expect(mutation).toStrictEqualTyped({ + data: undefined, + error: undefined, + loading: false, + called: false, + }); + } + + const [mutate] = getCurrentSnapshot(); + + const promise = mutate({ variables }); + + { + const [, mutation] = await takeSnapshot(); + + expect(mutation).toStrictEqualTyped({ + data: undefined, + error: undefined, + loading: true, + called: true, + }); + } + + setTimeout(() => { + link.simulateResult({ + result: { + data: { + createTodo: { + id: 1, + __typename: "Todo", + }, + }, + hasNext: true, + }, + }); + }); + + await expect(takeSnapshot).not.toRerender(); + + setTimeout(() => { + link.simulateResult( + { + result: { + incremental: [ + { + data: { + description: "Get milk!", + priority: "High", + __typename: "Todo", + }, + path: ["createTodo"], + }, + ], + hasNext: false, + }, + }, + true + ); + }); + + { + const [, mutation] = await takeSnapshot(); + + expect(mutation).toStrictEqualTyped({ + data: { + createTodo: { + id: 1, + description: "Get milk!", + priority: "High", + __typename: "Todo", + }, + }, + error: undefined, + loading: false, + called: true, + }); + } + + await expect(promise).resolves.toStrictEqualTyped({ + data: { + createTodo: { + id: 1, + description: "Get milk!", + priority: "High", + __typename: "Todo", + }, + }, + }); + + expect(console.error).not.toHaveBeenCalled(); +}); + +test("resolves with resulting errors and calls onError callback", async () => { + using _ = spyOnConsole("error"); + const mutation = gql` + mutation createTodo($description: String!, $priority: String) { + createTodo(description: $description, priority: $priority) { + id + ... @defer { + description + priority + } + } + } + `; + const variables = { + description: "Get milk!", + }; + + const link = new MockSubscriptionLink(); + + const client = new ApolloClient({ + link, + cache: new InMemoryCache(), + incrementalHandler: new Defer20220824Handler(), + }); + + const onError = jest.fn(); + using _disabledAct = disableActEnvironment(); + const { takeSnapshot, getCurrentSnapshot } = await renderHookToSnapshotStream( + () => useMutation(mutation, { onError }), + { + wrapper: createClientWrapper(client), + } + ); + + { + const [, result] = await takeSnapshot(); + + expect(result).toStrictEqualTyped({ + data: undefined, + error: undefined, + loading: false, + called: false, + }); + } + + const [createTodo] = getCurrentSnapshot(); + + const promise = createTodo({ variables }); + + { + const [, result] = await takeSnapshot(); + + expect(result).toStrictEqualTyped({ + data: undefined, + error: undefined, + loading: true, + called: true, + }); + } + + link.simulateResult({ + result: { + data: { + createTodo: { + id: 1, + __typename: "Todo", + }, + }, + hasNext: true, + }, + }); + + await expect(takeSnapshot).not.toRerender(); + + link.simulateResult( + { + result: { + incremental: [ + { + data: null, + errors: [{ message: CREATE_TODO_ERROR }], + path: ["createTodo"], + }, + ], + hasNext: false, + }, + }, + true + ); + + await expect(promise).rejects.toThrow( + new CombinedGraphQLErrors({ errors: [{ message: CREATE_TODO_ERROR }] }) + ); + + { + const [, result] = await takeSnapshot(); + + expect(result).toStrictEqualTyped({ + data: undefined, + error: new CombinedGraphQLErrors({ + data: { createTodo: { __typename: "Todo", id: 1 } }, + errors: [{ message: CREATE_TODO_ERROR }], + }), + loading: false, + called: true, + }); + } + + await expect(takeSnapshot).not.toRerender(); + + expect(onError).toHaveBeenCalledTimes(1); + expect(onError).toHaveBeenLastCalledWith( + new CombinedGraphQLErrors({ + data: { createTodo: { __typename: "Todo", id: 1 } }, + errors: [{ message: CREATE_TODO_ERROR }], + }), + expect.anything() + ); + expect(console.error).not.toHaveBeenCalled(); +}); + +test("calls the update function with the final merged result data", async () => { + using _ = spyOnConsole("error"); + const mutation = gql` + mutation createTodo($description: String!, $priority: String) { + createTodo(description: $description, priority: $priority) { + id + ... @defer { + description + priority + } + } + } + `; + const variables = { + description: "Get milk!", + }; + + const link = new MockSubscriptionLink(); + const update = jest.fn(); + const client = new ApolloClient({ + link, + cache: new InMemoryCache(), + incrementalHandler: new Defer20220824Handler(), + }); + + using _disabledAct = disableActEnvironment(); + const { takeSnapshot, getCurrentSnapshot } = await renderHookToSnapshotStream( + () => useMutation(mutation, { update }), + { + wrapper: createClientWrapper(client), + } + ); + + { + const [, result] = await takeSnapshot(); + + expect(result).toStrictEqualTyped({ + data: undefined, + error: undefined, + loading: false, + called: false, + }); + } + + const [createTodo] = getCurrentSnapshot(); + + const promiseReturnedByMutate = createTodo({ variables }); + + { + const [, result] = await takeSnapshot(); + + expect(result).toStrictEqualTyped({ + data: undefined, + error: undefined, + loading: true, + called: true, + }); + } + + link.simulateResult({ + result: { + data: { + createTodo: { + id: 1, + __typename: "Todo", + }, + }, + hasNext: true, + }, + }); + + await expect(takeSnapshot).not.toRerender(); + + link.simulateResult( + { + result: { + incremental: [ + { + data: { + description: "Get milk!", + priority: "High", + __typename: "Todo", + }, + path: ["createTodo"], + }, + ], + hasNext: false, + }, + }, + true + ); + + await expect(promiseReturnedByMutate).resolves.toStrictEqualTyped({ + data: { + createTodo: { + id: 1, + description: "Get milk!", + priority: "High", + __typename: "Todo", + }, + }, + }); + + { + const [, result] = await takeSnapshot(); + + expect(result).toStrictEqualTyped({ + data: { + createTodo: { + id: 1, + description: "Get milk!", + priority: "High", + __typename: "Todo", + }, + }, + error: undefined, + loading: false, + called: true, + }); + } + + await expect(takeSnapshot).not.toRerender(); + + expect(update).toHaveBeenCalledTimes(1); + expect(update).toHaveBeenCalledWith( + // the first item is the cache, which we don't need to make any + // assertions against in this test + expect.anything(), + // second argument is the result + expect.objectContaining({ + data: { + createTodo: { + id: 1, + description: "Get milk!", + priority: "High", + __typename: "Todo", + }, + }, + }), + // third argument is an object containing context and variables + // but we only care about variables here + expect.objectContaining({ variables }) + ); + + expect(console.error).not.toHaveBeenCalled(); +}); + From 013c56883b0aa2c73960ba7d142d9229406695b0 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 4 Sep 2025 23:29:45 -0600 Subject: [PATCH 64/97] Move useLoadableQuery defer tests to own file --- .../hooks/__tests__/useLoadableQuery.test.tsx | 324 ------------- .../useLoadableQuery/defer20220824.test.tsx | 436 ++++++++++++++++++ 2 files changed, 436 insertions(+), 324 deletions(-) create mode 100644 src/react/hooks/__tests__/useLoadableQuery/defer20220824.test.tsx diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index 53818cc4eda..1a49e9dca1c 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -1531,163 +1531,6 @@ it("works with startTransition to change variables", async () => { }); }); -it('does not suspend deferred queries with data in the cache and using a "cache-and-network" fetch policy', async () => { - interface Data { - greeting: { - __typename: string; - message: string; - recipient: { name: string; __typename: string }; - }; - } - - const query: TypedDocumentNode> = gql` - query { - greeting { - message - ... @defer { - recipient { - name - } - } - } - } - `; - - const link = new MockSubscriptionLink(); - const cache = new InMemoryCache(); - cache.writeQuery({ - query, - data: { - greeting: { - __typename: "Greeting", - message: "Hello cached", - recipient: { __typename: "Person", name: "Cached Alice" }, - }, - }, - }); - const client = new ApolloClient({ - cache, - link, - incrementalHandler: new Defer20220824Handler(), - }); - - using _disabledAct = disableActEnvironment(); - const renderStream = createDefaultProfiler(); - const { SuspenseFallback, ReadQueryHook } = - createDefaultProfiledComponents(renderStream); - - function App() { - useTrackRenders(); - const [loadQuery, queryRef] = useLoadableQuery(query, { - fetchPolicy: "cache-and-network", - }); - return ( -
- - }> - {queryRef && } - -
- ); - } - - const { user } = await renderWithClient( - , - { - client, - }, - renderStream - ); - - // initial render - await renderStream.takeRender(); - - await user.click(screen.getByText("Load todo")); - - { - const { snapshot, renderedComponents } = await renderStream.takeRender(); - - expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); - - expect(snapshot.result).toStrictEqualTyped({ - data: { - greeting: { - __typename: "Greeting", - message: "Hello cached", - recipient: { __typename: "Person", name: "Cached Alice" }, - }, - }, - dataState: "complete", - error: undefined, - networkStatus: NetworkStatus.loading, - }); - } - - link.simulateResult({ - result: { - data: { - greeting: { __typename: "Greeting", message: "Hello world" }, - }, - hasNext: true, - }, - }); - - { - const { snapshot, renderedComponents } = await renderStream.takeRender(); - - expect(renderedComponents).toStrictEqual([ReadQueryHook]); - expect(snapshot.result).toStrictEqualTyped({ - data: { - greeting: { - __typename: "Greeting", - message: "Hello world", - recipient: { __typename: "Person", name: "Cached Alice" }, - }, - }, - dataState: "streaming", - error: undefined, - networkStatus: NetworkStatus.streaming, - }); - } - - link.simulateResult( - { - result: { - incremental: [ - { - data: { - recipient: { name: "Alice", __typename: "Person" }, - __typename: "Greeting", - }, - path: ["greeting"], - }, - ], - hasNext: false, - }, - }, - true - ); - - { - const { snapshot, renderedComponents } = await renderStream.takeRender(); - - expect(renderedComponents).toStrictEqual([ReadQueryHook]); - expect(snapshot.result).toStrictEqualTyped({ - data: { - greeting: { - __typename: "Greeting", - message: "Hello world", - recipient: { __typename: "Person", name: "Alice" }, - }, - }, - dataState: "complete", - error: undefined, - networkStatus: NetworkStatus.ready, - }); - } - - await expect(renderStream).not.toRerender(); -}); it("reacts to cache updates", async () => { const { query, mocks } = useSimpleQueryCase(); @@ -4553,173 +4396,6 @@ it('suspends and does not use partial data when changing variables and using a " } }); -it('does not suspend deferred queries with partial data in the cache and using a "cache-first" fetch policy with `returnPartialData`', async () => { - interface QueryData { - greeting: { - __typename: string; - message?: string; - recipient?: { - __typename: string; - name: string; - }; - }; - } - - const query: TypedDocumentNode> = gql` - query { - greeting { - message - ... on Greeting @defer { - recipient { - name - } - } - } - } - `; - - const link = new MockSubscriptionLink(); - const cache = new InMemoryCache(); - - { - // We are intentionally writing partial data to the cache. Supress console - // warnings to avoid unnecessary noise in the test. - using _consoleSpy = spyOnConsole("error"); - - cache.writeQuery({ - query, - data: { - greeting: { - __typename: "Greeting", - recipient: { __typename: "Person", name: "Cached Alice" }, - }, - }, - }); - } - - const client = new ApolloClient({ - link, - cache, - incrementalHandler: new Defer20220824Handler(), - }); - - using _disabledAct = disableActEnvironment(); - const renderStream = createDefaultProfiler>(); - const { SuspenseFallback, ReadQueryHook } = - createDefaultProfiledComponents(renderStream); - - function App() { - useTrackRenders(); - const [loadTodo, queryRef] = useLoadableQuery(query, { - fetchPolicy: "cache-first", - returnPartialData: true, - }); - - return ( -
- - }> - {queryRef && } - -
- ); - } - - const { user } = await renderWithClient( - , - { - client, - }, - renderStream - ); - - // initial render - await renderStream.takeRender(); - - await user.click(screen.getByText("Load todo")); - - { - const { snapshot, renderedComponents } = await renderStream.takeRender(); - - expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); - expect(snapshot.result).toStrictEqualTyped({ - data: { - greeting: { - __typename: "Greeting", - recipient: { __typename: "Person", name: "Cached Alice" }, - }, - }, - dataState: "partial", - error: undefined, - networkStatus: NetworkStatus.loading, - }); - } - - link.simulateResult({ - result: { - data: { - greeting: { message: "Hello world", __typename: "Greeting" }, - }, - hasNext: true, - }, - }); - - { - const { snapshot, renderedComponents } = await renderStream.takeRender(); - - expect(renderedComponents).toStrictEqual([ReadQueryHook]); - expect(snapshot.result).toStrictEqualTyped({ - data: { - greeting: { - __typename: "Greeting", - message: "Hello world", - recipient: { __typename: "Person", name: "Cached Alice" }, - }, - }, - dataState: "streaming", - error: undefined, - networkStatus: NetworkStatus.streaming, - }); - } - - link.simulateResult( - { - result: { - incremental: [ - { - data: { - __typename: "Greeting", - recipient: { name: "Alice", __typename: "Person" }, - }, - path: ["greeting"], - }, - ], - hasNext: false, - }, - }, - true - ); - - { - const { snapshot, renderedComponents } = await renderStream.takeRender(); - - expect(renderedComponents).toStrictEqual([ReadQueryHook]); - expect(snapshot.result).toStrictEqualTyped({ - data: { - greeting: { - __typename: "Greeting", - message: "Hello world", - recipient: { __typename: "Person", name: "Alice" }, - }, - }, - dataState: "complete", - error: undefined, - networkStatus: NetworkStatus.ready, - }); - } - - await expect(renderStream).not.toRerender(); -}); it("throws when calling loadQuery on first render", async () => { // We don't provide this functionality with React 19 anymore since it requires internals access diff --git a/src/react/hooks/__tests__/useLoadableQuery/defer20220824.test.tsx b/src/react/hooks/__tests__/useLoadableQuery/defer20220824.test.tsx new file mode 100644 index 00000000000..51d424a6144 --- /dev/null +++ b/src/react/hooks/__tests__/useLoadableQuery/defer20220824.test.tsx @@ -0,0 +1,436 @@ +import { screen } from "@testing-library/react"; +import type { + AsyncRenderFn, + RenderStream, +} from "@testing-library/react-render-stream"; +import { + createRenderStream, + disableActEnvironment, + useTrackRenders, +} from "@testing-library/react-render-stream"; +import { userEvent } from "@testing-library/user-event"; +import React, { Suspense } from "react"; +import { ErrorBoundary as ReactErrorBoundary } from "react-error-boundary"; + +import type { DataState, TypedDocumentNode } from "@apollo/client"; +import { ApolloClient, gql, NetworkStatus } from "@apollo/client"; +import { InMemoryCache } from "@apollo/client/cache"; +import { Defer20220824Handler } from "@apollo/client/incremental"; +import type { QueryRef } from "@apollo/client/react"; +import { + ApolloProvider, + useLoadableQuery, + useReadQuery, +} from "@apollo/client/react"; +import { MockSubscriptionLink } from "@apollo/client/testing"; +import { renderAsync, spyOnConsole } from "@apollo/client/testing/internal"; +import type { DeepPartial } from "@apollo/client/utilities"; + +function createDefaultProfiler() { + return createRenderStream({ + initialSnapshot: { + error: null as Error | null, + result: null as useReadQuery.Result | null, + }, + skipNonTrackingRenders: true, + }); +} + +function createDefaultProfiledComponents< + Snapshot extends { + result: useReadQuery.Result | null; + error?: Error | null; + }, + TData = Snapshot["result"] extends useReadQuery.Result | null ? + TData + : unknown, + TStates extends DataState["dataState"] = Snapshot["result"] extends ( + useReadQuery.Result | null + ) ? + TStates + : "complete" | "streaming", +>(profiler: RenderStream) { + function SuspenseFallback() { + useTrackRenders(); + return

Loading

; + } + + function ReadQueryHook({ + queryRef, + }: { + queryRef: QueryRef; + }) { + useTrackRenders(); + profiler.mergeSnapshot({ + result: useReadQuery(queryRef), + } as unknown as Partial); + + return null; + } + + function ErrorFallback({ error }: { error: Error }) { + useTrackRenders(); + profiler.mergeSnapshot({ error } as Partial); + + return
Oops
; + } + + function ErrorBoundary({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); + } + + return { + SuspenseFallback, + ReadQueryHook, + ErrorFallback, + ErrorBoundary, + }; +} + +async function renderWithClient( + ui: React.ReactElement, + options: { client: ApolloClient }, + { render: doRender }: { render: AsyncRenderFn | typeof renderAsync } +) { + const { client } = options; + const user = userEvent.setup(); + + const utils = await doRender(ui, { + wrapper: ({ children }: { children: React.ReactNode }) => ( + {children} + ), + }); + + return { ...utils, user }; +} + +test('does not suspend deferred queries with data in the cache and using a "cache-and-network" fetch policy', async () => { + interface Data { + greeting: { + __typename: string; + message: string; + recipient: { name: string; __typename: string }; + }; + } + + const query: TypedDocumentNode> = gql` + query { + greeting { + message + ... @defer { + recipient { + name + } + } + } + } + `; + + const link = new MockSubscriptionLink(); + const cache = new InMemoryCache(); + cache.writeQuery({ + query, + data: { + greeting: { + __typename: "Greeting", + message: "Hello cached", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + }); + const client = new ApolloClient({ + cache, + link, + incrementalHandler: new Defer20220824Handler(), + }); + + using _disabledAct = disableActEnvironment(); + const renderStream = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(renderStream); + + function App() { + useTrackRenders(); + const [loadQuery, queryRef] = useLoadableQuery(query, { + fetchPolicy: "cache-and-network", + }); + return ( +
+ + }> + {queryRef && } + +
+ ); + } + + const { user } = await renderWithClient( + , + { + client, + }, + { render: renderAsync } + ); + + // initial render + await renderStream.takeRender(); + + await user.click(screen.getByText("Load todo")); + + { + const { snapshot, renderedComponents } = await renderStream.takeRender(); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + + expect(snapshot.result).toStrictEqualTyped({ + data: { + greeting: { + __typename: "Greeting", + message: "Hello cached", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + dataState: "complete", + error: undefined, + networkStatus: NetworkStatus.loading, + }); + } + + link.simulateResult({ + result: { + data: { + greeting: { __typename: "Greeting", message: "Hello world" }, + }, + hasNext: true, + }, + }); + + { + const { snapshot, renderedComponents } = await renderStream.takeRender(); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toStrictEqualTyped({ + data: { + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + dataState: "streaming", + error: undefined, + networkStatus: NetworkStatus.streaming, + }); + } + + link.simulateResult( + { + result: { + incremental: [ + { + data: { + recipient: { name: "Alice", __typename: "Person" }, + __typename: "Greeting", + }, + path: ["greeting"], + }, + ], + hasNext: false, + }, + }, + true + ); + + { + const { snapshot, renderedComponents } = await renderStream.takeRender(); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toStrictEqualTyped({ + data: { + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Alice" }, + }, + }, + dataState: "complete", + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await expect(renderStream).not.toRerender(); +}); + +test('does not suspend deferred queries with partial data in the cache and using a "cache-first" fetch policy with `returnPartialData`', async () => { + interface QueryData { + greeting: { + __typename: string; + message?: string; + recipient?: { + __typename: string; + name: string; + }; + }; + } + + const query: TypedDocumentNode> = gql` + query { + greeting { + message + ... on Greeting @defer { + recipient { + name + } + } + } + } + `; + + const link = new MockSubscriptionLink(); + const cache = new InMemoryCache(); + + { + // We are intentionally writing partial data to the cache. Supress console + // warnings to avoid unnecessary noise in the test. + using _consoleSpy = spyOnConsole("error"); + + cache.writeQuery({ + query, + data: { + greeting: { + __typename: "Greeting", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + }); + } + + const client = new ApolloClient({ + link, + cache, + incrementalHandler: new Defer20220824Handler(), + }); + + using _disabledAct = disableActEnvironment(); + const renderStream = createDefaultProfiler>(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(renderStream); + + function App() { + useTrackRenders(); + const [loadTodo, queryRef] = useLoadableQuery(query, { + fetchPolicy: "cache-first", + returnPartialData: true, + }); + + return ( +
+ + }> + {queryRef && } + +
+ ); + } + + const { user } = await renderWithClient( + , + { + client, + }, + { render: renderAsync } + ); + + // initial render + await renderStream.takeRender(); + + await user.click(screen.getByText("Load todo")); + + { + const { snapshot, renderedComponents } = await renderStream.takeRender(); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot.result).toStrictEqualTyped({ + data: { + greeting: { + __typename: "Greeting", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + dataState: "partial", + error: undefined, + networkStatus: NetworkStatus.loading, + }); + } + + link.simulateResult({ + result: { + data: { + greeting: { message: "Hello world", __typename: "Greeting" }, + }, + hasNext: true, + }, + }); + + { + const { snapshot, renderedComponents } = await renderStream.takeRender(); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toStrictEqualTyped({ + data: { + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + dataState: "streaming", + error: undefined, + networkStatus: NetworkStatus.streaming, + }); + } + + link.simulateResult( + { + result: { + incremental: [ + { + data: { + __typename: "Greeting", + recipient: { name: "Alice", __typename: "Person" }, + }, + path: ["greeting"], + }, + ], + hasNext: false, + }, + }, + true + ); + + { + const { snapshot, renderedComponents } = await renderStream.takeRender(); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toStrictEqualTyped({ + data: { + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Alice" }, + }, + }, + dataState: "complete", + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await expect(renderStream).not.toRerender(); +}); + From 3e0d4deb3842bd8f4d90ee5b7a1c1e8554b4b5a0 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 4 Sep 2025 23:30:04 -0600 Subject: [PATCH 65/97] Move createQueryPreloader tests to own file --- .../__tests__/createQueryPreloader.test.tsx | 90 --------- .../defer20220824.test.tsx | 173 ++++++++++++++++++ 2 files changed, 173 insertions(+), 90 deletions(-) create mode 100644 src/react/query-preloader/__tests__/createQueryPreloader/defer20220824.test.tsx diff --git a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx index f05defde306..96f4838e0d3 100644 --- a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx +++ b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx @@ -1806,96 +1806,6 @@ test("does not suspend and returns partial data when `returnPartialData` is `tru } }); -test("suspends deferred queries until initial chunk loads then rerenders with deferred data", async () => { - const query = gql` - query { - greeting { - message - ... on Greeting @defer { - recipient { - name - } - } - } - } - `; - - const link = new MockSubscriptionLink(); - const client = new ApolloClient({ - cache: new InMemoryCache(), - link, - incrementalHandler: new Defer20220824Handler(), - }); - - const preloadQuery = createQueryPreloader(client); - const queryRef = preloadQuery(query); - - using _disabledAct = disableActEnvironment(); - const { renderStream } = await renderDefaultTestApp({ client, queryRef }); - - { - const { renderedComponents } = await renderStream.takeRender(); - - expect(renderedComponents).toStrictEqual(["App", "SuspenseFallback"]); - } - - link.simulateResult({ - result: { - data: { greeting: { message: "Hello world", __typename: "Greeting" } }, - hasNext: true, - }, - }); - - { - const { snapshot, renderedComponents } = await renderStream.takeRender(); - - expect(renderedComponents).toStrictEqual(["ReadQueryHook"]); - expect(snapshot.result).toStrictEqualTyped({ - data: markAsStreaming({ - greeting: { message: "Hello world", __typename: "Greeting" }, - }), - dataState: "streaming", - error: undefined, - networkStatus: NetworkStatus.streaming, - }); - } - - link.simulateResult( - { - result: { - incremental: [ - { - data: { - recipient: { name: "Alice", __typename: "Person" }, - __typename: "Greeting", - }, - path: ["greeting"], - }, - ], - hasNext: false, - }, - }, - true - ); - - { - const { snapshot, renderedComponents } = await renderStream.takeRender(); - - expect(renderedComponents).toStrictEqual(["ReadQueryHook"]); - expect(snapshot.result).toStrictEqualTyped({ - data: { - greeting: { - __typename: "Greeting", - message: "Hello world", - recipient: { __typename: "Person", name: "Alice" }, - }, - }, - dataState: "complete", - error: undefined, - networkStatus: NetworkStatus.ready, - }); - } -}); test("masks result when dataMasking is `true`", async () => { const { query, mocks } = setupMaskedVariablesCase(); diff --git a/src/react/query-preloader/__tests__/createQueryPreloader/defer20220824.test.tsx b/src/react/query-preloader/__tests__/createQueryPreloader/defer20220824.test.tsx new file mode 100644 index 00000000000..ca4b7f09998 --- /dev/null +++ b/src/react/query-preloader/__tests__/createQueryPreloader/defer20220824.test.tsx @@ -0,0 +1,173 @@ +import { + createRenderStream, + disableActEnvironment, + useTrackRenders, +} from "@testing-library/react-render-stream"; +import React, { Suspense } from "react"; +import { ErrorBoundary } from "react-error-boundary"; + +import type { DataState } from "@apollo/client"; +import { ApolloClient, gql, NetworkStatus } from "@apollo/client"; +import { InMemoryCache } from "@apollo/client/cache"; +import { Defer20220824Handler } from "@apollo/client/incremental"; +import type { QueryRef } from "@apollo/client/react"; +import { + ApolloProvider, + createQueryPreloader, + useReadQuery, +} from "@apollo/client/react"; +import { MockSubscriptionLink } from "@apollo/client/testing"; +import { markAsStreaming } from "@apollo/client/testing/internal"; + +async function renderDefaultTestApp< + TData, + TStates extends DataState["dataState"] = "complete" | "streaming", +>({ + client, + queryRef, +}: { + client: ApolloClient; + queryRef: QueryRef; +}) { + const renderStream = createRenderStream({ + initialSnapshot: { + result: null as useReadQuery.Result | null, + error: null as Error | null, + }, + }); + + function ReadQueryHook() { + useTrackRenders({ name: "ReadQueryHook" }); + renderStream.mergeSnapshot({ result: useReadQuery(queryRef) }); + + return null; + } + + function SuspenseFallback() { + useTrackRenders({ name: "SuspenseFallback" }); + return

Loading

; + } + + function ErrorFallback({ error }: { error: Error }) { + useTrackRenders({ name: "ErrorFallback" }); + renderStream.mergeSnapshot({ error }); + + return null; + } + + function App() { + useTrackRenders({ name: "App" }); + + return ( + + }> + + + + ); + } + + const utils = await renderStream.render(, { + wrapper: ({ children }) => ( + {children} + ), + }); + + function rerender() { + return utils.rerender(); + } + + return { ...utils, rerender, renderStream }; +} + +test("suspends deferred queries until initial chunk loads then rerenders with deferred data", async () => { + const query = gql` + query { + greeting { + message + ... on Greeting @defer { + recipient { + name + } + } + } + } + `; + + const link = new MockSubscriptionLink(); + const client = new ApolloClient({ + cache: new InMemoryCache(), + link, + incrementalHandler: new Defer20220824Handler(), + }); + + const preloadQuery = createQueryPreloader(client); + const queryRef = preloadQuery(query); + + using _disabledAct = disableActEnvironment(); + const { renderStream } = await renderDefaultTestApp({ client, queryRef }); + + { + const { renderedComponents } = await renderStream.takeRender(); + + expect(renderedComponents).toStrictEqual(["App", "SuspenseFallback"]); + } + + link.simulateResult({ + result: { + data: { greeting: { message: "Hello world", __typename: "Greeting" } }, + hasNext: true, + }, + }); + + { + const { snapshot, renderedComponents } = await renderStream.takeRender(); + + expect(renderedComponents).toStrictEqual(["ReadQueryHook"]); + expect(snapshot.result).toStrictEqualTyped({ + data: markAsStreaming({ + greeting: { message: "Hello world", __typename: "Greeting" }, + }), + dataState: "streaming", + error: undefined, + networkStatus: NetworkStatus.streaming, + }); + } + + link.simulateResult( + { + result: { + incremental: [ + { + data: { + recipient: { name: "Alice", __typename: "Person" }, + __typename: "Greeting", + }, + path: ["greeting"], + }, + ], + hasNext: false, + }, + }, + true + ); + + { + const { snapshot, renderedComponents } = await renderStream.takeRender(); + + expect(renderedComponents).toStrictEqual(["ReadQueryHook"]); + expect(snapshot.result).toStrictEqualTyped({ + data: { + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Alice" }, + }, + }, + dataState: "complete", + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } +}); + From 6b0e0027b45b83b7d08cba5f23d4198ff5d927b7 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 4 Sep 2025 23:36:10 -0600 Subject: [PATCH 66/97] Use defer helpers instead of mock subscription link --- .../useLoadableQuery/defer20220824.test.tsx | 94 ++++++------ .../useMutation/defer20220824.test.tsx | 140 ++++++++---------- .../defer20220824.test.tsx | 47 +++--- 3 files changed, 124 insertions(+), 157 deletions(-) diff --git a/src/react/hooks/__tests__/useLoadableQuery/defer20220824.test.tsx b/src/react/hooks/__tests__/useLoadableQuery/defer20220824.test.tsx index 51d424a6144..f27a4a14123 100644 --- a/src/react/hooks/__tests__/useLoadableQuery/defer20220824.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery/defer20220824.test.tsx @@ -22,8 +22,11 @@ import { useLoadableQuery, useReadQuery, } from "@apollo/client/react"; -import { MockSubscriptionLink } from "@apollo/client/testing"; -import { renderAsync, spyOnConsole } from "@apollo/client/testing/internal"; +import { + mockDefer20220824, + renderAsync, + spyOnConsole, +} from "@apollo/client/testing/internal"; import type { DeepPartial } from "@apollo/client/utilities"; function createDefaultProfiler() { @@ -130,7 +133,9 @@ test('does not suspend deferred queries with data in the cache and using a "cach } `; - const link = new MockSubscriptionLink(); + const { httpLink, enqueueInitialChunk, enqueueSubsequentChunk } = + mockDefer20220824(); + const cache = new InMemoryCache(); cache.writeQuery({ query, @@ -144,7 +149,7 @@ test('does not suspend deferred queries with data in the cache and using a "cach }); const client = new ApolloClient({ cache, - link, + link: httpLink, incrementalHandler: new Defer20220824Handler(), }); @@ -200,13 +205,11 @@ test('does not suspend deferred queries with data in the cache and using a "cach }); } - link.simulateResult({ - result: { - data: { - greeting: { __typename: "Greeting", message: "Hello world" }, - }, - hasNext: true, + enqueueInitialChunk({ + data: { + greeting: { __typename: "Greeting", message: "Hello world" }, }, + hasNext: true, }); { @@ -227,23 +230,18 @@ test('does not suspend deferred queries with data in the cache and using a "cach }); } - link.simulateResult( - { - result: { - incremental: [ - { - data: { - recipient: { name: "Alice", __typename: "Person" }, - __typename: "Greeting", - }, - path: ["greeting"], - }, - ], - hasNext: false, + enqueueSubsequentChunk({ + incremental: [ + { + data: { + recipient: { name: "Alice", __typename: "Person" }, + __typename: "Greeting", + }, + path: ["greeting"], }, - }, - true - ); + ], + hasNext: false, + }); { const { snapshot, renderedComponents } = await renderStream.takeRender(); @@ -291,7 +289,9 @@ test('does not suspend deferred queries with partial data in the cache and using } `; - const link = new MockSubscriptionLink(); + const { httpLink, enqueueInitialChunk, enqueueSubsequentChunk } = + mockDefer20220824(); + const cache = new InMemoryCache(); { @@ -311,7 +311,7 @@ test('does not suspend deferred queries with partial data in the cache and using } const client = new ApolloClient({ - link, + link: httpLink, cache, incrementalHandler: new Defer20220824Handler(), }); @@ -368,13 +368,11 @@ test('does not suspend deferred queries with partial data in the cache and using }); } - link.simulateResult({ - result: { - data: { - greeting: { message: "Hello world", __typename: "Greeting" }, - }, - hasNext: true, + enqueueInitialChunk({ + data: { + greeting: { message: "Hello world", __typename: "Greeting" }, }, + hasNext: true, }); { @@ -395,23 +393,18 @@ test('does not suspend deferred queries with partial data in the cache and using }); } - link.simulateResult( - { - result: { - incremental: [ - { - data: { - __typename: "Greeting", - recipient: { name: "Alice", __typename: "Person" }, - }, - path: ["greeting"], - }, - ], - hasNext: false, + enqueueSubsequentChunk({ + incremental: [ + { + data: { + __typename: "Greeting", + recipient: { name: "Alice", __typename: "Person" }, + }, + path: ["greeting"], }, - }, - true - ); + ], + hasNext: false, + }); { const { snapshot, renderedComponents } = await renderStream.takeRender(); @@ -433,4 +426,3 @@ test('does not suspend deferred queries with partial data in the cache and using await expect(renderStream).not.toRerender(); }); - diff --git a/src/react/hooks/__tests__/useMutation/defer20220824.test.tsx b/src/react/hooks/__tests__/useMutation/defer20220824.test.tsx index 45b046eefd6..42ebb4e9cde 100644 --- a/src/react/hooks/__tests__/useMutation/defer20220824.test.tsx +++ b/src/react/hooks/__tests__/useMutation/defer20220824.test.tsx @@ -8,9 +8,9 @@ import { ApolloClient, CombinedGraphQLErrors } from "@apollo/client"; import { InMemoryCache } from "@apollo/client/cache"; import { Defer20220824Handler } from "@apollo/client/incremental"; import { useMutation } from "@apollo/client/react"; -import { MockSubscriptionLink } from "@apollo/client/testing"; import { createClientWrapper, + mockDefer20220824, spyOnConsole, } from "@apollo/client/testing/internal"; @@ -33,10 +33,11 @@ test("resolves a deferred mutation with the full result", async () => { description: "Get milk!", }; - const link = new MockSubscriptionLink(); + const { httpLink, enqueueInitialChunk, enqueueSubsequentChunk } = + mockDefer20220824(); const client = new ApolloClient({ - link, + link: httpLink, cache: new InMemoryCache(), incrementalHandler: new Defer20220824Handler(), }); @@ -73,41 +74,30 @@ test("resolves a deferred mutation with the full result", async () => { }); } - setTimeout(() => { - link.simulateResult({ - result: { - data: { - createTodo: { - id: 1, - __typename: "Todo", - }, - }, - hasNext: true, + enqueueInitialChunk({ + data: { + createTodo: { + id: 1, + __typename: "Todo", }, - }); + }, + hasNext: true, }); await expect(takeSnapshot).not.toRerender(); - setTimeout(() => { - link.simulateResult( + enqueueSubsequentChunk({ + incremental: [ { - result: { - incremental: [ - { - data: { - description: "Get milk!", - priority: "High", - __typename: "Todo", - }, - path: ["createTodo"], - }, - ], - hasNext: false, + data: { + description: "Get milk!", + priority: "High", + __typename: "Todo", }, + path: ["createTodo"], }, - true - ); + ], + hasNext: false, }); { @@ -159,10 +149,11 @@ test("resolves with resulting errors and calls onError callback", async () => { description: "Get milk!", }; - const link = new MockSubscriptionLink(); + const { httpLink, enqueueInitialChunk, enqueueSubsequentChunk } = + mockDefer20220824(); const client = new ApolloClient({ - link, + link: httpLink, cache: new InMemoryCache(), incrementalHandler: new Defer20220824Handler(), }); @@ -202,35 +193,28 @@ test("resolves with resulting errors and calls onError callback", async () => { }); } - link.simulateResult({ - result: { - data: { - createTodo: { - id: 1, - __typename: "Todo", - }, + enqueueInitialChunk({ + data: { + createTodo: { + id: 1, + __typename: "Todo", }, - hasNext: true, }, + hasNext: true, }); await expect(takeSnapshot).not.toRerender(); - link.simulateResult( - { - result: { - incremental: [ - { - data: null, - errors: [{ message: CREATE_TODO_ERROR }], - path: ["createTodo"], - }, - ], - hasNext: false, + enqueueSubsequentChunk({ + incremental: [ + { + data: null, + errors: [{ message: CREATE_TODO_ERROR }], + path: ["createTodo"], }, - }, - true - ); + ], + hasNext: false, + }); await expect(promise).rejects.toThrow( new CombinedGraphQLErrors({ errors: [{ message: CREATE_TODO_ERROR }] }) @@ -280,10 +264,11 @@ test("calls the update function with the final merged result data", async () => description: "Get milk!", }; - const link = new MockSubscriptionLink(); + const { httpLink, enqueueInitialChunk, enqueueSubsequentChunk } = + mockDefer20220824(); const update = jest.fn(); const client = new ApolloClient({ - link, + link: httpLink, cache: new InMemoryCache(), incrementalHandler: new Defer20220824Handler(), }); @@ -322,38 +307,31 @@ test("calls the update function with the final merged result data", async () => }); } - link.simulateResult({ - result: { - data: { - createTodo: { - id: 1, - __typename: "Todo", - }, + enqueueInitialChunk({ + data: { + createTodo: { + id: 1, + __typename: "Todo", }, - hasNext: true, }, + hasNext: true, }); await expect(takeSnapshot).not.toRerender(); - link.simulateResult( - { - result: { - incremental: [ - { - data: { - description: "Get milk!", - priority: "High", - __typename: "Todo", - }, - path: ["createTodo"], - }, - ], - hasNext: false, + enqueueSubsequentChunk({ + incremental: [ + { + data: { + description: "Get milk!", + priority: "High", + __typename: "Todo", + }, + path: ["createTodo"], }, - }, - true - ); + ], + hasNext: false, + }); await expect(promiseReturnedByMutate).resolves.toStrictEqualTyped({ data: { diff --git a/src/react/query-preloader/__tests__/createQueryPreloader/defer20220824.test.tsx b/src/react/query-preloader/__tests__/createQueryPreloader/defer20220824.test.tsx index ca4b7f09998..196afa27533 100644 --- a/src/react/query-preloader/__tests__/createQueryPreloader/defer20220824.test.tsx +++ b/src/react/query-preloader/__tests__/createQueryPreloader/defer20220824.test.tsx @@ -16,8 +16,10 @@ import { createQueryPreloader, useReadQuery, } from "@apollo/client/react"; -import { MockSubscriptionLink } from "@apollo/client/testing"; -import { markAsStreaming } from "@apollo/client/testing/internal"; +import { + markAsStreaming, + mockDefer20220824, +} from "@apollo/client/testing/internal"; async function renderDefaultTestApp< TData, @@ -94,10 +96,12 @@ test("suspends deferred queries until initial chunk loads then rerenders with de } `; - const link = new MockSubscriptionLink(); + const { httpLink, enqueueInitialChunk, enqueueSubsequentChunk } = + mockDefer20220824(); + const client = new ApolloClient({ cache: new InMemoryCache(), - link, + link: httpLink, incrementalHandler: new Defer20220824Handler(), }); @@ -113,11 +117,9 @@ test("suspends deferred queries until initial chunk loads then rerenders with de expect(renderedComponents).toStrictEqual(["App", "SuspenseFallback"]); } - link.simulateResult({ - result: { - data: { greeting: { message: "Hello world", __typename: "Greeting" } }, - hasNext: true, - }, + enqueueInitialChunk({ + data: { greeting: { message: "Hello world", __typename: "Greeting" } }, + hasNext: true, }); { @@ -134,23 +136,18 @@ test("suspends deferred queries until initial chunk loads then rerenders with de }); } - link.simulateResult( - { - result: { - incremental: [ - { - data: { - recipient: { name: "Alice", __typename: "Person" }, - __typename: "Greeting", - }, - path: ["greeting"], - }, - ], - hasNext: false, + enqueueSubsequentChunk({ + incremental: [ + { + data: { + recipient: { name: "Alice", __typename: "Person" }, + __typename: "Greeting", + }, + path: ["greeting"], }, - }, - true - ); + ], + hasNext: false, + }); { const { snapshot, renderedComponents } = await renderStream.takeRender(); From 560824e5b9953b8070733f88697d934e976d7330 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Fri, 5 Sep 2025 00:09:20 -0600 Subject: [PATCH 67/97] Use render helper in useLoadableQuery similar to other tests --- .../useLoadableQuery/defer20220824.test.tsx | 276 ++++++++---------- 1 file changed, 125 insertions(+), 151 deletions(-) diff --git a/src/react/hooks/__tests__/useLoadableQuery/defer20220824.test.tsx b/src/react/hooks/__tests__/useLoadableQuery/defer20220824.test.tsx index f27a4a14123..26a07ede75c 100644 --- a/src/react/hooks/__tests__/useLoadableQuery/defer20220824.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery/defer20220824.test.tsx @@ -1,114 +1,113 @@ -import { screen } from "@testing-library/react"; -import type { - AsyncRenderFn, - RenderStream, -} from "@testing-library/react-render-stream"; +import type { RenderOptions } from "@testing-library/react"; import { createRenderStream, disableActEnvironment, useTrackRenders, } from "@testing-library/react-render-stream"; -import { userEvent } from "@testing-library/user-event"; import React, { Suspense } from "react"; -import { ErrorBoundary as ReactErrorBoundary } from "react-error-boundary"; +import { ErrorBoundary } from "react-error-boundary"; -import type { DataState, TypedDocumentNode } from "@apollo/client"; +import type { + DataState, + ErrorLike, + OperationVariables, + TypedDocumentNode, +} from "@apollo/client"; import { ApolloClient, gql, NetworkStatus } from "@apollo/client"; import { InMemoryCache } from "@apollo/client/cache"; import { Defer20220824Handler } from "@apollo/client/incremental"; import type { QueryRef } from "@apollo/client/react"; +import { useLoadableQuery, useReadQuery } from "@apollo/client/react"; import { - ApolloProvider, - useLoadableQuery, - useReadQuery, -} from "@apollo/client/react"; -import { + createClientWrapper, mockDefer20220824, - renderAsync, spyOnConsole, } from "@apollo/client/testing/internal"; -import type { DeepPartial } from "@apollo/client/utilities"; +import { invariant } from "@apollo/client/utilities/invariant"; + +async function renderHook< + TData, + TVariables extends OperationVariables, + TStates extends DataState["dataState"] = DataState["dataState"], + Props = never, +>( + renderHook: ( + props: Props extends never ? undefined : Props + ) => useLoadableQuery.Result, + options: Pick & { initialProps?: Props } +) { + function UseReadQuery({ + queryRef, + }: { + queryRef: QueryRef; + }) { + useTrackRenders({ name: "useReadQuery" }); + mergeSnapshot({ result: useReadQuery(queryRef) }); -function createDefaultProfiler() { - return createRenderStream({ - initialSnapshot: { - error: null as Error | null, - result: null as useReadQuery.Result | null, - }, - skipNonTrackingRenders: true, - }); -} + return null; + } -function createDefaultProfiledComponents< - Snapshot extends { - result: useReadQuery.Result | null; - error?: Error | null; - }, - TData = Snapshot["result"] extends useReadQuery.Result | null ? - TData - : unknown, - TStates extends DataState["dataState"] = Snapshot["result"] extends ( - useReadQuery.Result | null - ) ? - TStates - : "complete" | "streaming", ->(profiler: RenderStream) { function SuspenseFallback() { - useTrackRenders(); - return

Loading

; + useTrackRenders({ name: "SuspenseFallback" }); + + return null; } - function ReadQueryHook({ - queryRef, - }: { - queryRef: QueryRef; - }) { - useTrackRenders(); - profiler.mergeSnapshot({ - result: useReadQuery(queryRef), - } as unknown as Partial); + function ErrorFallback() { + useTrackRenders({ name: "ErrorBoundary" }); return null; } - function ErrorFallback({ error }: { error: Error }) { - useTrackRenders(); - profiler.mergeSnapshot({ error } as Partial); + function App({ props }: { props: Props | undefined }) { + useTrackRenders({ name: "useLoadableQuery" }); + const [loadQuery, queryRef] = renderHook(props as any); - return
Oops
; - } + mergeSnapshot({ loadQuery }); - function ErrorBoundary({ children }: { children: React.ReactNode }) { return ( - - {children} - + }> + replaceSnapshot({ error })} + > + {queryRef && } + + ); } - return { - SuspenseFallback, - ReadQueryHook, - ErrorFallback, - ErrorBoundary, - }; -} + const { + render, + getCurrentRender, + takeRender, + mergeSnapshot, + replaceSnapshot, + } = createRenderStream< + | { + loadQuery: useLoadableQuery.LoadQueryFunction; + result?: useReadQuery.Result; + } + | { error: ErrorLike } + >({ initialSnapshot: { loadQuery: null as any } }); -async function renderWithClient( - ui: React.ReactElement, - options: { client: ApolloClient }, - { render: doRender }: { render: AsyncRenderFn | typeof renderAsync } -) { - const { client } = options; - const user = userEvent.setup(); + const utils = await render(, options); - const utils = await doRender(ui, { - wrapper: ({ children }: { children: React.ReactNode }) => ( - {children} - ), - }); + function rerender(props: Props) { + return utils.rerender(); + } - return { ...utils, user }; + function getCurrentSnapshot() { + const { snapshot } = getCurrentRender(); + invariant( + "loadQuery" in snapshot, + "Expected rendered hook instead of error boundary" + ); + + return snapshot; + } + + return { takeRender, rerender, getCurrentSnapshot }; } test('does not suspend deferred queries with data in the cache and using a "cache-and-network" fetch policy', async () => { @@ -154,43 +153,27 @@ test('does not suspend deferred queries with data in the cache and using a "cach }); using _disabledAct = disableActEnvironment(); - const renderStream = createDefaultProfiler(); - const { SuspenseFallback, ReadQueryHook } = - createDefaultProfiledComponents(renderStream); - - function App() { - useTrackRenders(); - const [loadQuery, queryRef] = useLoadableQuery(query, { - fetchPolicy: "cache-and-network", - }); - return ( -
- - }> - {queryRef && } - -
- ); - } - - const { user } = await renderWithClient( - , - { - client, - }, - { render: renderAsync } + const { takeRender, getCurrentSnapshot } = await renderHook( + () => useLoadableQuery(query, { fetchPolicy: "cache-and-network" }), + { wrapper: createClientWrapper(client) } ); - // initial render - await renderStream.takeRender(); + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useLoadableQuery"]); + } - await user.click(screen.getByText("Load todo")); + getCurrentSnapshot().loadQuery(); { - const { snapshot, renderedComponents } = await renderStream.takeRender(); - - expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + const { snapshot, renderedComponents } = await takeRender(); + invariant("result" in snapshot); + expect(renderedComponents).toStrictEqual([ + "useLoadableQuery", + "useReadQuery", + ]); expect(snapshot.result).toStrictEqualTyped({ data: { greeting: { @@ -213,9 +196,10 @@ test('does not suspend deferred queries with data in the cache and using a "cach }); { - const { snapshot, renderedComponents } = await renderStream.takeRender(); + const { snapshot, renderedComponents } = await takeRender(); - expect(renderedComponents).toStrictEqual([ReadQueryHook]); + invariant("result" in snapshot); + expect(renderedComponents).toStrictEqual(["useReadQuery"]); expect(snapshot.result).toStrictEqualTyped({ data: { greeting: { @@ -244,9 +228,10 @@ test('does not suspend deferred queries with data in the cache and using a "cach }); { - const { snapshot, renderedComponents } = await renderStream.takeRender(); + const { snapshot, renderedComponents } = await takeRender(); - expect(renderedComponents).toStrictEqual([ReadQueryHook]); + invariant("result" in snapshot); + expect(renderedComponents).toStrictEqual(["useReadQuery"]); expect(snapshot.result).toStrictEqualTyped({ data: { greeting: { @@ -261,7 +246,7 @@ test('does not suspend deferred queries with data in the cache and using a "cach }); } - await expect(renderStream).not.toRerender(); + await expect(takeRender).not.toRerender(); }); test('does not suspend deferred queries with partial data in the cache and using a "cache-first" fetch policy with `returnPartialData`', async () => { @@ -317,44 +302,31 @@ test('does not suspend deferred queries with partial data in the cache and using }); using _disabledAct = disableActEnvironment(); - const renderStream = createDefaultProfiler>(); - const { SuspenseFallback, ReadQueryHook } = - createDefaultProfiledComponents(renderStream); - - function App() { - useTrackRenders(); - const [loadTodo, queryRef] = useLoadableQuery(query, { - fetchPolicy: "cache-first", - returnPartialData: true, - }); - - return ( -
- - }> - {queryRef && } - -
- ); - } - - const { user } = await renderWithClient( - , - { - client, - }, - { render: renderAsync } + const { takeRender, getCurrentSnapshot } = await renderHook( + () => + useLoadableQuery(query, { + fetchPolicy: "cache-first", + returnPartialData: true, + }), + { wrapper: createClientWrapper(client) } ); - // initial render - await renderStream.takeRender(); + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useLoadableQuery"]); + } - await user.click(screen.getByText("Load todo")); + getCurrentSnapshot().loadQuery(); { - const { snapshot, renderedComponents } = await renderStream.takeRender(); + const { snapshot, renderedComponents } = await takeRender(); - expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + invariant("result" in snapshot); + expect(renderedComponents).toStrictEqual([ + "useLoadableQuery", + "useReadQuery", + ]); expect(snapshot.result).toStrictEqualTyped({ data: { greeting: { @@ -376,9 +348,10 @@ test('does not suspend deferred queries with partial data in the cache and using }); { - const { snapshot, renderedComponents } = await renderStream.takeRender(); + const { snapshot, renderedComponents } = await takeRender(); - expect(renderedComponents).toStrictEqual([ReadQueryHook]); + invariant("result" in snapshot); + expect(renderedComponents).toStrictEqual(["useReadQuery"]); expect(snapshot.result).toStrictEqualTyped({ data: { greeting: { @@ -407,9 +380,10 @@ test('does not suspend deferred queries with partial data in the cache and using }); { - const { snapshot, renderedComponents } = await renderStream.takeRender(); + const { snapshot, renderedComponents } = await takeRender(); - expect(renderedComponents).toStrictEqual([ReadQueryHook]); + invariant("result" in snapshot); + expect(renderedComponents).toStrictEqual(["useReadQuery"]); expect(snapshot.result).toStrictEqualTyped({ data: { greeting: { @@ -424,5 +398,5 @@ test('does not suspend deferred queries with partial data in the cache and using }); } - await expect(renderStream).not.toRerender(); + await expect(takeRender).not.toRerender(); }); From fe187416733f813f0f844560a5e572120ae2b7ee Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Fri, 5 Sep 2025 00:09:39 -0600 Subject: [PATCH 68/97] Remove unused import --- src/react/hooks/__tests__/useLoadableQuery.test.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index 1a49e9dca1c..bcd4be56365 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -32,7 +32,6 @@ import { NetworkStatus, } from "@apollo/client"; import { InMemoryCache } from "@apollo/client/cache"; -import { Defer20220824Handler } from "@apollo/client/incremental"; import type { QueryRef } from "@apollo/client/react"; import { ApolloProvider, @@ -1531,7 +1530,6 @@ it("works with startTransition to change variables", async () => { }); }); - it("reacts to cache updates", async () => { const { query, mocks } = useSimpleQueryCase(); const client = new ApolloClient({ @@ -4396,7 +4394,6 @@ it('suspends and does not use partial data when changing variables and using a " } }); - it("throws when calling loadQuery on first render", async () => { // We don't provide this functionality with React 19 anymore since it requires internals access if (IS_REACT_19) return; From 01d43251288c41be0ac73b78dcbc0fbcca8715b0 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Fri, 5 Sep 2025 00:12:48 -0600 Subject: [PATCH 69/97] Add a useLoadableQuery test suite for deferGraphQL17Alpha9 --- .../deferGraphQL17Alpha9.test.tsx | 406 ++++++++++++++++++ 1 file changed, 406 insertions(+) create mode 100644 src/react/hooks/__tests__/useLoadableQuery/deferGraphQL17Alpha9.test.tsx diff --git a/src/react/hooks/__tests__/useLoadableQuery/deferGraphQL17Alpha9.test.tsx b/src/react/hooks/__tests__/useLoadableQuery/deferGraphQL17Alpha9.test.tsx new file mode 100644 index 00000000000..d43bdb16e73 --- /dev/null +++ b/src/react/hooks/__tests__/useLoadableQuery/deferGraphQL17Alpha9.test.tsx @@ -0,0 +1,406 @@ +import type { RenderOptions } from "@testing-library/react"; +import { + createRenderStream, + disableActEnvironment, + useTrackRenders, +} from "@testing-library/react-render-stream"; +import React, { Suspense } from "react"; +import { ErrorBoundary } from "react-error-boundary"; + +import type { + DataState, + ErrorLike, + OperationVariables, + TypedDocumentNode, +} from "@apollo/client"; +import { ApolloClient, gql, NetworkStatus } from "@apollo/client"; +import { InMemoryCache } from "@apollo/client/cache"; +import { GraphQL17Alpha9Handler } from "@apollo/client/incremental"; +import type { QueryRef } from "@apollo/client/react"; +import { useLoadableQuery, useReadQuery } from "@apollo/client/react"; +import { + createClientWrapper, + mockDeferStreamGraphQL17Alpha9, + spyOnConsole, +} from "@apollo/client/testing/internal"; +import { invariant } from "@apollo/client/utilities/invariant"; + +async function renderHook< + TData, + TVariables extends OperationVariables, + TStates extends DataState["dataState"] = DataState["dataState"], + Props = never, +>( + renderHook: ( + props: Props extends never ? undefined : Props + ) => useLoadableQuery.Result, + options: Pick & { initialProps?: Props } +) { + function UseReadQuery({ + queryRef, + }: { + queryRef: QueryRef; + }) { + useTrackRenders({ name: "useReadQuery" }); + mergeSnapshot({ result: useReadQuery(queryRef) }); + + return null; + } + + function SuspenseFallback() { + useTrackRenders({ name: "SuspenseFallback" }); + + return null; + } + + function ErrorFallback() { + useTrackRenders({ name: "ErrorBoundary" }); + + return null; + } + + function App({ props }: { props: Props | undefined }) { + useTrackRenders({ name: "useLoadableQuery" }); + const [loadQuery, queryRef] = renderHook(props as any); + + mergeSnapshot({ loadQuery }); + + return ( + }> + replaceSnapshot({ error })} + > + {queryRef && } + + + ); + } + + const { + render, + getCurrentRender, + takeRender, + mergeSnapshot, + replaceSnapshot, + } = createRenderStream< + | { + loadQuery: useLoadableQuery.LoadQueryFunction; + result?: useReadQuery.Result; + } + | { error: ErrorLike } + >({ initialSnapshot: { loadQuery: null as any } }); + + const utils = await render(, options); + + function rerender(props: Props) { + return utils.rerender(); + } + + function getCurrentSnapshot() { + const { snapshot } = getCurrentRender(); + invariant( + "loadQuery" in snapshot, + "Expected rendered hook instead of error boundary" + ); + + return snapshot; + } + + return { takeRender, rerender, getCurrentSnapshot }; +} + +test('does not suspend deferred queries with data in the cache and using a "cache-and-network" fetch policy', async () => { + interface Data { + greeting: { + __typename: string; + message: string; + recipient: { name: string; __typename: string }; + }; + } + + const query: TypedDocumentNode> = gql` + query { + greeting { + message + ... @defer { + recipient { + name + } + } + } + } + `; + + const { httpLink, enqueueInitialChunk, enqueueSubsequentChunk } = + mockDeferStreamGraphQL17Alpha9(); + + const cache = new InMemoryCache(); + cache.writeQuery({ + query, + data: { + greeting: { + __typename: "Greeting", + message: "Hello cached", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + }); + const client = new ApolloClient({ + cache, + link: httpLink, + incrementalHandler: new GraphQL17Alpha9Handler(), + }); + + using _disabledAct = disableActEnvironment(); + const { takeRender, getCurrentSnapshot } = await renderHook( + () => useLoadableQuery(query, { fetchPolicy: "cache-and-network" }), + { wrapper: createClientWrapper(client) } + ); + + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useLoadableQuery"]); + } + + getCurrentSnapshot().loadQuery(); + + { + const { snapshot, renderedComponents } = await takeRender(); + + invariant("result" in snapshot); + expect(renderedComponents).toStrictEqual([ + "useLoadableQuery", + "useReadQuery", + ]); + expect(snapshot.result).toStrictEqualTyped({ + data: { + greeting: { + __typename: "Greeting", + message: "Hello cached", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + dataState: "complete", + error: undefined, + networkStatus: NetworkStatus.loading, + }); + } + + enqueueInitialChunk({ + data: { + greeting: { __typename: "Greeting", message: "Hello world" }, + }, + pending: [{ id: "0", path: ["greeting"] }], + hasNext: true, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + invariant("result" in snapshot); + expect(renderedComponents).toStrictEqual(["useReadQuery"]); + expect(snapshot.result).toStrictEqualTyped({ + data: { + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + dataState: "streaming", + error: undefined, + networkStatus: NetworkStatus.streaming, + }); + } + + enqueueSubsequentChunk({ + incremental: [ + { + data: { + recipient: { name: "Alice", __typename: "Person" }, + __typename: "Greeting", + }, + id: "0", + }, + ], + completed: [{ id: "0" }], + hasNext: false, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + invariant("result" in snapshot); + expect(renderedComponents).toStrictEqual(["useReadQuery"]); + expect(snapshot.result).toStrictEqualTyped({ + data: { + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Alice" }, + }, + }, + dataState: "complete", + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await expect(takeRender).not.toRerender(); +}); + +test('does not suspend deferred queries with partial data in the cache and using a "cache-first" fetch policy with `returnPartialData`', async () => { + interface QueryData { + greeting: { + __typename: string; + message?: string; + recipient?: { + __typename: string; + name: string; + }; + }; + } + + const query: TypedDocumentNode> = gql` + query { + greeting { + message + ... on Greeting @defer { + recipient { + name + } + } + } + } + `; + + const { httpLink, enqueueInitialChunk, enqueueSubsequentChunk } = + mockDeferStreamGraphQL17Alpha9(); + + const cache = new InMemoryCache(); + + { + // We are intentionally writing partial data to the cache. Supress console + // warnings to avoid unnecessary noise in the test. + using _consoleSpy = spyOnConsole("error"); + + cache.writeQuery({ + query, + data: { + greeting: { + __typename: "Greeting", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + }); + } + + const client = new ApolloClient({ + link: httpLink, + cache, + incrementalHandler: new GraphQL17Alpha9Handler(), + }); + + using _disabledAct = disableActEnvironment(); + const { takeRender, getCurrentSnapshot } = await renderHook( + () => + useLoadableQuery(query, { + fetchPolicy: "cache-first", + returnPartialData: true, + }), + { wrapper: createClientWrapper(client) } + ); + + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual(["useLoadableQuery"]); + } + + getCurrentSnapshot().loadQuery(); + + { + const { snapshot, renderedComponents } = await takeRender(); + + invariant("result" in snapshot); + expect(renderedComponents).toStrictEqual([ + "useLoadableQuery", + "useReadQuery", + ]); + expect(snapshot.result).toStrictEqualTyped({ + data: { + greeting: { + __typename: "Greeting", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + dataState: "partial", + error: undefined, + networkStatus: NetworkStatus.loading, + }); + } + + enqueueInitialChunk({ + data: { + greeting: { message: "Hello world", __typename: "Greeting" }, + }, + pending: [{ id: "0", path: ["greeting"] }], + hasNext: true, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + invariant("result" in snapshot); + expect(renderedComponents).toStrictEqual(["useReadQuery"]); + expect(snapshot.result).toStrictEqualTyped({ + data: { + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + dataState: "streaming", + error: undefined, + networkStatus: NetworkStatus.streaming, + }); + } + + enqueueSubsequentChunk({ + incremental: [ + { + data: { + __typename: "Greeting", + recipient: { name: "Alice", __typename: "Person" }, + }, + id: "0", + }, + ], + completed: [{ id: "0" }], + hasNext: false, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + invariant("result" in snapshot); + expect(renderedComponents).toStrictEqual(["useReadQuery"]); + expect(snapshot.result).toStrictEqualTyped({ + data: { + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Alice" }, + }, + }, + dataState: "complete", + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await expect(takeRender).not.toRerender(); +}); \ No newline at end of file From 6e4ad159f1e7e0f2d4d8338c288f0d3015b00e39 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Fri, 5 Sep 2025 00:14:33 -0600 Subject: [PATCH 70/97] Add a createQueryPreloader test suite for deferGraphQL17Alpha9 --- .../deferGraphQL17Alpha9.test.tsx | 171 ++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 src/react/query-preloader/__tests__/createQueryPreloader/deferGraphQL17Alpha9.test.tsx diff --git a/src/react/query-preloader/__tests__/createQueryPreloader/deferGraphQL17Alpha9.test.tsx b/src/react/query-preloader/__tests__/createQueryPreloader/deferGraphQL17Alpha9.test.tsx new file mode 100644 index 00000000000..c62bfeb7dc6 --- /dev/null +++ b/src/react/query-preloader/__tests__/createQueryPreloader/deferGraphQL17Alpha9.test.tsx @@ -0,0 +1,171 @@ +import { + createRenderStream, + disableActEnvironment, + useTrackRenders, +} from "@testing-library/react-render-stream"; +import React, { Suspense } from "react"; +import { ErrorBoundary } from "react-error-boundary"; + +import type { DataState } from "@apollo/client"; +import { ApolloClient, gql, NetworkStatus } from "@apollo/client"; +import { InMemoryCache } from "@apollo/client/cache"; +import { GraphQL17Alpha9Handler } from "@apollo/client/incremental"; +import type { QueryRef } from "@apollo/client/react"; +import { + ApolloProvider, + createQueryPreloader, + useReadQuery, +} from "@apollo/client/react"; +import { + markAsStreaming, + mockDeferStreamGraphQL17Alpha9, +} from "@apollo/client/testing/internal"; + +async function renderDefaultTestApp< + TData, + TStates extends DataState["dataState"] = "complete" | "streaming", +>({ + client, + queryRef, +}: { + client: ApolloClient; + queryRef: QueryRef; +}) { + const renderStream = createRenderStream({ + initialSnapshot: { + result: null as useReadQuery.Result | null, + error: null as Error | null, + }, + }); + + function ReadQueryHook() { + useTrackRenders({ name: "ReadQueryHook" }); + renderStream.mergeSnapshot({ result: useReadQuery(queryRef) }); + + return null; + } + + function SuspenseFallback() { + useTrackRenders({ name: "SuspenseFallback" }); + return

Loading

; + } + + function ErrorFallback({ error }: { error: Error }) { + useTrackRenders({ name: "ErrorFallback" }); + renderStream.mergeSnapshot({ error }); + + return null; + } + + function App() { + useTrackRenders({ name: "App" }); + + return ( + + }> + + + + ); + } + + const utils = await renderStream.render(, { + wrapper: ({ children }) => ( + {children} + ), + }); + + function rerender() { + return utils.rerender(); + } + + return { ...utils, rerender, renderStream }; +} + +test("suspends deferred queries until initial chunk loads then rerenders with deferred data", async () => { + const query = gql` + query { + greeting { + message + ... on Greeting @defer { + recipient { + name + } + } + } + } + `; + + const { httpLink, enqueueInitialChunk, enqueueSubsequentChunk } = + mockDeferStreamGraphQL17Alpha9(); + + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: httpLink, + incrementalHandler: new GraphQL17Alpha9Handler(), + }); + + const preloadQuery = createQueryPreloader(client); + const queryRef = preloadQuery(query); + + using _disabledAct = disableActEnvironment(); + const { renderStream } = await renderDefaultTestApp({ client, queryRef }); + + { + const { renderedComponents } = await renderStream.takeRender(); + + expect(renderedComponents).toStrictEqual(["App", "SuspenseFallback"]); + } + + enqueueInitialChunk({ + data: { greeting: { message: "Hello world", __typename: "Greeting" } }, + pending: [{ id: "0", path: ["greeting"] }], + hasNext: true, + }); + + { + const { snapshot, renderedComponents } = await renderStream.takeRender(); + + expect(renderedComponents).toStrictEqual(["ReadQueryHook"]); + expect(snapshot.result).toStrictEqualTyped({ + data: markAsStreaming({ + greeting: { message: "Hello world", __typename: "Greeting" }, + }), + dataState: "streaming", + error: undefined, + networkStatus: NetworkStatus.streaming, + }); + } + + enqueueSubsequentChunk({ + incremental: [ + { + data: { + recipient: { name: "Alice", __typename: "Person" }, + __typename: "Greeting", + }, + id: "0", + }, + ], + completed: [{ id: "0" }], + hasNext: false, + }); + + { + const { snapshot, renderedComponents } = await renderStream.takeRender(); + + expect(renderedComponents).toStrictEqual(["ReadQueryHook"]); + expect(snapshot.result).toStrictEqualTyped({ + data: { + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Alice" }, + }, + }, + dataState: "complete", + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } +}); \ No newline at end of file From c217efa8ea861caf5bf6f0e8a0af22d57025677c Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Fri, 5 Sep 2025 00:17:25 -0600 Subject: [PATCH 71/97] Add a doc block --- src/incremental/handlers/graphql17Alpha9.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/incremental/handlers/graphql17Alpha9.ts b/src/incremental/handlers/graphql17Alpha9.ts index c8934c97fab..edad4b2834f 100644 --- a/src/incremental/handlers/graphql17Alpha9.ts +++ b/src/incremental/handlers/graphql17Alpha9.ts @@ -171,6 +171,10 @@ class IncrementalRequest } } +/** + * Provides handling for the incremental delivery specification implemented by + * graphql.js version `17.0.0-alpha.9`. + */ export class GraphQL17Alpha9Handler implements Incremental.Handler> { From 93ef59d683d8dd09fda9f4cc776e5fb95617bafa Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Fri, 5 Sep 2025 00:18:04 -0600 Subject: [PATCH 72/97] Mark methods as internal --- src/incremental/handlers/graphql17Alpha9.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/incremental/handlers/graphql17Alpha9.ts b/src/incremental/handlers/graphql17Alpha9.ts index edad4b2834f..71bce701865 100644 --- a/src/incremental/handlers/graphql17Alpha9.ts +++ b/src/incremental/handlers/graphql17Alpha9.ts @@ -178,6 +178,7 @@ class IncrementalRequest export class GraphQL17Alpha9Handler implements Incremental.Handler> { + /** @internal */ isIncrementalResult( result: ApolloLink.Result ): result is @@ -186,6 +187,7 @@ export class GraphQL17Alpha9Handler return "hasNext" in result; } + /** @internal */ prepareRequest(request: ApolloLink.Request): ApolloLink.Request { if (hasDirectives(["defer"], request.query)) { const context = request.context ?? {}; @@ -198,6 +200,7 @@ export class GraphQL17Alpha9Handler return request; } + /** @internal */ extractErrors(result: ApolloLink.Result) { const acc: GraphQLFormattedError[] = []; const push = ({ @@ -221,6 +224,7 @@ export class GraphQL17Alpha9Handler } } + /** @internal */ startRequest(_: { query: DocumentNode }) { return new IncrementalRequest(); } From 102756eff21734c970700473ad4f34f3d17289d9 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Fri, 5 Sep 2025 00:18:36 -0600 Subject: [PATCH 73/97] Run extract api --- .api-reports/api-report-incremental.api.md | 104 +++++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/.api-reports/api-report-incremental.api.md b/.api-reports/api-report-incremental.api.md index efdd684674c..f5ce7d7230e 100644 --- a/.api-reports/api-report-incremental.api.md +++ b/.api-reports/api-report-incremental.api.md @@ -80,6 +80,102 @@ class DeferRequest> implements Incremental hasNext: boolean; } +// @public (undocumented) +export namespace GraphQL17Alpha9Handler { + // (undocumented) + export type Chunk = InitialResult | SubsequentResult; + // (undocumented) + export interface CompletedResult { + // (undocumented) + errors?: ReadonlyArray; + // (undocumented) + id: string; + } + // (undocumented) + export interface GraphQL17Alpha9Result extends HKT { + // (undocumented) + arg1: unknown; + // (undocumented) + arg2: unknown; + // (undocumented) + return: GraphQL17Alpha9Handler.Chunk>; + } + // (undocumented) + export interface IncrementalDeferResult> { + // (undocumented) + data: TData; + // (undocumented) + errors?: ReadonlyArray; + // (undocumented) + extensions?: Record; + // (undocumented) + id: string; + // (undocumented) + subPath?: Incremental.Path; + } + // (undocumented) + export type IncrementalResult = IncrementalDeferResult | IncrementalStreamResult; + // (undocumented) + export interface IncrementalStreamResult> { + // (undocumented) + errors?: ReadonlyArray; + // (undocumented) + extensions?: Record; + // (undocumented) + id: string; + // (undocumented) + items: TData; + // (undocumented) + subPath?: Incremental.Path; + } + // (undocumented) + export type InitialResult> = { + data: TData; + errors?: ReadonlyArray; + pending: ReadonlyArray; + hasNext: boolean; + extensions?: Record; + }; + // (undocumented) + export interface PendingResult { + // (undocumented) + id: string; + // (undocumented) + label?: string; + // (undocumented) + path: Incremental.Path; + } + // (undocumented) + export type SubsequentResult = { + hasNext: boolean; + pending?: ReadonlyArray; + incremental?: ReadonlyArray>; + completed?: ReadonlyArray; + extensions?: Record; + }; + // (undocumented) + export interface TypeOverrides { + // (undocumented) + AdditionalApolloLinkResultTypes: GraphQL17Alpha9Result; + } +} + +// @public +export class GraphQL17Alpha9Handler implements Incremental.Handler> { + // @internal @deprecated (undocumented) + extractErrors(result: ApolloLink.Result): GraphQLFormattedError[] | undefined; + // @internal @deprecated (undocumented) + isIncrementalResult(result: ApolloLink.Result): result is GraphQL17Alpha9Handler.InitialResult | GraphQL17Alpha9Handler.SubsequentResult; + // @internal @deprecated (undocumented) + prepareRequest(request: ApolloLink.Request): ApolloLink.Request; + // Warning: (ae-forgotten-export) The symbol "IncrementalRequest" needs to be exported by the entry point index.d.ts + // + // @internal @deprecated (undocumented) + startRequest(_: { + query: DocumentNode; + }): IncrementalRequest; +} + // @public (undocumented) export namespace Incremental { // @internal @deprecated (undocumented) @@ -106,6 +202,14 @@ export namespace Incremental { export type Path = ReadonlyArray; } +// @public (undocumented) +class IncrementalRequest implements Incremental.IncrementalRequest, TData> { + // (undocumented) + handle(cacheData: TData | DeepPartial | null | undefined, chunk: GraphQL17Alpha9Handler.Chunk): FormattedExecutionResult; + // (undocumented) + hasNext: boolean; +} + // @public (undocumented) export namespace NotImplementedHandler { // (undocumented) From 16b44574ff8d261833e5a2444c8741469ec9d00b Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Fri, 5 Sep 2025 00:26:31 -0600 Subject: [PATCH 74/97] Update size limits --- .size-limits.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.size-limits.json b/.size-limits.json index 7f303c892bf..c2bb067567d 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,6 +1,6 @@ { - "import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (CJS)": 43857, - "import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (production) (CJS)": 38699, - "import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\"": 33415, - "import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (production)": 27498 + "import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (CJS)": 44246, + "import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (production) (CJS)": 39057, + "import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\"": 33470, + "import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (production)": 27490 } From 108e9f01fc88723f4cf6de83e5593b23297c116f Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Fri, 5 Sep 2025 11:19:03 -0600 Subject: [PATCH 75/97] Formatting --- .../__tests__/graphql17Alpha9/defer.test.ts | 20 ++++++++++++++----- .../__tests__/useBackgroundQuery.test.tsx | 2 -- .../deferGraphQL17Alpha9.test.tsx | 2 +- .../useMutation/defer20220824.test.tsx | 1 - .../__tests__/createQueryPreloader.test.tsx | 1 - .../defer20220824.test.tsx | 1 - .../deferGraphQL17Alpha9.test.tsx | 2 +- 7 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/incremental/handlers/__tests__/graphql17Alpha9/defer.test.ts b/src/incremental/handlers/__tests__/graphql17Alpha9/defer.test.ts index fdf145ed68e..a2aed33b372 100644 --- a/src/incremental/handlers/__tests__/graphql17Alpha9/defer.test.ts +++ b/src/incremental/handlers/__tests__/graphql17Alpha9/defer.test.ts @@ -794,7 +794,9 @@ describe("graphql-js test cases", () => { a { ... @defer { b { - c { d } + c { + d + } } } } @@ -804,7 +806,9 @@ describe("graphql-js test cases", () => { someField ... @defer { b { - e { f } + e { + f + } } } } @@ -891,7 +895,9 @@ describe("graphql-js test cases", () => { a { ... @defer { b { - c { d } + c { + d + } } } } @@ -900,8 +906,12 @@ describe("graphql-js test cases", () => { a { ... @defer { b { - c { d } - e { f } + c { + d + } + e { + f + } } } } diff --git a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx index d1c0db9e893..6e4c07f6e7f 100644 --- a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx +++ b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx @@ -1390,7 +1390,6 @@ it("works with startTransition to change variables", async () => { } }); - it("reacts to cache updates", async () => { const { query, mocks } = setupSimpleCase(); @@ -3671,7 +3670,6 @@ it('suspends and does not use partial data when changing variables and using a " await expect(renderStream).not.toRerender({ timeout: 50 }); }); - it.each([ "cache-first", "network-only", diff --git a/src/react/hooks/__tests__/useLoadableQuery/deferGraphQL17Alpha9.test.tsx b/src/react/hooks/__tests__/useLoadableQuery/deferGraphQL17Alpha9.test.tsx index d43bdb16e73..c4fee82fef3 100644 --- a/src/react/hooks/__tests__/useLoadableQuery/deferGraphQL17Alpha9.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery/deferGraphQL17Alpha9.test.tsx @@ -403,4 +403,4 @@ test('does not suspend deferred queries with partial data in the cache and using } await expect(takeRender).not.toRerender(); -}); \ No newline at end of file +}); diff --git a/src/react/hooks/__tests__/useMutation/defer20220824.test.tsx b/src/react/hooks/__tests__/useMutation/defer20220824.test.tsx index 42ebb4e9cde..5319ccdc587 100644 --- a/src/react/hooks/__tests__/useMutation/defer20220824.test.tsx +++ b/src/react/hooks/__tests__/useMutation/defer20220824.test.tsx @@ -387,4 +387,3 @@ test("calls the update function with the final merged result data", async () => expect(console.error).not.toHaveBeenCalled(); }); - diff --git a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx index 96f4838e0d3..f09581c7dfd 100644 --- a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx +++ b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx @@ -1806,7 +1806,6 @@ test("does not suspend and returns partial data when `returnPartialData` is `tru } }); - test("masks result when dataMasking is `true`", async () => { const { query, mocks } = setupMaskedVariablesCase(); const client = new ApolloClient({ diff --git a/src/react/query-preloader/__tests__/createQueryPreloader/defer20220824.test.tsx b/src/react/query-preloader/__tests__/createQueryPreloader/defer20220824.test.tsx index 196afa27533..024033c91ff 100644 --- a/src/react/query-preloader/__tests__/createQueryPreloader/defer20220824.test.tsx +++ b/src/react/query-preloader/__tests__/createQueryPreloader/defer20220824.test.tsx @@ -167,4 +167,3 @@ test("suspends deferred queries until initial chunk loads then rerenders with de }); } }); - diff --git a/src/react/query-preloader/__tests__/createQueryPreloader/deferGraphQL17Alpha9.test.tsx b/src/react/query-preloader/__tests__/createQueryPreloader/deferGraphQL17Alpha9.test.tsx index c62bfeb7dc6..5917f770217 100644 --- a/src/react/query-preloader/__tests__/createQueryPreloader/deferGraphQL17Alpha9.test.tsx +++ b/src/react/query-preloader/__tests__/createQueryPreloader/deferGraphQL17Alpha9.test.tsx @@ -168,4 +168,4 @@ test("suspends deferred queries until initial chunk loads then rerenders with de networkStatus: NetworkStatus.ready, }); } -}); \ No newline at end of file +}); From bfacd3eb25b1d6d38f02cba899cea86017f1935d Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Fri, 5 Sep 2025 11:59:54 -0600 Subject: [PATCH 76/97] Fix lint errors --- .../handlers/__tests__/graphql17Alpha9/defer.test.ts | 2 +- .../query-preloader/__tests__/createQueryPreloader.test.tsx | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/incremental/handlers/__tests__/graphql17Alpha9/defer.test.ts b/src/incremental/handlers/__tests__/graphql17Alpha9/defer.test.ts index a2aed33b372..4462476513e 100644 --- a/src/incremental/handlers/__tests__/graphql17Alpha9/defer.test.ts +++ b/src/incremental/handlers/__tests__/graphql17Alpha9/defer.test.ts @@ -25,6 +25,7 @@ import { NetworkStatus, Observable, } from "@apollo/client"; +import { GraphQL17Alpha9Handler } from "@apollo/client/incremental"; import { markAsStreaming, mockDefer20220824, @@ -33,7 +34,6 @@ import { } from "@apollo/client/testing/internal"; import { - GraphQL17Alpha9Handler, hasIncrementalChunks, // eslint-disable-next-line local-rules/no-relative-imports } from "../../graphql17Alpha9.js"; diff --git a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx index f09581c7dfd..023f3e9ffc3 100644 --- a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx +++ b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx @@ -25,7 +25,6 @@ import { InMemoryCache, NetworkStatus, } from "@apollo/client"; -import { Defer20220824Handler } from "@apollo/client/incremental"; import type { PreloadedQueryRef, QueryRef } from "@apollo/client/react"; import { ApolloProvider, @@ -33,7 +32,7 @@ import { useReadQuery, } from "@apollo/client/react"; import { unwrapQueryRef } from "@apollo/client/react/internal"; -import { MockLink, MockSubscriptionLink } from "@apollo/client/testing"; +import { MockLink } from "@apollo/client/testing"; import type { MaskedVariablesCaseData, SimpleCaseData, @@ -41,7 +40,6 @@ import type { } from "@apollo/client/testing/internal"; import { createClientWrapper, - markAsStreaming, renderHookAsync, setupMaskedVariablesCase, setupSimpleCase, From 5c51186bda6bff23bab5f2438a3ee9db1cf54390 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Fri, 5 Sep 2025 12:08:31 -0600 Subject: [PATCH 77/97] Remove unused imports --- src/react/hooks/__tests__/useQuery/defer20220824.test.tsx | 1 - src/react/hooks/__tests__/useQuery/deferGraphQL17Alpha2.test.tsx | 1 - 2 files changed, 2 deletions(-) diff --git a/src/react/hooks/__tests__/useQuery/defer20220824.test.tsx b/src/react/hooks/__tests__/useQuery/defer20220824.test.tsx index a43a83dc428..d15c2e78200 100644 --- a/src/react/hooks/__tests__/useQuery/defer20220824.test.tsx +++ b/src/react/hooks/__tests__/useQuery/defer20220824.test.tsx @@ -2,7 +2,6 @@ import { disableActEnvironment, renderHookToSnapshotStream, } from "@testing-library/react-render-stream"; -import React from "react"; import { ApolloClient, diff --git a/src/react/hooks/__tests__/useQuery/deferGraphQL17Alpha2.test.tsx b/src/react/hooks/__tests__/useQuery/deferGraphQL17Alpha2.test.tsx index 60db1cde900..0e967508933 100644 --- a/src/react/hooks/__tests__/useQuery/deferGraphQL17Alpha2.test.tsx +++ b/src/react/hooks/__tests__/useQuery/deferGraphQL17Alpha2.test.tsx @@ -2,7 +2,6 @@ import { disableActEnvironment, renderHookToSnapshotStream, } from "@testing-library/react-render-stream"; -import React from "react"; import { ApolloClient, From cb0e021f2f7d41b377f3f42c67520698fccb2b1b Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Fri, 5 Sep 2025 12:08:44 -0600 Subject: [PATCH 78/97] Fix useSuspenseQuery test that just emitted errors --- .../useSuspenseQuery/deferGraphQL17Alpha9.test.tsx | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/react/hooks/__tests__/useSuspenseQuery/deferGraphQL17Alpha9.test.tsx b/src/react/hooks/__tests__/useSuspenseQuery/deferGraphQL17Alpha9.test.tsx index 063aa94590c..51770ed06e9 100644 --- a/src/react/hooks/__tests__/useSuspenseQuery/deferGraphQL17Alpha9.test.tsx +++ b/src/react/hooks/__tests__/useSuspenseQuery/deferGraphQL17Alpha9.test.tsx @@ -1717,11 +1717,14 @@ test("throws graphql errors returned by deferred queries", async () => { } `; - const { httpLink, enqueueInitialChunk } = mockDeferStreamGraphQL17Alpha9(); - const client = new ApolloClient({ cache: new InMemoryCache(), - link: httpLink, + link: new ApolloLink(() => { + return of({ + data: null, + errors: [{ message: "Could not fetch greeting" }], + }).pipe(delay(20)); + }), incrementalHandler: new GraphQL17Alpha9Handler(), }); @@ -1739,11 +1742,6 @@ test("throws graphql errors returned by deferred queries", async () => { expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); } - enqueueInitialChunk({ - errors: [{ message: "Could not fetch greeting" }], - hasNext: false, - }); - { const { snapshot, renderedComponents } = await takeRender(); From 8a8927ffc4b66dc578cc2859e5d331d306d8b497 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Fri, 5 Sep 2025 12:22:35 -0600 Subject: [PATCH 79/97] Move `@defer` tests from ApolloClient/* to client.watchQuery/defer20220824 --- .../__tests__/ApolloClient/general.test.ts | 156 ----- .../ApolloClient/multiple-results.test.ts | 400 ------------- .../client.watchQuery/defer20220824.test.ts | 558 ++++++++++++++++++ 3 files changed, 558 insertions(+), 556 deletions(-) delete mode 100644 src/core/__tests__/ApolloClient/multiple-results.test.ts create mode 100644 src/core/__tests__/client.watchQuery/defer20220824.test.ts diff --git a/src/core/__tests__/ApolloClient/general.test.ts b/src/core/__tests__/ApolloClient/general.test.ts index 49ecefaa437..588141c8e8a 100644 --- a/src/core/__tests__/ApolloClient/general.test.ts +++ b/src/core/__tests__/ApolloClient/general.test.ts @@ -9,12 +9,10 @@ import type { ObservableQuery, TypedDocumentNode } from "@apollo/client"; import { ApolloClient, NetworkStatus } from "@apollo/client"; import { InMemoryCache } from "@apollo/client/cache"; import { CombinedGraphQLErrors } from "@apollo/client/errors"; -import { Defer20220824Handler } from "@apollo/client/incremental"; import { ApolloLink } from "@apollo/client/link"; import { ClientAwarenessLink } from "@apollo/client/link/client-awareness"; import { MockLink } from "@apollo/client/testing"; import { - mockDefer20220824, ObservableStream, spyOnConsole, wait, @@ -7548,160 +7546,6 @@ describe("ApolloClient", () => { ) ).toBeUndefined(); }); - - it("deduplicates queries as long as a query still has deferred chunks", async () => { - const query = gql` - query LazyLoadLuke { - people(id: 1) { - id - name - friends { - id - ... @defer { - name - } - } - } - } - `; - - const outgoingRequestSpy = jest.fn(((operation, forward) => - forward(operation)) satisfies ApolloLink.RequestHandler); - const defer = mockDefer20220824(); - const client = new ApolloClient({ - cache: new InMemoryCache({}), - link: new ApolloLink(outgoingRequestSpy).concat(defer.httpLink), - incrementalHandler: new Defer20220824Handler(), - }); - - const query1 = new ObservableStream( - client.watchQuery({ query, fetchPolicy: "network-only" }) - ); - const query2 = new ObservableStream( - client.watchQuery({ query, fetchPolicy: "network-only" }) - ); - expect(outgoingRequestSpy).toHaveBeenCalledTimes(1); - - const initialData = { - people: { - __typename: "Person", - id: 1, - name: "Luke", - friends: [ - { - __typename: "Person", - id: 5, - } as { __typename: "Person"; id: number; name?: string }, - { - __typename: "Person", - id: 8, - } as { __typename: "Person"; id: number; name?: string }, - ], - }, - }; - const initialResult: ObservableQuery.Result = { - data: initialData, - dataState: "streaming", - loading: true, - networkStatus: NetworkStatus.streaming, - partial: true, - }; - - defer.enqueueInitialChunk({ - data: initialData, - hasNext: true, - }); - - await expect(query1).toEmitTypedValue({ - data: undefined, - dataState: "empty", - loading: true, - networkStatus: NetworkStatus.loading, - partial: true, - }); - await expect(query2).toEmitTypedValue({ - data: undefined, - dataState: "empty", - loading: true, - networkStatus: NetworkStatus.loading, - partial: true, - }); - - await expect(query1).toEmitTypedValue(initialResult); - await expect(query2).toEmitTypedValue(initialResult); - - const query3 = new ObservableStream( - client.watchQuery({ query, fetchPolicy: "network-only" }) - ); - await expect(query3).toEmitTypedValue(initialResult); - expect(outgoingRequestSpy).toHaveBeenCalledTimes(1); - - const firstChunk = { - incremental: [ - { - data: { - name: "Leia", - }, - path: ["people", "friends", 0], - }, - ], - hasNext: true, - }; - const resultAfterFirstChunk = structuredClone( - initialResult - ) as ObservableQuery.Result; - resultAfterFirstChunk.data.people.friends[0].name = "Leia"; - - defer.enqueueSubsequentChunk(firstChunk); - - await expect(query1).toEmitTypedValue(resultAfterFirstChunk); - await expect(query2).toEmitTypedValue(resultAfterFirstChunk); - await expect(query3).toEmitTypedValue(resultAfterFirstChunk); - - const query4 = new ObservableStream( - client.watchQuery({ query, fetchPolicy: "network-only" }) - ); - await expect(query4).toEmitTypedValue(resultAfterFirstChunk); - expect(outgoingRequestSpy).toHaveBeenCalledTimes(1); - - const secondChunk = { - incremental: [ - { - data: { - name: "Han Solo", - }, - path: ["people", "friends", 1], - }, - ], - hasNext: false, - }; - const resultAfterSecondChunk = { - ...structuredClone(resultAfterFirstChunk), - loading: false, - networkStatus: NetworkStatus.ready, - dataState: "complete", - partial: false, - } as ObservableQuery.Result; - resultAfterSecondChunk.data.people.friends[1].name = "Han Solo"; - - defer.enqueueSubsequentChunk(secondChunk); - - await expect(query1).toEmitTypedValue(resultAfterSecondChunk); - await expect(query2).toEmitTypedValue(resultAfterSecondChunk); - await expect(query3).toEmitTypedValue(resultAfterSecondChunk); - await expect(query4).toEmitTypedValue(resultAfterSecondChunk); - - // TODO: Re-enable once below condition can be met - /* const query5 = */ new ObservableStream( - client.watchQuery({ query, fetchPolicy: "network-only" }) - ); - // TODO: Re-enable once notifyOnNetworkStatusChange controls whether we - // get the loading state. This test fails with the switch to RxJS for now - // since the initial value is emitted synchronously unlike zen-observable - // where the emitted result wasn't emitted until after this assertion. - // expect(query5).not.toEmitAnything(); - expect(outgoingRequestSpy).toHaveBeenCalledTimes(2); - }); }); describe("missing cache field warnings", () => { diff --git a/src/core/__tests__/ApolloClient/multiple-results.test.ts b/src/core/__tests__/ApolloClient/multiple-results.test.ts deleted file mode 100644 index 466e02c920e..00000000000 --- a/src/core/__tests__/ApolloClient/multiple-results.test.ts +++ /dev/null @@ -1,400 +0,0 @@ -import { GraphQLError } from "graphql"; -import { gql } from "graphql-tag"; - -import { ApolloClient, NetworkStatus } from "@apollo/client"; -import { InMemoryCache } from "@apollo/client/cache"; -import { Defer20220824Handler } from "@apollo/client/incremental"; -import { MockSubscriptionLink } from "@apollo/client/testing"; -import { ObservableStream, wait } from "@apollo/client/testing/internal"; - -describe("mutiple results", () => { - it("allows multiple query results from link", async () => { - const query = gql` - query LazyLoadLuke { - people_one(id: 1) { - name - friends @defer { - name - } - } - } - `; - - const initialData = { - people_one: { - name: "Luke Skywalker", - friends: null, - }, - }; - - const laterData = { - people_one: { - // XXX true defer's wouldn't send this - name: "Luke Skywalker", - friends: [{ name: "Leia Skywalker" }], - }, - }; - const link = new MockSubscriptionLink(); - const client = new ApolloClient({ - cache: new InMemoryCache(), - link, - incrementalHandler: new Defer20220824Handler(), - }); - - const observable = client.watchQuery({ - query, - variables: {}, - }); - const stream = new ObservableStream(observable); - - await expect(stream).toEmitTypedValue({ - data: undefined, - dataState: "empty", - loading: true, - networkStatus: NetworkStatus.loading, - partial: true, - }); - - // fire off first result - link.simulateResult({ result: { data: initialData } }); - - await expect(stream).toEmitTypedValue({ - data: initialData, - dataState: "complete", - loading: false, - networkStatus: 7, - partial: false, - }); - - link.simulateResult({ result: { data: laterData } }); - - await expect(stream).toEmitTypedValue({ - data: laterData, - dataState: "complete", - loading: false, - networkStatus: 7, - partial: false, - }); - }); - - it("allows multiple query results from link with ignored errors", async () => { - const query = gql` - query LazyLoadLuke { - people_one(id: 1) { - name - friends @defer { - name - } - } - } - `; - - const initialData = { - people_one: { - name: "Luke Skywalker", - friends: null, - }, - }; - - const laterData = { - people_one: { - // XXX true defer's wouldn't send this - name: "Luke Skywalker", - friends: [{ name: "Leia Skywalker" }], - }, - }; - const link = new MockSubscriptionLink(); - const client = new ApolloClient({ - cache: new InMemoryCache(), - link, - incrementalHandler: new Defer20220824Handler(), - }); - - const observable = client.watchQuery({ - query, - variables: {}, - errorPolicy: "ignore", - }); - const stream = new ObservableStream(observable); - - await expect(stream).toEmitTypedValue({ - data: undefined, - dataState: "empty", - loading: true, - networkStatus: NetworkStatus.loading, - partial: true, - }); - - // fire off first result - link.simulateResult({ result: { data: initialData } }); - - await expect(stream).toEmitTypedValue({ - data: initialData, - dataState: "complete", - loading: false, - networkStatus: 7, - partial: false, - }); - - link.simulateResult({ - result: { errors: [new GraphQLError("defer failed")] }, - }); - - await expect(stream).toEmitTypedValue({ - data: undefined, - dataState: "empty", - loading: false, - networkStatus: 7, - partial: true, - }); - - await wait(20); - link.simulateResult({ result: { data: laterData } }); - - await expect(stream).toEmitTypedValue({ - data: laterData, - dataState: "complete", - loading: false, - networkStatus: 7, - partial: false, - }); - }); - - it("strips errors from a result if ignored", async () => { - const query = gql` - query LazyLoadLuke { - people_one(id: 1) { - name - friends @defer { - name - } - } - } - `; - - const initialData = { - people_one: { - name: "Luke Skywalker", - friends: null, - }, - }; - - const laterData = { - people_one: { - // XXX true defer's wouldn't send this - name: "Luke Skywalker", - friends: [{ name: "Leia Skywalker" }], - }, - }; - const link = new MockSubscriptionLink(); - const client = new ApolloClient({ - cache: new InMemoryCache(), - link, - incrementalHandler: new Defer20220824Handler(), - }); - - const observable = client.watchQuery({ - query, - variables: {}, - errorPolicy: "ignore", - }); - const stream = new ObservableStream(observable); - - await expect(stream).toEmitTypedValue({ - data: undefined, - dataState: "empty", - loading: true, - networkStatus: NetworkStatus.loading, - partial: true, - }); - - // fire off first result - link.simulateResult({ result: { data: initialData } }); - - await expect(stream).toEmitTypedValue({ - data: initialData, - dataState: "complete", - loading: false, - networkStatus: 7, - partial: false, - }); - - // this should fire the `next` event without this error - link.simulateResult({ - result: { - errors: [new GraphQLError("defer failed")], - data: laterData, - }, - }); - - await expect(stream).toEmitTypedValue({ - data: laterData, - dataState: "complete", - loading: false, - networkStatus: 7, - partial: false, - }); - }); - - it.skip("allows multiple query results from link with all errors", async () => { - const query = gql` - query LazyLoadLuke { - people_one(id: 1) { - name - friends @defer { - name - } - } - } - `; - - const initialData = { - people_one: { - name: "Luke Skywalker", - friends: null, - }, - }; - - const laterData = { - people_one: { - // XXX true defer's wouldn't send this - name: "Luke Skywalker", - friends: [{ name: "Leia Skywalker" }], - }, - }; - const link = new MockSubscriptionLink(); - const client = new ApolloClient({ - cache: new InMemoryCache(), - link, - incrementalHandler: new Defer20220824Handler(), - }); - - const observable = client.watchQuery({ - query, - variables: {}, - errorPolicy: "all", - }); - const stream = new ObservableStream(observable); - - // fire off first result - link.simulateResult({ result: { data: initialData } }); - - await expect(stream).toEmitTypedValue({ - data: initialData, - dataState: "complete", - loading: false, - networkStatus: 7, - partial: false, - }); - - // this should fire the next event again - link.simulateResult({ - error: new Error("defer failed"), - }); - - await expect(stream).toEmitTypedValue({ - data: initialData, - dataState: "complete", - loading: false, - networkStatus: 7, - error: new Error("defer failed"), - partial: false, - }); - - link.simulateResult({ result: { data: laterData } }); - - await expect(stream).toEmitTypedValue({ - data: laterData, - dataState: "complete", - loading: false, - networkStatus: 7, - partial: false, - }); - }); - - it("emits error if an error is set with the none policy", async () => { - const query = gql` - query LazyLoadLuke { - people_one(id: 1) { - name - friends @defer { - name - } - } - } - `; - - const initialData = { - people_one: { - name: "Luke Skywalker", - friends: null, - }, - }; - - const link = new MockSubscriptionLink(); - const client = new ApolloClient({ - cache: new InMemoryCache(), - link, - incrementalHandler: new Defer20220824Handler(), - }); - - const observable = client.watchQuery({ - query, - variables: {}, - // errorPolicy: 'none', // this is the default - }); - const stream = new ObservableStream(observable); - - let count = 0; - observable.subscribe({ - next: (result) => { - // errors should never be passed since they are ignored - count++; - // loading - if (count === 1) { - expect(result.error).toBeUndefined(); - } - // first result - if (count === 2) { - expect(result.error).toBeUndefined(); - } - // error - if (count === 3) { - expect(result.error).toBeDefined(); - } - }, - }); - - await expect(stream).toEmitTypedValue({ - data: undefined, - dataState: "empty", - loading: true, - networkStatus: NetworkStatus.loading, - partial: true, - }); - - // fire off first result - link.simulateResult({ result: { data: initialData } }); - - await expect(stream).toEmitTypedValue({ - data: initialData, - dataState: "complete", - loading: false, - networkStatus: 7, - partial: false, - }); - - link.simulateResult({ error: new Error("defer failed") }); - - await expect(stream).toEmitTypedValue({ - data: initialData, - dataState: "complete", - error: new Error("defer failed"), - loading: false, - networkStatus: NetworkStatus.error, - partial: false, - }); - - await expect(stream).not.toEmitAnything(); - }); -}); diff --git a/src/core/__tests__/client.watchQuery/defer20220824.test.ts b/src/core/__tests__/client.watchQuery/defer20220824.test.ts new file mode 100644 index 00000000000..c142553980f --- /dev/null +++ b/src/core/__tests__/client.watchQuery/defer20220824.test.ts @@ -0,0 +1,558 @@ +import { GraphQLError } from "graphql"; +import { gql } from "graphql-tag"; + +import type { ObservableQuery } from "@apollo/client"; +import { ApolloClient, NetworkStatus } from "@apollo/client"; +import { InMemoryCache } from "@apollo/client/cache"; +import { Defer20220824Handler } from "@apollo/client/incremental"; +import { ApolloLink } from "@apollo/client/link"; +import { MockSubscriptionLink } from "@apollo/client/testing"; +import { + mockDefer20220824, + ObservableStream, + wait, +} from "@apollo/client/testing/internal"; + +test("allows multiple query results from link", async () => { + const query = gql` + query LazyLoadLuke { + people_one(id: 1) { + name + friends @defer { + name + } + } + } + `; + + const initialData = { + people_one: { + name: "Luke Skywalker", + friends: null, + }, + }; + + const laterData = { + people_one: { + // XXX true defer's wouldn't send this + name: "Luke Skywalker", + friends: [{ name: "Leia Skywalker" }], + }, + }; + const link = new MockSubscriptionLink(); + const client = new ApolloClient({ + cache: new InMemoryCache(), + link, + incrementalHandler: new Defer20220824Handler(), + }); + + const observable = client.watchQuery({ + query, + variables: {}, + }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitTypedValue({ + data: undefined, + dataState: "empty", + loading: true, + networkStatus: NetworkStatus.loading, + partial: true, + }); + + // fire off first result + link.simulateResult({ result: { data: initialData } }); + + await expect(stream).toEmitTypedValue({ + data: initialData, + dataState: "complete", + loading: false, + networkStatus: 7, + partial: false, + }); + + link.simulateResult({ result: { data: laterData } }); + + await expect(stream).toEmitTypedValue({ + data: laterData, + dataState: "complete", + loading: false, + networkStatus: 7, + partial: false, + }); +}); + +test("allows multiple query results from link with ignored errors", async () => { + const query = gql` + query LazyLoadLuke { + people_one(id: 1) { + name + friends @defer { + name + } + } + } + `; + + const initialData = { + people_one: { + name: "Luke Skywalker", + friends: null, + }, + }; + + const laterData = { + people_one: { + // XXX true defer's wouldn't send this + name: "Luke Skywalker", + friends: [{ name: "Leia Skywalker" }], + }, + }; + const link = new MockSubscriptionLink(); + const client = new ApolloClient({ + cache: new InMemoryCache(), + link, + incrementalHandler: new Defer20220824Handler(), + }); + + const observable = client.watchQuery({ + query, + variables: {}, + errorPolicy: "ignore", + }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitTypedValue({ + data: undefined, + dataState: "empty", + loading: true, + networkStatus: NetworkStatus.loading, + partial: true, + }); + + // fire off first result + link.simulateResult({ result: { data: initialData } }); + + await expect(stream).toEmitTypedValue({ + data: initialData, + dataState: "complete", + loading: false, + networkStatus: 7, + partial: false, + }); + + link.simulateResult({ + result: { errors: [new GraphQLError("defer failed")] }, + }); + + await expect(stream).toEmitTypedValue({ + data: undefined, + dataState: "empty", + loading: false, + networkStatus: 7, + partial: true, + }); + + await wait(20); + link.simulateResult({ result: { data: laterData } }); + + await expect(stream).toEmitTypedValue({ + data: laterData, + dataState: "complete", + loading: false, + networkStatus: 7, + partial: false, + }); +}); + +test("strips errors from a result if ignored", async () => { + const query = gql` + query LazyLoadLuke { + people_one(id: 1) { + name + friends @defer { + name + } + } + } + `; + + const initialData = { + people_one: { + name: "Luke Skywalker", + friends: null, + }, + }; + + const laterData = { + people_one: { + // XXX true defer's wouldn't send this + name: "Luke Skywalker", + friends: [{ name: "Leia Skywalker" }], + }, + }; + const link = new MockSubscriptionLink(); + const client = new ApolloClient({ + cache: new InMemoryCache(), + link, + incrementalHandler: new Defer20220824Handler(), + }); + + const observable = client.watchQuery({ + query, + variables: {}, + errorPolicy: "ignore", + }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitTypedValue({ + data: undefined, + dataState: "empty", + loading: true, + networkStatus: NetworkStatus.loading, + partial: true, + }); + + // fire off first result + link.simulateResult({ result: { data: initialData } }); + + await expect(stream).toEmitTypedValue({ + data: initialData, + dataState: "complete", + loading: false, + networkStatus: 7, + partial: false, + }); + + // this should fire the `next` event without this error + link.simulateResult({ + result: { + errors: [new GraphQLError("defer failed")], + data: laterData, + }, + }); + + await expect(stream).toEmitTypedValue({ + data: laterData, + dataState: "complete", + loading: false, + networkStatus: 7, + partial: false, + }); +}); + +test.skip("allows multiple query results from link with all errors", async () => { + const query = gql` + query LazyLoadLuke { + people_one(id: 1) { + name + friends @defer { + name + } + } + } + `; + + const initialData = { + people_one: { + name: "Luke Skywalker", + friends: null, + }, + }; + + const laterData = { + people_one: { + // XXX true defer's wouldn't send this + name: "Luke Skywalker", + friends: [{ name: "Leia Skywalker" }], + }, + }; + const link = new MockSubscriptionLink(); + const client = new ApolloClient({ + cache: new InMemoryCache(), + link, + incrementalHandler: new Defer20220824Handler(), + }); + + const observable = client.watchQuery({ + query, + variables: {}, + errorPolicy: "all", + }); + const stream = new ObservableStream(observable); + + // fire off first result + link.simulateResult({ result: { data: initialData } }); + + await expect(stream).toEmitTypedValue({ + data: initialData, + dataState: "complete", + loading: false, + networkStatus: 7, + partial: false, + }); + + // this should fire the next event again + link.simulateResult({ + error: new Error("defer failed"), + }); + + await expect(stream).toEmitTypedValue({ + data: initialData, + dataState: "complete", + loading: false, + networkStatus: 7, + error: new Error("defer failed"), + partial: false, + }); + + link.simulateResult({ result: { data: laterData } }); + + await expect(stream).toEmitTypedValue({ + data: laterData, + dataState: "complete", + loading: false, + networkStatus: 7, + partial: false, + }); +}); + +test("emits error if an error is set with the none policy", async () => { + const query = gql` + query LazyLoadLuke { + people_one(id: 1) { + name + friends @defer { + name + } + } + } + `; + + const initialData = { + people_one: { + name: "Luke Skywalker", + friends: null, + }, + }; + + const link = new MockSubscriptionLink(); + const client = new ApolloClient({ + cache: new InMemoryCache(), + link, + incrementalHandler: new Defer20220824Handler(), + }); + + const observable = client.watchQuery({ + query, + variables: {}, + // errorPolicy: 'none', // this is the default + }); + const stream = new ObservableStream(observable); + + let count = 0; + observable.subscribe({ + next: (result) => { + // errors should never be passed since they are ignored + count++; + // loading + if (count === 1) { + expect(result.error).toBeUndefined(); + } + // first result + if (count === 2) { + expect(result.error).toBeUndefined(); + } + // error + if (count === 3) { + expect(result.error).toBeDefined(); + } + }, + }); + + await expect(stream).toEmitTypedValue({ + data: undefined, + dataState: "empty", + loading: true, + networkStatus: NetworkStatus.loading, + partial: true, + }); + + // fire off first result + link.simulateResult({ result: { data: initialData } }); + + await expect(stream).toEmitTypedValue({ + data: initialData, + dataState: "complete", + loading: false, + networkStatus: 7, + partial: false, + }); + + link.simulateResult({ error: new Error("defer failed") }); + + await expect(stream).toEmitTypedValue({ + data: initialData, + dataState: "complete", + error: new Error("defer failed"), + loading: false, + networkStatus: NetworkStatus.error, + partial: false, + }); + + await expect(stream).not.toEmitAnything(); +}); + +test("deduplicates queries as long as a query still has deferred chunks", async () => { + const query = gql` + query LazyLoadLuke { + people(id: 1) { + id + name + friends { + id + ... @defer { + name + } + } + } + } + `; + + const outgoingRequestSpy = jest.fn(((operation, forward) => + forward(operation)) satisfies ApolloLink.RequestHandler); + const defer = mockDefer20220824(); + const client = new ApolloClient({ + cache: new InMemoryCache({}), + link: new ApolloLink(outgoingRequestSpy).concat(defer.httpLink), + incrementalHandler: new Defer20220824Handler(), + }); + + const query1 = new ObservableStream( + client.watchQuery({ query, fetchPolicy: "network-only" }) + ); + const query2 = new ObservableStream( + client.watchQuery({ query, fetchPolicy: "network-only" }) + ); + expect(outgoingRequestSpy).toHaveBeenCalledTimes(1); + + const initialData = { + people: { + __typename: "Person", + id: 1, + name: "Luke", + friends: [ + { + __typename: "Person", + id: 5, + } as { __typename: "Person"; id: number; name?: string }, + { + __typename: "Person", + id: 8, + } as { __typename: "Person"; id: number; name?: string }, + ], + }, + }; + const initialResult: ObservableQuery.Result = { + data: initialData, + dataState: "streaming", + loading: true, + networkStatus: NetworkStatus.streaming, + partial: true, + }; + + defer.enqueueInitialChunk({ + data: initialData, + hasNext: true, + }); + + await expect(query1).toEmitTypedValue({ + data: undefined, + dataState: "empty", + loading: true, + networkStatus: NetworkStatus.loading, + partial: true, + }); + await expect(query2).toEmitTypedValue({ + data: undefined, + dataState: "empty", + loading: true, + networkStatus: NetworkStatus.loading, + partial: true, + }); + + await expect(query1).toEmitTypedValue(initialResult); + await expect(query2).toEmitTypedValue(initialResult); + + const query3 = new ObservableStream( + client.watchQuery({ query, fetchPolicy: "network-only" }) + ); + await expect(query3).toEmitTypedValue(initialResult); + expect(outgoingRequestSpy).toHaveBeenCalledTimes(1); + + const firstChunk = { + incremental: [ + { + data: { + name: "Leia", + }, + path: ["people", "friends", 0], + }, + ], + hasNext: true, + }; + const resultAfterFirstChunk = structuredClone( + initialResult + ) as ObservableQuery.Result; + resultAfterFirstChunk.data.people.friends[0].name = "Leia"; + + defer.enqueueSubsequentChunk(firstChunk); + + await expect(query1).toEmitTypedValue(resultAfterFirstChunk); + await expect(query2).toEmitTypedValue(resultAfterFirstChunk); + await expect(query3).toEmitTypedValue(resultAfterFirstChunk); + + const query4 = new ObservableStream( + client.watchQuery({ query, fetchPolicy: "network-only" }) + ); + await expect(query4).toEmitTypedValue(resultAfterFirstChunk); + expect(outgoingRequestSpy).toHaveBeenCalledTimes(1); + + const secondChunk = { + incremental: [ + { + data: { + name: "Han Solo", + }, + path: ["people", "friends", 1], + }, + ], + hasNext: false, + }; + const resultAfterSecondChunk = { + ...structuredClone(resultAfterFirstChunk), + loading: false, + networkStatus: NetworkStatus.ready, + dataState: "complete", + partial: false, + } as ObservableQuery.Result; + resultAfterSecondChunk.data.people.friends[1].name = "Han Solo"; + + defer.enqueueSubsequentChunk(secondChunk); + + await expect(query1).toEmitTypedValue(resultAfterSecondChunk); + await expect(query2).toEmitTypedValue(resultAfterSecondChunk); + await expect(query3).toEmitTypedValue(resultAfterSecondChunk); + await expect(query4).toEmitTypedValue(resultAfterSecondChunk); + + // TODO: Re-enable once below condition can be met + /* const query5 = */ new ObservableStream( + client.watchQuery({ query, fetchPolicy: "network-only" }) + ); + // TODO: Re-enable once notifyOnNetworkStatusChange controls whether we + // get the loading state. This test fails with the switch to RxJS for now + // since the initial value is emitted synchronously unlike zen-observable + // where the emitted result wasn't emitted until after this assertion. + // expect(query5).not.toEmitAnything(); + expect(outgoingRequestSpy).toHaveBeenCalledTimes(2); +}); From a8231c0c072dd805f34c3589349d1ded4d9e05cc Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Fri, 5 Sep 2025 18:30:20 -0600 Subject: [PATCH 80/97] Restore multiple-results test --- .../ApolloClient/multiple-results.test.ts | 400 ++++++++++++++++++ 1 file changed, 400 insertions(+) create mode 100644 src/core/__tests__/ApolloClient/multiple-results.test.ts diff --git a/src/core/__tests__/ApolloClient/multiple-results.test.ts b/src/core/__tests__/ApolloClient/multiple-results.test.ts new file mode 100644 index 00000000000..466e02c920e --- /dev/null +++ b/src/core/__tests__/ApolloClient/multiple-results.test.ts @@ -0,0 +1,400 @@ +import { GraphQLError } from "graphql"; +import { gql } from "graphql-tag"; + +import { ApolloClient, NetworkStatus } from "@apollo/client"; +import { InMemoryCache } from "@apollo/client/cache"; +import { Defer20220824Handler } from "@apollo/client/incremental"; +import { MockSubscriptionLink } from "@apollo/client/testing"; +import { ObservableStream, wait } from "@apollo/client/testing/internal"; + +describe("mutiple results", () => { + it("allows multiple query results from link", async () => { + const query = gql` + query LazyLoadLuke { + people_one(id: 1) { + name + friends @defer { + name + } + } + } + `; + + const initialData = { + people_one: { + name: "Luke Skywalker", + friends: null, + }, + }; + + const laterData = { + people_one: { + // XXX true defer's wouldn't send this + name: "Luke Skywalker", + friends: [{ name: "Leia Skywalker" }], + }, + }; + const link = new MockSubscriptionLink(); + const client = new ApolloClient({ + cache: new InMemoryCache(), + link, + incrementalHandler: new Defer20220824Handler(), + }); + + const observable = client.watchQuery({ + query, + variables: {}, + }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitTypedValue({ + data: undefined, + dataState: "empty", + loading: true, + networkStatus: NetworkStatus.loading, + partial: true, + }); + + // fire off first result + link.simulateResult({ result: { data: initialData } }); + + await expect(stream).toEmitTypedValue({ + data: initialData, + dataState: "complete", + loading: false, + networkStatus: 7, + partial: false, + }); + + link.simulateResult({ result: { data: laterData } }); + + await expect(stream).toEmitTypedValue({ + data: laterData, + dataState: "complete", + loading: false, + networkStatus: 7, + partial: false, + }); + }); + + it("allows multiple query results from link with ignored errors", async () => { + const query = gql` + query LazyLoadLuke { + people_one(id: 1) { + name + friends @defer { + name + } + } + } + `; + + const initialData = { + people_one: { + name: "Luke Skywalker", + friends: null, + }, + }; + + const laterData = { + people_one: { + // XXX true defer's wouldn't send this + name: "Luke Skywalker", + friends: [{ name: "Leia Skywalker" }], + }, + }; + const link = new MockSubscriptionLink(); + const client = new ApolloClient({ + cache: new InMemoryCache(), + link, + incrementalHandler: new Defer20220824Handler(), + }); + + const observable = client.watchQuery({ + query, + variables: {}, + errorPolicy: "ignore", + }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitTypedValue({ + data: undefined, + dataState: "empty", + loading: true, + networkStatus: NetworkStatus.loading, + partial: true, + }); + + // fire off first result + link.simulateResult({ result: { data: initialData } }); + + await expect(stream).toEmitTypedValue({ + data: initialData, + dataState: "complete", + loading: false, + networkStatus: 7, + partial: false, + }); + + link.simulateResult({ + result: { errors: [new GraphQLError("defer failed")] }, + }); + + await expect(stream).toEmitTypedValue({ + data: undefined, + dataState: "empty", + loading: false, + networkStatus: 7, + partial: true, + }); + + await wait(20); + link.simulateResult({ result: { data: laterData } }); + + await expect(stream).toEmitTypedValue({ + data: laterData, + dataState: "complete", + loading: false, + networkStatus: 7, + partial: false, + }); + }); + + it("strips errors from a result if ignored", async () => { + const query = gql` + query LazyLoadLuke { + people_one(id: 1) { + name + friends @defer { + name + } + } + } + `; + + const initialData = { + people_one: { + name: "Luke Skywalker", + friends: null, + }, + }; + + const laterData = { + people_one: { + // XXX true defer's wouldn't send this + name: "Luke Skywalker", + friends: [{ name: "Leia Skywalker" }], + }, + }; + const link = new MockSubscriptionLink(); + const client = new ApolloClient({ + cache: new InMemoryCache(), + link, + incrementalHandler: new Defer20220824Handler(), + }); + + const observable = client.watchQuery({ + query, + variables: {}, + errorPolicy: "ignore", + }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitTypedValue({ + data: undefined, + dataState: "empty", + loading: true, + networkStatus: NetworkStatus.loading, + partial: true, + }); + + // fire off first result + link.simulateResult({ result: { data: initialData } }); + + await expect(stream).toEmitTypedValue({ + data: initialData, + dataState: "complete", + loading: false, + networkStatus: 7, + partial: false, + }); + + // this should fire the `next` event without this error + link.simulateResult({ + result: { + errors: [new GraphQLError("defer failed")], + data: laterData, + }, + }); + + await expect(stream).toEmitTypedValue({ + data: laterData, + dataState: "complete", + loading: false, + networkStatus: 7, + partial: false, + }); + }); + + it.skip("allows multiple query results from link with all errors", async () => { + const query = gql` + query LazyLoadLuke { + people_one(id: 1) { + name + friends @defer { + name + } + } + } + `; + + const initialData = { + people_one: { + name: "Luke Skywalker", + friends: null, + }, + }; + + const laterData = { + people_one: { + // XXX true defer's wouldn't send this + name: "Luke Skywalker", + friends: [{ name: "Leia Skywalker" }], + }, + }; + const link = new MockSubscriptionLink(); + const client = new ApolloClient({ + cache: new InMemoryCache(), + link, + incrementalHandler: new Defer20220824Handler(), + }); + + const observable = client.watchQuery({ + query, + variables: {}, + errorPolicy: "all", + }); + const stream = new ObservableStream(observable); + + // fire off first result + link.simulateResult({ result: { data: initialData } }); + + await expect(stream).toEmitTypedValue({ + data: initialData, + dataState: "complete", + loading: false, + networkStatus: 7, + partial: false, + }); + + // this should fire the next event again + link.simulateResult({ + error: new Error("defer failed"), + }); + + await expect(stream).toEmitTypedValue({ + data: initialData, + dataState: "complete", + loading: false, + networkStatus: 7, + error: new Error("defer failed"), + partial: false, + }); + + link.simulateResult({ result: { data: laterData } }); + + await expect(stream).toEmitTypedValue({ + data: laterData, + dataState: "complete", + loading: false, + networkStatus: 7, + partial: false, + }); + }); + + it("emits error if an error is set with the none policy", async () => { + const query = gql` + query LazyLoadLuke { + people_one(id: 1) { + name + friends @defer { + name + } + } + } + `; + + const initialData = { + people_one: { + name: "Luke Skywalker", + friends: null, + }, + }; + + const link = new MockSubscriptionLink(); + const client = new ApolloClient({ + cache: new InMemoryCache(), + link, + incrementalHandler: new Defer20220824Handler(), + }); + + const observable = client.watchQuery({ + query, + variables: {}, + // errorPolicy: 'none', // this is the default + }); + const stream = new ObservableStream(observable); + + let count = 0; + observable.subscribe({ + next: (result) => { + // errors should never be passed since they are ignored + count++; + // loading + if (count === 1) { + expect(result.error).toBeUndefined(); + } + // first result + if (count === 2) { + expect(result.error).toBeUndefined(); + } + // error + if (count === 3) { + expect(result.error).toBeDefined(); + } + }, + }); + + await expect(stream).toEmitTypedValue({ + data: undefined, + dataState: "empty", + loading: true, + networkStatus: NetworkStatus.loading, + partial: true, + }); + + // fire off first result + link.simulateResult({ result: { data: initialData } }); + + await expect(stream).toEmitTypedValue({ + data: initialData, + dataState: "complete", + loading: false, + networkStatus: 7, + partial: false, + }); + + link.simulateResult({ error: new Error("defer failed") }); + + await expect(stream).toEmitTypedValue({ + data: initialData, + dataState: "complete", + error: new Error("defer failed"), + loading: false, + networkStatus: NetworkStatus.error, + partial: false, + }); + + await expect(stream).not.toEmitAnything(); + }); +}); From 5e852487cdb34c02769915bc8d9ef6cd31ad5648 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Fri, 5 Sep 2025 18:30:41 -0600 Subject: [PATCH 81/97] Remove multiple-results tests from defer tests --- .../client.watchQuery/defer20220824.test.ts | 393 ------------------ 1 file changed, 393 deletions(-) diff --git a/src/core/__tests__/client.watchQuery/defer20220824.test.ts b/src/core/__tests__/client.watchQuery/defer20220824.test.ts index c142553980f..36c6ba5b8bb 100644 --- a/src/core/__tests__/client.watchQuery/defer20220824.test.ts +++ b/src/core/__tests__/client.watchQuery/defer20220824.test.ts @@ -1,4 +1,3 @@ -import { GraphQLError } from "graphql"; import { gql } from "graphql-tag"; import type { ObservableQuery } from "@apollo/client"; @@ -6,403 +5,11 @@ import { ApolloClient, NetworkStatus } from "@apollo/client"; import { InMemoryCache } from "@apollo/client/cache"; import { Defer20220824Handler } from "@apollo/client/incremental"; import { ApolloLink } from "@apollo/client/link"; -import { MockSubscriptionLink } from "@apollo/client/testing"; import { mockDefer20220824, ObservableStream, - wait, } from "@apollo/client/testing/internal"; -test("allows multiple query results from link", async () => { - const query = gql` - query LazyLoadLuke { - people_one(id: 1) { - name - friends @defer { - name - } - } - } - `; - - const initialData = { - people_one: { - name: "Luke Skywalker", - friends: null, - }, - }; - - const laterData = { - people_one: { - // XXX true defer's wouldn't send this - name: "Luke Skywalker", - friends: [{ name: "Leia Skywalker" }], - }, - }; - const link = new MockSubscriptionLink(); - const client = new ApolloClient({ - cache: new InMemoryCache(), - link, - incrementalHandler: new Defer20220824Handler(), - }); - - const observable = client.watchQuery({ - query, - variables: {}, - }); - const stream = new ObservableStream(observable); - - await expect(stream).toEmitTypedValue({ - data: undefined, - dataState: "empty", - loading: true, - networkStatus: NetworkStatus.loading, - partial: true, - }); - - // fire off first result - link.simulateResult({ result: { data: initialData } }); - - await expect(stream).toEmitTypedValue({ - data: initialData, - dataState: "complete", - loading: false, - networkStatus: 7, - partial: false, - }); - - link.simulateResult({ result: { data: laterData } }); - - await expect(stream).toEmitTypedValue({ - data: laterData, - dataState: "complete", - loading: false, - networkStatus: 7, - partial: false, - }); -}); - -test("allows multiple query results from link with ignored errors", async () => { - const query = gql` - query LazyLoadLuke { - people_one(id: 1) { - name - friends @defer { - name - } - } - } - `; - - const initialData = { - people_one: { - name: "Luke Skywalker", - friends: null, - }, - }; - - const laterData = { - people_one: { - // XXX true defer's wouldn't send this - name: "Luke Skywalker", - friends: [{ name: "Leia Skywalker" }], - }, - }; - const link = new MockSubscriptionLink(); - const client = new ApolloClient({ - cache: new InMemoryCache(), - link, - incrementalHandler: new Defer20220824Handler(), - }); - - const observable = client.watchQuery({ - query, - variables: {}, - errorPolicy: "ignore", - }); - const stream = new ObservableStream(observable); - - await expect(stream).toEmitTypedValue({ - data: undefined, - dataState: "empty", - loading: true, - networkStatus: NetworkStatus.loading, - partial: true, - }); - - // fire off first result - link.simulateResult({ result: { data: initialData } }); - - await expect(stream).toEmitTypedValue({ - data: initialData, - dataState: "complete", - loading: false, - networkStatus: 7, - partial: false, - }); - - link.simulateResult({ - result: { errors: [new GraphQLError("defer failed")] }, - }); - - await expect(stream).toEmitTypedValue({ - data: undefined, - dataState: "empty", - loading: false, - networkStatus: 7, - partial: true, - }); - - await wait(20); - link.simulateResult({ result: { data: laterData } }); - - await expect(stream).toEmitTypedValue({ - data: laterData, - dataState: "complete", - loading: false, - networkStatus: 7, - partial: false, - }); -}); - -test("strips errors from a result if ignored", async () => { - const query = gql` - query LazyLoadLuke { - people_one(id: 1) { - name - friends @defer { - name - } - } - } - `; - - const initialData = { - people_one: { - name: "Luke Skywalker", - friends: null, - }, - }; - - const laterData = { - people_one: { - // XXX true defer's wouldn't send this - name: "Luke Skywalker", - friends: [{ name: "Leia Skywalker" }], - }, - }; - const link = new MockSubscriptionLink(); - const client = new ApolloClient({ - cache: new InMemoryCache(), - link, - incrementalHandler: new Defer20220824Handler(), - }); - - const observable = client.watchQuery({ - query, - variables: {}, - errorPolicy: "ignore", - }); - const stream = new ObservableStream(observable); - - await expect(stream).toEmitTypedValue({ - data: undefined, - dataState: "empty", - loading: true, - networkStatus: NetworkStatus.loading, - partial: true, - }); - - // fire off first result - link.simulateResult({ result: { data: initialData } }); - - await expect(stream).toEmitTypedValue({ - data: initialData, - dataState: "complete", - loading: false, - networkStatus: 7, - partial: false, - }); - - // this should fire the `next` event without this error - link.simulateResult({ - result: { - errors: [new GraphQLError("defer failed")], - data: laterData, - }, - }); - - await expect(stream).toEmitTypedValue({ - data: laterData, - dataState: "complete", - loading: false, - networkStatus: 7, - partial: false, - }); -}); - -test.skip("allows multiple query results from link with all errors", async () => { - const query = gql` - query LazyLoadLuke { - people_one(id: 1) { - name - friends @defer { - name - } - } - } - `; - - const initialData = { - people_one: { - name: "Luke Skywalker", - friends: null, - }, - }; - - const laterData = { - people_one: { - // XXX true defer's wouldn't send this - name: "Luke Skywalker", - friends: [{ name: "Leia Skywalker" }], - }, - }; - const link = new MockSubscriptionLink(); - const client = new ApolloClient({ - cache: new InMemoryCache(), - link, - incrementalHandler: new Defer20220824Handler(), - }); - - const observable = client.watchQuery({ - query, - variables: {}, - errorPolicy: "all", - }); - const stream = new ObservableStream(observable); - - // fire off first result - link.simulateResult({ result: { data: initialData } }); - - await expect(stream).toEmitTypedValue({ - data: initialData, - dataState: "complete", - loading: false, - networkStatus: 7, - partial: false, - }); - - // this should fire the next event again - link.simulateResult({ - error: new Error("defer failed"), - }); - - await expect(stream).toEmitTypedValue({ - data: initialData, - dataState: "complete", - loading: false, - networkStatus: 7, - error: new Error("defer failed"), - partial: false, - }); - - link.simulateResult({ result: { data: laterData } }); - - await expect(stream).toEmitTypedValue({ - data: laterData, - dataState: "complete", - loading: false, - networkStatus: 7, - partial: false, - }); -}); - -test("emits error if an error is set with the none policy", async () => { - const query = gql` - query LazyLoadLuke { - people_one(id: 1) { - name - friends @defer { - name - } - } - } - `; - - const initialData = { - people_one: { - name: "Luke Skywalker", - friends: null, - }, - }; - - const link = new MockSubscriptionLink(); - const client = new ApolloClient({ - cache: new InMemoryCache(), - link, - incrementalHandler: new Defer20220824Handler(), - }); - - const observable = client.watchQuery({ - query, - variables: {}, - // errorPolicy: 'none', // this is the default - }); - const stream = new ObservableStream(observable); - - let count = 0; - observable.subscribe({ - next: (result) => { - // errors should never be passed since they are ignored - count++; - // loading - if (count === 1) { - expect(result.error).toBeUndefined(); - } - // first result - if (count === 2) { - expect(result.error).toBeUndefined(); - } - // error - if (count === 3) { - expect(result.error).toBeDefined(); - } - }, - }); - - await expect(stream).toEmitTypedValue({ - data: undefined, - dataState: "empty", - loading: true, - networkStatus: NetworkStatus.loading, - partial: true, - }); - - // fire off first result - link.simulateResult({ result: { data: initialData } }); - - await expect(stream).toEmitTypedValue({ - data: initialData, - dataState: "complete", - loading: false, - networkStatus: 7, - partial: false, - }); - - link.simulateResult({ error: new Error("defer failed") }); - - await expect(stream).toEmitTypedValue({ - data: initialData, - dataState: "complete", - error: new Error("defer failed"), - loading: false, - networkStatus: NetworkStatus.error, - partial: false, - }); - - await expect(stream).not.toEmitAnything(); -}); - test("deduplicates queries as long as a query still has deferred chunks", async () => { const query = gql` query LazyLoadLuke { From bb6dd7161da569b1818d04d32ec8e5b5ac0074eb Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Fri, 5 Sep 2025 18:31:00 -0600 Subject: [PATCH 82/97] Add new spec format tests --- .../deferGraphQL17Alpha9.test.ts | 175 ++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 src/core/__tests__/client.watchQuery/deferGraphQL17Alpha9.test.ts diff --git a/src/core/__tests__/client.watchQuery/deferGraphQL17Alpha9.test.ts b/src/core/__tests__/client.watchQuery/deferGraphQL17Alpha9.test.ts new file mode 100644 index 00000000000..035ce0525df --- /dev/null +++ b/src/core/__tests__/client.watchQuery/deferGraphQL17Alpha9.test.ts @@ -0,0 +1,175 @@ +import { gql } from "graphql-tag"; + +import type { ObservableQuery } from "@apollo/client"; +import { ApolloClient, NetworkStatus } from "@apollo/client"; +import { InMemoryCache } from "@apollo/client/cache"; +import { GraphQL17Alpha9Handler } from "@apollo/client/incremental"; +import { ApolloLink } from "@apollo/client/link"; +import { + mockDeferStreamGraphQL17Alpha9, + ObservableStream, +} from "@apollo/client/testing/internal"; + +test("deduplicates queries as long as a query still has deferred chunks", async () => { + const query = gql` + query LazyLoadLuke { + people(id: 1) { + id + name + friends { + id + ... @defer { + name + } + } + } + } + `; + + const outgoingRequestSpy = jest.fn(((operation, forward) => + forward(operation)) satisfies ApolloLink.RequestHandler); + const defer = mockDeferStreamGraphQL17Alpha9(); + const client = new ApolloClient({ + cache: new InMemoryCache({}), + link: new ApolloLink(outgoingRequestSpy).concat(defer.httpLink), + incrementalHandler: new GraphQL17Alpha9Handler(), + }); + + const query1 = new ObservableStream( + client.watchQuery({ query, fetchPolicy: "network-only" }) + ); + const query2 = new ObservableStream( + client.watchQuery({ query, fetchPolicy: "network-only" }) + ); + expect(outgoingRequestSpy).toHaveBeenCalledTimes(1); + + const initialData = { + people: { + __typename: "Person", + id: 1, + name: "Luke", + friends: [ + { + __typename: "Person", + id: 5, + } as { __typename: "Person"; id: number; name?: string }, + { + __typename: "Person", + id: 8, + } as { __typename: "Person"; id: number; name?: string }, + ], + }, + }; + const initialResult: ObservableQuery.Result = { + data: initialData, + dataState: "streaming", + loading: true, + networkStatus: NetworkStatus.streaming, + partial: true, + }; + + defer.enqueueInitialChunk({ + data: initialData, + pending: [ + { id: "0", path: ["people", "friends", 0] }, + { id: "1", path: ["people", "friends", 1] }, + ], + hasNext: true, + }); + + await expect(query1).toEmitTypedValue({ + data: undefined, + dataState: "empty", + loading: true, + networkStatus: NetworkStatus.loading, + partial: true, + }); + await expect(query2).toEmitTypedValue({ + data: undefined, + dataState: "empty", + loading: true, + networkStatus: NetworkStatus.loading, + partial: true, + }); + + await expect(query1).toEmitTypedValue(initialResult); + await expect(query2).toEmitTypedValue(initialResult); + + const query3 = new ObservableStream( + client.watchQuery({ query, fetchPolicy: "network-only" }) + ); + await expect(query3).toEmitTypedValue(initialResult); + expect(outgoingRequestSpy).toHaveBeenCalledTimes(1); + + const firstChunk: GraphQL17Alpha9Handler.SubsequentResult< + Record + > = { + incremental: [ + { + data: { + name: "Leia", + }, + id: "0", + }, + ], + completed: [{ id: "0" }], + hasNext: true, + }; + const resultAfterFirstChunk = structuredClone( + initialResult + ) as ObservableQuery.Result; + resultAfterFirstChunk.data.people.friends[0].name = "Leia"; + + defer.enqueueSubsequentChunk(firstChunk); + + await expect(query1).toEmitTypedValue(resultAfterFirstChunk); + await expect(query2).toEmitTypedValue(resultAfterFirstChunk); + await expect(query3).toEmitTypedValue(resultAfterFirstChunk); + + const query4 = new ObservableStream( + client.watchQuery({ query, fetchPolicy: "network-only" }) + ); + await expect(query4).toEmitTypedValue(resultAfterFirstChunk); + expect(outgoingRequestSpy).toHaveBeenCalledTimes(1); + + const secondChunk: GraphQL17Alpha9Handler.SubsequentResult< + Record + > = { + incremental: [ + { + data: { + name: "Han Solo", + }, + id: "1", + }, + ], + completed: [{ id: "1" }], + hasNext: false, + }; + const resultAfterSecondChunk = { + ...structuredClone(resultAfterFirstChunk), + loading: false, + networkStatus: NetworkStatus.ready, + dataState: "complete", + partial: false, + } as ObservableQuery.Result; + resultAfterSecondChunk.data.people.friends[1].name = "Han Solo"; + + defer.enqueueSubsequentChunk(secondChunk); + + await expect(query1).toEmitTypedValue(resultAfterSecondChunk); + await expect(query2).toEmitTypedValue(resultAfterSecondChunk); + await expect(query3).toEmitTypedValue(resultAfterSecondChunk); + await expect(query4).toEmitTypedValue(resultAfterSecondChunk); + + // TODO: Re-enable once below condition can be met + /* const query5 = */ new ObservableStream( + client.watchQuery({ query, fetchPolicy: "network-only" }) + ); + // TODO: Re-enable once notifyOnNetworkStatusChange controls whether we + // get the loading state. This test fails with the switch to RxJS for now + // since the initial value is emitted synchronously unlike zen-observable + // where the emitted result wasn't emitted until after this assertion. + // expect(query5).not.toEmitAnything(); + expect(outgoingRequestSpy).toHaveBeenCalledTimes(2); +}); From a9c5e7dcc2a7dd9d7287338edd2bad08be740433 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Fri, 5 Sep 2025 18:32:02 -0600 Subject: [PATCH 83/97] Remove defer in tests that don't test defer --- .../ApolloClient/multiple-results.test.ts | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/core/__tests__/ApolloClient/multiple-results.test.ts b/src/core/__tests__/ApolloClient/multiple-results.test.ts index 466e02c920e..1706bb859d7 100644 --- a/src/core/__tests__/ApolloClient/multiple-results.test.ts +++ b/src/core/__tests__/ApolloClient/multiple-results.test.ts @@ -13,7 +13,7 @@ describe("mutiple results", () => { query LazyLoadLuke { people_one(id: 1) { name - friends @defer { + friends { name } } @@ -29,7 +29,6 @@ describe("mutiple results", () => { const laterData = { people_one: { - // XXX true defer's wouldn't send this name: "Luke Skywalker", friends: [{ name: "Leia Skywalker" }], }, @@ -82,7 +81,7 @@ describe("mutiple results", () => { query LazyLoadLuke { people_one(id: 1) { name - friends @defer { + friends { name } } @@ -98,7 +97,6 @@ describe("mutiple results", () => { const laterData = { people_one: { - // XXX true defer's wouldn't send this name: "Luke Skywalker", friends: [{ name: "Leia Skywalker" }], }, @@ -165,7 +163,7 @@ describe("mutiple results", () => { query LazyLoadLuke { people_one(id: 1) { name - friends @defer { + friends { name } } @@ -181,7 +179,6 @@ describe("mutiple results", () => { const laterData = { people_one: { - // XXX true defer's wouldn't send this name: "Luke Skywalker", friends: [{ name: "Leia Skywalker" }], }, @@ -241,7 +238,7 @@ describe("mutiple results", () => { query LazyLoadLuke { people_one(id: 1) { name - friends @defer { + friends { name } } @@ -257,7 +254,6 @@ describe("mutiple results", () => { const laterData = { people_one: { - // XXX true defer's wouldn't send this name: "Luke Skywalker", friends: [{ name: "Leia Skywalker" }], }, @@ -317,7 +313,7 @@ describe("mutiple results", () => { query LazyLoadLuke { people_one(id: 1) { name - friends @defer { + friends { name } } From 8de454669bcdde26bf201837555d5ef60b76a79d Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 8 Sep 2025 10:19:43 -0600 Subject: [PATCH 84/97] Exclude useBackgroundQuery/useLoadableQuery subfolder tests from React 17 --- config/jest.config.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/jest.config.ts b/config/jest.config.ts index bf0997ccec5..4cd48b1fd7c 100644 --- a/config/jest.config.ts +++ b/config/jest.config.ts @@ -51,7 +51,9 @@ const react17TestFileIgnoreList = [ "src/react/hooks/__tests__/useSuspenseQuery.test.tsx", "src/react/hooks/__tests__/useSuspenseQuery/*", "src/react/hooks/__tests__/useBackgroundQuery.test.tsx", + "src/react/hooks/__tests__/useBackgroundQuery/*", "src/react/hooks/__tests__/useLoadableQuery.test.tsx", + "src/react/hooks/__tests__/useLoadableQuery/*", "src/react/hooks/__tests__/useQueryRefHandlers.test.tsx", "src/react/query-preloader/__tests__/createQueryPreloader.test.tsx", "src/react/ssr/__tests__/prerenderStatic.test.tsx", From 7512b6b720696a96d3254b90048a443e27c43175 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 8 Sep 2025 14:47:24 -0600 Subject: [PATCH 85/97] Update test with new test utils --- .../__tests__/graphql17Alpha9/defer.test.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/incremental/handlers/__tests__/graphql17Alpha9/defer.test.ts b/src/incremental/handlers/__tests__/graphql17Alpha9/defer.test.ts index 4462476513e..b797d36a5c1 100644 --- a/src/incremental/handlers/__tests__/graphql17Alpha9/defer.test.ts +++ b/src/incremental/handlers/__tests__/graphql17Alpha9/defer.test.ts @@ -28,7 +28,7 @@ import { import { GraphQL17Alpha9Handler } from "@apollo/client/incremental"; import { markAsStreaming, - mockDefer20220824, + mockDeferStreamGraphQL17Alpha9, ObservableStream, wait, } from "@apollo/client/testing/internal"; @@ -2379,9 +2379,8 @@ test("GraphQL17Alpha9Handler can be used with `ApolloClient`", async () => { }); }); -// TODO: Add test helpers for new format -test.failing("merges cache updates that happen concurrently", async () => { - const stream = mockDefer20220824(); +test("merges cache updates that happen concurrently", async () => { + const stream = mockDeferStreamGraphQL17Alpha9(); const client = new ApolloClient({ link: stream.httpLink, cache: new InMemoryCache(), @@ -2418,6 +2417,7 @@ test.failing("merges cache updates that happen concurrently", async () => { job: "Farmer", }, }, + pending: [{ id: "0", path: ["hero"] }], hasNext: true, }); @@ -2453,9 +2453,10 @@ test.failing("merges cache updates that happen concurrently", async () => { data: { name: "Luke", }, - path: ["hero"], + id: "0", }, ], + completed: [{ id: "0" }], hasNext: false, }); @@ -2650,9 +2651,8 @@ test("stream that returns an error but continues to stream", async () => { }); }); -// TODO: Update to use test utils with updated types -test.skip("handles final chunk of { hasNext: false } correctly in usage with Apollo Client", async () => { - const stream = mockDefer20220824(); +test("handles final chunk of { hasNext: false } correctly in usage with Apollo Client", async () => { + const stream = mockDeferStreamGraphQL17Alpha9(); const client = new ApolloClient({ link: stream.httpLink, cache: new InMemoryCache(), @@ -2675,6 +2675,7 @@ test.skip("handles final chunk of { hasNext: false } correctly in usage with Apo data: { allProducts: [null, null, null], }, + pending: [], errors: [ { message: From 96120c56b80b90c82bd49a8596310391286761a8 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 8 Sep 2025 15:37:13 -0600 Subject: [PATCH 86/97] Update error message --- src/incremental/handlers/graphql17Alpha9.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/incremental/handlers/graphql17Alpha9.ts b/src/incremental/handlers/graphql17Alpha9.ts index 71bce701865..653914918fc 100644 --- a/src/incremental/handlers/graphql17Alpha9.ts +++ b/src/incremental/handlers/graphql17Alpha9.ts @@ -112,7 +112,7 @@ class IncrementalRequest const pending = this.pending.find(({ id }) => incremental.id === id); invariant( pending, - "Could not find pending chunk for incremental value. Please file an issue because this is a bug in Apollo Client." + "Could not find pending chunk for incremental value. Please file an issue for the Apollo Client team to investigate." ); let { data } = incremental; From 0772538413c265184d0d85d32a83f5e7850059b7 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 8 Sep 2025 15:39:06 -0600 Subject: [PATCH 87/97] Use filter instead of indexOf and splice --- src/incremental/handlers/graphql17Alpha9.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/incremental/handlers/graphql17Alpha9.ts b/src/incremental/handlers/graphql17Alpha9.ts index 653914918fc..a014c1b2897 100644 --- a/src/incremental/handlers/graphql17Alpha9.ts +++ b/src/incremental/handlers/graphql17Alpha9.ts @@ -136,8 +136,7 @@ class IncrementalRequest if ("completed" in chunk && chunk.completed) { for (const completed of chunk.completed) { - const index = this.pending.findIndex(({ id }) => id === completed.id); - this.pending.splice(index, 1); + this.pending = this.pending.filter(({ id }) => id !== completed.id); if (completed.errors) { this.errors.push(...completed.errors); From b72deb0a3119bc7448e6af09a01a75e58fd9d6de Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 8 Sep 2025 15:46:31 -0600 Subject: [PATCH 88/97] Add tests for the new format for useMutation --- .../useMutation/deferGraphQL17Alpha9.test.tsx | 388 ++++++++++++++++++ 1 file changed, 388 insertions(+) create mode 100644 src/react/hooks/__tests__/useMutation/deferGraphQL17Alpha9.test.tsx diff --git a/src/react/hooks/__tests__/useMutation/deferGraphQL17Alpha9.test.tsx b/src/react/hooks/__tests__/useMutation/deferGraphQL17Alpha9.test.tsx new file mode 100644 index 00000000000..6cf690be0ef --- /dev/null +++ b/src/react/hooks/__tests__/useMutation/deferGraphQL17Alpha9.test.tsx @@ -0,0 +1,388 @@ +import { + disableActEnvironment, + renderHookToSnapshotStream, +} from "@testing-library/react-render-stream"; +import { gql } from "graphql-tag"; + +import { ApolloClient, CombinedGraphQLErrors } from "@apollo/client"; +import { InMemoryCache } from "@apollo/client/cache"; +import { GraphQL17Alpha9Handler } from "@apollo/client/incremental"; +import { useMutation } from "@apollo/client/react"; +import { + createClientWrapper, + mockDeferStreamGraphQL17Alpha9, + spyOnConsole, +} from "@apollo/client/testing/internal"; + +const CREATE_TODO_ERROR = "Failed to create item"; + +test("resolves a deferred mutation with the full result", async () => { + using _ = spyOnConsole("error"); + const mutation = gql` + mutation createTodo($description: String!, $priority: String) { + createTodo(description: $description, priority: $priority) { + id + ... @defer { + description + priority + } + } + } + `; + const variables = { + description: "Get milk!", + }; + + const { httpLink, enqueueInitialChunk, enqueueSubsequentChunk } = + mockDeferStreamGraphQL17Alpha9(); + + const client = new ApolloClient({ + link: httpLink, + cache: new InMemoryCache(), + incrementalHandler: new GraphQL17Alpha9Handler(), + }); + + using _disabledAct = disableActEnvironment(); + const { takeSnapshot, getCurrentSnapshot } = await renderHookToSnapshotStream( + () => useMutation(mutation), + { wrapper: createClientWrapper(client) } + ); + + { + const [, mutation] = await takeSnapshot(); + + expect(mutation).toStrictEqualTyped({ + data: undefined, + error: undefined, + loading: false, + called: false, + }); + } + + const [mutate] = getCurrentSnapshot(); + + const promise = mutate({ variables }); + + { + const [, mutation] = await takeSnapshot(); + + expect(mutation).toStrictEqualTyped({ + data: undefined, + error: undefined, + loading: true, + called: true, + }); + } + + enqueueInitialChunk({ + data: { + createTodo: { + id: 1, + __typename: "Todo", + }, + }, + pending: [{ id: "0", path: ["createTodo"] }], + hasNext: true, + }); + + await expect(takeSnapshot).not.toRerender(); + + enqueueSubsequentChunk({ + incremental: [ + { + data: { + description: "Get milk!", + priority: "High", + __typename: "Todo", + }, + id: "0", + }, + ], + completed: [{ id: "0" }], + hasNext: false, + }); + + { + const [, mutation] = await takeSnapshot(); + + expect(mutation).toStrictEqualTyped({ + data: { + createTodo: { + id: 1, + description: "Get milk!", + priority: "High", + __typename: "Todo", + }, + }, + error: undefined, + loading: false, + called: true, + }); + } + + await expect(promise).resolves.toStrictEqualTyped({ + data: { + createTodo: { + id: 1, + description: "Get milk!", + priority: "High", + __typename: "Todo", + }, + }, + }); + + expect(console.error).not.toHaveBeenCalled(); +}); + +test("resolves with resulting errors and calls onError callback", async () => { + using _ = spyOnConsole("error"); + const mutation = gql` + mutation createTodo($description: String!, $priority: String) { + createTodo(description: $description, priority: $priority) { + id + ... @defer { + description + priority + } + } + } + `; + const variables = { + description: "Get milk!", + }; + + const { httpLink, enqueueInitialChunk, enqueueSubsequentChunk } = + mockDeferStreamGraphQL17Alpha9(); + + const client = new ApolloClient({ + link: httpLink, + cache: new InMemoryCache(), + incrementalHandler: new GraphQL17Alpha9Handler(), + }); + + const onError = jest.fn(); + using _disabledAct = disableActEnvironment(); + const { takeSnapshot, getCurrentSnapshot } = await renderHookToSnapshotStream( + () => useMutation(mutation, { onError }), + { + wrapper: createClientWrapper(client), + } + ); + + { + const [, result] = await takeSnapshot(); + + expect(result).toStrictEqualTyped({ + data: undefined, + error: undefined, + loading: false, + called: false, + }); + } + + const [createTodo] = getCurrentSnapshot(); + + const promise = createTodo({ variables }); + + { + const [, result] = await takeSnapshot(); + + expect(result).toStrictEqualTyped({ + data: undefined, + error: undefined, + loading: true, + called: true, + }); + } + + enqueueInitialChunk({ + data: { + createTodo: { + id: 1, + __typename: "Todo", + }, + }, + pending: [{ id: "0", path: ["createTodo"] }], + hasNext: true, + }); + + await expect(takeSnapshot).not.toRerender(); + + enqueueSubsequentChunk({ + completed: [{ id: "0", errors: [{ message: CREATE_TODO_ERROR }] }], + hasNext: false, + }); + + await expect(promise).rejects.toThrow( + new CombinedGraphQLErrors({ errors: [{ message: CREATE_TODO_ERROR }] }) + ); + + { + const [, result] = await takeSnapshot(); + + expect(result).toStrictEqualTyped({ + data: undefined, + error: new CombinedGraphQLErrors({ + data: { createTodo: { __typename: "Todo", id: 1 } }, + errors: [{ message: CREATE_TODO_ERROR }], + }), + loading: false, + called: true, + }); + } + + await expect(takeSnapshot).not.toRerender(); + + expect(onError).toHaveBeenCalledTimes(1); + expect(onError).toHaveBeenLastCalledWith( + new CombinedGraphQLErrors({ + data: { createTodo: { __typename: "Todo", id: 1 } }, + errors: [{ message: CREATE_TODO_ERROR }], + }), + expect.anything() + ); + expect(console.error).not.toHaveBeenCalled(); +}); + +test("calls the update function with the final merged result data", async () => { + using _ = spyOnConsole("error"); + const mutation = gql` + mutation createTodo($description: String!, $priority: String) { + createTodo(description: $description, priority: $priority) { + id + ... @defer { + description + priority + } + } + } + `; + const variables = { + description: "Get milk!", + }; + + const { httpLink, enqueueInitialChunk, enqueueSubsequentChunk } = + mockDeferStreamGraphQL17Alpha9(); + const update = jest.fn(); + const client = new ApolloClient({ + link: httpLink, + cache: new InMemoryCache(), + incrementalHandler: new GraphQL17Alpha9Handler(), + }); + + using _disabledAct = disableActEnvironment(); + const { takeSnapshot, getCurrentSnapshot } = await renderHookToSnapshotStream( + () => useMutation(mutation, { update }), + { + wrapper: createClientWrapper(client), + } + ); + + { + const [, result] = await takeSnapshot(); + + expect(result).toStrictEqualTyped({ + data: undefined, + error: undefined, + loading: false, + called: false, + }); + } + + const [createTodo] = getCurrentSnapshot(); + + const promiseReturnedByMutate = createTodo({ variables }); + + { + const [, result] = await takeSnapshot(); + + expect(result).toStrictEqualTyped({ + data: undefined, + error: undefined, + loading: true, + called: true, + }); + } + + enqueueInitialChunk({ + data: { + createTodo: { + id: 1, + __typename: "Todo", + }, + }, + pending: [{ id: "0", path: ["createTodo"] }], + hasNext: true, + }); + + await expect(takeSnapshot).not.toRerender(); + + enqueueSubsequentChunk({ + incremental: [ + { + data: { + description: "Get milk!", + priority: "High", + __typename: "Todo", + }, + id: "0", + }, + ], + completed: [{ id: "0" }], + hasNext: false, + }); + + await expect(promiseReturnedByMutate).resolves.toStrictEqualTyped({ + data: { + createTodo: { + id: 1, + description: "Get milk!", + priority: "High", + __typename: "Todo", + }, + }, + }); + + { + const [, result] = await takeSnapshot(); + + expect(result).toStrictEqualTyped({ + data: { + createTodo: { + id: 1, + description: "Get milk!", + priority: "High", + __typename: "Todo", + }, + }, + error: undefined, + loading: false, + called: true, + }); + } + + await expect(takeSnapshot).not.toRerender(); + + expect(update).toHaveBeenCalledTimes(1); + expect(update).toHaveBeenCalledWith( + // the first item is the cache, which we don't need to make any + // assertions against in this test + expect.anything(), + // second argument is the result + expect.objectContaining({ + data: { + createTodo: { + id: 1, + description: "Get milk!", + priority: "High", + __typename: "Todo", + }, + }, + }), + // third argument is an object containing context and variables + // but we only care about variables here + expect.objectContaining({ variables }) + ); + + expect(console.error).not.toHaveBeenCalled(); +}); From b53ce0d50838a84aa53e855f9d2c14da26a4ed12 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 9 Sep 2025 13:21:29 -0600 Subject: [PATCH 89/97] Remove export/check for hasIncrementalChunks --- .../__tests__/graphql17Alpha9/defer.test.ts | 69 ------------------- src/incremental/handlers/graphql17Alpha9.ts | 3 +- 2 files changed, 1 insertion(+), 71 deletions(-) diff --git a/src/incremental/handlers/__tests__/graphql17Alpha9/defer.test.ts b/src/incremental/handlers/__tests__/graphql17Alpha9/defer.test.ts index b797d36a5c1..b61e7d2d4e7 100644 --- a/src/incremental/handlers/__tests__/graphql17Alpha9/defer.test.ts +++ b/src/incremental/handlers/__tests__/graphql17Alpha9/defer.test.ts @@ -33,11 +33,6 @@ import { wait, } from "@apollo/client/testing/internal"; -import { - hasIncrementalChunks, - // eslint-disable-next-line local-rules/no-relative-imports -} from "../../graphql17Alpha9.js"; - // This is the test setup of the `graphql-js` v17.0.0-alpha.9 release: // https://github.com/graphql/graphql-js/blob/3283f8adf52e77a47f148ff2f30185c8d11ff0f0/src/execution/__tests__/defer-test.ts @@ -247,7 +242,6 @@ describe("graphql-js test cases", () => { assert(!done); assert(handler.isIncrementalResult(chunk)); - expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { hero: { @@ -263,7 +257,6 @@ describe("graphql-js test cases", () => { assert(!done); assert(handler.isIncrementalResult(chunk)); - expect(hasIncrementalChunks(chunk)).toBe(true); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { hero: { @@ -295,7 +288,6 @@ describe("graphql-js test cases", () => { assert(chunk); expect(handler.isIncrementalResult(chunk)).toBe(false); - expect(hasIncrementalChunks(chunk)).toBe(false); }); it.skip("Does not disable defer with null if argument", async () => { @@ -332,7 +324,6 @@ describe("graphql-js test cases", () => { assert(!done); assert(handler.isIncrementalResult(chunk)); - expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: {}, }); @@ -344,7 +335,6 @@ describe("graphql-js test cases", () => { assert(!done); assert(handler.isIncrementalResult(chunk)); - expect(hasIncrementalChunks(chunk)).toBe(true); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { hero: { @@ -384,7 +374,6 @@ describe("graphql-js test cases", () => { assert(!done); assert(handler.isIncrementalResult(chunk)); - expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: {}, }); @@ -396,7 +385,6 @@ describe("graphql-js test cases", () => { assert(!done); assert(handler.isIncrementalResult(chunk)); - expect(hasIncrementalChunks(chunk)).toBe(true); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { hero: { @@ -442,7 +430,6 @@ describe("graphql-js test cases", () => { assert(!done); assert(handler.isIncrementalResult(chunk)); - expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { hero: {}, @@ -456,7 +443,6 @@ describe("graphql-js test cases", () => { assert(!done); assert(handler.isIncrementalResult(chunk)); - expect(hasIncrementalChunks(chunk)).toBe(true); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { hero: { @@ -498,7 +484,6 @@ describe("graphql-js test cases", () => { assert(!done); assert(handler.isIncrementalResult(chunk)); - expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { hero: { @@ -514,7 +499,6 @@ describe("graphql-js test cases", () => { assert(!done); assert(handler.isIncrementalResult(chunk)); - expect(hasIncrementalChunks(chunk)).toBe(true); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { hero: { @@ -554,7 +538,6 @@ describe("graphql-js test cases", () => { assert(!done); assert(handler.isIncrementalResult(chunk)); - expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { hero: {}, @@ -568,7 +551,6 @@ describe("graphql-js test cases", () => { assert(!done); assert(handler.isIncrementalResult(chunk)); - expect(hasIncrementalChunks(chunk)).toBe(true); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { hero: { @@ -604,7 +586,6 @@ describe("graphql-js test cases", () => { assert(!done); assert(handler.isIncrementalResult(chunk)); - expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { hero: {}, @@ -618,7 +599,6 @@ describe("graphql-js test cases", () => { assert(!done); assert(handler.isIncrementalResult(chunk)); - expect(hasIncrementalChunks(chunk)).toBe(true); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { hero: { @@ -657,7 +637,6 @@ describe("graphql-js test cases", () => { assert(!done); assert(handler.isIncrementalResult(chunk)); - expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: {}, }); @@ -669,7 +648,6 @@ describe("graphql-js test cases", () => { assert(!done); assert(handler.isIncrementalResult(chunk)); - expect(hasIncrementalChunks(chunk)).toBe(true); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { hero: { @@ -712,7 +690,6 @@ describe("graphql-js test cases", () => { assert(!done); assert(handler.isIncrementalResult(chunk)); - expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { hero: {}, @@ -726,7 +703,6 @@ describe("graphql-js test cases", () => { assert(!done); assert(handler.isIncrementalResult(chunk)); - expect(hasIncrementalChunks(chunk)).toBe(true); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { hero: { @@ -762,7 +738,6 @@ describe("graphql-js test cases", () => { assert(!done); assert(handler.isIncrementalResult(chunk)); - expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: {}, }); @@ -774,7 +749,6 @@ describe("graphql-js test cases", () => { assert(!done); assert(handler.isIncrementalResult(chunk)); - expect(hasIncrementalChunks(chunk)).toBe(true); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { hero: { @@ -840,7 +814,6 @@ describe("graphql-js test cases", () => { assert(!done); assert(handler.isIncrementalResult(chunk)); - expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: {}, }); @@ -852,7 +825,6 @@ describe("graphql-js test cases", () => { assert(!done); assert(handler.isIncrementalResult(chunk)); - expect(hasIncrementalChunks(chunk)).toBe(true); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { a: { @@ -872,7 +844,6 @@ describe("graphql-js test cases", () => { assert(!done); assert(handler.isIncrementalResult(chunk)); - expect(hasIncrementalChunks(chunk)).toBe(true); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { a: { @@ -943,7 +914,6 @@ describe("graphql-js test cases", () => { assert(!done); assert(handler.isIncrementalResult(chunk)); - expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: {}, }); @@ -955,7 +925,6 @@ describe("graphql-js test cases", () => { assert(!done); assert(handler.isIncrementalResult(chunk)); - expect(hasIncrementalChunks(chunk)).toBe(true); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { a: {}, @@ -971,7 +940,6 @@ describe("graphql-js test cases", () => { assert(!done); assert(handler.isIncrementalResult(chunk)); - expect(hasIncrementalChunks(chunk)).toBe(true); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { a: { @@ -1023,7 +991,6 @@ describe("graphql-js test cases", () => { assert(!done); assert(handler.isIncrementalResult(chunk)); - expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { hero: { @@ -1039,7 +1006,6 @@ describe("graphql-js test cases", () => { assert(!done); assert(handler.isIncrementalResult(chunk)); - expect(hasIncrementalChunks(chunk)).toBe(true); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { hero: { @@ -1099,7 +1065,6 @@ describe("graphql-js test cases", () => { assert(!done); assert(handler.isIncrementalResult(chunk)); - expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { hero: { @@ -1124,7 +1089,6 @@ describe("graphql-js test cases", () => { assert(!done); assert(handler.isIncrementalResult(chunk)); - expect(hasIncrementalChunks(chunk)).toBe(true); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { hero: { @@ -1176,7 +1140,6 @@ describe("graphql-js test cases", () => { assert(!done); assert(handler.isIncrementalResult(chunk)); - expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { hero: {}, @@ -1190,7 +1153,6 @@ describe("graphql-js test cases", () => { assert(!done); assert(handler.isIncrementalResult(chunk)); - expect(hasIncrementalChunks(chunk)).toBe(true); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { hero: { @@ -1257,7 +1219,6 @@ describe("graphql-js test cases", () => { assert(!done); assert(handler.isIncrementalResult(chunk)); - expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { hero: { @@ -1277,7 +1238,6 @@ describe("graphql-js test cases", () => { assert(!done); assert(handler.isIncrementalResult(chunk)); - expect(hasIncrementalChunks(chunk)).toBe(true); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { hero: { @@ -1333,7 +1293,6 @@ describe("graphql-js test cases", () => { assert(!done); assert(handler.isIncrementalResult(chunk)); - expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { hero: { @@ -1351,7 +1310,6 @@ describe("graphql-js test cases", () => { assert(!done); assert(handler.isIncrementalResult(chunk)); - expect(hasIncrementalChunks(chunk)).toBe(true); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { hero: { @@ -1416,7 +1374,6 @@ describe("graphql-js test cases", () => { assert(!done); assert(handler.isIncrementalResult(chunk)); - expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { a: { @@ -1434,7 +1391,6 @@ describe("graphql-js test cases", () => { assert(!done); assert(handler.isIncrementalResult(chunk)); - expect(hasIncrementalChunks(chunk)).toBe(true); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { a: { @@ -1481,7 +1437,6 @@ describe("graphql-js test cases", () => { assert(!done); assert(handler.isIncrementalResult(chunk)); - expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: {}, }); @@ -1493,7 +1448,6 @@ describe("graphql-js test cases", () => { assert(!done); assert(handler.isIncrementalResult(chunk)); - expect(hasIncrementalChunks(chunk)).toBe(true); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { hero: { @@ -1543,7 +1497,6 @@ describe("graphql-js test cases", () => { assert(!done); assert(handler.isIncrementalResult(chunk)); - expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { a: {}, @@ -1557,7 +1510,6 @@ describe("graphql-js test cases", () => { assert(!done); assert(handler.isIncrementalResult(chunk)); - expect(hasIncrementalChunks(chunk)).toBe(true); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { a: { @@ -1617,7 +1569,6 @@ describe("graphql-js test cases", () => { assert(!done); assert(handler.isIncrementalResult(chunk)); - expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { a: {}, @@ -1631,7 +1582,6 @@ describe("graphql-js test cases", () => { assert(!done); assert(handler.isIncrementalResult(chunk)); - expect(hasIncrementalChunks(chunk)).toBe(true); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { a: { @@ -1689,7 +1639,6 @@ describe("graphql-js test cases", () => { assert(!done); assert(handler.isIncrementalResult(chunk)); - expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: {}, }); @@ -1701,7 +1650,6 @@ describe("graphql-js test cases", () => { assert(!done); assert(handler.isIncrementalResult(chunk)); - expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: {}, errors: [ @@ -1764,7 +1712,6 @@ describe("graphql-js test cases", () => { assert(!done); assert(handler.isIncrementalResult(chunk)); - expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: {}, }); @@ -1776,7 +1723,6 @@ describe("graphql-js test cases", () => { assert(!done); assert(handler.isIncrementalResult(chunk)); - expect(hasIncrementalChunks(chunk)).toBe(true); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { a: { @@ -1849,7 +1795,6 @@ describe("graphql-js test cases", () => { assert(!done); assert(handler.isIncrementalResult(chunk)); - expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { a: {}, @@ -1863,7 +1808,6 @@ describe("graphql-js test cases", () => { assert(!done); assert(handler.isIncrementalResult(chunk)); - expect(hasIncrementalChunks(chunk)).toBe(true); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { a: { @@ -1881,7 +1825,6 @@ describe("graphql-js test cases", () => { assert(!done); assert(handler.isIncrementalResult(chunk)); - expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { a: { @@ -1937,7 +1880,6 @@ describe("graphql-js test cases", () => { assert(!done); assert(handler.isIncrementalResult(chunk)); - expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: {}, }); @@ -1949,7 +1891,6 @@ describe("graphql-js test cases", () => { assert(!done); assert(handler.isIncrementalResult(chunk)); - expect(hasIncrementalChunks(chunk)).toBe(true); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { hero: null, @@ -2003,7 +1944,6 @@ describe("graphql-js test cases", () => { assert(!done); assert(handler.isIncrementalResult(chunk)); - expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { hero: { @@ -2019,7 +1959,6 @@ describe("graphql-js test cases", () => { assert(!done); assert(handler.isIncrementalResult(chunk)); - expect(hasIncrementalChunks(chunk)).toBe(true); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { hero: { @@ -2077,7 +2016,6 @@ describe("graphql-js test cases", () => { assert(!done); assert(handler.isIncrementalResult(chunk)); - expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { hero: { @@ -2093,7 +2031,6 @@ describe("graphql-js test cases", () => { assert(!done); assert(handler.isIncrementalResult(chunk)); - expect(hasIncrementalChunks(chunk)).toBe(true); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { hero: { @@ -2139,7 +2076,6 @@ describe("graphql-js test cases", () => { assert(!done); assert(handler.isIncrementalResult(chunk)); - expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { hero: { @@ -2155,7 +2091,6 @@ describe("graphql-js test cases", () => { assert(!done); assert(handler.isIncrementalResult(chunk)); - expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { hero: { @@ -2205,7 +2140,6 @@ describe("graphql-js test cases", () => { assert(!done); assert(handler.isIncrementalResult(chunk)); - expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { hero: { @@ -2221,7 +2155,6 @@ describe("graphql-js test cases", () => { assert(!done); assert(handler.isIncrementalResult(chunk)); - expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { hero: { @@ -2276,7 +2209,6 @@ describe("graphql-js test cases", () => { assert(!done); assert(handler.isIncrementalResult(chunk)); - expect(hasIncrementalChunks(chunk)).toBe(false); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { hero: { @@ -2292,7 +2224,6 @@ describe("graphql-js test cases", () => { assert(!done); assert(handler.isIncrementalResult(chunk)); - expect(hasIncrementalChunks(chunk)).toBe(true); expect(request.handle(undefined, chunk)).toStrictEqualTyped({ data: { hero: { diff --git a/src/incremental/handlers/graphql17Alpha9.ts b/src/incremental/handlers/graphql17Alpha9.ts index a014c1b2897..d43230298dd 100644 --- a/src/incremental/handlers/graphql17Alpha9.ts +++ b/src/incremental/handlers/graphql17Alpha9.ts @@ -229,8 +229,7 @@ export class GraphQL17Alpha9Handler } } -// only exported for use in tests -export function hasIncrementalChunks( +function hasIncrementalChunks( result: Record ): result is Required { return isNonEmptyArray(result.incremental); From c7fba99e16da522fdbc35b9c16cdb8df0dda4c2c Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 9 Sep 2025 22:41:23 -0600 Subject: [PATCH 90/97] Add changeset --- .changeset/little-yaks-decide.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 .changeset/little-yaks-decide.md diff --git a/.changeset/little-yaks-decide.md b/.changeset/little-yaks-decide.md new file mode 100644 index 00000000000..53aa1d9cd75 --- /dev/null +++ b/.changeset/little-yaks-decide.md @@ -0,0 +1,17 @@ +--- +"@apollo/client": minor +--- + +Support the newer incremental delivery format for the `@defer` directive implemented in `graphql@17.0.0-alpha.9`. Import the `GraphQL17Alpha9Handler` to use the newer incremental delivery format with `@defer`. + +```ts +import { GraphQL17Alpha9Handler } from "@apollo/client/incremental"; + +const client = new ApolloClient({ + // ... + incrementalHandler: new GraphQL17Alpha9Handler(), +}); +``` + +> [!NOTE] +> In order to use the `GraphQL17Alpha9Handler`, the GraphQL server MUST implement the newer incremental delivery format. You may see errors or unusual behavior if you use the wrong handler. If you are using Apollo Router, continue to use the `Defer20220824Handler` because Apollo Router does not yet support the newer incremental delivery format. From c4a4228eb4123c19b15bbb5b27de154a7175a093 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 9 Sep 2025 22:49:26 -0600 Subject: [PATCH 91/97] Update size limits --- .size-limits.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.size-limits.json b/.size-limits.json index c2bb067567d..feea5be9df0 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,6 +1,6 @@ { - "import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (CJS)": 44246, - "import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (production) (CJS)": 39057, - "import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\"": 33470, + "import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (CJS)": 44249, + "import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (production) (CJS)": 38998, + "import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\"": 33462, "import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (production)": 27490 } From 8052f24efaa3e9d19524b48839000470922c279e Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 9 Sep 2025 23:45:03 -0600 Subject: [PATCH 92/97] Fix name of test file --- ...eferGraphQL17Alpha2.test.tsx => deferGraphQL17Alpha9.test.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/react/hooks/__tests__/useQuery/{deferGraphQL17Alpha2.test.tsx => deferGraphQL17Alpha9.test.tsx} (100%) diff --git a/src/react/hooks/__tests__/useQuery/deferGraphQL17Alpha2.test.tsx b/src/react/hooks/__tests__/useQuery/deferGraphQL17Alpha9.test.tsx similarity index 100% rename from src/react/hooks/__tests__/useQuery/deferGraphQL17Alpha2.test.tsx rename to src/react/hooks/__tests__/useQuery/deferGraphQL17Alpha9.test.tsx From e0890e606c7663e880fc070c332080e2ac2e68a3 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 9 Sep 2025 23:48:21 -0600 Subject: [PATCH 93/97] Initialize a new DeepMerger each time --- src/incremental/handlers/graphql17Alpha9.ts | 23 +++++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/incremental/handlers/graphql17Alpha9.ts b/src/incremental/handlers/graphql17Alpha9.ts index d43230298dd..6f700bb4345 100644 --- a/src/incremental/handlers/graphql17Alpha9.ts +++ b/src/incremental/handlers/graphql17Alpha9.ts @@ -86,7 +86,6 @@ class IncrementalRequest private errors: GraphQLFormattedError[] = []; private extensions: Record = {}; private pending: GraphQL17Alpha9Handler.PendingResult[] = []; - private merger = new DeepMerger(); handle( cacheData: TData | DeepPartial | null | undefined = this.data, @@ -99,7 +98,7 @@ class IncrementalRequest this.pending.push(...chunk.pending); } - this.merge(chunk); + this.merge(chunk, new DeepMerger()); if (hasIncrementalChunks(chunk)) { for (const incremental of chunk.incremental) { @@ -126,11 +125,14 @@ class IncrementalRequest data = parent as typeof data; } - this.merge({ - data: data as TData, - extensions: incremental.extensions, - errors: incremental.errors, - }); + this.merge( + { + data: data as TData, + extensions: incremental.extensions, + errors: incremental.errors, + }, + new DeepMerger() + ); } } @@ -157,9 +159,12 @@ class IncrementalRequest return result; } - private merge(normalized: FormattedExecutionResult) { + private merge( + normalized: FormattedExecutionResult, + merger: DeepMerger + ) { if (normalized.data !== undefined) { - this.data = this.merger.merge(this.data, normalized.data); + this.data = merger.merge(this.data, normalized.data); } if (normalized.errors) { From d59dd307183e898f320dc6990f93f312316dac0b Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 9 Sep 2025 23:49:10 -0600 Subject: [PATCH 94/97] Fix incorrect assertion for useQuery test due to bug in handler --- .../hooks/__tests__/useQuery/deferGraphQL17Alpha9.test.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/react/hooks/__tests__/useQuery/deferGraphQL17Alpha9.test.tsx b/src/react/hooks/__tests__/useQuery/deferGraphQL17Alpha9.test.tsx index 0e967508933..d70c095de26 100644 --- a/src/react/hooks/__tests__/useQuery/deferGraphQL17Alpha9.test.tsx +++ b/src/react/hooks/__tests__/useQuery/deferGraphQL17Alpha9.test.tsx @@ -522,7 +522,12 @@ test("should handle deferred queries with fetch policy no-cache", async () => { dataState: "complete", loading: false, networkStatus: NetworkStatus.ready, - previousData: undefined, + previousData: { + greeting: { + message: "Hello world", + __typename: "Greeting", + }, + }, variables: {}, }); From 27fc9dc8dd0a333d774c8223b777d112470dd8ac Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 9 Sep 2025 23:55:28 -0600 Subject: [PATCH 95/97] Update size limits --- .size-limits.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.size-limits.json b/.size-limits.json index feea5be9df0..722c44c586a 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,6 +1,6 @@ { - "import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (CJS)": 44249, - "import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (production) (CJS)": 38998, + "import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (CJS)": 44188, + "import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (production) (CJS)": 39024, "import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\"": 33462, "import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (production)": 27490 } From 000cef46602e63d4b4f3a826089c87cad1582083 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 16 Sep 2025 09:56:38 -0600 Subject: [PATCH 96/97] Fix duplicate errors in extractErrors from initial chunk --- src/incremental/handlers/graphql17Alpha9.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/incremental/handlers/graphql17Alpha9.ts b/src/incremental/handlers/graphql17Alpha9.ts index 6f700bb4345..7e4d59af29b 100644 --- a/src/incremental/handlers/graphql17Alpha9.ts +++ b/src/incremental/handlers/graphql17Alpha9.ts @@ -217,10 +217,10 @@ export class GraphQL17Alpha9Handler } }; - push(result); - if (this.isIncrementalResult(result)) { push(new IncrementalRequest().handle(undefined, result)); + } else { + push(result); } if (acc.length) { From 14e5a9862956f24873aee56c873707f1aa1cede6 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 16 Sep 2025 10:01:09 -0600 Subject: [PATCH 97/97] Update size limits --- .size-limits.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.size-limits.json b/.size-limits.json index 722c44c586a..e48d56978cf 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,6 +1,6 @@ { - "import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (CJS)": 44188, - "import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (production) (CJS)": 39024, + "import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (CJS)": 44206, + "import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (production) (CJS)": 39060, "import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\"": 33462, "import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (production)": 27490 }