From e032398dd67ee5a1f9c0a27d371a912b6adac852 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 14 Aug 2024 16:03:11 -0600 Subject: [PATCH 01/12] Add failing test for watchFragment with non-identifiable object --- src/__tests__/ApolloClient.ts | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/__tests__/ApolloClient.ts b/src/__tests__/ApolloClient.ts index bcf9978ef90..cb60a66c6e1 100644 --- a/src/__tests__/ApolloClient.ts +++ b/src/__tests__/ApolloClient.ts @@ -2515,6 +2515,36 @@ describe("ApolloClient", () => { }); } }); + + it("reports complete as false when `from` is not identifiable", async () => { + const cache = new InMemoryCache(); + const client = new ApolloClient({ + cache, + link: ApolloLink.empty(), + }); + const ItemFragment = gql` + fragment ItemFragment on Item { + id + text + } + `; + + const observable = client.watchFragment({ + fragment: ItemFragment, + from: {}, + }); + + const stream = new ObservableStream(observable); + + { + const result = await stream.takeNext(); + + expect(result).toStrictEqual({ + data: {}, + complete: false, + }); + } + }); }); describe("defaultOptions", () => { From 0db86b66a3dc1f1dd457b6a28f95f000a16159a1 Mon Sep 17 00:00:00 2001 From: Ryo Matsukawa <76232929+ryo-manba@users.noreply.github.com> Date: Sat, 1 Feb 2025 12:24:55 +0900 Subject: [PATCH 02/12] Fix `watchFragment` reports `complete=false` when from object is not identifiable --- src/__tests__/ApolloClient.ts | 1 + src/cache/core/cache.ts | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/__tests__/ApolloClient.ts b/src/__tests__/ApolloClient.ts index cb60a66c6e1..2232e674e8f 100644 --- a/src/__tests__/ApolloClient.ts +++ b/src/__tests__/ApolloClient.ts @@ -2542,6 +2542,7 @@ describe("ApolloClient", () => { expect(result).toStrictEqual({ data: {}, complete: false, + missing: "Unable to identify object", }); } }); diff --git a/src/cache/core/cache.ts b/src/cache/core/cache.ts index cb953152c45..7b2e30c44d5 100644 --- a/src/cache/core/cache.ts +++ b/src/cache/core/cache.ts @@ -226,10 +226,23 @@ export abstract class ApolloCache implements DataProxy { } = options; const query = this.getFragmentDoc(fragment, fragmentName); + const id = typeof from === "string" ? from : this.identify(from); + + if (!id) { + return new Observable((observer) => { + observer.next({ + data: {} as DeepPartial, + complete: false, + missing: "Unable to identify object", + }); + return () => {}; + }); + } + const diffOptions: Cache.DiffOptions = { ...otherOptions, returnPartialData: true, - id: typeof from === "string" ? from : this.identify(from), + id, query, optimistic, }; From e7ed70bda502f1ca360b4146ffe728e2fbc57c5a Mon Sep 17 00:00:00 2001 From: Ryo Matsukawa <76232929+ryo-manba@users.noreply.github.com> Date: Sat, 1 Feb 2025 12:36:30 +0900 Subject: [PATCH 03/12] Add changeset --- .changeset/tame-rocks-build.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/tame-rocks-build.md diff --git a/.changeset/tame-rocks-build.md b/.changeset/tame-rocks-build.md new file mode 100644 index 00000000000..37cd3c47621 --- /dev/null +++ b/.changeset/tame-rocks-build.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +Fix `watchFragment` reports `complete=false` when from object is not identifiable From 0e84efda73b442f156690b984724ca24462224c0 Mon Sep 17 00:00:00 2001 From: Ryo Matsukawa <76232929+ryo-manba@users.noreply.github.com> Date: Sat, 8 Feb 2025 16:52:12 +0900 Subject: [PATCH 04/12] POC: fix watchFragment complete status --- src/__tests__/ApolloClient.ts | 115 +++++++++++++++++++++++++++- src/cache/core/cache.ts | 12 --- src/cache/inmemory/readFromStore.ts | 33 ++++++++ 3 files changed, 146 insertions(+), 14 deletions(-) diff --git a/src/__tests__/ApolloClient.ts b/src/__tests__/ApolloClient.ts index 2232e674e8f..a4f264233df 100644 --- a/src/__tests__/ApolloClient.ts +++ b/src/__tests__/ApolloClient.ts @@ -12,7 +12,7 @@ import { Kind } from "graphql"; import { Observable } from "../utilities"; import { ApolloLink } from "../link/core"; import { HttpLink } from "../link/http"; -import { InMemoryCache } from "../cache"; +import { InMemoryCache, MissingFieldError } from "../cache"; import { itAsync } from "../testing"; import { ObservableStream, spyOnConsole } from "../testing/internal"; import { TypedDocumentNode } from "@graphql-typed-document-node/core"; @@ -2542,10 +2542,121 @@ describe("ApolloClient", () => { expect(result).toStrictEqual({ data: {}, complete: false, - missing: "Unable to identify object", + missing: { + id: "Can't find field 'id' on ROOT_QUERY object", + text: "Can't find field 'text' on ROOT_QUERY object", + }, }); } }); + it.only("reports diffs correctly when using getFragmentDoc", async () => { + const cache = new InMemoryCache(); + + const diffWithFragment = cache.diff({ + query: cache["getFragmentDoc"](gql` + fragment FooFragment on Foo { + foo + } + `), + id: cache.identify({}), + returnPartialData: true, + optimistic: true, + }); + + expect(diffWithFragment).toStrictEqual({ + result: {}, + complete: false, + missing: [ + new MissingFieldError( + "Can't find field 'foo' on ROOT_QUERY object", + expect.anything(), // query + expect.anything() // variables + ), + ], + }); + + await new Promise((res) => { + cache.watch({ + query: cache["getFragmentDoc"](gql` + fragment FooFragment on Foo { + foo + } + `), + id: cache.identify({}), + returnPartialData: true, + immediate: true, + optimistic: true, + callback: (diff) => { + expect(diff).toStrictEqual({ + result: {}, + complete: false, + missing: [ + new MissingFieldError( + "Can't find field 'foo' on ROOT_QUERY object", + expect.anything(), // query + expect.anything() // variables + ), + ], + }); + res(void 0); + }, + }); + }); + }); + it("reports diffs correctly when not using getFragmentDoc", async () => { + const cache = new InMemoryCache(); + + const diffWithoutFragment = cache.diff({ + query: gql` + query { + foo + } + `, + id: cache.identify({}), + returnPartialData: true, + optimistic: true, + }); + + expect(diffWithoutFragment).toStrictEqual({ + result: {}, + complete: false, + missing: [ + new MissingFieldError( + "Can't find field 'foo' on ROOT_QUERY object", + expect.anything(), // query + expect.anything() // variables + ), + ], + }); + + await new Promise((res) => { + cache.watch({ + query: gql` + query { + foo + } + `, + id: cache.identify({}), + returnPartialData: true, + immediate: true, + optimistic: true, + callback: (diff) => { + expect(diff).toStrictEqual({ + result: {}, + complete: false, + missing: [ + new MissingFieldError( + "Can't find field 'foo' on ROOT_QUERY object", + expect.anything(), // query + expect.anything() // variables + ), + ], + }); + res(void 0); + }, + }); + }); + }); }); describe("defaultOptions", () => { diff --git a/src/cache/core/cache.ts b/src/cache/core/cache.ts index 7b2e30c44d5..c11156c5d30 100644 --- a/src/cache/core/cache.ts +++ b/src/cache/core/cache.ts @@ -227,18 +227,6 @@ export abstract class ApolloCache implements DataProxy { const query = this.getFragmentDoc(fragment, fragmentName); const id = typeof from === "string" ? from : this.identify(from); - - if (!id) { - return new Observable((observer) => { - observer.next({ - data: {} as DeepPartial, - complete: false, - missing: "Unable to identify object", - }); - return () => {}; - }); - } - const diffOptions: Cache.DiffOptions = { ...otherOptions, returnPartialData: true, diff --git a/src/cache/inmemory/readFromStore.ts b/src/cache/inmemory/readFromStore.ts index d89876d85c9..6d24e7da776 100644 --- a/src/cache/inmemory/readFromStore.ts +++ b/src/cache/inmemory/readFromStore.ts @@ -449,6 +449,39 @@ export class StoreReader { if (!fragment && selection.kind === Kind.FRAGMENT_SPREAD) { throw newInvariantError(`No fragment named %s`, selection.name.value); } + if (fragment) { + fragment.selectionSet.selections.forEach((subSelection) => { + if (isField(subSelection)) { + let subFieldValue = policies.readField( + { + fieldName: subSelection.name.value, + field: subSelection, + variables: context.variables, + from: objectOrReference, + }, + context + ); + + const subResultName = resultKeyNameFromField(subSelection); + + if (subFieldValue === void 0) { + missing = missingMerger.merge(missing, { + [subResultName]: `Can't find field '${ + subSelection.name.value + }' on ${ + isReference(objectOrReference) ? + objectOrReference.__ref + " object" + : "object " + JSON.stringify(objectOrReference, null, 2) + }`, + }); + } + + if (subFieldValue !== void 0) { + objectsToMerge.push({ [subResultName]: subFieldValue }); + } + } + }); + } if (fragment && policies.fragmentMatches(fragment, typename)) { fragment.selectionSet.selections.forEach(workSet.add, workSet); From aecd898e75b40a7ff15fb3515213e7d7fe15f89e Mon Sep 17 00:00:00 2001 From: Ryo Matsukawa <76232929+ryo-manba@users.noreply.github.com> Date: Sat, 8 Feb 2025 16:52:57 +0900 Subject: [PATCH 05/12] tweak --- src/cache/core/cache.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/cache/core/cache.ts b/src/cache/core/cache.ts index c11156c5d30..cb953152c45 100644 --- a/src/cache/core/cache.ts +++ b/src/cache/core/cache.ts @@ -226,11 +226,10 @@ export abstract class ApolloCache implements DataProxy { } = options; const query = this.getFragmentDoc(fragment, fragmentName); - const id = typeof from === "string" ? from : this.identify(from); const diffOptions: Cache.DiffOptions = { ...otherOptions, returnPartialData: true, - id, + id: typeof from === "string" ? from : this.identify(from), query, optimistic, }; From 6dc4c34487d2eedc7961425ee32a5e9de6cef78f Mon Sep 17 00:00:00 2001 From: Ryo Matsukawa <76232929+ryo-manba@users.noreply.github.com> Date: Sat, 8 Feb 2025 16:58:23 +0900 Subject: [PATCH 06/12] fix: execute only with `FRAGMENT_SPREAD` --- src/__tests__/ApolloClient.ts | 4 +++- src/cache/inmemory/readFromStore.ts | 11 +++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/__tests__/ApolloClient.ts b/src/__tests__/ApolloClient.ts index a4f264233df..2f0dc097b6e 100644 --- a/src/__tests__/ApolloClient.ts +++ b/src/__tests__/ApolloClient.ts @@ -2549,7 +2549,8 @@ describe("ApolloClient", () => { }); } }); - it.only("reports diffs correctly when using getFragmentDoc", async () => { + + it("reports diffs correctly when using getFragmentDoc", async () => { const cache = new InMemoryCache(); const diffWithFragment = cache.diff({ @@ -2603,6 +2604,7 @@ describe("ApolloClient", () => { }); }); }); + it("reports diffs correctly when not using getFragmentDoc", async () => { const cache = new InMemoryCache(); diff --git a/src/cache/inmemory/readFromStore.ts b/src/cache/inmemory/readFromStore.ts index 6d24e7da776..6f1ef5046bc 100644 --- a/src/cache/inmemory/readFromStore.ts +++ b/src/cache/inmemory/readFromStore.ts @@ -446,10 +446,13 @@ export class StoreReader { context.lookupFragment ); - if (!fragment && selection.kind === Kind.FRAGMENT_SPREAD) { - throw newInvariantError(`No fragment named %s`, selection.name.value); - } - if (fragment) { + if (selection.kind === Kind.FRAGMENT_SPREAD) { + if (!fragment) { + throw newInvariantError( + `No fragment named %s`, + selection.name.value + ); + } fragment.selectionSet.selections.forEach((subSelection) => { if (isField(subSelection)) { let subFieldValue = policies.readField( From abb86ec409442709d041ee1307b8472e1279532f Mon Sep 17 00:00:00 2001 From: Ryo Matsukawa <76232929+ryo-manba@users.noreply.github.com> Date: Sun, 13 Jul 2025 17:48:56 +0900 Subject: [PATCH 07/12] fix: watchFragment complete status for non-identifiable objects --- src/__tests__/ApolloClient.ts | 9 +++---- src/cache/inmemory/inMemoryCache.ts | 26 +++++++++++++++++++ src/cache/inmemory/readFromStore.ts | 40 ++--------------------------- 3 files changed, 31 insertions(+), 44 deletions(-) diff --git a/src/__tests__/ApolloClient.ts b/src/__tests__/ApolloClient.ts index 2f0dc097b6e..211f7617fcc 100644 --- a/src/__tests__/ApolloClient.ts +++ b/src/__tests__/ApolloClient.ts @@ -2542,10 +2542,7 @@ describe("ApolloClient", () => { expect(result).toStrictEqual({ data: {}, complete: false, - missing: { - id: "Can't find field 'id' on ROOT_QUERY object", - text: "Can't find field 'text' on ROOT_QUERY object", - }, + missing: "Can't determine completeness for fragment query on non-identifiable object", }); } }); @@ -2569,7 +2566,7 @@ describe("ApolloClient", () => { complete: false, missing: [ new MissingFieldError( - "Can't find field 'foo' on ROOT_QUERY object", + "Can't determine completeness for fragment query on non-identifiable object", expect.anything(), // query expect.anything() // variables ), @@ -2593,7 +2590,7 @@ describe("ApolloClient", () => { complete: false, missing: [ new MissingFieldError( - "Can't find field 'foo' on ROOT_QUERY object", + "Can't determine completeness for fragment query on non-identifiable object", expect.anything(), // query expect.anything() // variables ), diff --git a/src/cache/inmemory/inMemoryCache.ts b/src/cache/inmemory/inMemoryCache.ts index fe62023f165..f69ebb030bf 100644 --- a/src/cache/inmemory/inMemoryCache.ts +++ b/src/cache/inmemory/inMemoryCache.ts @@ -20,6 +20,7 @@ import { print, cacheSizes, defaultCacheSizes, + getMainDefinition, } from "../../utilities/index.js"; import type { InMemoryCacheConfig, NormalizedCacheObject } from "./types.js"; import { StoreReader } from "./readFromStore.js"; @@ -252,6 +253,31 @@ export class InMemoryCache extends ApolloCache { public diff( options: Cache.DiffOptions ): Cache.DiffResult { + // Detect non-identifiable objects with fragment queries + // Only apply this fix when: + // 1. options.id is explicitly undefined (not just missing) + // 2. The query is a pure fragment query (not a named query with fragments) + if (options.id === undefined && hasOwn.call(options, 'id')) { + const mainDef = getMainDefinition(options.query); + // Check if this is a pure fragment query (operation without name, only fragment spreads) + if (mainDef.kind === 'OperationDefinition' && + !mainDef.name && + mainDef.selectionSet.selections.every(sel => sel.kind === "FragmentSpread")) { + return { + result: {} as TData, + complete: false, + missing: [ + new MissingFieldError( + "Can't determine completeness for fragment query on non-identifiable object", + "Can't determine completeness for fragment query on non-identifiable object", + options.query, + options.variables + ) + ] + }; + } + } + return this.storeReader.diffQueryAgainstStore({ ...options, store: options.optimistic ? this.optimisticData : this.data, diff --git a/src/cache/inmemory/readFromStore.ts b/src/cache/inmemory/readFromStore.ts index 6f1ef5046bc..d89876d85c9 100644 --- a/src/cache/inmemory/readFromStore.ts +++ b/src/cache/inmemory/readFromStore.ts @@ -446,44 +446,8 @@ export class StoreReader { context.lookupFragment ); - if (selection.kind === Kind.FRAGMENT_SPREAD) { - if (!fragment) { - throw newInvariantError( - `No fragment named %s`, - selection.name.value - ); - } - fragment.selectionSet.selections.forEach((subSelection) => { - if (isField(subSelection)) { - let subFieldValue = policies.readField( - { - fieldName: subSelection.name.value, - field: subSelection, - variables: context.variables, - from: objectOrReference, - }, - context - ); - - const subResultName = resultKeyNameFromField(subSelection); - - if (subFieldValue === void 0) { - missing = missingMerger.merge(missing, { - [subResultName]: `Can't find field '${ - subSelection.name.value - }' on ${ - isReference(objectOrReference) ? - objectOrReference.__ref + " object" - : "object " + JSON.stringify(objectOrReference, null, 2) - }`, - }); - } - - if (subFieldValue !== void 0) { - objectsToMerge.push({ [subResultName]: subFieldValue }); - } - } - }); + if (!fragment && selection.kind === Kind.FRAGMENT_SPREAD) { + throw newInvariantError(`No fragment named %s`, selection.name.value); } if (fragment && policies.fragmentMatches(fragment, typename)) { From f4a7fa53f8de9f7f98c859d8aacf3759e9d01d73 Mon Sep 17 00:00:00 2001 From: Ryo Matsukawa <76232929+ryo-manba@users.noreply.github.com> Date: Sun, 13 Jul 2025 18:02:26 +0900 Subject: [PATCH 08/12] add test --- src/__tests__/ApolloClient.ts | 141 ++++++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) diff --git a/src/__tests__/ApolloClient.ts b/src/__tests__/ApolloClient.ts index 5bfb902bb81..6ac894255fa 100644 --- a/src/__tests__/ApolloClient.ts +++ b/src/__tests__/ApolloClient.ts @@ -2647,6 +2647,147 @@ describe("ApolloClient", () => { }); } }); + + it("reports complete as false when `from` is not identifiable", async () => { + const cache = new InMemoryCache(); + const client = new ApolloClient({ + cache, + link: ApolloLink.empty(), + }); + const ItemFragment = gql` + fragment ItemFragment on Item { + id + text + } + `; + + const observable = client.watchFragment({ + fragment: ItemFragment, + from: {}, + }); + + const stream = new ObservableStream(observable); + + { + const result = await stream.takeNext(); + + expect(result).toStrictEqual({ + data: {}, + complete: false, + missing: "Can't determine completeness for fragment query on non-identifiable object", + }); + } + }); + + it("reports diffs correctly when using getFragmentDoc", async () => { + const cache = new InMemoryCache(); + + const diffWithFragment = cache.diff({ + query: cache["getFragmentDoc"](gql` + fragment FooFragment on Foo { + foo + } + `), + id: cache.identify({}), + returnPartialData: true, + optimistic: true, + }); + + expect(diffWithFragment).toStrictEqual({ + result: {}, + complete: false, + missing: [ + new MissingFieldError( + "Can't determine completeness for fragment query on non-identifiable object", + expect.anything(), // query + expect.anything() // variables + ), + ], + }); + + await new Promise((res) => { + cache.watch({ + query: cache["getFragmentDoc"](gql` + fragment FooFragment on Foo { + foo + } + `), + id: cache.identify({}), + returnPartialData: true, + immediate: true, + optimistic: true, + callback: (diff) => { + expect(diff).toStrictEqual({ + result: {}, + complete: false, + missing: [ + new MissingFieldError( + "Can't determine completeness for fragment query on non-identifiable object", + expect.anything(), // query + expect.anything() // variables + ), + ], + }); + res(void 0); + }, + }); + }); + }); + + it("reports diffs correctly when not using getFragmentDoc", async () => { + const cache = new InMemoryCache(); + + const diffWithoutFragment = cache.diff({ + query: gql` + query { + foo + } + `, + id: cache.identify({}), + returnPartialData: true, + optimistic: true, + }); + + expect(diffWithoutFragment).toStrictEqual({ + result: {}, + complete: false, + missing: [ + new MissingFieldError( + "Can't find field 'foo' on ROOT_QUERY object", + expect.anything(), // query + expect.anything() // variables + ), + ], + }); + + await new Promise((res) => { + cache.watch({ + query: gql` + query { + foo + } + `, + id: cache.identify({}), + returnPartialData: true, + immediate: true, + optimistic: true, + callback: (diff) => { + expect(diff).toStrictEqual({ + result: {}, + complete: false, + missing: [ + new MissingFieldError( + "Can't find field 'foo' on ROOT_QUERY object", + expect.anything(), // query + expect.anything() // variables + ), + ], + }); + res(void 0); + }, + }); + }); + }); }); describe("defaultOptions", () => { From 90a53ab1a174a29e9ddffb22d5de129d086c1d90 Mon Sep 17 00:00:00 2001 From: Ryo Matsukawa <76232929+ryo-manba@users.noreply.github.com> Date: Sun, 13 Jul 2025 18:09:36 +0900 Subject: [PATCH 09/12] run format --- src/__tests__/ApolloClient.ts | 9 +++++++-- src/cache/inmemory/inMemoryCache.ts | 16 ++++++++++------ 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/__tests__/ApolloClient.ts b/src/__tests__/ApolloClient.ts index 6ac894255fa..2e4d9e0e5e2 100644 --- a/src/__tests__/ApolloClient.ts +++ b/src/__tests__/ApolloClient.ts @@ -14,7 +14,11 @@ import { Kind } from "graphql"; import { DeepPartial, Observable } from "../utilities"; import { ApolloLink, FetchResult } from "../link/core"; import { HttpLink } from "../link/http"; -import { createFragmentRegistry, InMemoryCache, MissingFieldError } from "../cache"; +import { + createFragmentRegistry, + InMemoryCache, + MissingFieldError, +} from "../cache"; import { ObservableStream, spyOnConsole } from "../testing/internal"; import { TypedDocumentNode } from "@graphql-typed-document-node/core"; import { invariant } from "../utilities/globals"; @@ -2674,7 +2678,8 @@ describe("ApolloClient", () => { expect(result).toStrictEqual({ data: {}, complete: false, - missing: "Can't determine completeness for fragment query on non-identifiable object", + missing: + "Can't determine completeness for fragment query on non-identifiable object", }); } }); diff --git a/src/cache/inmemory/inMemoryCache.ts b/src/cache/inmemory/inMemoryCache.ts index d1f552e55e5..08646b57eeb 100644 --- a/src/cache/inmemory/inMemoryCache.ts +++ b/src/cache/inmemory/inMemoryCache.ts @@ -261,12 +261,16 @@ export class InMemoryCache extends ApolloCache { // Only apply this fix when: // 1. options.id is explicitly undefined (not just missing) // 2. The query is a pure fragment query (not a named query with fragments) - if (options.id === undefined && hasOwn.call(options, 'id')) { + if (options.id === undefined && hasOwn.call(options, "id")) { const mainDef = getMainDefinition(options.query); // Check if this is a pure fragment query (operation without name, only fragment spreads) - if (mainDef.kind === 'OperationDefinition' && - !mainDef.name && - mainDef.selectionSet.selections.every(sel => sel.kind === "FragmentSpread")) { + if ( + mainDef.kind === "OperationDefinition" && + !mainDef.name && + mainDef.selectionSet.selections.every( + (sel) => sel.kind === "FragmentSpread" + ) + ) { return { result: {} as TData, complete: false, @@ -276,8 +280,8 @@ export class InMemoryCache extends ApolloCache { "Can't determine completeness for fragment query on non-identifiable object", options.query, options.variables - ) - ] + ), + ], }; } } From 5d89b88e6e79a6cae5fe0aa7b21b656b15236083 Mon Sep 17 00:00:00 2001 From: Ryo Matsukawa <76232929+ryo-manba@users.noreply.github.com> Date: Sun, 13 Jul 2025 18:16:17 +0900 Subject: [PATCH 10/12] fix: update tests for Issue #12003 fragment complete status --- src/__tests__/dataMasking.ts | 7 +++---- src/react/hooks/__tests__/useFragment.test.tsx | 3 +-- src/react/hooks/__tests__/useSuspenseFragment.test.tsx | 3 +-- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/__tests__/dataMasking.ts b/src/__tests__/dataMasking.ts index 8c386b67e6f..d3a2c5db6e5 100644 --- a/src/__tests__/dataMasking.ts +++ b/src/__tests__/dataMasking.ts @@ -1360,8 +1360,7 @@ describe("client.watchQuery", () => { const { data, complete } = await fragmentStream.takeNext(); expect(data).toEqual({}); - // TODO: Update when https://github.com/apollographql/apollo-client/issues/12003 is fixed - expect(complete).toBe(true); + expect(complete).toBe(false); } }); @@ -1437,8 +1436,8 @@ describe("client.watchQuery", () => { const { data, complete } = await fragmentStream.takeNext(); expect(data).toEqual({}); - // TODO: Update when https://github.com/apollographql/apollo-client/issues/12003 is fixed - expect(complete).toBe(true); + // Fixed: Issue #12003 - now correctly returns complete: false for non-identifiable objects + expect(complete).toBe(false); } }); diff --git a/src/react/hooks/__tests__/useFragment.test.tsx b/src/react/hooks/__tests__/useFragment.test.tsx index 3884c22338c..a5013b2990f 100644 --- a/src/react/hooks/__tests__/useFragment.test.tsx +++ b/src/react/hooks/__tests__/useFragment.test.tsx @@ -1639,8 +1639,7 @@ describe("useFragment", () => { const { data, complete } = await takeSnapshot(); expect(data).toEqual({}); - // TODO: Update when https://github.com/apollographql/apollo-client/issues/12003 is fixed - expect(complete).toBe(true); + expect(complete).toBe(false); } expect(console.warn).toHaveBeenCalledTimes(1); diff --git a/src/react/hooks/__tests__/useSuspenseFragment.test.tsx b/src/react/hooks/__tests__/useSuspenseFragment.test.tsx index d24d1804348..91b9b0dbdc3 100644 --- a/src/react/hooks/__tests__/useSuspenseFragment.test.tsx +++ b/src/react/hooks/__tests__/useSuspenseFragment.test.tsx @@ -870,8 +870,7 @@ it("does not rerender when fields with @nonreactive on nested fragment change", await expect(takeSnapshot).not.toRerender(); }); -// TODO: Update when https://github.com/apollographql/apollo-client/issues/12003 is fixed -it.failing( +it( "warns and suspends when passing parent object to `from` when key fields are missing", async () => { using _ = spyOnConsole("warn"); From f3faf77be02739d6e552de2c024c8d39ecc2871c Mon Sep 17 00:00:00 2001 From: Ryo Matsukawa <76232929+ryo-manba@users.noreply.github.com> Date: Sun, 13 Jul 2025 18:22:16 +0900 Subject: [PATCH 11/12] run format --- .../__tests__/useSuspenseFragment.test.tsx | 85 +++++++++---------- 1 file changed, 41 insertions(+), 44 deletions(-) diff --git a/src/react/hooks/__tests__/useSuspenseFragment.test.tsx b/src/react/hooks/__tests__/useSuspenseFragment.test.tsx index 91b9b0dbdc3..6d572779afe 100644 --- a/src/react/hooks/__tests__/useSuspenseFragment.test.tsx +++ b/src/react/hooks/__tests__/useSuspenseFragment.test.tsx @@ -870,63 +870,60 @@ it("does not rerender when fields with @nonreactive on nested fragment change", await expect(takeSnapshot).not.toRerender(); }); -it( - "warns and suspends when passing parent object to `from` when key fields are missing", - async () => { - using _ = spyOnConsole("warn"); +it("warns and suspends when passing parent object to `from` when key fields are missing", async () => { + using _ = spyOnConsole("warn"); - interface Fragment { - age: number; + interface Fragment { + age: number; + } + + const fragment: TypedDocumentNode = gql` + fragment UserFields on User { + age } + `; - const fragment: TypedDocumentNode = gql` - fragment UserFields on User { - age - } - `; + const client = new ApolloClient({ cache: new InMemoryCache() }); - const client = new ApolloClient({ cache: new InMemoryCache() }); + const { replaceSnapshot, render, takeRender } = + createDefaultRenderStream(); + const { SuspenseFallback } = createDefaultTrackedComponents(); - const { replaceSnapshot, render, takeRender } = - createDefaultRenderStream(); - const { SuspenseFallback } = createDefaultTrackedComponents(); + function App() { + const result = useSuspenseFragment({ + fragment, + from: { __typename: "User" }, + }); - function App() { - const result = useSuspenseFragment({ - fragment, - from: { __typename: "User" }, - }); + replaceSnapshot({ result }); - replaceSnapshot({ result }); + return null; + } - return null; + using _disabledAct = disableActEnvironment(); + await render( + }> + + , + { + wrapper: ({ children }) => ( + {children} + ), } + ); - using _disabledAct = disableActEnvironment(); - await render( - }> - - , - { - wrapper: ({ children }) => ( - {children} - ), - } - ); - - expect(console.warn).toHaveBeenCalledTimes(1); - expect(console.warn).toHaveBeenCalledWith( - "Could not identify object passed to `from` for '%s' fragment, either because the object is non-normalized or the key fields are missing. If you are masking this object, please ensure the key fields are requested by the parent object.", - "UserFields" - ); + expect(console.warn).toHaveBeenCalledTimes(1); + expect(console.warn).toHaveBeenCalledWith( + "Could not identify object passed to `from` for '%s' fragment, either because the object is non-normalized or the key fields are missing. If you are masking this object, please ensure the key fields are requested by the parent object.", + "UserFields" + ); - { - const { renderedComponents } = await takeRender(); + { + const { renderedComponents } = await takeRender(); - expect(renderedComponents).toStrictEqual([SuspenseFallback]); - } + expect(renderedComponents).toStrictEqual([SuspenseFallback]); } -); +}); test("returns null if `from` is `null`", async () => { interface ItemFragment { From 739b62f50914a1b90b729102a71296ece3f5ab8a Mon Sep 17 00:00:00 2001 From: Ryo Matsukawa <76232929+ryo-manba@users.noreply.github.com> Date: Sun, 13 Jul 2025 18:55:07 +0900 Subject: [PATCH 12/12] tweak --- src/__tests__/dataMasking.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/__tests__/dataMasking.ts b/src/__tests__/dataMasking.ts index d3a2c5db6e5..4c1c150ce0c 100644 --- a/src/__tests__/dataMasking.ts +++ b/src/__tests__/dataMasking.ts @@ -1436,7 +1436,6 @@ describe("client.watchQuery", () => { const { data, complete } = await fragmentStream.takeNext(); expect(data).toEqual({}); - // Fixed: Issue #12003 - now correctly returns complete: false for non-identifiable objects expect(complete).toBe(false); } });