From d57429df336412bfdce5fc92b8299360c522d121 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 14 Jan 2025 10:07:42 -0700 Subject: [PATCH 1/9] Leave `TData` alone in `Unmasked` if it does not contain fragment refs (#12267) --- .api-reports/api-report-cache.api.md | 6 ++-- .api-reports/api-report-core.api.md | 6 ++-- .api-reports/api-report-masking.api.md | 6 ++-- .api-reports/api-report-react.api.md | 6 ++-- .../api-report-react_components.api.md | 6 ++-- .api-reports/api-report-react_context.api.md | 6 ++-- .api-reports/api-report-react_hoc.api.md | 6 ++-- .api-reports/api-report-react_hooks.api.md | 6 ++-- .api-reports/api-report-react_internal.api.md | 6 ++-- .api-reports/api-report-react_ssr.api.md | 6 ++-- .api-reports/api-report-testing.api.md | 6 ++-- .api-reports/api-report-testing_core.api.md | 6 ++-- .api-reports/api-report-utilities.api.md | 6 ++-- .api-reports/api-report.api.md | 6 ++-- .changeset/beige-eggs-nail.md | 5 ++++ src/masking/__benches__/types.bench.ts | 30 +++++++++---------- src/masking/types.ts | 7 +++-- 17 files changed, 66 insertions(+), 60 deletions(-) create mode 100644 .changeset/beige-eggs-nail.md diff --git a/.api-reports/api-report-cache.api.md b/.api-reports/api-report-cache.api.md index 6c52897efbb..359812c3f7b 100644 --- a/.api-reports/api-report-cache.api.md +++ b/.api-reports/api-report-cache.api.md @@ -766,14 +766,13 @@ export function makeVar(value: T): ReactiveVar; // Warning: (ae-forgotten-export) The symbol "DataMasking" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "RemoveMaskedMarker" needs to be exported by the entry point index.d.ts -// Warning: (ae-forgotten-export) The symbol "ContainsFragmentsRefs" needs to be exported by the entry point index.d.ts // // @public type MaybeMasked = DataMasking extends { mode: "unmask"; } ? TData extends any ? true extends IsAny ? TData : TData extends { __masked?: true; -} ? Prettify> : true extends ContainsFragmentsRefs ? Unmasked : TData : never : DataMasking extends { +} ? Prettify> : Unmasked : never : DataMasking extends { mode: "preserveTypes"; } ? TData : TData; @@ -1113,11 +1112,12 @@ export type TypePolicy = { // @public (undocumented) type unionToIntersection = (T extends unknown ? (x: T) => unknown : never) extends ((x: infer U) => unknown) ? U : never; +// Warning: (ae-forgotten-export) The symbol "ContainsFragmentsRefs" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "UnwrapFragmentRefs" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "RemoveFragmentName" needs to be exported by the entry point index.d.ts // // @public -type Unmasked = true extends IsAny ? TData : TData extends object ? UnwrapFragmentRefs>> : TData; +type Unmasked = true extends IsAny ? TData : TData extends object ? true extends ContainsFragmentsRefs ? UnwrapFragmentRefs>> : TData : TData; // @public (undocumented) type UnwrapFragmentRefs = true extends IsAny ? TData : TData extends any ? string extends keyof TData ? TData : keyof TData extends never ? TData : TData extends { diff --git a/.api-reports/api-report-core.api.md b/.api-reports/api-report-core.api.md index f1d74960f19..8047e356cd6 100644 --- a/.api-reports/api-report-core.api.md +++ b/.api-reports/api-report-core.api.md @@ -1409,14 +1409,13 @@ interface MaskOperationOptions { type MaybeAsync = T | PromiseLike; // Warning: (ae-forgotten-export) The symbol "RemoveMaskedMarker" needs to be exported by the entry point index.d.ts -// Warning: (ae-forgotten-export) The symbol "ContainsFragmentsRefs" needs to be exported by the entry point index.d.ts // // @public export type MaybeMasked = DataMasking extends { mode: "unmask"; } ? TData extends any ? true extends IsAny ? TData : TData extends { __masked?: true; -} ? Prettify> : true extends ContainsFragmentsRefs ? Unmasked : TData : never : DataMasking extends { +} ? Prettify> : Unmasked : never : DataMasking extends { mode: "preserveTypes"; } ? TData : TData; @@ -2410,11 +2409,12 @@ type UnionToIntersection = (U extends any ? (k: U) => void : never) extends ( // @public (undocumented) type unionToIntersection = (T extends unknown ? (x: T) => unknown : never) extends ((x: infer U) => unknown) ? U : never; +// Warning: (ae-forgotten-export) The symbol "ContainsFragmentsRefs" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "UnwrapFragmentRefs" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "RemoveFragmentName" needs to be exported by the entry point index.d.ts // // @public -export type Unmasked = true extends IsAny ? TData : TData extends object ? UnwrapFragmentRefs>> : TData; +export type Unmasked = true extends IsAny ? TData : TData extends object ? true extends ContainsFragmentsRefs ? UnwrapFragmentRefs>> : TData : TData; // @public (undocumented) type UnwrapFragmentRefs = true extends IsAny ? TData : TData extends any ? string extends keyof TData ? TData : keyof TData extends never ? TData : TData extends { diff --git a/.api-reports/api-report-masking.api.md b/.api-reports/api-report-masking.api.md index 04786e31bbd..cb7d0207d49 100644 --- a/.api-reports/api-report-masking.api.md +++ b/.api-reports/api-report-masking.api.md @@ -437,14 +437,13 @@ export function maskFragment(data: TData, document: TypedDocume export function maskOperation(data: TData, document: DocumentNode | TypedDocumentNode, cache: ApolloCache): TData; // Warning: (ae-forgotten-export) The symbol "RemoveMaskedMarker" needs to be exported by the entry point index.d.ts -// Warning: (ae-forgotten-export) The symbol "ContainsFragmentsRefs" needs to be exported by the entry point index.d.ts // // @public export type MaybeMasked = DataMasking extends { mode: "unmask"; } ? TData extends any ? true extends IsAny ? TData : TData extends { __masked?: true; -} ? Prettify> : true extends ContainsFragmentsRefs ? Unmasked : TData : never : DataMasking extends { +} ? Prettify> : Unmasked : never : DataMasking extends { mode: "preserveTypes"; } ? TData : TData; @@ -612,11 +611,12 @@ type Transaction = (c: ApolloCache) => void; // @public (undocumented) type unionToIntersection = (T extends unknown ? (x: T) => unknown : never) extends ((x: infer U) => unknown) ? U : never; +// Warning: (ae-forgotten-export) The symbol "ContainsFragmentsRefs" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "UnwrapFragmentRefs" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "RemoveFragmentName" needs to be exported by the entry point index.d.ts // // @public -export type Unmasked = true extends IsAny ? TData : TData extends object ? UnwrapFragmentRefs>> : TData; +export type Unmasked = true extends IsAny ? TData : TData extends object ? true extends ContainsFragmentsRefs ? UnwrapFragmentRefs>> : TData : TData; // @public (undocumented) type UnwrapFragmentRefs = true extends IsAny ? TData : TData extends any ? string extends keyof TData ? TData : keyof TData extends never ? TData : TData extends { diff --git a/.api-reports/api-report-react.api.md b/.api-reports/api-report-react.api.md index 904d5977115..20f8f310d1a 100644 --- a/.api-reports/api-report-react.api.md +++ b/.api-reports/api-report-react.api.md @@ -1155,14 +1155,13 @@ type MaybeAsync = T | PromiseLike; // Warning: (ae-forgotten-export) The symbol "DataMasking" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "RemoveMaskedMarker" needs to be exported by the entry point index.d.ts -// Warning: (ae-forgotten-export) The symbol "ContainsFragmentsRefs" needs to be exported by the entry point index.d.ts // // @public type MaybeMasked = DataMasking extends { mode: "unmask"; } ? TData extends any ? true extends IsAny ? TData : TData extends { __masked?: true; -} ? Prettify> : true extends ContainsFragmentsRefs ? Unmasked : TData : never : DataMasking extends { +} ? Prettify> : Unmasked : never : DataMasking extends { mode: "preserveTypes"; } ? TData : TData; @@ -2206,11 +2205,12 @@ type UnionToIntersection = (U extends any ? (k: U) => void : never) extends ( // @public (undocumented) type unionToIntersection = (T extends unknown ? (x: T) => unknown : never) extends ((x: infer U) => unknown) ? U : never; +// Warning: (ae-forgotten-export) The symbol "ContainsFragmentsRefs" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "UnwrapFragmentRefs" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "RemoveFragmentName" needs to be exported by the entry point index.d.ts // // @public -type Unmasked = true extends IsAny ? TData : TData extends object ? UnwrapFragmentRefs>> : TData; +type Unmasked = true extends IsAny ? TData : TData extends object ? true extends ContainsFragmentsRefs ? UnwrapFragmentRefs>> : TData : TData; // @public (undocumented) type UnwrapFragmentRefs = true extends IsAny ? TData : TData extends any ? string extends keyof TData ? TData : keyof TData extends never ? TData : TData extends { diff --git a/.api-reports/api-report-react_components.api.md b/.api-reports/api-report-react_components.api.md index 1238640825e..5f9586fb590 100644 --- a/.api-reports/api-report-react_components.api.md +++ b/.api-reports/api-report-react_components.api.md @@ -1018,14 +1018,13 @@ type MaybeAsync = T | PromiseLike; // Warning: (ae-forgotten-export) The symbol "DataMasking" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "RemoveMaskedMarker" needs to be exported by the entry point index.d.ts -// Warning: (ae-forgotten-export) The symbol "ContainsFragmentsRefs" needs to be exported by the entry point index.d.ts // // @public type MaybeMasked = DataMasking extends { mode: "unmask"; } ? TData extends any ? true extends IsAny ? TData : TData extends { __masked?: true; -} ? Prettify> : true extends ContainsFragmentsRefs ? Unmasked : TData : never : DataMasking extends { +} ? Prettify> : Unmasked : never : DataMasking extends { mode: "preserveTypes"; } ? TData : TData; @@ -1939,11 +1938,12 @@ type UnionToIntersection = (U extends any ? (k: U) => void : never) extends ( // @public (undocumented) type unionToIntersection = (T extends unknown ? (x: T) => unknown : never) extends ((x: infer U) => unknown) ? U : never; +// Warning: (ae-forgotten-export) The symbol "ContainsFragmentsRefs" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "UnwrapFragmentRefs" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "RemoveFragmentName" needs to be exported by the entry point index.d.ts // // @public -type Unmasked = true extends IsAny ? TData : TData extends object ? UnwrapFragmentRefs>> : TData; +type Unmasked = true extends IsAny ? TData : TData extends object ? true extends ContainsFragmentsRefs ? UnwrapFragmentRefs>> : TData : TData; // @public (undocumented) type UnwrapFragmentRefs = true extends IsAny ? TData : TData extends any ? string extends keyof TData ? TData : keyof TData extends never ? TData : TData extends { diff --git a/.api-reports/api-report-react_context.api.md b/.api-reports/api-report-react_context.api.md index 3fdc6fa3414..2e299c662af 100644 --- a/.api-reports/api-report-react_context.api.md +++ b/.api-reports/api-report-react_context.api.md @@ -1015,14 +1015,13 @@ type MaybeAsync = T | PromiseLike; // Warning: (ae-forgotten-export) The symbol "DataMasking" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "RemoveMaskedMarker" needs to be exported by the entry point index.d.ts -// Warning: (ae-forgotten-export) The symbol "ContainsFragmentsRefs" needs to be exported by the entry point index.d.ts // // @public type MaybeMasked = DataMasking extends { mode: "unmask"; } ? TData extends any ? true extends IsAny ? TData : TData extends { __masked?: true; -} ? Prettify> : true extends ContainsFragmentsRefs ? Unmasked : TData : never : DataMasking extends { +} ? Prettify> : Unmasked : never : DataMasking extends { mode: "preserveTypes"; } ? TData : TData; @@ -1859,11 +1858,12 @@ type UnionToIntersection = (U extends any ? (k: U) => void : never) extends ( // @public (undocumented) type unionToIntersection = (T extends unknown ? (x: T) => unknown : never) extends ((x: infer U) => unknown) ? U : never; +// Warning: (ae-forgotten-export) The symbol "ContainsFragmentsRefs" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "UnwrapFragmentRefs" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "RemoveFragmentName" needs to be exported by the entry point index.d.ts // // @public -type Unmasked = true extends IsAny ? TData : TData extends object ? UnwrapFragmentRefs>> : TData; +type Unmasked = true extends IsAny ? TData : TData extends object ? true extends ContainsFragmentsRefs ? UnwrapFragmentRefs>> : TData : TData; // @public (undocumented) type UnwrapFragmentRefs = true extends IsAny ? TData : TData extends any ? string extends keyof TData ? TData : keyof TData extends never ? TData : TData extends { diff --git a/.api-reports/api-report-react_hoc.api.md b/.api-reports/api-report-react_hoc.api.md index 6fe263dd146..72d4ff340a0 100644 --- a/.api-reports/api-report-react_hoc.api.md +++ b/.api-reports/api-report-react_hoc.api.md @@ -1022,14 +1022,13 @@ type MaybeAsync = T | PromiseLike; // Warning: (ae-forgotten-export) The symbol "DataMasking" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "RemoveMaskedMarker" needs to be exported by the entry point index.d.ts -// Warning: (ae-forgotten-export) The symbol "ContainsFragmentsRefs" needs to be exported by the entry point index.d.ts // // @public type MaybeMasked = DataMasking extends { mode: "unmask"; } ? TData extends any ? true extends IsAny ? TData : TData extends { __masked?: true; -} ? Prettify> : true extends ContainsFragmentsRefs ? Unmasked : TData : never : DataMasking extends { +} ? Prettify> : Unmasked : never : DataMasking extends { mode: "preserveTypes"; } ? TData : TData; @@ -1863,11 +1862,12 @@ type UnionToIntersection = (U extends any ? (k: U) => void : never) extends ( // @public (undocumented) type unionToIntersection = (T extends unknown ? (x: T) => unknown : never) extends ((x: infer U) => unknown) ? U : never; +// Warning: (ae-forgotten-export) The symbol "ContainsFragmentsRefs" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "UnwrapFragmentRefs" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "RemoveFragmentName" needs to be exported by the entry point index.d.ts // // @public -type Unmasked = true extends IsAny ? TData : TData extends object ? UnwrapFragmentRefs>> : TData; +type Unmasked = true extends IsAny ? TData : TData extends object ? true extends ContainsFragmentsRefs ? UnwrapFragmentRefs>> : TData : TData; // @public (undocumented) type UnwrapFragmentRefs = true extends IsAny ? TData : TData extends any ? string extends keyof TData ? TData : keyof TData extends never ? TData : TData extends { diff --git a/.api-reports/api-report-react_hooks.api.md b/.api-reports/api-report-react_hooks.api.md index 1c5b8fb2058..2ee9cf537ef 100644 --- a/.api-reports/api-report-react_hooks.api.md +++ b/.api-reports/api-report-react_hooks.api.md @@ -1104,14 +1104,13 @@ type MaybeAsync = T | PromiseLike; // Warning: (ae-forgotten-export) The symbol "DataMasking" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "RemoveMaskedMarker" needs to be exported by the entry point index.d.ts -// Warning: (ae-forgotten-export) The symbol "ContainsFragmentsRefs" needs to be exported by the entry point index.d.ts // // @public type MaybeMasked = DataMasking extends { mode: "unmask"; } ? TData extends any ? true extends IsAny ? TData : TData extends { __masked?: true; -} ? Prettify> : true extends ContainsFragmentsRefs ? Unmasked : TData : never : DataMasking extends { +} ? Prettify> : Unmasked : never : DataMasking extends { mode: "preserveTypes"; } ? TData : TData; @@ -2029,11 +2028,12 @@ type UnionToIntersection = (U extends any ? (k: U) => void : never) extends ( // @public (undocumented) type unionToIntersection = (T extends unknown ? (x: T) => unknown : never) extends ((x: infer U) => unknown) ? U : never; +// Warning: (ae-forgotten-export) The symbol "ContainsFragmentsRefs" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "UnwrapFragmentRefs" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "RemoveFragmentName" needs to be exported by the entry point index.d.ts // // @public -type Unmasked = true extends IsAny ? TData : TData extends object ? UnwrapFragmentRefs>> : TData; +type Unmasked = true extends IsAny ? TData : TData extends object ? true extends ContainsFragmentsRefs ? UnwrapFragmentRefs>> : TData : TData; // @public (undocumented) type UnwrapFragmentRefs = true extends IsAny ? TData : TData extends any ? string extends keyof TData ? TData : keyof TData extends never ? TData : TData extends { diff --git a/.api-reports/api-report-react_internal.api.md b/.api-reports/api-report-react_internal.api.md index 92a7a3747e6..2672be8d5eb 100644 --- a/.api-reports/api-report-react_internal.api.md +++ b/.api-reports/api-report-react_internal.api.md @@ -1114,14 +1114,13 @@ type MaybeAsync = T | PromiseLike; // Warning: (ae-forgotten-export) The symbol "DataMasking" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "RemoveMaskedMarker" needs to be exported by the entry point index.d.ts -// Warning: (ae-forgotten-export) The symbol "ContainsFragmentsRefs" needs to be exported by the entry point index.d.ts // // @public type MaybeMasked = DataMasking extends { mode: "unmask"; } ? TData extends any ? true extends IsAny ? TData : TData extends { __masked?: true; -} ? Prettify> : true extends ContainsFragmentsRefs ? Unmasked : TData : never : DataMasking extends { +} ? Prettify> : Unmasked : never : DataMasking extends { mode: "preserveTypes"; } ? TData : TData; @@ -2081,11 +2080,12 @@ type UnionToIntersection = (U extends any ? (k: U) => void : never) extends ( // @public (undocumented) type unionToIntersection = (T extends unknown ? (x: T) => unknown : never) extends ((x: infer U) => unknown) ? U : never; +// Warning: (ae-forgotten-export) The symbol "ContainsFragmentsRefs" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "UnwrapFragmentRefs" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "RemoveFragmentName" needs to be exported by the entry point index.d.ts // // @public -type Unmasked = true extends IsAny ? TData : TData extends object ? UnwrapFragmentRefs>> : TData; +type Unmasked = true extends IsAny ? TData : TData extends object ? true extends ContainsFragmentsRefs ? UnwrapFragmentRefs>> : TData : TData; // @public (undocumented) type UnwrapFragmentRefs = true extends IsAny ? TData : TData extends any ? string extends keyof TData ? TData : keyof TData extends never ? TData : TData extends { diff --git a/.api-reports/api-report-react_ssr.api.md b/.api-reports/api-report-react_ssr.api.md index 83958f92452..581bf4efb1b 100644 --- a/.api-reports/api-report-react_ssr.api.md +++ b/.api-reports/api-report-react_ssr.api.md @@ -1000,14 +1000,13 @@ type MaybeAsync = T | PromiseLike; // Warning: (ae-forgotten-export) The symbol "DataMasking" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "RemoveMaskedMarker" needs to be exported by the entry point index.d.ts -// Warning: (ae-forgotten-export) The symbol "ContainsFragmentsRefs" needs to be exported by the entry point index.d.ts // // @public type MaybeMasked = DataMasking extends { mode: "unmask"; } ? TData extends any ? true extends IsAny ? TData : TData extends { __masked?: true; -} ? Prettify> : true extends ContainsFragmentsRefs ? Unmasked : TData : never : DataMasking extends { +} ? Prettify> : Unmasked : never : DataMasking extends { mode: "preserveTypes"; } ? TData : TData; @@ -1844,11 +1843,12 @@ type UnionToIntersection = (U extends any ? (k: U) => void : never) extends ( // @public (undocumented) type unionToIntersection = (T extends unknown ? (x: T) => unknown : never) extends ((x: infer U) => unknown) ? U : never; +// Warning: (ae-forgotten-export) The symbol "ContainsFragmentsRefs" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "UnwrapFragmentRefs" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "RemoveFragmentName" needs to be exported by the entry point index.d.ts // // @public -type Unmasked = true extends IsAny ? TData : TData extends object ? UnwrapFragmentRefs>> : TData; +type Unmasked = true extends IsAny ? TData : TData extends object ? true extends ContainsFragmentsRefs ? UnwrapFragmentRefs>> : TData : TData; // @public (undocumented) type UnwrapFragmentRefs = true extends IsAny ? TData : TData extends any ? string extends keyof TData ? TData : keyof TData extends never ? TData : TData extends { diff --git a/.api-reports/api-report-testing.api.md b/.api-reports/api-report-testing.api.md index 18bbb81892d..09c0be41e58 100644 --- a/.api-reports/api-report-testing.api.md +++ b/.api-reports/api-report-testing.api.md @@ -989,14 +989,13 @@ type MaybeAsync = T | PromiseLike; // Warning: (ae-forgotten-export) The symbol "DataMasking" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "RemoveMaskedMarker" needs to be exported by the entry point index.d.ts -// Warning: (ae-forgotten-export) The symbol "ContainsFragmentsRefs" needs to be exported by the entry point index.d.ts // // @public type MaybeMasked = DataMasking extends { mode: "unmask"; } ? TData extends any ? true extends IsAny ? TData : TData extends { __masked?: true; -} ? Prettify> : true extends ContainsFragmentsRefs ? Unmasked : TData : never : DataMasking extends { +} ? Prettify> : Unmasked : never : DataMasking extends { mode: "preserveTypes"; } ? TData : TData; @@ -1895,11 +1894,12 @@ type UnionToIntersection = (U extends any ? (k: U) => void : never) extends ( // @public (undocumented) type unionToIntersection = (T extends unknown ? (x: T) => unknown : never) extends ((x: infer U) => unknown) ? U : never; +// Warning: (ae-forgotten-export) The symbol "ContainsFragmentsRefs" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "UnwrapFragmentRefs" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "RemoveFragmentName" needs to be exported by the entry point index.d.ts // // @public -type Unmasked = true extends IsAny ? TData : TData extends object ? UnwrapFragmentRefs>> : TData; +type Unmasked = true extends IsAny ? TData : TData extends object ? true extends ContainsFragmentsRefs ? UnwrapFragmentRefs>> : TData : TData; // @public (undocumented) type UnwrapFragmentRefs = true extends IsAny ? TData : TData extends any ? string extends keyof TData ? TData : keyof TData extends never ? TData : TData extends { diff --git a/.api-reports/api-report-testing_core.api.md b/.api-reports/api-report-testing_core.api.md index 9b98b8e17de..fbf3f238903 100644 --- a/.api-reports/api-report-testing_core.api.md +++ b/.api-reports/api-report-testing_core.api.md @@ -988,14 +988,13 @@ type MaybeAsync = T | PromiseLike; // Warning: (ae-forgotten-export) The symbol "DataMasking" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "RemoveMaskedMarker" needs to be exported by the entry point index.d.ts -// Warning: (ae-forgotten-export) The symbol "ContainsFragmentsRefs" needs to be exported by the entry point index.d.ts // // @public type MaybeMasked = DataMasking extends { mode: "unmask"; } ? TData extends any ? true extends IsAny ? TData : TData extends { __masked?: true; -} ? Prettify> : true extends ContainsFragmentsRefs ? Unmasked : TData : never : DataMasking extends { +} ? Prettify> : Unmasked : never : DataMasking extends { mode: "preserveTypes"; } ? TData : TData; @@ -1852,11 +1851,12 @@ type UnionToIntersection = (U extends any ? (k: U) => void : never) extends ( // @public (undocumented) type unionToIntersection = (T extends unknown ? (x: T) => unknown : never) extends ((x: infer U) => unknown) ? U : never; +// Warning: (ae-forgotten-export) The symbol "ContainsFragmentsRefs" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "UnwrapFragmentRefs" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "RemoveFragmentName" needs to be exported by the entry point index.d.ts // // @public -type Unmasked = true extends IsAny ? TData : TData extends object ? UnwrapFragmentRefs>> : TData; +type Unmasked = true extends IsAny ? TData : TData extends object ? true extends ContainsFragmentsRefs ? UnwrapFragmentRefs>> : TData : TData; // @public (undocumented) type UnwrapFragmentRefs = true extends IsAny ? TData : TData extends any ? string extends keyof TData ? TData : keyof TData extends never ? TData : TData extends { diff --git a/.api-reports/api-report-utilities.api.md b/.api-reports/api-report-utilities.api.md index 6905e929046..dfdc8e611c2 100644 --- a/.api-reports/api-report-utilities.api.md +++ b/.api-reports/api-report-utilities.api.md @@ -1697,14 +1697,13 @@ export function maybeDeepFreeze(obj: T): T; // Warning: (ae-forgotten-export) The symbol "DataMasking" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "RemoveMaskedMarker" needs to be exported by the entry point index.d.ts -// Warning: (ae-forgotten-export) The symbol "ContainsFragmentsRefs" needs to be exported by the entry point index.d.ts // // @public type MaybeMasked = DataMasking extends { mode: "unmask"; } ? TData extends any ? true extends IsAny ? TData : TData extends { __masked?: true; -} ? Prettify> : true extends ContainsFragmentsRefs ? Unmasked : TData : never : DataMasking extends { +} ? Prettify> : Unmasked : never : DataMasking extends { mode: "preserveTypes"; } ? TData : TData; @@ -2773,11 +2772,12 @@ type unionToIntersection = (T extends unknown ? (x: T) => unknown : never) ex // @public (undocumented) type UnionToIntersection_2 = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never; +// Warning: (ae-forgotten-export) The symbol "ContainsFragmentsRefs" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "UnwrapFragmentRefs" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "RemoveFragmentName" needs to be exported by the entry point index.d.ts // // @public -type Unmasked = true extends IsAny ? TData : TData extends object ? UnwrapFragmentRefs>> : TData; +type Unmasked = true extends IsAny ? TData : TData extends object ? true extends ContainsFragmentsRefs ? UnwrapFragmentRefs>> : TData : TData; // @public (undocumented) type UnwrapFragmentRefs = true extends IsAny ? TData : TData extends any ? string extends keyof TData ? TData : keyof TData extends never ? TData : TData extends { diff --git a/.api-reports/api-report.api.md b/.api-reports/api-report.api.md index 42e41d0e105..ba939adbf2d 100644 --- a/.api-reports/api-report.api.md +++ b/.api-reports/api-report.api.md @@ -1590,14 +1590,13 @@ interface MaskOperationOptions { type MaybeAsync = T | PromiseLike; // Warning: (ae-forgotten-export) The symbol "RemoveMaskedMarker" needs to be exported by the entry point index.d.ts -// Warning: (ae-forgotten-export) The symbol "ContainsFragmentsRefs" needs to be exported by the entry point index.d.ts // // @public export type MaybeMasked = DataMasking extends { mode: "unmask"; } ? TData extends any ? true extends IsAny ? TData : TData extends { __masked?: true; -} ? Prettify> : true extends ContainsFragmentsRefs ? Unmasked : TData : never : DataMasking extends { +} ? Prettify> : Unmasked : never : DataMasking extends { mode: "preserveTypes"; } ? TData : TData; @@ -2875,11 +2874,12 @@ type UnionToIntersection = (U extends any ? (k: U) => void : never) extends ( // @public (undocumented) type unionToIntersection = (T extends unknown ? (x: T) => unknown : never) extends ((x: infer U) => unknown) ? U : never; +// Warning: (ae-forgotten-export) The symbol "ContainsFragmentsRefs" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "UnwrapFragmentRefs" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "RemoveFragmentName" needs to be exported by the entry point index.d.ts // // @public -export type Unmasked = true extends IsAny ? TData : TData extends object ? UnwrapFragmentRefs>> : TData; +export type Unmasked = true extends IsAny ? TData : TData extends object ? true extends ContainsFragmentsRefs ? UnwrapFragmentRefs>> : TData : TData; // @public (undocumented) type UnwrapFragmentRefs = true extends IsAny ? TData : TData extends any ? string extends keyof TData ? TData : keyof TData extends never ? TData : TData extends { diff --git a/.changeset/beige-eggs-nail.md b/.changeset/beige-eggs-nail.md new file mode 100644 index 00000000000..73dfcefcd7b --- /dev/null +++ b/.changeset/beige-eggs-nail.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +Maintain the `TData` type when used with `Unmasked` when `TData` is not a masked type generated from GraphQL Codegen. diff --git a/src/masking/__benches__/types.bench.ts b/src/masking/__benches__/types.bench.ts index 97a67e69b4e..1ea136c1d6e 100644 --- a/src/masking/__benches__/types.bench.ts +++ b/src/masking/__benches__/types.bench.ts @@ -248,7 +248,7 @@ test("unmasks DeepPartial types", (prefix) => { test("Unmasked handles odd types", (prefix) => { bench(prefix + "empty type instantiations", () => { attest<{}, Unmasked<{}>>(); - }).types([80, "instantiations"]); + }).types([111, "instantiations"]); bench(prefix + "empty type functionality", () => { expectTypeOf>().toEqualTypeOf<{}>(); @@ -256,7 +256,7 @@ test("Unmasked handles odd types", (prefix) => { bench(prefix + "generic record type instantiations", () => { attest, Unmasked>>(); - }).types([99, "instantiations"]); + }).types([115, "instantiations"]); bench(prefix + "generic record type functionality", () => { expectTypeOf>>().toEqualTypeOf< @@ -284,7 +284,7 @@ test("Unmasked handles odd types", (prefix) => { test("MaybeMasked handles odd types", (prefix) => { bench(prefix + "empty type instantiations", () => { attest<{}, MaybeMasked<{}>>(); - }).types([76, "instantiations"]); + }).types([41, "instantiations"]); bench(prefix + "empty type functionality", () => { expectTypeOf>().toEqualTypeOf<{}>(); @@ -292,7 +292,7 @@ test("MaybeMasked handles odd types", (prefix) => { bench(prefix + "generic record type instantiations", () => { attest, MaybeMasked>>(); - }).types([93, "instantiations"]); + }).types([46, "instantiations"]); bench(prefix + "generic record type functionality", () => { expectTypeOf>>().toEqualTypeOf< Record @@ -301,14 +301,14 @@ test("MaybeMasked handles odd types", (prefix) => { bench(prefix + "unknown instantiations", () => { attest>(); - }).types([54, "instantiations"]); + }).types([41, "instantiations"]); bench(prefix + "unknown functionality", () => { expectTypeOf>().toBeUnknown(); }); bench(prefix + "any instantiations", () => { attest>(); - }).types([49, "instantiations"]); + }).types([43, "instantiations"]); bench(prefix + "any functionality", () => { expectTypeOf>().toBeAny(); }); @@ -321,13 +321,13 @@ test("distributed members on MaybeMasked", (prefix) => { [MaybeMasked | null | undefined], [MaybeMasked] >(); - }).types([55, "instantiations"]); + }).types([49, "instantiations"]); })(); (function unresolvedGenerics() { bench(prefix + "two unresolved generics distribute", () => { attest<[MaybeMasked | MaybeMasked], [MaybeMasked]>(); - }).types([61, "instantiations"]); + }).types([50, "instantiations"]); })(); }); @@ -475,7 +475,7 @@ test("does not detect `$fragmentRefs` if type contains `any`", (prefix) => { bench(prefix + "instantiations", () => { return {} as MaybeMasked; - }).types([6, "instantiations"]); + }).types([1, "instantiations"]); bench(prefix + "functionality", () => { const x = {} as MaybeMasked; @@ -514,7 +514,7 @@ test("does not detect `$fragmentRefs` if type is a record type", (prefix) => { bench(prefix + "instantiations", () => { return {} as MaybeMasked; - }).types([6, "instantiations"]); + }).types([1, "instantiations"]); bench(prefix + "functionality", () => { const x = {} as MaybeMasked; @@ -532,7 +532,7 @@ test("does not detect `$fragmentRefs` on types with index signatures", (prefix) bench(prefix + "instantiations", () => { return {} as MaybeMasked; - }).types([6, "instantiations"]); + }).types([1, "instantiations"]); bench(prefix + "functionality", () => { const x = {} as MaybeMasked; @@ -565,7 +565,7 @@ test("detects `$fragmentRefs` on types with index signatures", (prefix) => { bench(prefix + "instantiations", () => { return {} as MaybeMasked; - }).types([6, "instantiations"]); + }).types([1, "instantiations"]); bench(prefix + "functionality", () => { const x = {} as Unmasked; @@ -588,7 +588,7 @@ test("recursive types: no error 'Type instantiation is excessively deep and poss bench(prefix + "instantiations", () => { return {} as MaybeMasked; - }).types([6, "instantiations"]); + }).types([1, "instantiations"]); bench(prefix + "functionality", () => { const x = {} as MaybeMasked; @@ -604,7 +604,7 @@ test("MaybeMasked can be called with a generic if `mode` is not set to `unmask`" bench(prefix + "Result generic - instantiations", () => { const maybeMasked: MaybeMasked = arg; return maybeMasked; - }).types(); + }).types([1, "instantiations"]); bench(prefix + "Result generic - functionality", () => { const maybeMasked: MaybeMasked = arg; @@ -615,7 +615,7 @@ test("MaybeMasked can be called with a generic if `mode` is not set to `unmask`" bench(prefix + "Result generic - instantiations", () => { const maybeMasked: MaybeMasked = arg; return maybeMasked; - }).types(); + }).types([1, "instantiations"]); bench(prefix + "Result generic - functionality", () => { const maybeMasked: MaybeMasked = arg; diff --git a/src/masking/types.ts b/src/masking/types.ts index f2c5f4cb4ca..62c43e9075a 100644 --- a/src/masking/types.ts +++ b/src/masking/types.ts @@ -45,8 +45,7 @@ export type MaybeMasked = // prevent "Type instantiation is excessively deep and possibly infinite." true extends IsAny ? TData : TData extends { __masked?: true } ? Prettify> - : true extends ContainsFragmentsRefs ? Unmasked - : TData + : Unmasked : never : DataMasking extends { mode: "preserveTypes" } ? TData : TData; @@ -57,5 +56,7 @@ export type MaybeMasked = export type Unmasked = true extends IsAny ? TData : TData extends object ? - UnwrapFragmentRefs>> + true extends ContainsFragmentsRefs ? + UnwrapFragmentRefs>> + : TData : TData; From 3601246f6e7b4f8b2937e0d431e6b5a6964f9066 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 14 Jan 2025 10:41:25 -0700 Subject: [PATCH 2/9] Handle tagged/branded primitive types when used with `Unmasked` (#12270) --- .api-reports/api-report-cache.api.md | 2 +- .api-reports/api-report-core.api.md | 2 +- .api-reports/api-report-masking.api.md | 2 +- .api-reports/api-report-react.api.md | 2 +- .../api-report-react_components.api.md | 2 +- .api-reports/api-report-react_context.api.md | 2 +- .api-reports/api-report-react_hoc.api.md | 2 +- .api-reports/api-report-react_hooks.api.md | 2 +- .api-reports/api-report-react_internal.api.md | 2 +- .api-reports/api-report-react_ssr.api.md | 2 +- .api-reports/api-report-testing.api.md | 2 +- .api-reports/api-report-testing_core.api.md | 2 +- .api-reports/api-report-utilities.api.md | 6 ++-- .api-reports/api-report.api.md | 2 +- .changeset/tiny-ants-bow.md | 5 +++ src/masking/__benches__/types.bench.ts | 33 +++++++++++++++++++ src/masking/internal/types.ts | 10 ++++-- src/utilities/index.ts | 1 + 18 files changed, 62 insertions(+), 19 deletions(-) create mode 100644 .changeset/tiny-ants-bow.md diff --git a/.api-reports/api-report-cache.api.md b/.api-reports/api-report-cache.api.md index 359812c3f7b..8c362513806 100644 --- a/.api-reports/api-report-cache.api.md +++ b/.api-reports/api-report-cache.api.md @@ -1120,7 +1120,7 @@ type unionToIntersection = (T extends unknown ? (x: T) => unknown : never) ex type Unmasked = true extends IsAny ? TData : TData extends object ? true extends ContainsFragmentsRefs ? UnwrapFragmentRefs>> : TData : TData; // @public (undocumented) -type UnwrapFragmentRefs = true extends IsAny ? TData : TData extends any ? string extends keyof TData ? TData : keyof TData extends never ? TData : TData extends { +type UnwrapFragmentRefs = true extends IsAny ? TData : TData extends any ? TData extends Primitive ? TData : string extends keyof TData ? TData : keyof TData extends never ? TData : TData extends { " $fragmentRefs"?: infer FragmentRefs; } ? UnwrapFragmentRefs | RemoveFragmentName[keyof NonNullable]>>>> : TData extends object ? { [K in keyof TData]: UnwrapFragmentRefs; diff --git a/.api-reports/api-report-core.api.md b/.api-reports/api-report-core.api.md index 8047e356cd6..75937521116 100644 --- a/.api-reports/api-report-core.api.md +++ b/.api-reports/api-report-core.api.md @@ -2417,7 +2417,7 @@ type unionToIntersection = (T extends unknown ? (x: T) => unknown : never) ex export type Unmasked = true extends IsAny ? TData : TData extends object ? true extends ContainsFragmentsRefs ? UnwrapFragmentRefs>> : TData : TData; // @public (undocumented) -type UnwrapFragmentRefs = true extends IsAny ? TData : TData extends any ? string extends keyof TData ? TData : keyof TData extends never ? TData : TData extends { +type UnwrapFragmentRefs = true extends IsAny ? TData : TData extends any ? TData extends Primitive ? TData : string extends keyof TData ? TData : keyof TData extends never ? TData : TData extends { " $fragmentRefs"?: infer FragmentRefs; } ? UnwrapFragmentRefs | RemoveFragmentName[keyof NonNullable]>>>> : TData extends object ? { [K in keyof TData]: UnwrapFragmentRefs; diff --git a/.api-reports/api-report-masking.api.md b/.api-reports/api-report-masking.api.md index cb7d0207d49..37019fcb7bc 100644 --- a/.api-reports/api-report-masking.api.md +++ b/.api-reports/api-report-masking.api.md @@ -619,7 +619,7 @@ type unionToIntersection = (T extends unknown ? (x: T) => unknown : never) ex export type Unmasked = true extends IsAny ? TData : TData extends object ? true extends ContainsFragmentsRefs ? UnwrapFragmentRefs>> : TData : TData; // @public (undocumented) -type UnwrapFragmentRefs = true extends IsAny ? TData : TData extends any ? string extends keyof TData ? TData : keyof TData extends never ? TData : TData extends { +type UnwrapFragmentRefs = true extends IsAny ? TData : TData extends any ? TData extends Primitive ? TData : string extends keyof TData ? TData : keyof TData extends never ? TData : TData extends { " $fragmentRefs"?: infer FragmentRefs; } ? UnwrapFragmentRefs | RemoveFragmentName[keyof NonNullable]>>>> : TData extends object ? { [K in keyof TData]: UnwrapFragmentRefs; diff --git a/.api-reports/api-report-react.api.md b/.api-reports/api-report-react.api.md index 20f8f310d1a..bd6bae479c4 100644 --- a/.api-reports/api-report-react.api.md +++ b/.api-reports/api-report-react.api.md @@ -2213,7 +2213,7 @@ type unionToIntersection = (T extends unknown ? (x: T) => unknown : never) ex type Unmasked = true extends IsAny ? TData : TData extends object ? true extends ContainsFragmentsRefs ? UnwrapFragmentRefs>> : TData : TData; // @public (undocumented) -type UnwrapFragmentRefs = true extends IsAny ? TData : TData extends any ? string extends keyof TData ? TData : keyof TData extends never ? TData : TData extends { +type UnwrapFragmentRefs = true extends IsAny ? TData : TData extends any ? TData extends Primitive ? TData : string extends keyof TData ? TData : keyof TData extends never ? TData : TData extends { " $fragmentRefs"?: infer FragmentRefs; } ? UnwrapFragmentRefs | RemoveFragmentName[keyof NonNullable]>>>> : TData extends object ? { [K in keyof TData]: UnwrapFragmentRefs; diff --git a/.api-reports/api-report-react_components.api.md b/.api-reports/api-report-react_components.api.md index 5f9586fb590..b1c44362975 100644 --- a/.api-reports/api-report-react_components.api.md +++ b/.api-reports/api-report-react_components.api.md @@ -1946,7 +1946,7 @@ type unionToIntersection = (T extends unknown ? (x: T) => unknown : never) ex type Unmasked = true extends IsAny ? TData : TData extends object ? true extends ContainsFragmentsRefs ? UnwrapFragmentRefs>> : TData : TData; // @public (undocumented) -type UnwrapFragmentRefs = true extends IsAny ? TData : TData extends any ? string extends keyof TData ? TData : keyof TData extends never ? TData : TData extends { +type UnwrapFragmentRefs = true extends IsAny ? TData : TData extends any ? TData extends Primitive ? TData : string extends keyof TData ? TData : keyof TData extends never ? TData : TData extends { " $fragmentRefs"?: infer FragmentRefs; } ? UnwrapFragmentRefs | RemoveFragmentName[keyof NonNullable]>>>> : TData extends object ? { [K in keyof TData]: UnwrapFragmentRefs; diff --git a/.api-reports/api-report-react_context.api.md b/.api-reports/api-report-react_context.api.md index 2e299c662af..0ab3648be73 100644 --- a/.api-reports/api-report-react_context.api.md +++ b/.api-reports/api-report-react_context.api.md @@ -1866,7 +1866,7 @@ type unionToIntersection = (T extends unknown ? (x: T) => unknown : never) ex type Unmasked = true extends IsAny ? TData : TData extends object ? true extends ContainsFragmentsRefs ? UnwrapFragmentRefs>> : TData : TData; // @public (undocumented) -type UnwrapFragmentRefs = true extends IsAny ? TData : TData extends any ? string extends keyof TData ? TData : keyof TData extends never ? TData : TData extends { +type UnwrapFragmentRefs = true extends IsAny ? TData : TData extends any ? TData extends Primitive ? TData : string extends keyof TData ? TData : keyof TData extends never ? TData : TData extends { " $fragmentRefs"?: infer FragmentRefs; } ? UnwrapFragmentRefs | RemoveFragmentName[keyof NonNullable]>>>> : TData extends object ? { [K in keyof TData]: UnwrapFragmentRefs; diff --git a/.api-reports/api-report-react_hoc.api.md b/.api-reports/api-report-react_hoc.api.md index 72d4ff340a0..25ca4e27760 100644 --- a/.api-reports/api-report-react_hoc.api.md +++ b/.api-reports/api-report-react_hoc.api.md @@ -1870,7 +1870,7 @@ type unionToIntersection = (T extends unknown ? (x: T) => unknown : never) ex type Unmasked = true extends IsAny ? TData : TData extends object ? true extends ContainsFragmentsRefs ? UnwrapFragmentRefs>> : TData : TData; // @public (undocumented) -type UnwrapFragmentRefs = true extends IsAny ? TData : TData extends any ? string extends keyof TData ? TData : keyof TData extends never ? TData : TData extends { +type UnwrapFragmentRefs = true extends IsAny ? TData : TData extends any ? TData extends Primitive ? TData : string extends keyof TData ? TData : keyof TData extends never ? TData : TData extends { " $fragmentRefs"?: infer FragmentRefs; } ? UnwrapFragmentRefs | RemoveFragmentName[keyof NonNullable]>>>> : TData extends object ? { [K in keyof TData]: UnwrapFragmentRefs; diff --git a/.api-reports/api-report-react_hooks.api.md b/.api-reports/api-report-react_hooks.api.md index 2ee9cf537ef..b293da7529f 100644 --- a/.api-reports/api-report-react_hooks.api.md +++ b/.api-reports/api-report-react_hooks.api.md @@ -2036,7 +2036,7 @@ type unionToIntersection = (T extends unknown ? (x: T) => unknown : never) ex type Unmasked = true extends IsAny ? TData : TData extends object ? true extends ContainsFragmentsRefs ? UnwrapFragmentRefs>> : TData : TData; // @public (undocumented) -type UnwrapFragmentRefs = true extends IsAny ? TData : TData extends any ? string extends keyof TData ? TData : keyof TData extends never ? TData : TData extends { +type UnwrapFragmentRefs = true extends IsAny ? TData : TData extends any ? TData extends Primitive ? TData : string extends keyof TData ? TData : keyof TData extends never ? TData : TData extends { " $fragmentRefs"?: infer FragmentRefs; } ? UnwrapFragmentRefs | RemoveFragmentName[keyof NonNullable]>>>> : TData extends object ? { [K in keyof TData]: UnwrapFragmentRefs; diff --git a/.api-reports/api-report-react_internal.api.md b/.api-reports/api-report-react_internal.api.md index 2672be8d5eb..c7e5bffee1a 100644 --- a/.api-reports/api-report-react_internal.api.md +++ b/.api-reports/api-report-react_internal.api.md @@ -2088,7 +2088,7 @@ type unionToIntersection = (T extends unknown ? (x: T) => unknown : never) ex type Unmasked = true extends IsAny ? TData : TData extends object ? true extends ContainsFragmentsRefs ? UnwrapFragmentRefs>> : TData : TData; // @public (undocumented) -type UnwrapFragmentRefs = true extends IsAny ? TData : TData extends any ? string extends keyof TData ? TData : keyof TData extends never ? TData : TData extends { +type UnwrapFragmentRefs = true extends IsAny ? TData : TData extends any ? TData extends Primitive ? TData : string extends keyof TData ? TData : keyof TData extends never ? TData : TData extends { " $fragmentRefs"?: infer FragmentRefs; } ? UnwrapFragmentRefs | RemoveFragmentName[keyof NonNullable]>>>> : TData extends object ? { [K in keyof TData]: UnwrapFragmentRefs; diff --git a/.api-reports/api-report-react_ssr.api.md b/.api-reports/api-report-react_ssr.api.md index 581bf4efb1b..fd416ee7db5 100644 --- a/.api-reports/api-report-react_ssr.api.md +++ b/.api-reports/api-report-react_ssr.api.md @@ -1851,7 +1851,7 @@ type unionToIntersection = (T extends unknown ? (x: T) => unknown : never) ex type Unmasked = true extends IsAny ? TData : TData extends object ? true extends ContainsFragmentsRefs ? UnwrapFragmentRefs>> : TData : TData; // @public (undocumented) -type UnwrapFragmentRefs = true extends IsAny ? TData : TData extends any ? string extends keyof TData ? TData : keyof TData extends never ? TData : TData extends { +type UnwrapFragmentRefs = true extends IsAny ? TData : TData extends any ? TData extends Primitive ? TData : string extends keyof TData ? TData : keyof TData extends never ? TData : TData extends { " $fragmentRefs"?: infer FragmentRefs; } ? UnwrapFragmentRefs | RemoveFragmentName[keyof NonNullable]>>>> : TData extends object ? { [K in keyof TData]: UnwrapFragmentRefs; diff --git a/.api-reports/api-report-testing.api.md b/.api-reports/api-report-testing.api.md index 09c0be41e58..876fd753717 100644 --- a/.api-reports/api-report-testing.api.md +++ b/.api-reports/api-report-testing.api.md @@ -1902,7 +1902,7 @@ type unionToIntersection = (T extends unknown ? (x: T) => unknown : never) ex type Unmasked = true extends IsAny ? TData : TData extends object ? true extends ContainsFragmentsRefs ? UnwrapFragmentRefs>> : TData : TData; // @public (undocumented) -type UnwrapFragmentRefs = true extends IsAny ? TData : TData extends any ? string extends keyof TData ? TData : keyof TData extends never ? TData : TData extends { +type UnwrapFragmentRefs = true extends IsAny ? TData : TData extends any ? TData extends Primitive ? TData : string extends keyof TData ? TData : keyof TData extends never ? TData : TData extends { " $fragmentRefs"?: infer FragmentRefs; } ? UnwrapFragmentRefs | RemoveFragmentName[keyof NonNullable]>>>> : TData extends object ? { [K in keyof TData]: UnwrapFragmentRefs; diff --git a/.api-reports/api-report-testing_core.api.md b/.api-reports/api-report-testing_core.api.md index fbf3f238903..56d109dd464 100644 --- a/.api-reports/api-report-testing_core.api.md +++ b/.api-reports/api-report-testing_core.api.md @@ -1859,7 +1859,7 @@ type unionToIntersection = (T extends unknown ? (x: T) => unknown : never) ex type Unmasked = true extends IsAny ? TData : TData extends object ? true extends ContainsFragmentsRefs ? UnwrapFragmentRefs>> : TData : TData; // @public (undocumented) -type UnwrapFragmentRefs = true extends IsAny ? TData : TData extends any ? string extends keyof TData ? TData : keyof TData extends never ? TData : TData extends { +type UnwrapFragmentRefs = true extends IsAny ? TData : TData extends any ? TData extends Primitive ? TData : string extends keyof TData ? TData : keyof TData extends never ? TData : TData extends { " $fragmentRefs"?: infer FragmentRefs; } ? UnwrapFragmentRefs | RemoveFragmentName[keyof NonNullable]>>>> : TData extends object ? { [K in keyof TData]: UnwrapFragmentRefs; diff --git a/.api-reports/api-report-utilities.api.md b/.api-reports/api-report-utilities.api.md index dfdc8e611c2..6f944d1a205 100644 --- a/.api-reports/api-report-utilities.api.md +++ b/.api-reports/api-report-utilities.api.md @@ -729,8 +729,6 @@ type DeepOmitArray = { [P in keyof T]: DeepOmit; }; -// Warning: (ae-forgotten-export) The symbol "Primitive" needs to be exported by the entry point index.d.ts -// // @public (undocumented) type DeepOmitPrimitive = Primitive | Function; @@ -2142,7 +2140,7 @@ export type Prettify = { export function preventUnhandledRejection(promise: Promise): Promise; // @public (undocumented) -type Primitive = null | undefined | string | number | boolean | symbol | bigint; +export type Primitive = null | undefined | string | number | boolean | symbol | bigint; // @public (undocumented) const print_2: ((ast: ASTNode) => string) & { @@ -2780,7 +2778,7 @@ type UnionToIntersection_2 = (U extends any ? (k: U) => void : never) extends type Unmasked = true extends IsAny ? TData : TData extends object ? true extends ContainsFragmentsRefs ? UnwrapFragmentRefs>> : TData : TData; // @public (undocumented) -type UnwrapFragmentRefs = true extends IsAny ? TData : TData extends any ? string extends keyof TData ? TData : keyof TData extends never ? TData : TData extends { +type UnwrapFragmentRefs = true extends IsAny ? TData : TData extends any ? TData extends Primitive ? TData : string extends keyof TData ? TData : keyof TData extends never ? TData : TData extends { " $fragmentRefs"?: infer FragmentRefs; } ? UnwrapFragmentRefs | RemoveFragmentName[keyof NonNullable]>>>> : TData extends object ? { [K in keyof TData]: UnwrapFragmentRefs; diff --git a/.api-reports/api-report.api.md b/.api-reports/api-report.api.md index ba939adbf2d..f56f037b9df 100644 --- a/.api-reports/api-report.api.md +++ b/.api-reports/api-report.api.md @@ -2882,7 +2882,7 @@ type unionToIntersection = (T extends unknown ? (x: T) => unknown : never) ex export type Unmasked = true extends IsAny ? TData : TData extends object ? true extends ContainsFragmentsRefs ? UnwrapFragmentRefs>> : TData : TData; // @public (undocumented) -type UnwrapFragmentRefs = true extends IsAny ? TData : TData extends any ? string extends keyof TData ? TData : keyof TData extends never ? TData : TData extends { +type UnwrapFragmentRefs = true extends IsAny ? TData : TData extends any ? TData extends Primitive ? TData : string extends keyof TData ? TData : keyof TData extends never ? TData : TData extends { " $fragmentRefs"?: infer FragmentRefs; } ? UnwrapFragmentRefs | RemoveFragmentName[keyof NonNullable]>>>> : TData extends object ? { [K in keyof TData]: UnwrapFragmentRefs; diff --git a/.changeset/tiny-ants-bow.md b/.changeset/tiny-ants-bow.md new file mode 100644 index 00000000000..562c69be6a8 --- /dev/null +++ b/.changeset/tiny-ants-bow.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +Fix handling of tagged/branded primitive types when used as scalar values with `Unmasked`. diff --git a/src/masking/__benches__/types.bench.ts b/src/masking/__benches__/types.bench.ts index 1ea136c1d6e..1feb1550401 100644 --- a/src/masking/__benches__/types.bench.ts +++ b/src/masking/__benches__/types.bench.ts @@ -627,3 +627,36 @@ test("MaybeMasked can be called with a generic if `mode` is not set to `unmask`" withGenericResult({} as any); withGenericDocument({} as any); }); + +test("Unmasked handles branded primitive types", (prefix) => { + type Branded = T & { __branded?: Name }; + type UUID = Branded; + type Source = { + __typename: "Foo"; + id: UUID; + name: string; + } & { " $fragmentName"?: "UserFieldsFragment" } & { + " $fragmentRefs"?: { + FooFragment: FooFragment; + }; + }; + type FooFragment = { + __typename: "Foo"; + age: number; + } & { " $fragmentName"?: "UserFieldsFragment" }; + + bench(prefix + "instantiations", () => { + return {} as Unmasked; + }).types([5, "instantiations"]); + + bench(prefix + "functionality", () => { + const x = {} as Unmasked; + + expectTypeOf(x).branded.toEqualTypeOf<{ + __typename: "Foo"; + id: UUID; + name: string; + age: number; + }>(); + }); +}); diff --git a/src/masking/internal/types.ts b/src/masking/internal/types.ts index 828734a8934..0f893bb8e6d 100644 --- a/src/masking/internal/types.ts +++ b/src/masking/internal/types.ts @@ -1,11 +1,17 @@ -import type { Prettify, RemoveIndexSignature } from "../../utilities/index.js"; +import type { + Prettify, + Primitive, + RemoveIndexSignature, +} from "../../utilities/index.js"; export type IsAny = 0 extends 1 & T ? true : false; export type UnwrapFragmentRefs = true extends IsAny ? TData : TData extends any ? - // Leave TData alone if it is Record and not a specific shape + // Ensure tagged/branded types are left alone (i.e. type UUID = string & { ... }) + TData extends Primitive ? TData + : // Leave TData alone if it is Record and not a specific shape string extends keyof TData ? TData : // short-circuit on empty object keyof TData extends never ? TData diff --git a/src/utilities/index.ts b/src/utilities/index.ts index f4e7705deb6..cf2bfc5f07e 100644 --- a/src/utilities/index.ts +++ b/src/utilities/index.ts @@ -137,6 +137,7 @@ export type { DeepOmit } from "./types/DeepOmit.js"; export type { DeepPartial } from "./types/DeepPartial.js"; export type { OnlyRequiredProperties } from "./types/OnlyRequiredProperties.js"; export type { Prettify } from "./types/Prettify.js"; +export type { Primitive } from "./types/Primitive.js"; export type { UnionToIntersection } from "./types/UnionToIntersection.js"; export type { NoInfer } from "./types/NoInfer.js"; export type { RemoveIndexSignature } from "./types/RemoveIndexSignature.js"; From a4ae9b41d58cf363fec4bfd5d8c4a2e4b8126c48 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 14 Jan 2025 10:50:19 -0700 Subject: [PATCH 3/9] Version Packages (#12269) Co-authored-by: github-actions[bot] --- .changeset/beige-eggs-nail.md | 5 ----- .changeset/tiny-ants-bow.md | 5 ----- CHANGELOG.md | 8 ++++++++ package-lock.json | 4 ++-- package.json | 2 +- 5 files changed, 11 insertions(+), 13 deletions(-) delete mode 100644 .changeset/beige-eggs-nail.md delete mode 100644 .changeset/tiny-ants-bow.md diff --git a/.changeset/beige-eggs-nail.md b/.changeset/beige-eggs-nail.md deleted file mode 100644 index 73dfcefcd7b..00000000000 --- a/.changeset/beige-eggs-nail.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@apollo/client": patch ---- - -Maintain the `TData` type when used with `Unmasked` when `TData` is not a masked type generated from GraphQL Codegen. diff --git a/.changeset/tiny-ants-bow.md b/.changeset/tiny-ants-bow.md deleted file mode 100644 index 562c69be6a8..00000000000 --- a/.changeset/tiny-ants-bow.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@apollo/client": patch ---- - -Fix handling of tagged/branded primitive types when used as scalar values with `Unmasked`. diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f94749fc14..971e53bdf72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # @apollo/client +## 3.12.6 + +### Patch Changes + +- [#12267](https://github.com/apollographql/apollo-client/pull/12267) [`d57429d`](https://github.com/apollographql/apollo-client/commit/d57429df336412bfdce5fc92b8299360c522d121) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Maintain the `TData` type when used with `Unmasked` when `TData` is not a masked type generated from GraphQL Codegen. + +- [#12270](https://github.com/apollographql/apollo-client/pull/12270) [`3601246`](https://github.com/apollographql/apollo-client/commit/3601246f6e7b4f8b2937e0d431e6b5a6964f9066) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Fix handling of tagged/branded primitive types when used as scalar values with `Unmasked`. + ## 3.12.5 ### Patch Changes diff --git a/package-lock.json b/package-lock.json index 0c256410e09..bb622e36c3e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@apollo/client", - "version": "3.12.5", + "version": "3.12.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@apollo/client", - "version": "3.12.5", + "version": "3.12.6", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 70f0c82c577..c4652499024 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/client", - "version": "3.12.5", + "version": "3.12.6", "description": "A fully-featured caching GraphQL client.", "private": true, "keywords": [ From cfb1ecdf60bf0f87b43358d4f048e10b480ceee9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 14 Jan 2025 11:04:02 -0700 Subject: [PATCH 4/9] chore(deps): update cimg/node docker tag to v23.6.0 (#12263) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .circleci/config.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 063b2abecc1..c8751534afe 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -15,7 +15,7 @@ jobs: Lint: docker: - - image: cimg/node:23.5.0 + - image: cimg/node:23.6.0 steps: - checkout - run: npm version @@ -24,7 +24,7 @@ jobs: Formatting: docker: - - image: cimg/node:23.5.0 + - image: cimg/node:23.6.0 steps: - checkout - run: npm ci @@ -32,7 +32,7 @@ jobs: Tests: docker: - - image: cimg/node:23.5.0 + - image: cimg/node:23.6.0 parameters: project: type: string @@ -53,7 +53,7 @@ jobs: path: reports/junit Attest: docker: - - image: cimg/node:23.5.0 + - image: cimg/node:23.6.0 steps: - checkout - run: npm ci @@ -61,7 +61,7 @@ jobs: BuildTarball: docker: - - image: cimg/node:23.5.0 + - image: cimg/node:23.6.0 steps: - checkout - run: npm run ci:precheck @@ -80,7 +80,7 @@ jobs: react: type: string docker: - - image: cimg/node:23.5.0-browsers + - image: cimg/node:23.6.0-browsers steps: - checkout - attach_workspace: @@ -118,7 +118,7 @@ jobs: externalPackage: type: string docker: - - image: cimg/node:23.5.0 + - image: cimg/node:23.6.0 steps: - checkout - attach_workspace: From d548a3cfbc32c6bd39862721c8493a55e74863a4 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 16 Jan 2025 10:31:33 -0700 Subject: [PATCH 5/9] Update `useLazyQuery` tests for robustness (#12260) --- .../hooks/__tests__/useLazyQuery.test.tsx | 2359 +++++++++++------ src/testing/matchers/index.d.ts | 9 +- src/testing/matchers/index.ts | 2 + src/testing/matchers/toEqualQueryResult.ts | 72 + 4 files changed, 1649 insertions(+), 793 deletions(-) create mode 100644 src/testing/matchers/toEqualQueryResult.ts diff --git a/src/react/hooks/__tests__/useLazyQuery.test.tsx b/src/react/hooks/__tests__/useLazyQuery.test.tsx index ed99fdd8905..802638fef98 100644 --- a/src/react/hooks/__tests__/useLazyQuery.test.tsx +++ b/src/react/hooks/__tests__/useLazyQuery.test.tsx @@ -25,13 +25,16 @@ import { import { useLazyQuery } from "../useLazyQuery"; import { QueryResult } from "../../types/types"; import { InvariantError } from "../../../utilities/globals"; -import { Masked, MaskedDocumentNode } from "../../../masking"; +import { Masked, MaskedDocumentNode, Unmasked } from "../../../masking"; import { expectTypeOf } from "expect-type"; import { disableActEnvironment, renderHookToSnapshotStream, } from "@testing-library/react-render-stream"; +const IS_REACT_17 = React.version.startsWith("17"); +const IS_REACT_18 = React.version.startsWith("18"); + describe("useLazyQuery Hook", () => { const helloQuery: TypedDocumentNode<{ hello: string; @@ -49,60 +52,6 @@ describe("useLazyQuery Hook", () => { delay: 50, }, ]; - const { result } = renderHook(() => useLazyQuery(helloQuery), { - wrapper: ({ children }) => ( - {children} - ), - }); - - expect(result.current[1].loading).toBe(false); - expect(result.current[1].data).toBe(undefined); - const execute = result.current[0]; - setTimeout(() => execute()); - - await waitFor( - () => { - expect(result.current[1].loading).toBe(true); - }, - { interval: 1 } - ); - - await waitFor( - () => { - expect(result.current[1].loading).toBe(false); - }, - { interval: 1 } - ); - expect(result.current[1].data).toEqual({ hello: "world" }); - }); - - it("should set `called` to false by default", async () => { - const mocks = [ - { - request: { query: helloQuery }, - result: { data: { hello: "world" } }, - delay: 20, - }, - ]; - const { result } = renderHook(() => useLazyQuery(helloQuery), { - wrapper: ({ children }) => ( - {children} - ), - }); - - expect(result.current[1].loading).toBe(false); - expect(result.current[1].data).toBe(undefined); - expect(result.current[1].called).toBe(false); - }); - - it("should set `called` to true after calling the lazy execute function", async () => { - const mocks = [ - { - request: { query: helloQuery }, - result: { data: { hello: "world" } }, - delay: 20, - }, - ]; using _disabledAct = disableActEnvironment(); const { takeSnapshot, getCurrentSnapshot } = @@ -114,27 +63,66 @@ describe("useLazyQuery Hook", () => { { const [, result] = await takeSnapshot(); - expect(result.loading).toBe(false); - expect(result.called).toBe(false); + + expect(result).toEqualQueryResult({ + data: undefined, + error: undefined, + called: false, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); } - const execute = getCurrentSnapshot()[0]; + const [execute] = getCurrentSnapshot(); + setTimeout(() => execute()); { const [, result] = await takeSnapshot(); - expect(result.loading).toBe(true); - expect(result.called).toBe(true); + + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); } { const [, result] = await takeSnapshot(); - expect(result.loading).toBe(false); - expect(result.called).toBe(true); + + expect(result).toEqualQueryResult({ + data: { hello: "world" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); } }); - it("should override `skip` if lazy mode execution function is called", async () => { + it("should set `called` to false by default", async () => { + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( + () => useLazyQuery(helloQuery), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + const [, { called }] = await takeSnapshot(); + + expect(called).toBe(false); + }); + + it("should set `called` to true after calling the lazy execute function", async () => { const mocks = [ { request: { query: helloQuery }, @@ -145,34 +133,31 @@ describe("useLazyQuery Hook", () => { using _disabledAct = disableActEnvironment(); const { takeSnapshot, getCurrentSnapshot } = - await renderHookToSnapshotStream( - // skip isn’t actually an option on the types - () => useLazyQuery(helloQuery, { skip: true } as any), - { - wrapper: ({ children }) => ( - {children} - ), - } - ); + await renderHookToSnapshotStream(() => useLazyQuery(helloQuery), { + wrapper: ({ children }) => ( + {children} + ), + }); { - const [, result] = await takeSnapshot(); - expect(result.loading).toBe(false); - expect(result.called).toBe(false); + const [, { loading, called }] = await takeSnapshot(); + expect(loading).toBe(false); + expect(called).toBe(false); } + const execute = getCurrentSnapshot()[0]; setTimeout(() => execute()); { - const [, result] = await takeSnapshot(); - expect(result.loading).toBe(true); - expect(result.called).toBe(true); + const [, { loading, called }] = await takeSnapshot(); + expect(loading).toBe(true); + expect(called).toBe(true); } { - const [, result] = await takeSnapshot(); - expect(result.loading).toBe(false); - expect(result.called).toBe(true); + const [, { loading, called }] = await takeSnapshot(); + expect(loading).toBe(false); + expect(called).toBe(true); } }); @@ -207,7 +192,16 @@ describe("useLazyQuery Hook", () => { { const [, result] = await takeSnapshot(); - expect(result.loading).toBe(false); + + expect(result).toEqualQueryResult({ + data: undefined, + error: undefined, + called: false, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: { id: 1 }, + }); } const execute = getCurrentSnapshot()[0]; @@ -215,12 +209,27 @@ describe("useLazyQuery Hook", () => { { const [, result] = await takeSnapshot(); - expect(result.loading).toBe(true); + + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: { id: 1 }, + }); } { const [, result] = await takeSnapshot(); - expect(result.loading).toBe(false); - expect(result.data).toEqual({ hello: "world 1" }); + + expect(result).toEqualQueryResult({ + data: { hello: "world 1" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: { id: 1 }, + }); } }); @@ -244,41 +253,69 @@ describe("useLazyQuery Hook", () => { }, ]; - const { result } = renderHook( - () => - useLazyQuery(query, { - variables: { id: 1 }, - }), - { - wrapper: ({ children }) => ( - {children} - ), - } - ); + using _disabledAct = disableActEnvironment(); + const { takeSnapshot, getCurrentSnapshot } = + await renderHookToSnapshotStream( + () => + useLazyQuery(query, { + variables: { id: 1 }, + }), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const [, result] = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: undefined, + error: undefined, + called: false, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: { id: 1 }, + }); + } - const execute = result.current[0]; + const [execute] = getCurrentSnapshot(); setTimeout(() => execute({ variables: { id: 2 } })); - await waitFor( - () => { - expect(result.current[1].loading).toBe(true); - }, - { interval: 1 } - ); + { + const [, result] = await takeSnapshot(); - await waitFor( - () => { - expect(result.current[1].loading).toBe(false); - }, - { interval: 1 } - ); - expect(result.current[1].data).toEqual({ hello: "world 2" }); + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: { id: 2 }, + }); + } + + { + const [, result] = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: { hello: "world 2" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: { id: 2 }, + }); + } }); it("should merge variables from original hook and execution function", async () => { const counterQuery: TypedDocumentNode< { counter: number; + vars: Record; }, { hookVar?: boolean; @@ -312,15 +349,15 @@ describe("useLazyQuery Hook", () => { (request) => new Observable((observer) => { if (request.operationName === "GetCounter") { - observer.next({ - data: { - counter: ++count, - vars: request.variables, - }, - }); setTimeout(() => { + observer.next({ + data: { + counter: ++count, + vars: request.variables, + }, + }); observer.complete(); - }, 10); + }, 50); } else { observer.error( new Error( @@ -332,49 +369,46 @@ describe("useLazyQuery Hook", () => { ), }); - const { result } = renderHook( - () => { - const [exec, query] = useLazyQuery(counterQuery, { - notifyOnNetworkStatusChange: true, - variables: { - hookVar: true, - }, - defaultOptions: { + using __disabledAct = disableActEnvironment(); + const { takeSnapshot, getCurrentSnapshot } = + await renderHookToSnapshotStream( + () => { + return useLazyQuery(counterQuery, { + notifyOnNetworkStatusChange: true, variables: { - localDefaultVar: true, + hookVar: true, }, - }, - }); - return { - exec, - query, - }; - }, - { - wrapper: ({ children }) => ( - {children} - ), - } - ); + defaultOptions: { + variables: { + localDefaultVar: true, + }, + }, + }); + }, + { + wrapper: ({ children }) => ( + {children} + ), + } + ); - await waitFor( - () => { - expect(result.current.query.loading).toBe(false); - }, - { interval: 1 } - ); - await waitFor( - () => { - expect(result.current.query.called).toBe(false); - }, - { interval: 1 } - ); - await waitFor( - () => { - expect(result.current.query.data).toBeUndefined(); - }, - { interval: 1 } - ); + { + const [, result] = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: undefined, + error: undefined, + called: false, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: { + globalDefaultVar: true, + localDefaultVar: true, + hookVar: true, + }, + }); + } const expectedFinalData = { counter: 1, @@ -386,154 +420,185 @@ describe("useLazyQuery Hook", () => { }, }; - let execResult: QueryResult; - await act(async () => { - execResult = await result.current.exec({ + const [execute] = getCurrentSnapshot(); + const execResult = await execute({ + variables: { + execVar: true, + }, + }); + + // TODO: Determine if the return value makes sense. Other fetching functions + // (`refetch`, `fetchMore`, etc.) resolve with an `ApolloQueryResult` type + // which contain a subset of this data. + expect(execResult).toEqualQueryResult({ + data: expectedFinalData, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: { + globalDefaultVar: true, + localDefaultVar: true, + hookVar: true, + execVar: true, + }, + }); + + { + const [, result] = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, variables: { + globalDefaultVar: true, + localDefaultVar: true, + hookVar: true, execVar: true, }, }); - }); + } - await waitFor( - () => { - expect(execResult.loading).toBe(false); - }, - { interval: 1 } - ); - await waitFor( - () => { - expect(execResult.called).toBe(true); - }, - { interval: 1 } - ); - await waitFor( - () => { - expect(execResult.networkStatus).toBe(NetworkStatus.ready); - }, - { interval: 1 } - ); - await waitFor( - () => { - expect(execResult.data).toEqual(expectedFinalData); - }, - { interval: 1 } - ); - await waitFor( - () => { - expect(result.current.query.called).toBe(true); - }, - { interval: 1 } - ); - await waitFor( - () => { - expect(result.current.query.loading).toBe(false); - }, - { interval: 10 } - ); + { + const [, result] = await takeSnapshot(); - expect(result.current.query.called).toBe(true); - expect(result.current.query.data).toEqual(expectedFinalData); + expect(result).toEqualQueryResult({ + data: expectedFinalData, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: { + globalDefaultVar: true, + localDefaultVar: true, + hookVar: true, + execVar: true, + }, + }); + } - const refetchPromise = result.current.query.reobserve({ + const refetchResult = await getCurrentSnapshot()[1].reobserve({ fetchPolicy: "network-only", nextFetchPolicy: "cache-first", variables: { execVar: false, }, }); - await act(() => refetchPromise); - const refetchResult = await refetchPromise; - expect(refetchResult.loading).toBe(false); - expect(refetchResult.data).toEqual({ - counter: 2, - vars: { - execVar: false, - }, + expect(refetchResult).toEqual({ + data: { counter: 2, vars: { execVar: false } }, + loading: false, + networkStatus: NetworkStatus.ready, }); - await waitFor( - () => { - expect(result.current.query.loading).toBe(false); - }, - { interval: 1 } - ); - await waitFor( - () => { - expect(result.current.query.called).toBe(true); + { + const [, result] = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: expectedFinalData, + called: true, + loading: true, + networkStatus: NetworkStatus.setVariables, + previousData: expectedFinalData, + variables: { execVar: false }, + }); + } + + { + const [, result] = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: { counter: 2, vars: { execVar: false } }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: expectedFinalData, + variables: { execVar: false }, + }); + } + + const execResult2 = await getCurrentSnapshot()[0]({ + fetchPolicy: "cache-and-network", + nextFetchPolicy: "cache-first", + variables: { + execVar: true, }, - { interval: 1 } - ); - await waitFor( - () => { - expect(result.current.query.data).toEqual({ - counter: 2, - vars: { - execVar: false, - }, - }); + }); + + expect(execResult2).toEqualQueryResult({ + data: { counter: 3, vars: { ...expectedFinalData.vars, execVar: true } }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { counter: 2, vars: { execVar: false } }, + variables: { + globalDefaultVar: true, + localDefaultVar: true, + hookVar: true, + execVar: true, }, - { interval: 1 } - ); + }); - let execResult2: QueryResult; - await act(async () => { - execResult2 = await result.current.exec({ - fetchPolicy: "cache-and-network", - nextFetchPolicy: "cache-first", + { + const [, result] = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: { counter: 2, vars: { execVar: false } }, + called: true, + loading: true, + networkStatus: NetworkStatus.setVariables, + previousData: { counter: 2, vars: { execVar: false } }, variables: { + globalDefaultVar: true, + localDefaultVar: true, + hookVar: true, execVar: true, }, }); - }); + } - await waitFor( - () => { - expect(execResult2.loading).toBe(false); - }, - { interval: 1 } - ); - await waitFor( - () => { - expect(execResult2.called).toBe(true); - }, - { interval: 1 } - ); - await waitFor( - () => { - expect(execResult2.data).toEqual({ - counter: 3, - vars: { - ...expectedFinalData.vars, - execVar: true, - }, - }); - }, - { interval: 1 } - ); + // For some reason we get an extra render in React 18 of the same thing + if (IS_REACT_18) { + const [, result] = await takeSnapshot(); - await waitFor( - () => { - expect(result.current.query.called).toBe(true); - }, - { interval: 1 } - ); + expect(result).toEqualQueryResult({ + data: { counter: 2, vars: { execVar: false } }, + called: true, + loading: true, + networkStatus: NetworkStatus.setVariables, + previousData: { counter: 2, vars: { execVar: false } }, + variables: { + globalDefaultVar: true, + localDefaultVar: true, + hookVar: true, + execVar: true, + }, + }); + } - await waitFor( - () => { - expect(result.current.query.loading).toBe(false); - }, - { interval: 10 } - ); - expect(result.current.query.called).toBe(true); - expect(result.current.query.data).toEqual({ - counter: 3, - vars: { - ...expectedFinalData.vars, - execVar: true, - }, - }); + { + const [, result] = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: { + counter: 3, + vars: { ...expectedFinalData.vars, execVar: true }, + }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { counter: 2, vars: { execVar: false } }, + variables: { + globalDefaultVar: true, + localDefaultVar: true, + hookVar: true, + execVar: true, + }, + }); + } }); it("changing queries", async () => { @@ -573,34 +638,72 @@ describe("useLazyQuery Hook", () => { { const [, result] = await takeSnapshot(); - expect(result.loading).toBe(false); - expect(result.data).toBe(undefined); + + expect(result).toEqualQueryResult({ + data: undefined, + error: undefined, + called: false, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); } - const execute = getCurrentSnapshot()[0]; + const execute = getCurrentSnapshot()[0]; setTimeout(() => execute()); { const [, result] = await takeSnapshot(); - expect(result.loading).toBe(true); + + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); } { const [, result] = await takeSnapshot(); - expect(result.loading).toBe(false); - expect(result.data).toEqual({ hello: "world" }); + + expect(result).toEqualQueryResult({ + data: { hello: "world" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); } setTimeout(() => execute({ query: query2 })); { const [, result] = await takeSnapshot(); - expect(result.loading).toBe(true); + + expect(result).toEqualQueryResult({ + data: { hello: "world" }, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: { hello: "world" }, + variables: {}, + }); } { const [, result] = await takeSnapshot(); - expect(result.loading).toBe(false); - expect(result.data).toEqual({ name: "changed" }); + + expect(result).toEqualQueryResult({ + data: { name: "changed" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { hello: "world" }, + variables: {}, + }); } }); @@ -634,33 +737,71 @@ describe("useLazyQuery Hook", () => { { const [, result] = await takeSnapshot(); - expect(result.loading).toBe(false); + + expect(result).toEqualQueryResult({ + data: undefined, + called: false, + error: undefined, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); } const execute = getCurrentSnapshot()[0]; setTimeout(() => execute()); { const [, result] = await takeSnapshot(); - expect(result.loading).toBe(true); + + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); } { const [, result] = await takeSnapshot(); - expect(result.loading).toBe(false); - expect(result.data).toEqual({ hello: "world 1" }); + + expect(result).toEqualQueryResult({ + data: { hello: "world 1" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); } setTimeout(() => execute()); { const [, result] = await takeSnapshot(); - expect(result.loading).toBe(true); - expect(result.data).toEqual({ hello: "world 1" }); + + expect(result).toEqualQueryResult({ + data: { hello: "world 1" }, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: { hello: "world 1" }, + variables: {}, + }); } { const [, result] = await takeSnapshot(); - expect(result.loading).toBe(false); - expect(result.data).toEqual({ hello: "world 2" }); + + expect(result).toEqualQueryResult({ + data: { hello: "world 2" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { hello: "world 1" }, + variables: {}, + }); } }); @@ -694,40 +835,72 @@ describe("useLazyQuery Hook", () => { { const [, result] = await takeSnapshot(); - expect(result.loading).toBe(false); - expect(result.data).toBe(undefined); - expect(result.previousData).toBe(undefined); + + expect(result).toEqualQueryResult({ + data: undefined, + error: undefined, + called: false, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); } const execute = getCurrentSnapshot()[0]; setTimeout(() => execute()); { const [, result] = await takeSnapshot(); - expect(result.loading).toBe(true); - expect(result.data).toBe(undefined); - expect(result.previousData).toBe(undefined); + + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); } { const [, result] = await takeSnapshot(); - expect(result.loading).toBe(false); - expect(result.data).toEqual({ hello: "world 1" }); - expect(result.previousData).toBe(undefined); + + expect(result).toEqualQueryResult({ + data: { hello: "world 1" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); } const refetch = getCurrentSnapshot()[1].refetch; setTimeout(() => refetch!()); + { const [, result] = await takeSnapshot(); - expect(result.loading).toBe(true); - expect(result.data).toEqual({ hello: "world 1" }); - expect(result.previousData).toEqual({ hello: "world 1" }); + + expect(result).toEqualQueryResult({ + data: { hello: "world 1" }, + called: true, + loading: true, + networkStatus: NetworkStatus.refetch, + previousData: { hello: "world 1" }, + variables: {}, + }); } { const [, result] = await takeSnapshot(); - expect(result.loading).toBe(false); - expect(result.data).toEqual({ hello: "world 2" }); - expect(result.previousData).toEqual({ hello: "world 1" }); + + expect(result).toEqualQueryResult({ + data: { hello: "world 2" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { hello: "world 1" }, + variables: {}, + }); } }); @@ -760,8 +933,16 @@ describe("useLazyQuery Hook", () => { { const [, result] = await takeSnapshot(); - expect(result.loading).toBe(false); - expect(result.data).toBe(undefined); + + expect(result).toEqualQueryResult({ + data: undefined, + error: undefined, + called: false, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); } await tick(); @@ -769,25 +950,54 @@ describe("useLazyQuery Hook", () => { { const [, result] = await takeSnapshot(); - expect(result.loading).toBe(true); + + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); } { const [, result] = await takeSnapshot(); - expect(result.loading).toBe(false); - expect(result.data).toEqual({ hello: "world 1" }); + + expect(result).toEqualQueryResult({ + data: { hello: "world 1" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); } { const [, result] = await takeSnapshot(); - expect(result.loading).toBe(false); - expect(result.data).toEqual({ hello: "world 2" }); + + expect(result).toEqualQueryResult({ + data: { hello: "world 2" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { hello: "world 1" }, + variables: {}, + }); } { const [, result] = await takeSnapshot(); - expect(result.loading).toBe(false); - expect(result.data).toEqual({ hello: "world 3" }); + + expect(result).toEqualQueryResult({ + data: { hello: "world 3" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { hello: "world 2" }, + variables: {}, + }); } getCurrentSnapshot()[1].stopPolling(); @@ -844,39 +1054,70 @@ describe("useLazyQuery Hook", () => { { const [, result] = await takeSnapshot(); - expect(result.loading).toBe(false); - expect(result.data).toBe(undefined); - expect(result.previousData).toBe(undefined); + + expect(result).toEqualQueryResult({ + data: undefined, + error: undefined, + called: false, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); } const execute = getCurrentSnapshot()[0]; setTimeout(() => execute({ variables: { id: 1 } })); { const [, result] = await takeSnapshot(); - expect(result.loading).toBe(true); - expect(result.data).toBe(undefined); - expect(result.previousData).toBe(undefined); + + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: { id: 1 }, + }); } { const [, result] = await takeSnapshot(); - expect(result.loading).toBe(false); - expect(result.data).toEqual(data1); - expect(result.previousData).toBe(undefined); + + expect(result).toEqualQueryResult({ + data: data1, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: { id: 1 }, + }); } setTimeout(() => execute({ variables: { id: 2 } })); { const [, result] = await takeSnapshot(); - expect(result.loading).toBe(true); - expect(result.data).toBe(undefined); - expect(result.previousData).toEqual(data1); + + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.setVariables, + previousData: data1, + variables: { id: 2 }, + }); } { const [, result] = await takeSnapshot(); - expect(result.loading).toBe(false); - expect(result.data).toEqual(data2); - expect(result.previousData).toEqual(data1); + + expect(result).toEqualQueryResult({ + data: data2, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: data1, + variables: { id: 2 }, + }); } }); @@ -895,37 +1136,61 @@ describe("useLazyQuery Hook", () => { cache.writeQuery({ query: helloQuery, data: { hello: "from cache" } }); - const { result } = renderHook( - () => useLazyQuery(helloQuery, { fetchPolicy: "cache-and-network" }), - { - wrapper: ({ children }) => ( - {children} - ), - } - ); + using _disabledAct = disableActEnvironment(); + const { takeSnapshot, getCurrentSnapshot } = + await renderHookToSnapshotStream( + () => useLazyQuery(helloQuery, { fetchPolicy: "cache-and-network" }), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const [, result] = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: undefined, + error: undefined, + called: false, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); + } + + const [execute] = getCurrentSnapshot(); - expect(result.current[1].loading).toBe(false); - expect(result.current[1].data).toBe(undefined); - const execute = result.current[0]; setTimeout(() => execute()); - await waitFor( - () => { - expect(result.current[1].loading).toBe(true); - }, - { interval: 1 } - ); + { + const [, result] = await takeSnapshot(); - // TODO: FIXME - expect(result.current[1].data).toEqual({ hello: "from cache" }); + expect(result).toEqualQueryResult({ + // TODO: FIXME + data: { hello: "from cache" }, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); + } - await waitFor( - () => { - expect(result.current[1].loading).toBe(false); - }, - { interval: 1 } - ); - expect(result.current[1].data).toEqual({ hello: "from link" }); + { + const [, result] = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: { hello: "from link" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { hello: "from cache" }, + variables: {}, + }); + } }); it("should return a promise from the execution function which resolves with the result", async () => { @@ -936,39 +1201,67 @@ describe("useLazyQuery Hook", () => { delay: 20, }, ]; - const { result } = renderHook(() => useLazyQuery(helloQuery), { - wrapper: ({ children }) => ( - {children} - ), - }); - expect(result.current[1].loading).toBe(false); - expect(result.current[1].data).toBe(undefined); - const execute = result.current[0]; + using _disabledAct = disableActEnvironment(); + const { takeSnapshot, getCurrentSnapshot } = + await renderHookToSnapshotStream(() => useLazyQuery(helloQuery), { + wrapper: ({ children }) => ( + {children} + ), + }); + + { + const [, result] = await takeSnapshot(); - const executeResult = new Promise>((resolve) => { - setTimeout(() => resolve(execute())); - }); + expect(result).toEqualQueryResult({ + data: undefined, + error: undefined, + called: false, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); + } - await waitFor( - () => { - expect(result.current[1].loading).toBe(true); - }, - { interval: 1 } - ); + const [execute] = getCurrentSnapshot(); - let latestRenderResult: QueryResult; - await waitFor(() => { - latestRenderResult = result.current[1]; - expect(latestRenderResult.loading).toBe(false); - }); - await waitFor(() => { - latestRenderResult = result.current[1]; - expect(latestRenderResult.data).toEqual({ hello: "world" }); - }); + await tick(); + const executeResult = execute(); + + { + const [, result] = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); + } + + { + const [, result] = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: { hello: "world" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); + } - return executeResult.then((finalResult) => { - expect(finalResult).toEqual(latestRenderResult); + await expect(executeResult).resolves.toEqualQueryResult({ + data: { hello: "world" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, }); }); @@ -1019,76 +1312,109 @@ describe("useLazyQuery Hook", () => { }, ]; - const { result } = renderHook(() => useLazyQuery(query), { - wrapper: ({ children }) => ( - {children} - ), - }); + using _disabledAct = disableActEnvironment(); + const { takeSnapshot, getCurrentSnapshot } = + await renderHookToSnapshotStream(() => useLazyQuery(query), { + wrapper: ({ children }) => ( + {children} + ), + }); - expect(result.current[1].loading).toBe(false); - expect(result.current[1].data).toBe(undefined); - const execute = result.current[0]; - let executeResult: any; - setTimeout(() => { - executeResult = execute({ variables: { filter: "PA" } }); - }); + { + const [, result] = await takeSnapshot(); - await waitFor( - () => { - expect(result.current[1].loading).toBe(true); - }, - { interval: 1 } - ); + expect(result).toEqualQueryResult({ + data: undefined, + error: undefined, + called: false, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); + } - await waitFor( - () => { - expect(result.current[1].loading).toBe(false); - }, - { interval: 1 } - ); - expect(result.current[1].data).toEqual({ - countries: { - code: "PA", - name: "Panama", - }, - }); + const [execute] = getCurrentSnapshot(); + + await tick(); + let executeResult = execute({ variables: { filter: "PA" } }); + + { + const [, result] = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: { filter: "PA" }, + }); + } + + { + const [, result] = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: { countries: { code: "PA", name: "Panama" } }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: { filter: "PA" }, + }); + } - expect(executeResult).toBeInstanceOf(Promise); - expect((await executeResult).data).toEqual({ - countries: { - code: "PA", - name: "Panama", + await expect(executeResult).resolves.toEqualQueryResult({ + data: { + countries: { + code: "PA", + name: "Panama", + }, }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: { filter: "PA" }, }); - setTimeout(() => { - executeResult = execute({ variables: { filter: "BA" } }); - }); + await tick(); + executeResult = execute({ variables: { filter: "BA" } }); - await waitFor( - () => { - expect(result.current[1].loading).toBe(false); - }, - { interval: 1 } - ); - await waitFor( - () => { - expect(result.current[1].data).toEqual({ - countries: { - code: "BA", - name: "Bahamas", - }, - }); - }, - { interval: 1 } - ); + { + const [, result] = await takeSnapshot(); - expect(executeResult).toBeInstanceOf(Promise); - expect((await executeResult).data).toEqual({ - countries: { - code: "BA", - name: "Bahamas", - }, + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.setVariables, + previousData: { countries: { code: "PA", name: "Panama" } }, + variables: { filter: "BA" }, + }); + } + + { + const [, result] = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: { countries: { code: "BA", name: "Bahamas" } }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { countries: { code: "PA", name: "Panama" } }, + variables: { filter: "BA" }, + }); + } + + await expect(executeResult).resolves.toEqualQueryResult({ + data: { countries: { code: "BA", name: "Bahamas" } }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { countries: { code: "PA", name: "Panama" } }, + variables: { filter: "BA" }, }); }); @@ -1124,52 +1450,88 @@ describe("useLazyQuery Hook", () => { { const [, result] = await takeSnapshot(); - expect(result.loading).toBe(false); - expect(result.data).toBeUndefined(); + + expect(result).toEqualQueryResult({ + data: undefined, + error: undefined, + called: false, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); } const executePromise = Promise.resolve().then(() => execute()); { const [, result] = await takeSnapshot(); - expect(result.loading).toBe(true); - expect(result.data).toBeUndefined(); - expect(result.error).toBe(undefined); + + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); } { const [, result] = await takeSnapshot(); - expect(result.loading).toBe(false); - expect(result.data).toBeUndefined(); - expect(result.error).toEqual( - new ApolloError({ graphQLErrors: [{ message: "error 1" }] }) - ); + + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: false, + networkStatus: NetworkStatus.error, + previousData: undefined, + error: new ApolloError({ graphQLErrors: [{ message: "error 1" }] }), + variables: {}, + }); } - await executePromise.then((result) => { - expect(result.loading).toBe(false); - expect(result.data).toBeUndefined(); - expect(result.error!.message).toBe("error 1"); + await expect(executePromise).resolves.toEqualQueryResult({ + data: undefined, + called: true, + loading: false, + networkStatus: NetworkStatus.error, + previousData: undefined, + error: new ApolloError({ graphQLErrors: [{ message: "error 1" }] }), + errors: [{ message: "error 1" }], + variables: {}, }); void execute(); { const [, result] = await takeSnapshot(); - expect(result.loading).toBe(true); - expect(result.data).toBeUndefined(); - expect(result.error).toEqual( - new ApolloError({ graphQLErrors: [{ message: "error 1" }] }) - ); + + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + error: new ApolloError({ graphQLErrors: [{ message: "error 1" }] }), + // TODO: Why is this only populated when in loading state? + errors: [{ message: "error 1" }], + variables: {}, + }); } { const [, result] = await takeSnapshot(); - expect(result.loading).toBe(false); - expect(result.data).toBeUndefined(); - expect(result.error).toEqual( - new ApolloError({ graphQLErrors: [{ message: "error 2" }] }) - ); + + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: false, + networkStatus: NetworkStatus.error, + previousData: undefined, + error: new ApolloError({ graphQLErrors: [{ message: "error 2" }] }), + variables: {}, + }); } }); @@ -1180,31 +1542,38 @@ describe("useLazyQuery Hook", () => { result: { errors: [new GraphQLError("error 1")], }, + delay: 20, }, ]; - const { result } = renderHook(() => useLazyQuery(helloQuery), { - wrapper: ({ children }) => ( - {children} - ), - }); - - const execute = result.current[0]; - await waitFor( - () => { - expect(result.current[1].loading).toBe(false); - void execute(); - }, - { interval: 1 } - ); - await waitFor( - () => { - expect(result.current[1].data).toBe(undefined); - void execute(); - }, - { interval: 1 } + using _disabledAct = disableActEnvironment(); + const { takeSnapshot, peekSnapshot } = await renderHookToSnapshotStream( + () => useLazyQuery(helloQuery), + { + wrapper: ({ children }) => ( + {children} + ), + } ); + const [execute] = await peekSnapshot(); + + { + const [, result] = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: undefined, + error: undefined, + called: false, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); + } + + void execute(); + // Making sure the rejection triggers a test failure. await wait(50); }); @@ -1230,11 +1599,14 @@ describe("useLazyQuery Hook", () => { link.simulateResult({ result: { data: { hello: "Greetings" } } }, true); - const queryResult = await promise!; - - expect(queryResult.data).toEqual({ hello: "Greetings" }); - expect(queryResult.loading).toBe(false); - expect(queryResult.networkStatus).toBe(NetworkStatus.ready); + await expect(promise!).resolves.toEqualQueryResult({ + data: { hello: "Greetings" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); }); it("handles resolving multiple in-flight requests when component unmounts", async () => { @@ -1262,12 +1634,15 @@ describe("useLazyQuery Hook", () => { const expectedResult = { data: { hello: "Greetings" }, + called: true, loading: false, networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, }; - await expect(promise1!).resolves.toMatchObject(expectedResult); - await expect(promise2!).resolves.toMatchObject(expectedResult); + await expect(promise1!).resolves.toEqualQueryResult(expectedResult); + await expect(promise2!).resolves.toEqualQueryResult(expectedResult); }); // https://github.com/apollographql/apollo-client/issues/9755 @@ -1302,36 +1677,93 @@ describe("useLazyQuery Hook", () => { }, ]; - const { result } = renderHook(() => useLazyQuery(query), { - wrapper: ({ children }) => ( - {children} - ), - }); + using _disabledAct = disableActEnvironment(); + const { takeSnapshot, peekSnapshot } = await renderHookToSnapshotStream( + () => useLazyQuery(query), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); - const [execute] = result.current; + const [execute] = await peekSnapshot(); - await act(async () => { - const promise1 = execute({ variables: { id: "1" } }); - const promise2 = execute({ variables: { id: "2" } }); + { + const [, result] = await takeSnapshot(); - await expect(promise1).resolves.toMatchObject({ - ...mocks[0].result, + expect(result).toEqualQueryResult({ + data: undefined, + error: undefined, + called: false, loading: false, - called: true, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {} as Variables, }); + } - await expect(promise2).resolves.toMatchObject({ - ...mocks[1].result, - loading: false, - called: true, - }); + const promise1 = execute({ variables: { id: "1" } }); + const promise2 = execute({ variables: { id: "2" } }); + + await expect(promise1).resolves.toEqualQueryResult({ + data: mocks[0].result.data, + loading: false, + called: true, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: { id: "2" }, }); - expect(result.current[1]).toMatchObject({ - ...mocks[1].result, + await expect(promise2).resolves.toEqualQueryResult({ + data: mocks[1].result.data, loading: false, called: true, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: { id: "2" }, }); + + if (IS_REACT_17) { + const [, result] = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: { id: "1" }, + }); + } + + { + const [, result] = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.setVariables, + previousData: undefined, + variables: { id: "2" }, + }); + } + + { + const [, result] = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: mocks[1].result.data, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: { id: "2" }, + }); + } + + await expect(takeSnapshot).not.toRerender(); }); it("uses the most recent options when the hook rerenders before execution", async () => { @@ -1365,31 +1797,84 @@ describe("useLazyQuery Hook", () => { }, ]; - const { result, rerender } = renderHook( - ({ id }) => useLazyQuery(query, { variables: { id } }), - { - initialProps: { id: "1" }, - wrapper: ({ children }) => ( - {children} - ), - } - ); + using _disabledAct = disableActEnvironment(); + const { takeSnapshot, getCurrentSnapshot, rerender } = + await renderHookToSnapshotStream( + ({ id }) => useLazyQuery(query, { variables: { id } }), + { + initialProps: { id: "1" }, + wrapper: ({ children }) => ( + {children} + ), + } + ); - rerender({ id: "2" }); + { + const [, result] = await takeSnapshot(); - const [execute] = result.current; + expect(result).toEqualQueryResult({ + data: undefined, + error: undefined, + called: false, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: { id: "1" }, + }); + } - let promise: Promise>; - act(() => { - promise = execute(); - }); + await rerender({ id: "2" }); - await waitFor(() => { - expect(result.current[1].data).toEqual(mocks[1].result.data); - }); + { + const [, result] = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: undefined, + error: undefined, + called: false, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: { id: "1" }, + }); + } + + const [execute] = getCurrentSnapshot(); + const promise = execute(); + + { + const [, result] = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: { id: "2" }, + }); + } + + { + const [, result] = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: mocks[1].result.data, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: { id: "2" }, + }); + } - await expect(promise!).resolves.toMatchObject({ + await expect(promise).resolves.toEqualQueryResult({ data: mocks[1].result.data, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: { id: "2" }, }); }); @@ -1409,31 +1894,82 @@ describe("useLazyQuery Hook", () => { }, ]; - const { result, rerender } = renderHook( - ({ query }) => useLazyQuery(query), - { + using _disabledAct = disableActEnvironment(); + const { takeSnapshot, getCurrentSnapshot, rerender } = + await renderHookToSnapshotStream(({ query }) => useLazyQuery(query), { initialProps: { query }, wrapper: ({ children }) => ( {children} ), - } - ); + }); - rerender({ query: helloQuery }); + { + const [, result] = await takeSnapshot(); - const [execute] = result.current; + expect(result).toEqualQueryResult({ + data: undefined, + error: undefined, + called: false, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); + } - let promise: Promise>; - act(() => { - promise = execute(); - }); + await rerender({ query: helloQuery }); - await waitFor(() => { - expect(result.current[1].data).toEqual({ hello: "Greetings" }); - }); + { + const [, result] = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: undefined, + error: undefined, + called: false, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); + } + + const [execute] = getCurrentSnapshot(); + + const promise = execute(); + + { + const [, result] = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); + } - await expect(promise!).resolves.toMatchObject({ + { + const [, result] = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: { hello: "Greetings" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); + } + + await expect(promise).resolves.toEqualQueryResult({ data: { hello: "Greetings" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, }); }); @@ -1499,7 +2035,7 @@ describe("useLazyQuery Hook", () => { "does not issue multiple network calls when calling execute again without variables with a %s fetch policy", async (fetchPolicy) => { interface Data { - user: { id: string; name: string }; + user: { id: string | null; name: string }; } interface Variables { @@ -1555,8 +2091,13 @@ describe("useLazyQuery Hook", () => { expect(fetchCount).toBe(1); await waitFor(() => { - expect(result.current[1].data).toEqual({ - user: { id: "2", name: "John Doe" }, + expect(result.current[1]).toEqualQueryResult({ + data: { user: { id: "2", name: "John Doe" } }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: { id: "2" }, }); }); @@ -1565,8 +2106,13 @@ describe("useLazyQuery Hook", () => { await act(() => result.current[0]()); await waitFor(() => { - expect(result.current[1].data).toEqual({ - user: { id: null, name: "John Default" }, + expect(result.current[1]).toEqualQueryResult({ + data: { user: { id: null, name: "John Default" } }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { user: { id: "2", name: "John Doe" } }, + variables: {}, }); }); @@ -1617,50 +2163,103 @@ describe("useLazyQuery Hook", () => { const trackClosureValue = jest.fn(); - const { result, rerender } = renderHook( - () => { - let count = countRef.current; - - return useLazyQuery(query, { - fetchPolicy: "cache-first", - variables: { id: "1" }, - onCompleted: () => { - trackClosureValue("onCompleted", count); - }, - onError: () => { - trackClosureValue("onError", count); - }, - skipPollAttempt: () => { - trackClosureValue("skipPollAttempt", count); - return false; - }, - nextFetchPolicy: (currentFetchPolicy) => { - trackClosureValue("nextFetchPolicy", count); - return currentFetchPolicy; - }, - }); - }, - { - wrapper: ({ children }) => ( - {children} - ), - } - ); + using _disabledAct = disableActEnvironment(); + const { takeSnapshot, getCurrentSnapshot, rerender } = + await renderHookToSnapshotStream( + () => { + let count = countRef.current; + + return useLazyQuery(query, { + fetchPolicy: "cache-first", + variables: { id: "1" }, + onCompleted: () => { + trackClosureValue("onCompleted", count); + }, + onError: () => { + trackClosureValue("onError", count); + }, + skipPollAttempt: () => { + trackClosureValue("skipPollAttempt", count); + return false; + }, + nextFetchPolicy: (currentFetchPolicy) => { + trackClosureValue("nextFetchPolicy", count); + return currentFetchPolicy; + }, + }); + }, + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const [, result] = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: undefined, + error: undefined, + called: false, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: { id: "1" }, + }); + } - const [originalExecute] = result.current; + const [originalExecute] = getCurrentSnapshot(); countRef.current++; - rerender(); + // TODO: Update when https://github.com/testing-library/react-render-stream-testing-library/issues/13 is fixed + await rerender(undefined); + + { + const [, result] = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: undefined, + error: undefined, + called: false, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: { id: "1" }, + }); + } + + let [execute] = getCurrentSnapshot(); + expect(execute).toBe(originalExecute); + + // Check for stale closures with onCompleted + await execute(); + + { + const [, result] = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: { id: "1" }, + }); + } - expect(result.current[0]).toBe(originalExecute); + { + const [, result] = await takeSnapshot(); - // Check for stale closures with onCompleted - await act(() => result.current[0]()); - await waitFor(() => { - expect(result.current[1].data).toEqual({ - user: { id: "1", name: "John Doe" }, + expect(result).toEqualQueryResult({ + data: { user: { id: "1", name: "John Doe" } }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: { id: "1" }, }); - }); + } // after fetch expect(trackClosureValue).toHaveBeenNthCalledWith(1, "nextFetchPolicy", 1); @@ -1668,17 +2267,55 @@ describe("useLazyQuery Hook", () => { trackClosureValue.mockClear(); countRef.current++; - rerender(); - expect(result.current[0]).toBe(originalExecute); + // TODO: Update when https://github.com/testing-library/react-render-stream-testing-library/issues/13 is fixed + await rerender(undefined); + + [execute] = getCurrentSnapshot(); + expect(execute).toBe(originalExecute); // Check for stale closures with onError - await act(() => result.current[0]({ variables: { id: "2" } })); - await waitFor(() => { - expect(result.current[1].error).toEqual( - new ApolloError({ graphQLErrors: [new GraphQLError("Oops")] }) - ); - }); + await execute({ variables: { id: "2" } }); + + { + const [, result] = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: { user: { id: "1", name: "John Doe" } }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: { id: "1" }, + }); + } + + { + const [, result] = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.setVariables, + previousData: { user: { id: "1", name: "John Doe" } }, + variables: { id: "2" }, + }); + } + + { + const [, result] = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: undefined, + error: new ApolloError({ graphQLErrors: [{ message: "Oops" }] }), + called: true, + loading: false, + networkStatus: NetworkStatus.error, + previousData: { user: { id: "1", name: "John Doe" } }, + variables: { id: "2" }, + }); + } // variables changed expect(trackClosureValue).toHaveBeenNthCalledWith(1, "nextFetchPolicy", 2); @@ -1688,16 +2325,53 @@ describe("useLazyQuery Hook", () => { trackClosureValue.mockClear(); countRef.current++; - rerender(); + // TODO: Update when https://github.com/testing-library/react-render-stream-testing-library/issues/13 is fixed + await rerender(undefined); + + [execute] = getCurrentSnapshot(); + expect(execute).toBe(originalExecute); - expect(result.current[0]).toBe(originalExecute); + await execute({ variables: { id: "3" } }); + + { + const [, result] = await takeSnapshot(); - await act(() => result.current[0]({ variables: { id: "3" } })); - await waitFor(() => { - expect(result.current[1].data).toEqual({ - user: { id: "3", name: "Johnny Three" }, + expect(result).toEqualQueryResult({ + data: undefined, + error: new ApolloError({ graphQLErrors: [{ message: "Oops" }] }), + called: true, + loading: false, + networkStatus: NetworkStatus.error, + previousData: { user: { id: "1", name: "John Doe" } }, + variables: { id: "2" }, }); - }); + } + + { + const [, result] = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.setVariables, + previousData: { user: { id: "1", name: "John Doe" } }, + variables: { id: "3" }, + }); + } + + { + const [, result] = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: { user: { id: "3", name: "Johnny Three" } }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { user: { id: "1", name: "John Doe" } }, + variables: { id: "3" }, + }); + } // variables changed expect(trackClosureValue).toHaveBeenNthCalledWith(1, "nextFetchPolicy", 3); @@ -1707,9 +2381,9 @@ describe("useLazyQuery Hook", () => { trackClosureValue.mockClear(); // Test for stale closures for skipPollAttempt - result.current[1].startPolling(20); + getCurrentSnapshot()[1].startPolling(20); await wait(50); - result.current[1].stopPolling(); + getCurrentSnapshot()[1].stopPolling(); expect(trackClosureValue).toHaveBeenCalledWith("skipPollAttempt", 3); }); @@ -1794,25 +2468,45 @@ describe("useLazyQuery Hook", () => { { const [, result] = await takeSnapshot(); - expect(result.loading).toBe(false); - expect(result.networkStatus).toBe(NetworkStatus.ready); - expect(result.data).toBeUndefined(); + + expect(result).toEqualQueryResult({ + data: undefined, + error: undefined, + called: false, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); } + const execute = getCurrentSnapshot()[0]; setTimeout(execute); { const [, result] = await takeSnapshot(); - expect(result.loading).toBe(true); - expect(result.networkStatus).toBe(NetworkStatus.loading); - expect(result.data).toBeUndefined(); + + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); } { const [, result] = await takeSnapshot(); - expect(result.loading).toBe(false); - expect(result.networkStatus).toBe(NetworkStatus.error); - expect(result.data).toBeUndefined(); - expect(result.error!.message).toBe("from the network"); + + expect(result).toEqualQueryResult({ + data: undefined, + error: new ApolloError({ networkError }), + called: true, + loading: false, + networkStatus: NetworkStatus.error, + previousData: undefined, + variables: {}, + }); } } @@ -1848,14 +2542,14 @@ describe("useLazyQuery Hook", () => { (request) => new Observable((observer) => { if (request.operationName === "GetCounter") { - observer.next({ - data: { - counter: ++count, - }, - }); setTimeout(() => { + observer.next({ + data: { + counter: ++count, + }, + }); observer.complete(); - }, 10); + }, 20); } else { observer.error( new Error( @@ -1869,93 +2563,77 @@ describe("useLazyQuery Hook", () => { const defaultFetchPolicy = "network-only"; - const { result } = renderHook( - () => { - const [exec, query] = useLazyQuery(counterQuery, { - defaultOptions: { - fetchPolicy: defaultFetchPolicy, - notifyOnNetworkStatusChange: true, - }, - }); - return { - exec, - query, - }; - }, - { - wrapper: ({ children }) => ( - {children} - ), - } - ); + using _disabledAct = disableActEnvironment(); + const { takeSnapshot, getCurrentSnapshot } = + await renderHookToSnapshotStream( + () => { + return useLazyQuery(counterQuery, { + defaultOptions: { + fetchPolicy: defaultFetchPolicy, + notifyOnNetworkStatusChange: true, + }, + }); + }, + { + wrapper: ({ children }) => ( + {children} + ), + } + ); - await waitFor( - () => { - expect(result.current.query.loading).toBe(false); - }, - { interval: 1 } - ); - await waitFor( - () => { - expect(result.current.query.called).toBe(false); - }, - { interval: 1 } - ); - await waitFor( - () => { - expect(result.current.query.data).toBeUndefined(); - }, - { interval: 1 } - ); + { + const [, result] = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: undefined, + error: undefined, + called: false, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); + } + + const [execute] = getCurrentSnapshot(); + const execResult = await execute(); - let execPromise: Promise; - await act(async () => { - execPromise = result.current.exec(); + expect(execResult).toEqualQueryResult({ + data: { counter: 1 }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, }); - const execResult = await execPromise!; - expect(execResult.loading).toBe(false); - expect(execResult.called).toBe(true); - expect(execResult.data).toEqual({ counter: 1 }); - await waitFor( - () => { - expect(result.current.query.loading).toBe(false); - }, - { interval: 1 } - ); - await waitFor( - () => { - expect(result.current.query.data).toMatchObject({ counter: 1 }); - }, - { interval: 1 } - ); - await waitFor( - () => { - expect(result.current.query.called).toBe(true); - }, - { interval: 1 } - ); + { + const [, result] = await takeSnapshot(); - await waitFor( - () => { - expect(result.current.query.loading).toBe(false); - }, - { interval: 1 } - ); - await waitFor( - () => { - expect(result.current.query.called).toBe(true); - }, - { interval: 1 } - ); - await waitFor( - () => { - expect(result.current.query.data).toEqual({ counter: 1 }); - }, - { interval: 1 } - ); + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); + } + + { + const [, result] = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: { counter: 1 }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); + } - const { options } = result.current.query.observable; + const { options } = getCurrentSnapshot()[1].observable; expect(options.fetchPolicy).toBe(defaultFetchPolicy); }); }); @@ -1979,9 +2657,18 @@ describe("useLazyQuery Hook", () => { { const [, result] = await takeSnapshot(); - expect(result.loading).toBe(false); - expect(result.data).toBeUndefined(); + + expect(result).toEqualQueryResult({ + data: undefined, + error: undefined, + called: false, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); } + const execute = getCurrentSnapshot()[0]; const promise = execute(); @@ -1989,34 +2676,50 @@ describe("useLazyQuery Hook", () => { { const [, result] = await takeSnapshot(); - expect(result.loading).toBe(true); - expect(result.data).toBeUndefined(); + + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); } await client.clearStore(); - const executionResult = await promise; - expect(executionResult.data).toBeUndefined(); - expect(executionResult.loading).toBe(true); - expect(executionResult.error).toEqual( - new ApolloError({ + await expect(promise).resolves.toEqualQueryResult({ + data: undefined, + error: new ApolloError({ networkError: new InvariantError( "Store reset while query was in flight (not completed in link chain)" ), - }) - ); + }), + errors: [], + loading: true, + networkStatus: NetworkStatus.loading, + called: true, + previousData: undefined, + variables: {}, + }); { const [, result] = await takeSnapshot(); - expect(result.loading).toBe(false); - expect(result.data).toBeUndefined(); - expect(result.error).toEqual( - new ApolloError({ + + expect(result).toEqualQueryResult({ + data: undefined, + error: new ApolloError({ networkError: new InvariantError( "Store reset while query was in flight (not completed in link chain)" ), - }) - ); + }), + called: true, + loading: false, + networkStatus: NetworkStatus.error, + previousData: undefined, + variables: {}, + }); } link.simulateResult({ result: { data: { hello: "Greetings" } } }, true); @@ -2027,6 +2730,7 @@ describe("useLazyQuery Hook", () => { describe("data masking", () => { it("masks queries when dataMasking is `true`", async () => { type UserFieldsFragment = { + __typename: "User"; age: number; } & { " $fragmentName"?: "UserFieldsFragment" }; @@ -2038,7 +2742,7 @@ describe("useLazyQuery Hook", () => { } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; } - const query: MaskedDocumentNode = gql` + const query: TypedDocumentNode> = gql` query MaskedQuery { currentUser { id @@ -2089,32 +2793,47 @@ describe("useLazyQuery Hook", () => { const [execute] = getCurrentSnapshot(); const result = await execute(); - expect(result.data).toEqual({ - currentUser: { - __typename: "User", - id: 1, - name: "Test User", + expect(result).toEqualQueryResult({ + data: { + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + }, }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, }); // Loading await takeSnapshot(); { - const [, { data }] = await takeSnapshot(); + const [, result] = await takeSnapshot(); - expect(data).toEqual({ - currentUser: { - __typename: "User", - id: 1, - name: "Test User", + expect(result).toEqualQueryResult({ + data: { + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + }, }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, }); } }); it("does not mask queries when dataMasking is `false`", async () => { type UserFieldsFragment = { + __typename: "User"; age: number; } & { " $fragmentName"?: "UserFieldsFragment" }; @@ -2126,7 +2845,10 @@ describe("useLazyQuery Hook", () => { } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; } - const query: TypedDocumentNode = gql` + const query: TypedDocumentNode< + Unmasked, + Record + > = gql` query MaskedQuery { currentUser { id @@ -2177,34 +2899,49 @@ describe("useLazyQuery Hook", () => { const [execute] = getCurrentSnapshot(); const result = await execute(); - expect(result.data).toEqual({ - currentUser: { - __typename: "User", - id: 1, - name: "Test User", - age: 30, + expect(result).toEqualQueryResult({ + data: { + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + age: 30, + }, }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, }); // Loading await takeSnapshot(); { - const [, { data }] = await takeSnapshot(); + const [, result] = await takeSnapshot(); - expect(data).toEqual({ - currentUser: { - __typename: "User", - id: 1, - name: "Test User", - age: 30, + expect(result).toEqualQueryResult({ + data: { + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + age: 30, + }, }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, }); } }); it("does not mask queries by default", async () => { type UserFieldsFragment = { + __typename: "User"; age: number; } & { " $fragmentName"?: "UserFieldsFragment" }; @@ -2216,7 +2953,10 @@ describe("useLazyQuery Hook", () => { } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; } - const query: TypedDocumentNode = gql` + const query: TypedDocumentNode< + Unmasked, + Record + > = gql` query MaskedQuery { currentUser { id @@ -2266,34 +3006,49 @@ describe("useLazyQuery Hook", () => { const [execute] = getCurrentSnapshot(); const result = await execute(); - expect(result.data).toEqual({ - currentUser: { - __typename: "User", - id: 1, - name: "Test User", - age: 30, + expect(result).toEqualQueryResult({ + data: { + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + age: 30, + }, }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, }); // Loading await takeSnapshot(); { - const [, { data }] = await takeSnapshot(); + const [, result] = await takeSnapshot(); - expect(data).toEqual({ - currentUser: { - __typename: "User", - id: 1, - name: "Test User", - age: 30, + expect(result).toEqualQueryResult({ + data: { + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + age: 30, + }, }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, }); } }); it("masks queries updated by the cache", async () => { type UserFieldsFragment = { + __typename: "User"; age: number; } & { " $fragmentName"?: "UserFieldsFragment" }; @@ -2305,7 +3060,7 @@ describe("useLazyQuery Hook", () => { } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; } - const query: MaskedDocumentNode = gql` + const query: TypedDocumentNode> = gql` query MaskedQuery { currentUser { id @@ -2360,14 +3115,21 @@ describe("useLazyQuery Hook", () => { await takeSnapshot(); { - const [, { data }] = await takeSnapshot(); + const [, result] = await takeSnapshot(); - expect(data).toEqual({ - currentUser: { - __typename: "User", - id: 1, - name: "Test User", + expect(result).toEqualQueryResult({ + data: { + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + }, }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, }); } @@ -2384,24 +3146,30 @@ describe("useLazyQuery Hook", () => { }); { - const [, { data, previousData }] = await takeSnapshot(); + const [, result] = await takeSnapshot(); - expect(data).toEqual({ - currentUser: { - __typename: "User", - id: 1, - name: "Test User (updated)", + expect(result).toEqualQueryResult({ + data: { + currentUser: { + __typename: "User", + id: 1, + name: "Test User (updated)", + }, }, - }); - - expect(previousData).toEqual({ - currentUser: { __typename: "User", id: 1, name: "Test User" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { + currentUser: { __typename: "User", id: 1, name: "Test User" }, + }, + variables: {}, }); } }); it("does not rerender when updating field in named fragment", async () => { type UserFieldsFragment = { + __typename: "User"; age: number; } & { " $fragmentName"?: "UserFieldsFragment" }; @@ -2413,7 +3181,7 @@ describe("useLazyQuery Hook", () => { } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; } - const query: MaskedDocumentNode = gql` + const query: TypedDocumentNode> = gql` query MaskedQuery { currentUser { id @@ -2468,14 +3236,21 @@ describe("useLazyQuery Hook", () => { await takeSnapshot(); { - const [, { data }] = await takeSnapshot(); + const [, result] = await takeSnapshot(); - expect(data).toEqual({ - currentUser: { - __typename: "User", - id: 1, - name: "Test User", + expect(result).toEqualQueryResult({ + data: { + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + }, }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, }); } diff --git a/src/testing/matchers/index.d.ts b/src/testing/matchers/index.d.ts index 3bd1f606dae..1313967147b 100644 --- a/src/testing/matchers/index.d.ts +++ b/src/testing/matchers/index.d.ts @@ -3,10 +3,11 @@ import type { DocumentNode, OperationVariables, } from "../../core/index.js"; -import type { QueryRef } from "../../react/index.js"; +import type { QueryRef, QueryResult } from "../../react/index.js"; import { NextRenderOptions, ObservableStream } from "../internal/index.js"; import { RenderStreamMatchers } from "@testing-library/react-render-stream/expect"; import { TakeOptions } from "../internal/ObservableStream.js"; +import { CheckedKeys } from "./toEqualQueryResult.js"; interface ApolloCustomMatchers { /** @@ -67,6 +68,12 @@ interface ApolloCustomMatchers { toEmitMatchedValue: T extends ObservableStream ? (value: any, options?: TakeOptions) => Promise : { error: "matcher needs to be called on an ObservableStream instance" }; + + toEqualQueryResult: T extends QueryResult ? + (expected: Pick, CheckedKeys>) => R + : T extends Promise> ? + (expected: Pick, CheckedKeys>) => R + : { error: "matchers needs to be called on a QueryResult" }; } declare global { diff --git a/src/testing/matchers/index.ts b/src/testing/matchers/index.ts index 149a20c1cf4..338d8ac734d 100644 --- a/src/testing/matchers/index.ts +++ b/src/testing/matchers/index.ts @@ -10,6 +10,7 @@ import { toEmitMatchedValue } from "./toEmitMatchedValue.js"; import { toEmitNext } from "./toEmitNext.js"; import { toEmitValue } from "./toEmitValue.js"; import { toEmitValueStrict } from "./toEmitValueStrict.js"; +import { toEqualQueryResult } from "./toEqualQueryResult.js"; expect.extend({ toComplete, @@ -19,6 +20,7 @@ expect.extend({ toEmitNext, toEmitValue, toEmitValueStrict, + toEqualQueryResult, toBeDisposed, toHaveSuspenseCacheEntryUsing, toMatchDocument, diff --git a/src/testing/matchers/toEqualQueryResult.ts b/src/testing/matchers/toEqualQueryResult.ts new file mode 100644 index 00000000000..22d223085d2 --- /dev/null +++ b/src/testing/matchers/toEqualQueryResult.ts @@ -0,0 +1,72 @@ +import { iterableEquality } from "@jest/expect-utils"; +import type { MatcherFunction } from "expect"; +import type { QueryResult } from "../../react/index.js"; + +const CHECKED_KEYS = [ + "loading", + "error", + "errors", + "data", + "variables", + "networkStatus", + "errors", + "called", + "previousData", +] as const; + +export type CheckedKeys = (typeof CHECKED_KEYS)[number]; + +const hasOwnProperty = (obj: Record, key: string) => + Object.prototype.hasOwnProperty.call(obj, key); + +export const toEqualQueryResult: MatcherFunction< + [queryResult: Pick, CheckedKeys>] +> = function (actual, expected) { + const queryResult = actual as QueryResult; + const hint = this.utils.matcherHint( + this.isNot ? ".not.toEqualQueryResult" : "toEqualQueryResult", + "queryResult", + "expected", + { isNot: this.isNot, promise: this.promise } + ); + + const checkedQueryResult = CHECKED_KEYS.reduce( + (memo, key) => { + if (hasOwnProperty(queryResult, key)) { + memo[key] = queryResult[key]; + } + + return memo; + }, + {} as Partial> + ); + + const pass = this.equals( + checkedQueryResult, + expected, + // https://github.com/jestjs/jest/blob/22029ba06b69716699254bb9397f2b3bc7b3cf3b/packages/expect/src/matchers.ts#L62-L67 + [...this.customTesters, iterableEquality], + true + ); + + return { + pass, + message: () => { + if (pass) { + return hint + `\n\nExpected: not ${this.utils.printExpected(expected)}`; + } + + return ( + hint + + "\n\n" + + this.utils.printDiffOrStringify( + expected, + checkedQueryResult, + "Expected", + "Received", + true + ) + ); + }, + }; +}; From d2032042f9e32b6dc9bd895da789359517a995fb Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 16 Jan 2025 10:43:51 -0700 Subject: [PATCH 6/9] Refactor `useQuery` tests to use `toEqualQueryResult` - Part 1 (#12273) --- src/react/hooks/__tests__/useQuery.test.tsx | 1962 +++++++++++------ src/testing/matchers/index.d.ts | 7 + src/testing/matchers/index.ts | 2 + .../matchers/toEqualApolloQueryResult.ts | 44 + 4 files changed, 1385 insertions(+), 630 deletions(-) create mode 100644 src/testing/matchers/toEqualApolloQueryResult.ts diff --git a/src/react/hooks/__tests__/useQuery.test.tsx b/src/react/hooks/__tests__/useQuery.test.tsx index 2edf19d2a36..769a89ae265 100644 --- a/src/react/hooks/__tests__/useQuery.test.tsx +++ b/src/react/hooks/__tests__/useQuery.test.tsx @@ -43,6 +43,7 @@ import { } from "@testing-library/react-render-stream"; const IS_REACT_17 = React.version.startsWith("17"); +const IS_REACT_18 = React.version.startsWith("18"); describe("useQuery Hook", () => { describe("General use", () => { @@ -63,17 +64,39 @@ describe("useQuery Hook", () => { {children} ); - const { result } = renderHook(() => useQuery(query), { wrapper }); - - expect(result.current.loading).toBe(true); - expect(result.current.data).toBe(undefined); - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( + () => useQuery(query), + { wrapper } ); - expect(result.current.data).toEqual({ hello: "world" }); + + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: undefined, + loading: true, + networkStatus: NetworkStatus.loading, + called: true, + previousData: undefined, + variables: {}, + }); + } + + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: { hello: "world" }, + loading: false, + networkStatus: NetworkStatus.ready, + called: true, + previousData: undefined, + variables: {}, + }); + } + + await expect(takeSnapshot).not.toRerender(); }); it("useQuery result is referentially stable", async () => { @@ -91,25 +114,61 @@ describe("useQuery Hook", () => { const wrapper = ({ children }: any) => ( {children} ); - const { result, rerender } = renderHook(() => useQuery(query), { - wrapper, - }); + + using _disabledAct = disableActEnvironment(); + const { takeSnapshot, rerender } = await renderHookToSnapshotStream( + () => useQuery(query), + { + wrapper, + } + ); + + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); + } + let oldResult: QueryResult; - await waitFor(() => { - result.current.loading === false; - }); + { + const result = (oldResult = await takeSnapshot()); - rerender({ children: null }); + expect(result).toEqualQueryResult({ + data: { hello: "world" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); + } - await waitFor(() => { - expect(result.current.networkStatus).toBe(NetworkStatus.ready); - oldResult = result.current; - }); + await rerender({ children: null }); - await waitFor(() => { - expect(oldResult === result.current).toBe(true); - }); + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: { hello: "world" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); + + expect(result).toBe(oldResult); + } + + await expect(takeSnapshot).not.toRerender(); }); it("useQuery produces the expected renders initially", async () => { @@ -127,26 +186,57 @@ describe("useQuery Hook", () => { const wrapper = ({ children }: any) => ( {children} ); - const { result, rerender } = renderHook(() => useQuery(query), { - wrapper, - }); - await waitFor(() => result.current.loading === false); + using _disabledAct = disableActEnvironment(); + const { takeSnapshot, rerender } = await renderHookToSnapshotStream( + () => useQuery(query), + { + wrapper, + } + ); - rerender({ children: null }); + { + const result = await takeSnapshot(); - await waitFor(() => { - expect(result.current.loading).toBe(false); - }); - expect(result.current.data).toEqual({ hello: "world" }); - expect(result.current.loading).toBe(false); - expect(result.current.data).toEqual({ hello: "world" }); + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); + } - // Repeat frame because rerender forces useQuery to be called again - await waitFor(() => { - expect(result.current.loading).toBe(false); - }); - expect(result.current.data).toEqual({ hello: "world" }); + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: { hello: "world" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); + } + + await rerender({ children: null }); + + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: { hello: "world" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); + } + + await expect(takeSnapshot).not.toRerender(); }); it("useQuery produces the expected frames when variables change", async () => { @@ -168,51 +258,71 @@ describe("useQuery Hook", () => { const wrapper = ({ children }: any) => ( {children} ); - const { result, rerender } = renderHook( + + using _disabledAct = disableActEnvironment(); + const { takeSnapshot, rerender } = await renderHookToSnapshotStream( (options) => useQuery(query, options), { wrapper, initialProps: { variables: { id: 1 } } } ); - await waitFor(() => result.current.loading === false); - await waitFor(() => { - expect(result.current.loading).toBe(false); - }); - await waitFor(() => { - expect(result.current.data).toEqual({ hello: "world 1" }); - }); - await waitFor(() => { - expect(result.current.networkStatus).toBe(NetworkStatus.ready); - }); - await waitFor(() => { - expect(result.current.loading).toBe(false); - }); - await waitFor(() => { - expect(result.current.data).toEqual({ hello: "world 1" }); - }); - await waitFor(() => { - expect(result.current.networkStatus).toBe(NetworkStatus.ready); - }); - rerender({ variables: { id: 2 } }); - await waitFor(() => { - expect(result.current.loading).toBe(true); - }); - await waitFor(() => { - expect(result.current.data).toEqual({ hello: "world 2" }); - }); - await waitFor(() => { - expect(result.current.networkStatus).toBe(NetworkStatus.ready); - }); - await waitFor(() => { - expect(result.current.loading).toBe(false); - }); - await waitFor(() => { - expect(result.current.data).toEqual({ hello: "world 2" }); - }); - await waitFor(() => { - expect(result.current.networkStatus).toBe(NetworkStatus.ready); - }); + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: { id: 1 }, + }); + } + + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: { hello: "world 1" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: { id: 1 }, + }); + } + + await rerender({ variables: { id: 2 } }); + + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.setVariables, + previousData: { hello: "world 1" }, + variables: { id: 2 }, + }); + } + + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: { hello: "world 2" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { hello: "world 1" }, + variables: { id: 2 }, + }); + } }); + // TODO: Refactor this test. This test does not test the thing it says it + // does as there is no cache interaction in this test. This is essentially + // just a repeat of prior tests that rerender and check the result. it("should read and write results from the cache", async () => { const query = gql` { @@ -233,23 +343,54 @@ describe("useQuery Hook", () => { ); - const { result, rerender } = renderHook(() => useQuery(query), { - wrapper, - }); - expect(result.current.loading).toBe(true); - expect(result.current.data).toBe(undefined); - - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } + using _disabledAct = disableActEnvironment(); + const { takeSnapshot, rerender } = await renderHookToSnapshotStream( + () => useQuery(query), + { + wrapper, + } ); - expect(result.current.data).toEqual({ hello: "world" }); - rerender(); - expect(result.current.loading).toBe(false); - expect(result.current.data).toEqual({ hello: "world" }); + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); + } + + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: { hello: "world" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); + } + + await rerender(undefined); + + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: { hello: "world" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); + } }); it("should preserve functions between renders", async () => { @@ -272,21 +413,54 @@ describe("useQuery Hook", () => { ); - const { result } = renderHook(() => useQuery(query), { wrapper }); - expect(result.current.loading).toBe(true); - const { refetch, fetchMore, startPolling, stopPolling, subscribeToMore } = - result.current; - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } + using _disabledAct = disableActEnvironment(); + const { takeSnapshot, rerender } = await renderHookToSnapshotStream( + () => useQuery(query), + { wrapper } ); - expect(refetch).toBe(result.current.refetch); - expect(fetchMore).toBe(result.current.fetchMore); - expect(startPolling).toBe(result.current.startPolling); - expect(stopPolling).toBe(result.current.stopPolling); - expect(subscribeToMore).toBe(result.current.subscribeToMore); + + const { + loading, + refetch, + fetchMore, + startPolling, + stopPolling, + subscribeToMore, + } = await takeSnapshot(); + expect(loading).toBe(true); + + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: { hello: "world" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); + + expect(refetch).toBe(result.refetch); + expect(fetchMore).toBe(result.fetchMore); + expect(startPolling).toBe(result.startPolling); + expect(stopPolling).toBe(result.stopPolling); + expect(subscribeToMore).toBe(result.subscribeToMore); + } + + await rerender(undefined); + + { + const result = await takeSnapshot(); + + expect(refetch).toBe(result.refetch); + expect(fetchMore).toBe(result.fetchMore); + expect(startPolling).toBe(result.startPolling); + expect(stopPolling).toBe(result.stopPolling); + expect(subscribeToMore).toBe(result.subscribeToMore); + } + + await expect(takeSnapshot).not.toRerender(); }); it("should set called to true by default", async () => { @@ -309,12 +483,15 @@ describe("useQuery Hook", () => { ); - const { result, unmount } = renderHook(() => useQuery(query), { - wrapper, - }); + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( + () => useQuery(query), + { wrapper } + ); - expect(result.current.called).toBe(true); - unmount(); + const { called } = await takeSnapshot(); + + expect(called).toBe(true); }); it("should set called to false when skip option is true", async () => { @@ -337,15 +514,19 @@ describe("useQuery Hook", () => { ); - const { result, unmount } = renderHook( + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( () => useQuery(query, { skip: true }), { wrapper } ); - expect(result.current.called).toBe(false); - unmount(); + const { called } = await takeSnapshot(); + + expect(called).toBe(false); }); + // TODO: Remove this test after PR is reviewed since this is basically a + // duplicate of "useQuery produces the expected frames when variables change" it("should work with variables", async () => { const query = gql` query ($id: Int) { @@ -371,32 +552,65 @@ describe("useQuery Hook", () => { ); - const { result, rerender } = renderHook( + using _disabledAct = disableActEnvironment(); + const { takeSnapshot, rerender } = await renderHookToSnapshotStream( ({ id }) => useQuery(query, { variables: { id } }), { wrapper, initialProps: { id: 1 } } ); - expect(result.current.loading).toBe(true); - expect(result.current.data).toBe(undefined); - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); - expect(result.current.data).toEqual({ hello: "world 1" }); + { + const result = await takeSnapshot(); - rerender({ id: 2 }); - expect(result.current.loading).toBe(true); - expect(result.current.data).toBe(undefined); + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: { id: 1 }, + }); + } - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); - expect(result.current.data).toEqual({ hello: "world 2" }); + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: { hello: "world 1" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: { id: 1 }, + }); + } + + await rerender({ id: 2 }); + + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.setVariables, + previousData: { hello: "world 1" }, + variables: { id: 2 }, + }); + } + + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: { hello: "world 2" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { hello: "world 1" }, + variables: { id: 2 }, + }); + } }); it("should return the same results for the same variables", async () => { @@ -424,35 +638,80 @@ describe("useQuery Hook", () => { ); - const { result, rerender } = renderHook( + using _disabledAct = disableActEnvironment(); + const { takeSnapshot, rerender } = await renderHookToSnapshotStream( ({ id }) => useQuery(query, { variables: { id } }), { wrapper, initialProps: { id: 1 } } ); - expect(result.current.loading).toBe(true); - expect(result.current.data).toBe(undefined); - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); - expect(result.current.data).toEqual({ hello: "world 1" }); + { + const result = await takeSnapshot(); - rerender({ id: 2 }); - expect(result.current.loading).toBe(true); - expect(result.current.data).toBe(undefined); - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); - expect(result.current.data).toEqual({ hello: "world 2" }); + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: { id: 1 }, + }); + } - rerender({ id: 2 }); - expect(result.current.loading).toBe(false); - expect(result.current.data).toEqual({ hello: "world 2" }); + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: { hello: "world 1" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: { id: 1 }, + }); + } + + await rerender({ id: 2 }); + + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.setVariables, + previousData: { hello: "world 1" }, + variables: { id: 2 }, + }); + } + + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: { hello: "world 2" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { hello: "world 1" }, + variables: { id: 2 }, + }); + } + + await rerender({ id: 2 }); + + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: { hello: "world 2" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { hello: "world 1" }, + variables: { id: 2 }, + }); + } }); it("should work with variables 2", async () => { @@ -484,44 +743,93 @@ describe("useQuery Hook", () => { ); - const { result, rerender } = renderHook( + using _disabledAct = disableActEnvironment(); + const { takeSnapshot, rerender } = await renderHookToSnapshotStream( ({ name }) => useQuery(query, { variables: { name } }), { wrapper, initialProps: { name: "" } } ); - expect(result.current.loading).toBe(true); - expect(result.current.data).toBe(undefined); - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); - expect(result.current.data).toEqual({ names: ["Alice", "Bob", "Eve"] }); + { + const result = await takeSnapshot(); - rerender({ name: "z" }); - expect(result.current.loading).toBe(true); - expect(result.current.data).toBe(undefined); + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: { name: "" }, + }); + } - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); - expect(result.current.data).toEqual({ names: [] }); + { + const result = await takeSnapshot(); - rerender({ name: "zz" }); - expect(result.current.loading).toBe(true); - expect(result.current.data).toBe(undefined); + expect(result).toEqualQueryResult({ + data: { names: ["Alice", "Bob", "Eve"] }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: { name: "" }, + }); + } - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); - expect(result.current.data).toEqual({ names: [] }); + await rerender({ name: "z" }); + + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.setVariables, + previousData: { names: ["Alice", "Bob", "Eve"] }, + variables: { name: "z" }, + }); + } + + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: { names: [] }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { names: ["Alice", "Bob", "Eve"] }, + variables: { name: "z" }, + }); + } + + await rerender({ name: "zz" }); + + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.setVariables, + previousData: { names: [] }, + variables: { name: "zz" }, + }); + } + + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: { names: [] }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { names: [] }, + variables: { name: "zz" }, + }); + } }); // An unsuccessful attempt to reproduce https://github.com/apollographql/apollo-client/issues/9135. @@ -555,66 +863,152 @@ describe("useQuery Hook", () => { const cache = new InMemoryCache(); let setName: any; - const { result } = renderHook( - () => { - const [name, setName1] = React.useState("world 1"); - setName = setName1; - return [ - useQuery(query, { variables: { name } }), - useMutation(mutation, { - update(cache, { data }) { - cache.writeQuery({ - query, - data: { hello: data.updateGreeting }, - }); - }, - }), - ] as const; - }, + + using _disabledAct = disableActEnvironment(); + // TODO: Take a deeper look into this to better understand what this is + // trying to test. There are a few problems with this: + // + // 1. We execute the mutation and a setState at the same time. What is + // that meant to accomplish? + // 2. The update callback in the useMutation is writing data for none of + // the results in the mocks. The mutation returns `updateName: true`, + // yet the callback is trying to set a value from `data.updateGreeting` + // 3. The update callback in `useMutation` does not use `variables`, so + // the written cache result does not affect any of the queries from the + // `useQuery` returned here. + // + // My recommendation is to just delete the `useMutation` as part of this + // render callback as it doesn't seem to serve much of a purpose for this + // test. + const { takeSnapshot, getCurrentSnapshot } = + await renderHookToSnapshotStream( + () => { + const [name, setName1] = React.useState("world 1"); + setName = setName1; + return [ + useQuery(query, { variables: { name } }), + useMutation(mutation, { + update(cache, { data }) { + cache.writeQuery({ + query, + data: { hello: data.updateGreeting }, + }); + }, + }), + ] as const; + }, + { + wrapper: ({ children }) => ( + + {children} + + ), + } + ); + + { + const [useQueryResult] = await takeSnapshot(); + + expect(useQueryResult).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: { name: "world 1" }, + }); + } + + { + const [useQueryResult] = await takeSnapshot(); + + expect(useQueryResult).toEqualQueryResult({ + data: { hello: "world 1" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: { name: "world 1" }, + }); + } + + const [, [mutate]] = getCurrentSnapshot(); + + void mutate({ variables: { name: "world 2" } }); + setName("world 2"); + + if (IS_REACT_17) { { - wrapper: ({ children }) => ( - - {children} - - ), + const [useQueryResult] = await takeSnapshot(); + + expect(useQueryResult).toEqualQueryResult({ + data: { hello: "world 1" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: { name: "world 1" }, + }); } - ); - expect(result.current[0].loading).toBe(true); - expect(result.current[0].data).toBe(undefined); - expect(result.current[0].variables).toEqual({ name: "world 1" }); - await waitFor( - () => { - expect(result.current[0].loading).toBe(false); - }, - { interval: 1 } - ); + { + const [useQueryResult] = await takeSnapshot(); - expect(result.current[0].data).toEqual({ hello: "world 1" }); - expect(result.current[0].variables).toEqual({ name: "world 1" }); + expect(useQueryResult).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.setVariables, + previousData: { hello: "world 1" }, + variables: { name: "world 2" }, + }); + } + } - const mutate = result.current[1][0]; - act(() => { - void mutate({ variables: { name: "world 2" } }); - setName("world 2"); - }); + { + const [useQueryResult] = await takeSnapshot(); + + expect(useQueryResult).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.setVariables, + previousData: { hello: "world 1" }, + variables: { name: "world 2" }, + }); + } - expect(result.current[0].loading).toBe(true); - expect(result.current[0].data).toBe(undefined); - expect(result.current[0].variables).toEqual({ name: "world 2" }); + { + const [useQueryResult] = await takeSnapshot(); - await waitFor(() => { - expect(result.current[0].loading).toBe(false); - }); - await waitFor(() => { - expect(result.current[0].data).toEqual({ hello: "world 2" }); - }); - await waitFor(() => { - expect(result.current[0].variables).toEqual({ name: "world 2" }); - }); + expect(useQueryResult).toEqualQueryResult({ + data: { hello: "world 2" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { hello: "world 1" }, + variables: { name: "world 2" }, + }); + } + + if (IS_REACT_18) { + const [useQueryResult] = await takeSnapshot(); + + expect(useQueryResult).toEqualQueryResult({ + data: { hello: "world 2" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { hello: "world 1" }, + variables: { name: "world 2" }, + }); + } + + await expect(takeSnapshot).not.toRerender(); }); // TODO: Rewrite this test + // Context: https://legacy.reactjs.org/blog/2020/02/26/react-v16.13.0.html#warnings-for-some-updates-during-render it("should not error when forcing an update with React >= 16.13.0", async () => { const CAR_QUERY: DocumentNode = gql` query { @@ -724,21 +1118,41 @@ describe("useQuery Hook", () => { }, ]; - const { result } = renderHook(() => useQuery(query, { ssr: false }), { - wrapper: ({ children }) => ( - {children} - ), - }); - - expect(result.current.loading).toBe(true); - expect(result.current.data).toBe(undefined); - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( + () => useQuery(query, { ssr: false }), + { + wrapper: ({ children }) => ( + {children} + ), + } ); - expect(result.current.data).toEqual({ hello: "world" }); + + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); + } + + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: { hello: "world" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); + } }); it("should keep `no-cache` results when the tree is re-rendered", async () => { @@ -801,36 +1215,94 @@ describe("useQuery Hook", () => { { const [result0, result1] = await takeSnapshot(); - expect(result0.loading).toBe(true); - expect(result0.data).toStrictEqual(undefined); - expect(result1.loading).toBe(true); - expect(result1.data).toStrictEqual(undefined); + + expect(result0).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); + + expect(result1).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); } { const [result0, result1] = await takeSnapshot(); - expect(result0.loading).toBe(false); - expect(result0.data).toStrictEqual(allPeopleData); - expect(result1.loading).toBe(true); - expect(result1.data).toStrictEqual(undefined); + + expect(result0).toEqualQueryResult({ + data: allPeopleData, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); + + expect(result1).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); } { const [result0, result1] = await takeSnapshot(); - expect(result0.loading).toBe(false); - expect(result0.data).toStrictEqual(allPeopleData); - expect(result1.loading).toBe(false); - expect(result1.data).toStrictEqual(allThingsData); + + expect(result0).toEqualQueryResult({ + data: allPeopleData, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); + + expect(result1).toEqualQueryResult({ + data: allThingsData, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); } await rerender({}); + { const [result0, result1] = await takeSnapshot(); - expect(result0.loading).toBe(false); - expect(result0.data).toStrictEqual(allPeopleData); - expect(result1.loading).toBe(false); - expect(result1.data).toStrictEqual(allThingsData); + + expect(result0).toEqualQueryResult({ + data: allPeopleData, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); + + expect(result1).toEqualQueryResult({ + data: allThingsData, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); } + await expect(takeSnapshot).not.toRerender(); }); @@ -850,15 +1322,25 @@ describe("useQuery Hook", () => { { request: { query: query1 }, result: { data: { hello: "world" } }, + delay: 20, }, { request: { query: query2 }, result: { data: { hello: "world", name: "world" } }, + delay: 20, + maxUsageCount: Number.POSITIVE_INFINITY, }, ]; const cache = new InMemoryCache(); - const { result, rerender } = renderHook( + + using _disabledAct = disableActEnvironment(); + const { takeSnapshot, rerender } = await renderHookToSnapshotStream( + // TODO: I don't think this needs to be a polling query as it has + // nothing to do with this test. This test checks to ensure that + // changing queries executes the new query and returns the right value. + // We should consider removing the pollling from this test and save it + // for a polling-specific test instead. ({ query }) => useQuery(query, { pollInterval: 10 }), { wrapper: ({ children }) => ( @@ -870,17 +1352,49 @@ describe("useQuery Hook", () => { } ); - expect(result.current.loading).toBe(true); - rerender({ query: query2 }); - expect(result.current.loading).toBe(true); + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); + } + + await rerender({ query: query2 }); + + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); + } + + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: mocks[1].result.data, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); + } - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); - expect(result.current.data).toEqual(mocks[1].result.data); + // We do not include expect(takeSnapshot).not.toRerender() here because + // this is a polling query. }); it("`cache-and-network` fetch policy", async () => { @@ -904,7 +1418,8 @@ describe("useQuery Hook", () => { cache.writeQuery({ query, data: { hello: "from cache" } }); - const { result } = renderHook( + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( () => useQuery(query, { fetchPolicy: "cache-and-network" }), { wrapper: ({ children }) => ( @@ -914,16 +1429,33 @@ describe("useQuery Hook", () => { ); // TODO: FIXME - expect(result.current.loading).toBe(true); - expect(result.current.data).toEqual({ hello: "from cache" }); + { + const result = await takeSnapshot(); - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); - expect(result.current.data).toEqual({ hello: "from link" }); + expect(result).toEqualQueryResult({ + data: { hello: "from cache" }, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); + } + + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: { hello: "from link" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { hello: "from cache" }, + variables: {}, + }); + } + + await expect(takeSnapshot).not.toRerender(); }); it("should not use the cache when using `network-only`", async () => { @@ -945,7 +1477,8 @@ describe("useQuery Hook", () => { data: { hello: "from cache" }, }); - const { result } = renderHook( + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( () => useQuery(query, { fetchPolicy: "network-only" }), { wrapper: ({ children }) => ( @@ -956,18 +1489,36 @@ describe("useQuery Hook", () => { } ); - expect(result.current.loading).toBe(true); - expect(result.current.data).toBeUndefined(); + { + const result = await takeSnapshot(); - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); - expect(result.current.data).toEqual({ hello: "from link" }); + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); + } + + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: { hello: "from link" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); + } + + await expect(takeSnapshot).not.toRerender(); }); + // TODO: Move this to ssr useQuery tests it("should use the cache when in ssrMode and fetchPolicy is `network-only`", async () => { const query = gql` query { @@ -986,7 +1537,9 @@ describe("useQuery Hook", () => { }); const client = new ApolloClient({ link, cache, ssrMode: true }); - const { result } = renderHook( + + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( () => useQuery(query, { fetchPolicy: "network-only" }), { wrapper: ({ children }) => ( @@ -995,19 +1548,23 @@ describe("useQuery Hook", () => { } ); - expect(result.current.loading).toBe(false); - expect(result.current.data).toEqual({ hello: "from cache" }); + { + const result = await takeSnapshot(); - await expect( - waitFor( - () => { - expect(result.current.data).toEqual({ hello: "from link" }); - }, - { interval: 1, timeout: 20 } - ) - ).rejects.toThrow(); + expect(result).toEqualQueryResult({ + data: { hello: "from cache" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); + } + + await expect(takeSnapshot).not.toRerender(); }); + // TODO: Move this to ssr useQuery tests it("should not hang when ssrMode is true but the cache is not populated for some reason", async () => { const query = gql` query { @@ -1025,22 +1582,43 @@ describe("useQuery Hook", () => { ssrMode: true, }); - const { result } = renderHook(() => useQuery(query), { - wrapper: ({ children }) => ( - {children} - ), - }); + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( + () => useQuery(query), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); - expect(result.current.loading).toBe(true); - expect(result.current.data).toBeUndefined(); + { + const result = await takeSnapshot(); - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); - expect(result.current.data).toEqual({ hello: "from link" }); + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); + } + + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: { hello: "from link" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); + } + + await expect(takeSnapshot).not.toRerender(); }); }); @@ -1054,6 +1632,7 @@ describe("useQuery Hook", () => { const link = mockSingleLink({ request: { query }, result: { data: { hello: "from link" } }, + delay: 20, }); const client = new ApolloClient({ @@ -1061,19 +1640,21 @@ describe("useQuery Hook", () => { cache: new InMemoryCache(), }); - const fetchPolicyLog: (string | undefined)[] = []; - let defaultFetchPolicy: WatchQueryFetchPolicy = "cache-and-network"; - const { result } = renderHook( + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( () => { const result = useQuery(query, { defaultOptions: { fetchPolicy: defaultFetchPolicy, }, }); - fetchPolicyLog.push(result.observable.options.fetchPolicy); - return result; + return { + result, + fetchPolicy: result.observable.options.fetchPolicy, + defaultFetchPolicy, + }; }, { wrapper: ({ children }) => ( @@ -1082,26 +1663,43 @@ describe("useQuery Hook", () => { } ); - expect(result.current.loading).toBe(true); - expect(result.current.data).toBeUndefined(); - expect(fetchPolicyLog).toEqual(["cache-and-network"]); + { + const { result, fetchPolicy, defaultFetchPolicy } = + await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); + + expect(fetchPolicy).toBe("cache-and-network"); + expect(defaultFetchPolicy).toBe("cache-and-network"); + } // Change the default fetchPolicy to verify that it is not used the second // time useQuery is called. defaultFetchPolicy = "network-only"; - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); + { + const { result, fetchPolicy, defaultFetchPolicy } = + await takeSnapshot(); - expect(result.current.data).toEqual({ hello: "from link" }); - expect(fetchPolicyLog).toEqual([ - "cache-and-network", - "cache-and-network", - ]); + expect(result).toEqualQueryResult({ + data: { hello: "from link" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); + + expect(fetchPolicy).toBe("cache-and-network"); + expect(defaultFetchPolicy).toBe("network-only"); + } }); it("can provide individual default variables", async () => { @@ -1120,12 +1718,14 @@ describe("useQuery Hook", () => { link: new ApolloLink( (request) => new Observable((observer) => { - observer.next({ - data: { - vars: request.variables, - }, - }); - observer.complete(); + setTimeout(() => { + observer.next({ + data: { + vars: request.variables, + }, + }); + observer.complete(); + }, 20); }) ), @@ -1141,178 +1741,205 @@ describe("useQuery Hook", () => { }, }); - const fetchPolicyLog: (string | undefined)[] = []; - - const { result } = renderHook( - () => { - const result = useQuery(query, { - defaultOptions: { - fetchPolicy: "cache-and-network", + using _disabledAct = disableActEnvironment(); + const { takeSnapshot, getCurrentSnapshot } = + await renderHookToSnapshotStream( + () => { + const result = useQuery(query, { + defaultOptions: { + fetchPolicy: "cache-and-network", + variables: { + sourceOfVar: "local", + isGlobal: false, + } as OperationVariables, + }, variables: { - sourceOfVar: "local", - isGlobal: false, - } as OperationVariables, - }, - variables: { - mandatory: true, - }, - }); - fetchPolicyLog.push(result.observable.options.fetchPolicy); - return result; - }, - { - wrapper: ({ children }) => ( - {children} - ), - } - ); + mandatory: true, + }, + }); - expect(result.current.loading).toBe(true); - expect(result.current.data).toBeUndefined(); - expect(result.current.observable.variables).toEqual({ - sourceOfVar: "local", - isGlobal: false, - mandatory: true, - }); + return { + result, + // Provide a snapshot of these values for this render, rather + // than checking the mutable value on result.observable. + fetchPolicy: result.observable.options.fetchPolicy, + variables: result.observable.variables, + }; + }, + { + wrapper: ({ children }) => ( + {children} + ), + } + ); - expect(result.current.observable.options.fetchPolicy).toBe( - "cache-and-network" - ); + { + const { result, fetchPolicy, variables } = await takeSnapshot(); + const { observable } = result; + + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: { + sourceOfVar: "local", + isGlobal: false, + mandatory: true, + }, + }); - expect( - // The defaultOptions field is for useQuery options (QueryHookOptions), - // not the more general WatchQueryOptions that ObservableQuery sees. - "defaultOptions" in result.current.observable.options - ).toBe(false); + expect(variables).toEqual({ + sourceOfVar: "local", + isGlobal: false, + mandatory: true, + }); + expect(fetchPolicy).toBe("cache-and-network"); + expect( + // The defaultOptions field is for useQuery options (QueryHookOptions), + // not the more general WatchQueryOptions that ObservableQuery sees. + "defaultOptions" in observable.options + ).toBe(false); + } - expect(fetchPolicyLog).toEqual(["cache-and-network"]); + { + const { result, fetchPolicy, variables } = await takeSnapshot(); - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); + expect(result).toEqualQueryResult({ + data: { + vars: { sourceOfVar: "local", isGlobal: false, mandatory: true }, + }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: { + sourceOfVar: "local", + isGlobal: false, + mandatory: true, + }, + }); - expect(result.current.data).toEqual({ - vars: { + expect(variables).toEqual({ sourceOfVar: "local", isGlobal: false, mandatory: true, + }); + expect(fetchPolicy).toBe("cache-and-network"); + } + + const { + result: { observable }, + } = getCurrentSnapshot(); + const finalResult = await observable.reobserve({ + fetchPolicy: "network-only", + nextFetchPolicy: "cache-first", + variables: { + // Since reobserve replaces the variables object rather than merging + // the individual variables together, we need to include the current + // variables manually if we want them to show up in the output below. + ...observable.variables, + sourceOfVar: "reobserve", }, }); - expect(fetchPolicyLog).toEqual([ - "cache-and-network", - "cache-and-network", - ]); - - const reobservePromise = act(() => - result.current.observable - .reobserve({ - fetchPolicy: "network-only", - nextFetchPolicy: "cache-first", - variables: { - // Since reobserve replaces the variables object rather than merging - // the individual variables together, we need to include the current - // variables manually if we want them to show up in the output below. - ...result.current.observable.variables, - sourceOfVar: "reobserve", - }, - }) - .then((finalResult) => { - expect(finalResult.loading).toBe(false); - expect(finalResult.data).toEqual({ - vars: { - sourceOfVar: "reobserve", - isGlobal: false, - mandatory: true, - }, - }); - }) - ); - - expect(result.current.observable.options.fetchPolicy).toBe("cache-first"); - - expect(result.current.observable.variables).toEqual({ - sourceOfVar: "reobserve", - isGlobal: false, - mandatory: true, + expect(finalResult).toEqualApolloQueryResult({ + data: { + vars: { + sourceOfVar: "reobserve", + isGlobal: false, + mandatory: true, + }, + }, + loading: false, + networkStatus: NetworkStatus.ready, }); - await reobservePromise; + { + const { result, fetchPolicy, variables } = await takeSnapshot(); - expect(result.current.observable.options.fetchPolicy).toBe("cache-first"); + expect(result).toEqualQueryResult({ + data: { + vars: { + sourceOfVar: "reobserve", + isGlobal: false, + mandatory: true, + }, + }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { + vars: { sourceOfVar: "local", isGlobal: false, mandatory: true }, + }, + variables: { + sourceOfVar: "reobserve", + isGlobal: false, + mandatory: true, + }, + }); - expect(result.current.loading).toBe(false); - expect(result.current.data).toEqual({ - vars: { + expect(variables).toEqual({ sourceOfVar: "reobserve", isGlobal: false, mandatory: true, - }, - }); - expect(result.current.observable.variables).toEqual( - result.current.data!.vars - ); - - expect(fetchPolicyLog).toEqual([ - "cache-and-network", - "cache-and-network", - "cache-first", - ]); - - const reobserveNoVarMergePromise = act(() => - result.current.observable - .reobserve({ - fetchPolicy: "network-only", - nextFetchPolicy: "cache-first", - variables: { - // This reobservation is like the one above, with no variable merging. - // ...result.current.observable.variables, - sourceOfVar: "reobserve without variable merge", - }, - }) - .then((finalResult) => { - expect(finalResult.loading).toBe(false); - expect(finalResult.data).toEqual({ - vars: { - sourceOfVar: "reobserve without variable merge", - // Since we didn't merge in result.current.observable.variables, we - // don't see these variables anymore: - // isGlobal: false, - // mandatory: true, - }, - }); - }) - ); + }); + expect(fetchPolicy).toBe("cache-first"); + } - expect(result.current.observable.options.fetchPolicy).toBe("cache-first"); + const finalResultNoVarMerge = + await getCurrentSnapshot().result.observable.reobserve({ + fetchPolicy: "network-only", + nextFetchPolicy: "cache-first", + variables: { + // This reobservation is like the one above, with no variable merging. + // ...result.current.observable.variables, + sourceOfVar: "reobserve without variable merge", + }, + }); - expect(result.current.observable.variables).toEqual({ - sourceOfVar: "reobserve without variable merge", + expect(finalResultNoVarMerge).toEqualApolloQueryResult({ + // Since we didn't merge in result.current.observable.variables, we + // don't see these variables anymore: + // isGlobal: false, + // mandatory: true, + data: { vars: { sourceOfVar: "reobserve without variable merge" } }, + loading: false, + networkStatus: NetworkStatus.ready, }); - await reobserveNoVarMergePromise; + { + const { result, fetchPolicy, variables } = await takeSnapshot(); - expect(result.current.observable.options.fetchPolicy).toBe("cache-first"); + expect(result).toEqualQueryResult({ + data: { + vars: { + sourceOfVar: "reobserve without variable merge", + }, + }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { + vars: { + sourceOfVar: "reobserve", + isGlobal: false, + mandatory: true, + }, + }, + variables: { + sourceOfVar: "reobserve without variable merge", + }, + }); - expect(result.current.loading).toBe(false); - expect(result.current.data).toEqual({ - vars: { + expect(variables).toEqual({ sourceOfVar: "reobserve without variable merge", - }, - }); - expect(result.current.observable.variables).toEqual( - result.current.data!.vars - ); + }); + expect(fetchPolicy).toBe("cache-first"); + } - expect(fetchPolicyLog).toEqual([ - "cache-and-network", - "cache-and-network", - "cache-first", - "cache-first", - ]); + await expect(takeSnapshot).not.toRerender(); }); it("defaultOptions do not confuse useQuery when unskipping a query (issue #9635)", async () => { @@ -1352,89 +1979,127 @@ describe("useQuery Hook", () => { const defaultFetchPolicy = "network-only"; - const { result } = renderHook( - () => { - const [skip, setSkip] = useState(true); - return { - setSkip, - query: useQuery(query, { + using _disabledAct = disableActEnvironment(); + const { takeSnapshot, getCurrentSnapshot } = + await renderHookToSnapshotStream( + () => { + const [skip, setSkip] = useState(true); + const result = useQuery(query, { skip, defaultOptions: { fetchPolicy: defaultFetchPolicy, }, - }), - }; - }, - { - wrapper: ({ children }) => ( - {children} - ), - } - ); - - expect(result.current.query.loading).toBe(false); - expect(result.current.query.networkStatus).toBe(NetworkStatus.ready); - expect(result.current.query.data).toBeUndefined(); + }); - await expect( - waitFor( - () => { - expect(result.current.query.data).toEqual({ counter: 1 }); + return { + setSkip, + query: result, + fetchPolicy: result.observable.options.fetchPolicy, + }; }, - { interval: 1, timeout: 20 } - ) - ).rejects.toThrow(); + { + wrapper: ({ children }) => ( + {children} + ), + } + ); - act(() => { - result.current.setSkip(false); - }); - expect(result.current.query.loading).toBe(true); - expect(result.current.query.networkStatus).toBe(NetworkStatus.loading); - expect(result.current.query.data).toBeUndefined(); - await waitFor( - () => { - expect(result.current.query.loading).toBe(false); - }, - { interval: 1 } - ); - expect(result.current.query.networkStatus).toBe(NetworkStatus.ready); - expect(result.current.query.data).toEqual({ counter: 1 }); + { + const { query } = await takeSnapshot(); + + expect(query).toEqualQueryResult({ + data: undefined, + error: undefined, + called: false, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); + } - const { options } = result.current.query.observable; - expect(options.fetchPolicy).toBe(defaultFetchPolicy); + getCurrentSnapshot().setSkip(false); - act(() => { - result.current.setSkip(true); - }); - expect(result.current.query.loading).toBe(false); - expect(result.current.query.networkStatus).toBe(NetworkStatus.ready); - expect(result.current.query.data).toBeUndefined(); + { + const { query } = await takeSnapshot(); - await expect( - waitFor( - () => { - expect(result.current.query.data).toEqual({ counter: 1 }); - }, - { interval: 1, timeout: 20 } - ) - ).rejects.toThrow(); + expect(query).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); + } - act(() => { - result.current.setSkip(false); - }); - expect(result.current.query.loading).toBe(true); - expect(result.current.query.networkStatus).toBe(NetworkStatus.loading); - expect(result.current.query.data).toEqual({ counter: 1 }); - await waitFor( - () => { - expect(result.current.query.loading).toBe(false); - }, - { interval: 1 } - ); - expect(result.current.query.networkStatus).toBe(NetworkStatus.ready); - expect(result.current.query.data).toEqual({ counter: 2 }); + { + const { query, fetchPolicy } = await takeSnapshot(); + + expect(query).toEqualQueryResult({ + data: { counter: 1 }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); + + expect(fetchPolicy).toBe(defaultFetchPolicy); + } + + getCurrentSnapshot().setSkip(true); + + { + const { query, fetchPolicy } = await takeSnapshot(); + + expect(query).toEqualQueryResult({ + // TODO: wut? + data: undefined, + // TODO: wut? + called: false, + error: undefined, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { counter: 1 }, + variables: {}, + }); + + expect(fetchPolicy).toBe("standby"); + } + + getCurrentSnapshot().setSkip(false); + + { + const { query, fetchPolicy } = await takeSnapshot(); + + expect(query).toEqualQueryResult({ + // TODO: wut? + data: { counter: 1 }, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: { counter: 1 }, + variables: {}, + }); + + expect(fetchPolicy).toBe(defaultFetchPolicy); + } + + { + const { query, fetchPolicy } = await takeSnapshot(); + + expect(query).toEqualQueryResult({ + data: { counter: 2 }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { counter: 1 }, + variables: {}, + }); - expect(options.fetchPolicy).toBe(defaultFetchPolicy); + expect(fetchPolicy).toBe(defaultFetchPolicy); + } }); }); @@ -1452,10 +2117,12 @@ describe("useQuery Hook", () => { const client = new ApolloClient({ link, cache: new InMemoryCache(), + // TODO: is this really needed for this test? ssrMode: true, }); - const { result } = renderHook( + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( () => useQuery(query, { client }) // We deliberately do not provide the usual ApolloProvider wrapper for // this test, since we are providing the client directly to useQuery. @@ -1468,17 +2135,31 @@ describe("useQuery Hook", () => { // } ); - expect(result.current.loading).toBe(true); - expect(result.current.data).toBeUndefined(); + { + const result = await takeSnapshot(); - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); + } + + { + const result = await takeSnapshot(); - expect(result.current.data).toEqual({ hello: "from link" }); + expect(result).toEqualQueryResult({ + data: { hello: "from link" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); + } }); describe("", () => { @@ -1509,41 +2190,55 @@ describe("useQuery Hook", () => { ), }); - const { result } = renderHook( - () => - useQuery(query, { - fetchPolicy: "cache-and-network", - }), - { - wrapper: ({ children }) => ( - - {children} - - ), - } - ); + using _disabledAct = disableActEnvironment(); + const { takeSnapshot, getCurrentSnapshot } = + await renderHookToSnapshotStream( + () => + useQuery(query, { + fetchPolicy: "cache-and-network", + }), + { + wrapper: ({ children }) => ( + + {children} + + ), + } + ); - expect(result.current.loading).toBe(true); - expect(result.current.networkStatus).toBe(NetworkStatus.loading); - expect(result.current.data).toBe(undefined); + { + const result = await takeSnapshot(); - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); - expect(result.current.networkStatus).toBe(NetworkStatus.ready); - expect(result.current.data).toEqual({ - linkCount: 1, - }); + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); + } + + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: { linkCount: 1 }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); + } function checkObservableQueries(expectedLinkCount: number) { const obsQueries = client.getObservableQueries("all"); + const { observable } = getCurrentSnapshot(); expect(obsQueries.size).toBe(2); - const activeSet = new Set(); - const inactiveSet = new Set(); + const activeSet = new Set(); + const inactiveSet = new Set(); obsQueries.forEach((obsQuery) => { if (obsQuery.hasObservers()) { expect(inactiveSet.has(obsQuery)).toBe(false); @@ -1566,36 +2261,43 @@ describe("useQuery Hook", () => { checkObservableQueries(1); - await act(() => - result.current.reobserve().then((result) => { - expect(result.loading).toBe(false); - expect(result.loading).toBe(false); - expect(result.networkStatus).toBe(NetworkStatus.ready); - expect(result.data).toEqual({ - linkCount: 2, - }); - }) - ); - - await waitFor(() => { - expect(result.current.loading).toBe(false); + await expect( + getCurrentSnapshot().observable.reobserve() + ).resolves.toEqualApolloQueryResult({ + data: { linkCount: 2 }, + loading: false, + networkStatus: NetworkStatus.ready, }); - await waitFor( - () => { - expect(result.current.data).toEqual({ - linkCount: 2, - }); - }, - { interval: 1 } - ); - await waitFor( - () => { - expect(result.current.networkStatus).toBe(NetworkStatus.ready); - }, - { interval: 1 } - ); + + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: { linkCount: 1 }, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: { linkCount: 1 }, + variables: {}, + }); + } + + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: { linkCount: 2 }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { linkCount: 1 }, + variables: {}, + }); + } checkObservableQueries(2); + + await expect(takeSnapshot).not.toRerender(); }); }); diff --git a/src/testing/matchers/index.d.ts b/src/testing/matchers/index.d.ts index 1313967147b..00e0906c88b 100644 --- a/src/testing/matchers/index.d.ts +++ b/src/testing/matchers/index.d.ts @@ -1,5 +1,6 @@ import type { ApolloClient, + ApolloQueryResult, DocumentNode, OperationVariables, } from "../../core/index.js"; @@ -69,6 +70,12 @@ interface ApolloCustomMatchers { (value: any, options?: TakeOptions) => Promise : { error: "matcher needs to be called on an ObservableStream instance" }; + toEqualApolloQueryResult: T extends ApolloQueryResult ? + (expected: ApolloQueryResult) => R + : T extends Promise> ? + (expected: ApolloQueryResult) => R + : { error: "matchers needs to be called on an ApolloQueryResult" }; + toEqualQueryResult: T extends QueryResult ? (expected: Pick, CheckedKeys>) => R : T extends Promise> ? diff --git a/src/testing/matchers/index.ts b/src/testing/matchers/index.ts index 338d8ac734d..e24f491d42f 100644 --- a/src/testing/matchers/index.ts +++ b/src/testing/matchers/index.ts @@ -10,6 +10,7 @@ import { toEmitMatchedValue } from "./toEmitMatchedValue.js"; import { toEmitNext } from "./toEmitNext.js"; import { toEmitValue } from "./toEmitValue.js"; import { toEmitValueStrict } from "./toEmitValueStrict.js"; +import { toEqualApolloQueryResult } from "./toEqualApolloQueryResult.js"; import { toEqualQueryResult } from "./toEqualQueryResult.js"; expect.extend({ @@ -20,6 +21,7 @@ expect.extend({ toEmitNext, toEmitValue, toEmitValueStrict, + toEqualApolloQueryResult, toEqualQueryResult, toBeDisposed, toHaveSuspenseCacheEntryUsing, diff --git a/src/testing/matchers/toEqualApolloQueryResult.ts b/src/testing/matchers/toEqualApolloQueryResult.ts new file mode 100644 index 00000000000..fffeab907c6 --- /dev/null +++ b/src/testing/matchers/toEqualApolloQueryResult.ts @@ -0,0 +1,44 @@ +import { iterableEquality } from "@jest/expect-utils"; +import type { MatcherFunction } from "expect"; +import type { ApolloQueryResult } from "../../core/index.js"; + +export const toEqualApolloQueryResult: MatcherFunction< + [queryResult: ApolloQueryResult] +> = function (actual, expected) { + const queryResult = actual as ApolloQueryResult; + const hint = this.utils.matcherHint( + this.isNot ? ".not.toEqualApolloQueryResult" : "toEqualApolloQueryResult", + "queryResult", + "expected", + { isNot: this.isNot, promise: this.promise } + ); + + const pass = this.equals( + queryResult, + expected, + // https://github.com/jestjs/jest/blob/22029ba06b69716699254bb9397f2b3bc7b3cf3b/packages/expect/src/matchers.ts#L62-L67 + [...this.customTesters, iterableEquality], + true + ); + + return { + pass, + message: () => { + if (pass) { + return hint + `\n\nExpected: not ${this.utils.printExpected(expected)}`; + } + + return ( + hint + + "\n\n" + + this.utils.printDiffOrStringify( + expected, + queryResult, + "Expected", + "Received", + true + ) + ); + }, + }; +}; From bc25dfe7ad44ecc92597d298238f230364b5dba9 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 16 Jan 2025 10:56:54 -0700 Subject: [PATCH 7/9] Refactor `useQuery` tests to use `toEqualQueryResult` - Part 2 (#12274) --- src/react/hooks/__tests__/useQuery.test.tsx | 1975 ++++++++++++------- 1 file changed, 1309 insertions(+), 666 deletions(-) diff --git a/src/react/hooks/__tests__/useQuery.test.tsx b/src/react/hooks/__tests__/useQuery.test.tsx index 769a89ae265..2501a16919f 100644 --- a/src/react/hooks/__tests__/useQuery.test.tsx +++ b/src/react/hooks/__tests__/useQuery.test.tsx @@ -2330,53 +2330,73 @@ describe("useQuery Hook", () => { ); - const { result } = renderHook( - () => useQuery(query, { pollInterval: 10 }), - { wrapper } - ); + using _disabledAct = disableActEnvironment(); + const { takeSnapshot, getCurrentSnapshot } = + await renderHookToSnapshotStream( + () => useQuery(query, { pollInterval: 10 }), + { wrapper } + ); - expect(result.current.loading).toBe(true); - expect(result.current.data).toBe(undefined); + { + const result = await takeSnapshot(); - await waitFor( - () => { - expect(result.current.data).toEqual({ hello: "world 1" }); - }, - { interval: 1 } - ); + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); + } - expect(result.current.loading).toBe(false); + { + const result = await takeSnapshot(); - await waitFor( - () => { - expect(result.current.data).toEqual({ hello: "world 2" }); - }, - { interval: 1 } - ); + expect(result).toEqualQueryResult({ + data: { hello: "world 1" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); + } - expect(result.current.loading).toBe(false); + { + const result = await takeSnapshot(); - await waitFor( - () => { - expect(result.current.data).toEqual({ hello: "world 3" }); - }, - { interval: 1 } - ); + expect(result).toEqualQueryResult({ + data: { hello: "world 2" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { hello: "world 1" }, + variables: {}, + }); + } - expect(result.current.loading).toBe(false); + { + const result = await takeSnapshot(); - const { data: previousData } = result.current; - result.current.stopPolling(); - await expect( - waitFor( - () => { - expect(result.current.data).not.toEqual(previousData); - }, - { interval: 1, timeout: 20 } - ) - ).rejects.toThrow(); + expect(result).toEqualQueryResult({ + data: { hello: "world 3" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { hello: "world 2" }, + variables: {}, + }); + } + + getCurrentSnapshot().stopPolling(); + + await expect(takeSnapshot).not.toRerender(); }); + // TODO: Refactor the initial state of this test. This test states that + // `skip` goes from `true` -> `false`, but it starts out unskipped before + // enabling it. it("should start polling when skip goes from true to false", async () => { const query = gql` { @@ -2412,46 +2432,97 @@ describe("useQuery Hook", () => { ), } ); + { const result = await takeSnapshot(); - expect(result.loading).toBe(true); - expect(result.data).toBe(undefined); + + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); } + { const result = await takeSnapshot(); - expect(result.loading).toBe(false); - expect(result.data).toEqual({ hello: "world 1" }); + + expect(result).toEqualQueryResult({ + data: { hello: "world 1" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); } await rerender({ skip: true }); + { - const snapshot = await takeSnapshot(); - expect(snapshot.loading).toBe(false); - expect(snapshot.data).toEqual(undefined); + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + // TODO: wut? + data: undefined, + // TODO: wut? + called: false, + // TODO: wut? + error: undefined, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { hello: "world 1" }, + variables: {}, + }); } await expect(takeSnapshot).not.toRerender({ timeout: 100 }); await rerender({ skip: false }); + { const result = await takeSnapshot(); - expect(result.loading).toBe(false); - expect(result.data).toEqual({ hello: "world 1" }); + + expect(result).toEqualQueryResult({ + data: { hello: "world 1" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { hello: "world 1" }, + variables: {}, + }); } { const result = await takeSnapshot(); - expect(result.loading).toBe(false); - expect(result.data).toEqual({ hello: "world 2" }); + + expect(result).toEqualQueryResult({ + data: { hello: "world 2" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { hello: "world 1" }, + variables: {}, + }); } { const result = await takeSnapshot(); - expect(result.loading).toBe(false); - expect(result.data).toEqual({ hello: "world 3" }); + + expect(result).toEqualQueryResult({ + data: { hello: "world 3" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { hello: "world 2" }, + variables: {}, + }); } }); + // TODO: Move out of "polling" query tests since this doesn't test polling it("should return data from network when clients default fetch policy set to network-only", async () => { const query = gql` { @@ -2482,18 +2553,39 @@ describe("useQuery Hook", () => { ); - const { result } = renderHook(() => useQuery(query), { wrapper }); + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( + () => useQuery(query), + { wrapper } + ); + + { + const result = await takeSnapshot(); - expect(result.current.loading).toBe(true); - expect(result.current.data).toBe(undefined); + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); + } - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); - expect(result.current.data).toEqual(data); + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); + } + + await expect(takeSnapshot).not.toRerender(); }); it("should stop polling when component unmounts", async () => { @@ -2540,14 +2632,28 @@ describe("useQuery Hook", () => { { const result = await takeSnapshot(); - expect(result.loading).toBe(true); - expect(result.data).toBe(undefined); + + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); } { const result = await takeSnapshot(); - expect(result.loading).toBe(false); - expect(result.data).toEqual({ hello: "world 1" }); + + expect(result).toEqualQueryResult({ + data: { hello: "world 1" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); expect(requestSpy).toHaveBeenCalled(); } @@ -2630,27 +2736,45 @@ describe("useQuery Hook", () => { ); { - const snapshot = await takeSnapshot(); + const result = await takeSnapshot(); - expect(snapshot.loading).toBe(true); - expect(snapshot.data).toBeUndefined(); + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); } { - const snapshot = await takeSnapshot(); + const result = await takeSnapshot(); - expect(snapshot.loading).toBe(false); - expect(snapshot.data).toEqual({ hello: "world 1" }); + expect(result).toEqualQueryResult({ + data: { hello: "world 1" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); expect(requestSpy).toHaveBeenCalledTimes(1); } await wait(10); { - const snapshot = await takeSnapshot(); + const result = await takeSnapshot(); - expect(snapshot.loading).toBe(false); - expect(snapshot.data).toEqual({ hello: "world 2" }); + expect(result).toEqualQueryResult({ + data: { hello: "world 2" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { hello: "world 1" }, + variables: {}, + }); expect(requestSpy).toHaveBeenCalledTimes(2); } @@ -2700,21 +2824,37 @@ describe("useQuery Hook", () => { ); - const { result, unmount } = renderHook( + using _disabledAct = disableActEnvironment(); + const { takeSnapshot, unmount } = await renderHookToSnapshotStream( () => useQuery(query, { pollInterval: 10 }), { wrapper } ); - expect(result.current.loading).toBe(true); - expect(result.current.data).toBe(undefined); + { + const result = await takeSnapshot(); - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); - expect(result.current.data).toEqual({ hello: "world 1" }); + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); + } + + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: { hello: "world 1" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); + } const requestSpyCallCount = requestSpy.mock.calls.length; expect(requestSpy).toHaveBeenCalledTimes(requestSpyCallCount); @@ -2789,27 +2929,45 @@ describe("useQuery Hook", () => { ); { - const snapshot = await takeSnapshot(); + const result = await takeSnapshot(); - expect(snapshot.loading).toBe(true); - expect(snapshot.data).toBeUndefined(); + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); } { - const snapshot = await takeSnapshot(); + const result = await takeSnapshot(); - expect(snapshot.loading).toBe(false); - expect(snapshot.data).toEqual({ hello: "world 1" }); + expect(result).toEqualQueryResult({ + data: { hello: "world 1" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); expect(requestSpy).toHaveBeenCalledTimes(1); } await wait(10); { - const snapshot = await takeSnapshot(); + const result = await takeSnapshot(); - expect(snapshot.loading).toBe(false); - expect(snapshot.data).toEqual({ hello: "world 2" }); + expect(result).toEqualQueryResult({ + data: { hello: "world 2" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { hello: "world 1" }, + variables: {}, + }); expect(requestSpy).toHaveBeenCalledTimes(2); } @@ -2859,65 +3017,96 @@ describe("useQuery Hook", () => { ); - const { result } = renderHook( - () => useQuery(query, { pollInterval: 20 }), - { wrapper } - ); + using _disabledAct = disableActEnvironment(); + const { takeSnapshot, getCurrentSnapshot } = + await renderHookToSnapshotStream( + () => useQuery(query, { pollInterval: 20 }), + { wrapper } + ); - expect(result.current.loading).toBe(true); - expect(result.current.data).toBe(undefined); + { + const result = await takeSnapshot(); - await waitFor( - () => { - expect(result.current.data).toEqual({ hello: "world 1" }); - }, - { interval: 1 } - ); - expect(result.current.loading).toBe(false); + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); + } - await waitFor( - () => { - expect(result.current.data).toEqual({ hello: "world 2" }); - }, - { interval: 1 } - ); - expect(result.current.loading).toBe(false); + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: { hello: "world 1" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); + } - result.current.stopPolling(); + { + const result = await takeSnapshot(); - await expect( - waitFor( - () => { - expect(result.current.data).toEqual({ hello: "world 3" }); - }, - { interval: 1, timeout: 20 } - ) - ).rejects.toThrow(); - result.current.startPolling(20); + expect(result).toEqualQueryResult({ + data: { hello: "world 2" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { hello: "world 1" }, + variables: {}, + }); + } + + getCurrentSnapshot().stopPolling(); + + await expect(takeSnapshot).not.toRerender({ timeout: 50 }); + + getCurrentSnapshot().startPolling(20); expect(requestSpy).toHaveBeenCalledTimes(2); expect(onErrorFn).toHaveBeenCalledTimes(0); - await waitFor( - () => { - expect(result.current.data).toEqual({ hello: "world 3" }); - }, - { interval: 1 } - ); - expect(result.current.loading).toBe(false); + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: { hello: "world 3" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { hello: "world 2" }, + variables: {}, + }); + } + + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: { hello: "world 4" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { hello: "world 3" }, + variables: {}, + }); + } - await waitFor( - () => { - expect(result.current.data).toEqual({ hello: "world 4" }); - }, - { interval: 1 } - ); - expect(result.current.loading).toBe(false); expect(requestSpy).toHaveBeenCalledTimes(4); expect(onErrorFn).toHaveBeenCalledTimes(0); requestSpy.mockRestore(); }); + // TODO: This test does not really check for an error so we should probably + // do something different. That said, there are several other tests that + // call stopPolling on its own so we should either move this up in the test + // suite, or delete it as its tested from other tests. it("should not throw an error if stopPolling is called manually", async () => { const query = gql` { @@ -2940,24 +3129,40 @@ describe("useQuery Hook", () => { ); - const { result, unmount } = renderHook(() => useQuery(query), { - wrapper, - }); + using _disabledAct = disableActEnvironment(); + const { takeSnapshot, getCurrentSnapshot, unmount } = + await renderHookToSnapshotStream(() => useQuery(query), { + wrapper, + }); - expect(result.current.loading).toBe(true); - expect(result.current.data).toBe(undefined); + { + const result = await takeSnapshot(); - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); + } - expect(result.current.data).toEqual({ hello: "world" }); + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: { hello: "world" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); + } unmount(); - result.current.stopPolling(); + getCurrentSnapshot().stopPolling(); }); describe("should prevent fetches when `skipPollAttempt` returns `false`", () => { @@ -3012,58 +3217,85 @@ describe("useQuery Hook", () => { { wrapper } ); - expect(result.current.loading).toBe(true); - expect(result.current.data).toBe(undefined); + expect(result.current).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); await waitFor( () => { - expect(result.current.data).toEqual({ hello: "world 1" }); + expect(result.current).toEqualQueryResult({ + data: { hello: "world 1" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); }, { interval: 1 } ); - expect(result.current.loading).toBe(false); - - await waitFor( - () => { - expect(result.current.data).toEqual({ hello: "world 2" }); - }, - { interval: 1 } - ); + await jest.advanceTimersByTimeAsync(12); + expect(result.current).toEqualQueryResult({ + data: { hello: "world 2" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { hello: "world 1" }, + variables: {}, + }); skipPollAttempt.mockImplementation(() => true); - expect(result.current.loading).toBe(false); - await jest.advanceTimersByTime(12); - await waitFor( - () => expect(result.current.data).toEqual({ hello: "world 2" }), - { interval: 1 } - ); + await jest.advanceTimersByTimeAsync(12); + expect(result.current).toEqualQueryResult({ + data: { hello: "world 2" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { hello: "world 1" }, + variables: {}, + }); - await jest.advanceTimersByTime(12); - await waitFor( - () => expect(result.current.data).toEqual({ hello: "world 2" }), - { interval: 1 } - ); + await jest.advanceTimersByTimeAsync(12); + expect(result.current).toEqualQueryResult({ + data: { hello: "world 2" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { hello: "world 1" }, + variables: {}, + }); - await jest.advanceTimersByTime(12); - await waitFor( - () => expect(result.current.data).toEqual({ hello: "world 2" }), - { interval: 1 } - ); + await jest.advanceTimersByTimeAsync(12); + expect(result.current).toEqualQueryResult({ + data: { hello: "world 2" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { hello: "world 1" }, + variables: {}, + }); skipPollAttempt.mockImplementation(() => false); - expect(result.current.loading).toBe(false); - await waitFor( - () => { - expect(result.current.data).toEqual({ hello: "world 3" }); - }, - { interval: 1 } - ); - }); - - it("when defined for a single query", async () => { + await jest.advanceTimersByTimeAsync(12); + expect(result.current).toEqualQueryResult({ + data: { hello: "world 3" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { hello: "world 2" }, + variables: {}, + }); + }); + + it("when defined for a single query", async () => { const skipPollAttempt = jest.fn().mockImplementation(() => false); const query = gql` @@ -3102,55 +3334,82 @@ describe("useQuery Hook", () => { { wrapper } ); - expect(result.current.loading).toBe(true); - expect(result.current.data).toBe(undefined); + expect(result.current).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); await waitFor( () => { - expect(result.current.data).toEqual({ hello: "world 1" }); + expect(result.current).toEqualQueryResult({ + data: { hello: "world 1" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); }, { interval: 1 } ); - expect(result.current.loading).toBe(false); - - await waitFor( - () => { - expect(result.current.data).toEqual({ hello: "world 2" }); - }, - { interval: 1 } - ); + await jest.advanceTimersByTimeAsync(12); + expect(result.current).toEqualQueryResult({ + data: { hello: "world 2" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { hello: "world 1" }, + variables: {}, + }); skipPollAttempt.mockImplementation(() => true); - expect(result.current.loading).toBe(false); - await jest.advanceTimersByTime(12); - await waitFor( - () => expect(result.current.data).toEqual({ hello: "world 2" }), - { interval: 1 } - ); + await jest.advanceTimersByTimeAsync(12); + expect(result.current).toEqualQueryResult({ + data: { hello: "world 2" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { hello: "world 1" }, + variables: {}, + }); - await jest.advanceTimersByTime(12); - await waitFor( - () => expect(result.current.data).toEqual({ hello: "world 2" }), - { interval: 1 } - ); + await jest.advanceTimersByTimeAsync(12); + expect(result.current).toEqualQueryResult({ + data: { hello: "world 2" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { hello: "world 1" }, + variables: {}, + }); - await jest.advanceTimersByTime(12); - await waitFor( - () => expect(result.current.data).toEqual({ hello: "world 2" }), - { interval: 1 } - ); + await jest.advanceTimersByTimeAsync(12); + expect(result.current).toEqualQueryResult({ + data: { hello: "world 2" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { hello: "world 1" }, + variables: {}, + }); skipPollAttempt.mockImplementation(() => false); - expect(result.current.loading).toBe(false); - await waitFor( - () => { - expect(result.current.data).toEqual({ hello: "world 3" }); - }, - { interval: 1 } - ); + await jest.advanceTimersByTimeAsync(12); + expect(result.current).toEqualQueryResult({ + data: { hello: "world 3" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { hello: "world 2" }, + variables: {}, + }); }); }); }); @@ -3181,18 +3440,38 @@ describe("useQuery Hook", () => { ); - const { result } = renderHook(() => useQuery(query), { wrapper }); - - expect(result.current.loading).toBe(true); - expect(result.current.data).toBe(undefined); - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( + () => useQuery(query), + { wrapper } ); - expect(result.current.error).toBeInstanceOf(ApolloError); - expect(result.current.error!.message).toBe("error"); + + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); + } + + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: undefined, + error: new ApolloError({ graphQLErrors: [{ message: "error" }] }), + called: true, + loading: false, + networkStatus: NetworkStatus.error, + previousData: undefined, + variables: {}, + }); + } }); it("calls `onError` when a GraphQL error is returned", async () => { @@ -3218,32 +3497,45 @@ describe("useQuery Hook", () => { ); const onError = jest.fn(); - const { result } = renderHook(() => useQuery(query, { onError }), { - wrapper, - }); + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( + () => useQuery(query, { onError }), + { wrapper } + ); - expect(result.current.loading).toBe(true); - expect(result.current.data).toBe(undefined); + { + const result = await takeSnapshot(); - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); + } - expect(result.current.data).toBeUndefined(); - expect(result.current.error).toBeInstanceOf(ApolloError); - expect(result.current.error!.message).toBe("error"); + { + const result = await takeSnapshot(); - await waitFor(() => { - expect(onError).toHaveBeenCalledTimes(1); - }); - await waitFor(() => { - expect(onError).toHaveBeenCalledWith( - new ApolloError({ graphQLErrors: [new GraphQLError("error")] }) - ); - }); + expect(result).toEqualQueryResult({ + data: undefined, + error: new ApolloError({ graphQLErrors: [{ message: "error" }] }), + called: true, + loading: false, + networkStatus: NetworkStatus.error, + previousData: undefined, + variables: {}, + }); + } + + expect(onError).toHaveBeenCalledTimes(1); + expect(onError).toHaveBeenCalledWith( + new ApolloError({ graphQLErrors: [{ message: "error" }] }) + ); + + await expect(takeSnapshot).not.toRerender(); }); it("calls `onError` when a network error has occurred", async () => { @@ -3267,36 +3559,47 @@ describe("useQuery Hook", () => { ); const onError = jest.fn(); - const { result } = renderHook(() => useQuery(query, { onError }), { - wrapper, - }); + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( + () => useQuery(query, { onError }), + { wrapper } + ); - expect(result.current.loading).toBe(true); - expect(result.current.data).toBe(undefined); - expect(result.current.error).toBe(undefined); + { + const result = await takeSnapshot(); - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); + } - expect(result.current.data).toBeUndefined(); - expect(result.current.error).toBeInstanceOf(ApolloError); - expect(result.current.error!.message).toBe("Could not fetch"); - expect(result.current.error!.networkError).toEqual( - new Error("Could not fetch") + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: undefined, + error: new ApolloError({ + networkError: new Error("Could not fetch"), + }), + called: true, + loading: false, + networkStatus: NetworkStatus.error, + previousData: undefined, + variables: {}, + }); + } + + expect(onError).toHaveBeenCalledTimes(1); + expect(onError).toHaveBeenCalledWith( + new ApolloError({ networkError: new Error("Could not fetch") }) ); - await waitFor(() => { - expect(onError).toHaveBeenCalledTimes(1); - }); - await waitFor(() => { - expect(onError).toHaveBeenCalledWith( - new ApolloError({ networkError: new Error("Could not fetch") }) - ); - }); + await expect(takeSnapshot).not.toRerender(); }); it("removes partial data from result when response has errors", async () => { @@ -3323,27 +3626,42 @@ describe("useQuery Hook", () => { ); const onError = jest.fn(); - const { result } = renderHook(() => useQuery(query, { onError }), { - wrapper, - }); + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( + () => useQuery(query, { onError }), + { wrapper } + ); - expect(result.current.loading).toBe(true); - expect(result.current.data).toBe(undefined); - expect(result.current.error).toBe(undefined); + { + const result = await takeSnapshot(); - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); + } - expect(result.current.data).toBeUndefined(); - expect(result.current.error).toBeInstanceOf(ApolloError); - expect(result.current.error!.message).toBe('Could not fetch "hello"'); - expect(result.current.error!.graphQLErrors).toEqual([ - new GraphQLError('Could not fetch "hello"'), - ]); + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: undefined, + error: new ApolloError({ + graphQLErrors: [{ message: 'Could not fetch "hello"' }], + }), + called: true, + loading: false, + networkStatus: NetworkStatus.error, + previousData: undefined, + variables: {}, + }); + } + + await expect(takeSnapshot).not.toRerender(); }); it('does not call `onError` when returning GraphQL errors while using an `errorPolicy` set to "ignore"', async () => { @@ -3369,28 +3687,43 @@ describe("useQuery Hook", () => { ); const onError = jest.fn(); - const { result } = renderHook( + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( () => useQuery(query, { onError, errorPolicy: "ignore" }), { wrapper } ); - expect(result.current.loading).toBe(true); - expect(result.current.data).toBe(undefined); - expect(result.current.error).toBe(undefined); + { + const result = await takeSnapshot(); - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); + } - expect(result.current.data).toBeUndefined(); - expect(result.current.error).toBeUndefined(); + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); + } await tick(); expect(onError).not.toHaveBeenCalled(); + + await expect(takeSnapshot).not.toRerender(); }); it('calls `onError` when a network error has occurred while using an `errorPolicy` set to "ignore"', async () => { @@ -3414,37 +3747,47 @@ describe("useQuery Hook", () => { ); const onError = jest.fn(); - const { result } = renderHook( + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( () => useQuery(query, { onError, errorPolicy: "ignore" }), { wrapper } ); - expect(result.current.loading).toBe(true); - expect(result.current.data).toBe(undefined); - expect(result.current.error).toBe(undefined); + { + const result = await takeSnapshot(); - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); + } - expect(result.current.data).toBeUndefined(); - expect(result.current.error).toBeInstanceOf(ApolloError); - expect(result.current.error!.message).toBe("Could not fetch"); - expect(result.current.error!.networkError).toEqual( - new Error("Could not fetch") + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: undefined, + error: new ApolloError({ + networkError: new Error("Could not fetch"), + }), + called: true, + loading: false, + networkStatus: NetworkStatus.error, + previousData: undefined, + variables: {}, + }); + } + + expect(onError).toHaveBeenCalledTimes(1); + expect(onError).toHaveBeenCalledWith( + new ApolloError({ networkError: new Error("Could not fetch") }) ); - await waitFor(() => { - expect(onError).toHaveBeenCalledTimes(1); - }); - await waitFor(() => { - expect(onError).toHaveBeenCalledWith( - new ApolloError({ networkError: new Error("Could not fetch") }) - ); - }); + await expect(takeSnapshot).not.toRerender(); }); it('returns partial data and discards GraphQL errors when using an `errorPolicy` set to "ignore"', async () => { @@ -3470,23 +3813,39 @@ describe("useQuery Hook", () => { ); - const { result } = renderHook( + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( () => useQuery(query, { errorPolicy: "ignore" }), { wrapper } ); - expect(result.current.loading).toBe(true); - expect(result.current.data).toBe(undefined); + { + const result = await takeSnapshot(); - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); + } - expect(result.current.data).toEqual({ hello: null }); - expect(result.current.error).toBeUndefined(); + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: { hello: null }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); + } + + await expect(takeSnapshot).not.toRerender(); }); it('calls `onCompleted` with partial data but avoids calling `onError` when using an `errorPolicy` set to "ignore"', async () => { @@ -3514,33 +3873,43 @@ describe("useQuery Hook", () => { const onError = jest.fn(); const onCompleted = jest.fn(); - const { result } = renderHook( + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( () => useQuery(query, { onError, onCompleted, errorPolicy: "ignore" }), { wrapper } ); - expect(result.current.loading).toBe(true); - expect(result.current.data).toBe(undefined); + { + const result = await takeSnapshot(); - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); + } - expect(result.current.data).toEqual({ hello: null }); - expect(result.current.error).toBeUndefined(); + { + const result = await takeSnapshot(); - await waitFor(() => { - expect(onCompleted).toHaveBeenCalledTimes(1); - }); - await waitFor(() => { - expect(onCompleted).toHaveBeenCalledWith({ hello: null }); - }); - await waitFor(() => { - expect(onError).not.toHaveBeenCalled(); - }); + expect(result).toEqualQueryResult({ + data: { hello: null }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); + } + + expect(onCompleted).toHaveBeenCalledTimes(1); + expect(onCompleted).toHaveBeenCalledWith({ hello: null }); + expect(onError).not.toHaveBeenCalled(); + + await expect(takeSnapshot).not.toRerender(); }); it('calls `onError` when returning GraphQL errors while using an `errorPolicy` set to "all"', async () => { @@ -3566,36 +3935,47 @@ describe("useQuery Hook", () => { ); const onError = jest.fn(); - const { result } = renderHook( + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( () => useQuery(query, { onError, errorPolicy: "all" }), { wrapper } ); - expect(result.current.loading).toBe(true); - expect(result.current.data).toBe(undefined); + { + const result = await takeSnapshot(); - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); + } - expect(result.current.data).toBeUndefined(); - expect(result.current.error).toBeInstanceOf(ApolloError); - expect(result.current.error!.message).toBe("error"); - expect(result.current.error!.graphQLErrors).toEqual([ - new GraphQLError("error"), - ]); + { + const result = await takeSnapshot(); - await waitFor(() => { - expect(onError).toHaveBeenCalledTimes(1); - }); - await waitFor(() => { - expect(onError).toHaveBeenCalledWith( - new ApolloError({ graphQLErrors: [new GraphQLError("error")] }) - ); - }); + expect(result).toEqualQueryResult({ + data: undefined, + error: new ApolloError({ graphQLErrors: [{ message: "error" }] }), + // TODO: Why does this only populate when errorPolicy is "all"? + errors: [{ message: "error" }], + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); + } + + expect(onError).toHaveBeenCalledTimes(1); + expect(onError).toHaveBeenCalledWith( + new ApolloError({ graphQLErrors: [new GraphQLError("error")] }) + ); + + await expect(takeSnapshot).not.toRerender(); }); it('returns partial data when returning GraphQL errors while using an `errorPolicy` set to "all"', async () => { @@ -3622,27 +4002,43 @@ describe("useQuery Hook", () => { ); const onError = jest.fn(); - const { result } = renderHook( + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( () => useQuery(query, { onError, errorPolicy: "all" }), { wrapper } ); - expect(result.current.loading).toBe(true); - expect(result.current.data).toBe(undefined); + { + const result = await takeSnapshot(); - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); + } - expect(result.current.data).toEqual({ hello: null }); - expect(result.current.error).toBeInstanceOf(ApolloError); - expect(result.current.error!.message).toBe('Could not fetch "hello"'); - expect(result.current.error!.graphQLErrors).toEqual([ - new GraphQLError('Could not fetch "hello"'), - ]); + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: { hello: null }, + error: new ApolloError({ + graphQLErrors: [{ message: 'Could not fetch "hello"' }], + }), + errors: [{ message: 'Could not fetch "hello"' }], + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); + } + + await expect(takeSnapshot).not.toRerender(); }); it('calls `onError` but not `onCompleted` when returning partial data with GraphQL errors while using an `errorPolicy` set to "all"', async () => { @@ -3670,41 +4066,51 @@ describe("useQuery Hook", () => { const onError = jest.fn(); const onCompleted = jest.fn(); - const { result } = renderHook( + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( () => useQuery(query, { onError, onCompleted, errorPolicy: "all" }), { wrapper } ); - expect(result.current.loading).toBe(true); - expect(result.current.data).toBe(undefined); - - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); - - expect(result.current.data).toEqual({ hello: null }); - expect(result.current.error).toBeInstanceOf(ApolloError); - expect(result.current.error!.message).toBe('Could not fetch "hello"'); - expect(result.current.error!.graphQLErrors).toEqual([ - new GraphQLError('Could not fetch "hello"'), - ]); + { + const result = await takeSnapshot(); - await waitFor(() => { - expect(onError).toHaveBeenCalledTimes(1); - }); - await waitFor(() => { - expect(onError).toHaveBeenCalledWith( - new ApolloError({ - graphQLErrors: [new GraphQLError('Could not fetch "hello"')], - }) - ); - }); - await waitFor(() => { - expect(onCompleted).not.toHaveBeenCalled(); - }); + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); + } + + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: { hello: null }, + error: new ApolloError({ + graphQLErrors: [{ message: 'Could not fetch "hello"' }], + }), + errors: [{ message: 'Could not fetch "hello"' }], + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); + } + + expect(onError).toHaveBeenCalledTimes(1); + expect(onError).toHaveBeenCalledWith( + new ApolloError({ + graphQLErrors: [new GraphQLError('Could not fetch "hello"')], + }) + ); + expect(onCompleted).not.toHaveBeenCalled(); + + await expect(takeSnapshot).not.toRerender(); }); it("calls `onError` a single time when refetching returns a successful result", async () => { @@ -3737,48 +4143,77 @@ describe("useQuery Hook", () => { ); const onError = jest.fn(); - const { result } = renderHook( - () => - useQuery(query, { - onError, - notifyOnNetworkStatusChange: true, - }), - { wrapper } - ); + using _disabledAct = disableActEnvironment(); + const { takeSnapshot, getCurrentSnapshot } = + await renderHookToSnapshotStream( + () => + useQuery(query, { + onError, + notifyOnNetworkStatusChange: true, + }), + { wrapper } + ); - expect(result.current.loading).toBe(true); - expect(result.current.data).toBe(undefined); + { + const result = await takeSnapshot(); - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); + } - expect(result.current.error).toBeInstanceOf(ApolloError); - expect(result.current.error!.message).toBe("error"); + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: undefined, + error: new ApolloError({ graphQLErrors: [{ message: "error" }] }), + called: true, + loading: false, + networkStatus: NetworkStatus.error, + previousData: undefined, + variables: {}, + }); + } - await new Promise((resolve) => setTimeout(resolve)); expect(onError).toHaveBeenCalledTimes(1); - await act(async () => void result.current.refetch()); - await waitFor( - () => { - expect(result.current.loading).toBe(true); - }, - { interval: 1 } - ); - expect(result.current.error).toBe(undefined); + void getCurrentSnapshot().refetch(); + + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.refetch, + previousData: undefined, + variables: {}, + }); + } + + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: { hello: "world" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); + } - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); - expect(result.current.data).toEqual({ hello: "world" }); expect(onError).toHaveBeenCalledTimes(1); + + await expect(takeSnapshot).not.toRerender(); }); it("should persist errors on re-render if they are still valid", async () => { @@ -3804,46 +4239,59 @@ describe("useQuery Hook", () => { ); - let updates = 0; - const { result, rerender } = renderHook( - () => (updates++, useQuery(query)), + using _disabledAct = disableActEnvironment(); + const { takeSnapshot, rerender } = await renderHookToSnapshotStream( + () => useQuery(query), { wrapper } ); - expect(result.current.loading).toBe(true); - expect(result.current.data).toBe(undefined); - expect(result.current.error).toBe(undefined); + { + const result = await takeSnapshot(); - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); + } - expect(result.current.loading).toBe(false); - expect(result.current.data).toBe(undefined); - expect(result.current.error).toBeInstanceOf(ApolloError); - expect(result.current.error!.message).toBe("error"); + { + const result = await takeSnapshot(); - rerender(); + expect(result).toEqualQueryResult({ + data: undefined, + error: new ApolloError({ graphQLErrors: [{ message: "error" }] }), + called: true, + loading: false, + networkStatus: NetworkStatus.error, + previousData: undefined, + variables: {}, + }); + } - expect(result.current.loading).toBe(false); - expect(result.current.data).toBe(undefined); - expect(result.current.error).toBeInstanceOf(ApolloError); - expect(result.current.error!.message).toBe("error"); + await rerender(undefined); - let previousUpdates = updates; - await expect( - waitFor( - () => { - expect(updates).not.toEqual(previousUpdates); - }, - { interval: 1, timeout: 20 } - ) - ).rejects.toThrow(); + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: undefined, + error: new ApolloError({ graphQLErrors: [{ message: "error" }] }), + called: true, + loading: false, + networkStatus: NetworkStatus.error, + previousData: undefined, + variables: {}, + }); + } + + await expect(takeSnapshot).not.toRerender(); }); + // TODO: Rewrite this test using renderHookToSnapshotStream it("should not return partial data from cache on refetch with errorPolicy: none (default) and notifyOnNetworkStatusChange: true", async () => { const query = gql` { @@ -3987,6 +4435,7 @@ describe("useQuery Hook", () => { expect(screen.queryByText(/partial data rendered/i)).toBeNull(); }); + // TODO: Rewrite this test using renderHookToSnapshotStream it("should return partial data from cache on refetch", async () => { const GET_DOG_DETAILS = gql` query dog($breed: String!) { @@ -4091,48 +4540,58 @@ describe("useQuery Hook", () => { ); - let updates = 0; - const { result, rerender } = renderHook( - () => ( - updates++, - useQuery(query, { onError: () => {}, onCompleted: () => {} }) - ), + using _disabledAct = disableActEnvironment(); + const { takeSnapshot, rerender } = await renderHookToSnapshotStream( + () => useQuery(query, { onError: () => {}, onCompleted: () => {} }), { wrapper } ); - expect(result.current.loading).toBe(true); - expect(result.current.data).toBe(undefined); - expect(result.current.error).toBe(undefined); + { + const result = await takeSnapshot(); - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); + } - expect(result.current.loading).toBe(false); - expect(result.current.data).toBe(undefined); - expect(result.current.error).toBeInstanceOf(ApolloError); - expect(result.current.error!.message).toBe("error"); + { + const result = await takeSnapshot(); - rerender(); + expect(result).toEqualQueryResult({ + data: undefined, + error: new ApolloError({ graphQLErrors: [{ message: "error" }] }), + called: true, + loading: false, + networkStatus: NetworkStatus.error, + previousData: undefined, + variables: {}, + }); + } - expect(result.current.loading).toBe(false); - expect(result.current.data).toBe(undefined); - expect(result.current.error).toBeInstanceOf(ApolloError); - expect(result.current.error!.message).toBe("error"); + await rerender(undefined); + + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: undefined, + error: new ApolloError({ graphQLErrors: [{ message: "error" }] }), + called: true, + loading: false, + networkStatus: NetworkStatus.error, + previousData: undefined, + variables: {}, + }); + } expect(onErrorFn).toHaveBeenCalledTimes(0); - let previousUpdates = updates; - await expect( - waitFor( - () => { - expect(updates).not.toEqual(previousUpdates); - }, - { interval: 1, timeout: 20 } - ) - ).rejects.toThrow(); + + await expect(takeSnapshot).not.toRerender(); }); it("should not persist errors when variables change", async () => { @@ -4172,7 +4631,8 @@ describe("useQuery Hook", () => { }, ]; - const { result, rerender } = renderHook( + using _disabledAct = disableActEnvironment(); + const { takeSnapshot, rerender } = await renderHookToSnapshotStream( ({ id }) => useQuery(query, { variables: { id } }), { wrapper: ({ children }) => ( @@ -4182,48 +4642,90 @@ describe("useQuery Hook", () => { } ); - expect(result.current.loading).toBe(true); - expect(result.current.data).toBe(undefined); - expect(result.current.error).toBe(undefined); + { + const result = await takeSnapshot(); - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: { id: 1 }, + }); + } - expect(result.current.data).toBe(undefined); - expect(result.current.error).toBeInstanceOf(ApolloError); - expect(result.current.error!.message).toBe("error"); + { + const result = await takeSnapshot(); - rerender({ id: 2 }); - expect(result.current.loading).toBe(true); - expect(result.current.data).toBe(undefined); - expect(result.current.error).toBe(undefined); + expect(result).toEqualQueryResult({ + data: undefined, + error: new ApolloError({ graphQLErrors: [{ message: "error" }] }), + called: true, + loading: false, + networkStatus: NetworkStatus.error, + previousData: undefined, + variables: { id: 1 }, + }); + } - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); - expect(result.current.data).toEqual({ hello: "world 2" }); - expect(result.current.error).toBe(undefined); + await rerender({ id: 2 }); - rerender({ id: 1 }); - expect(result.current.loading).toBe(true); - expect(result.current.data).toBe(undefined); - expect(result.current.error).toBe(undefined); + { + const result = await takeSnapshot(); - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); - expect(result.current.data).toEqual({ hello: "world 1" }); - expect(result.current.error).toBe(undefined); + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.setVariables, + previousData: undefined, + variables: { id: 2 }, + }); + } + + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: { hello: "world 2" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: { id: 2 }, + }); + } + + await rerender({ id: 1 }); + + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.setVariables, + previousData: { hello: "world 2" }, + variables: { id: 1 }, + }); + } + + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: { hello: "world 1" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { hello: "world 2" }, + variables: { id: 1 }, + }); + } + + await expect(takeSnapshot).not.toRerender(); }); it("should render multiple errors when refetching", async () => { @@ -4264,39 +4766,63 @@ describe("useQuery Hook", () => { { const result = await takeSnapshot(); - expect(result.loading).toBe(true); - expect(result.data).toBe(undefined); - expect(result.error).toBe(undefined); + + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); } { const result = await takeSnapshot(); - expect(result.loading).toBe(false); - expect(result.data).toBe(undefined); - expect(result.error).toBeInstanceOf(ApolloError); - expect(result.error!.message).toBe("error 1"); + + expect(result).toEqualQueryResult({ + data: undefined, + error: new ApolloError({ graphQLErrors: [{ message: "error 1" }] }), + called: true, + loading: false, + networkStatus: NetworkStatus.error, + previousData: undefined, + variables: {}, + }); } - const catchFn = jest.fn(); - getCurrentSnapshot().refetch().catch(catchFn); + await expect(getCurrentSnapshot().refetch()).rejects.toEqual( + new ApolloError({ graphQLErrors: [{ message: "error 2" }] }) + ); + { const result = await takeSnapshot(); - expect(result.loading).toBe(true); - expect(result.data).toBe(undefined); - expect(result.error).toBe(undefined); + + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.refetch, + previousData: undefined, + variables: {}, + }); } { const result = await takeSnapshot(); - expect(result.loading).toBe(false); - expect(result.data).toBe(undefined); - expect(result.error).toBeInstanceOf(ApolloError); - expect(result.error!.message).toBe("error 2"); + + expect(result).toEqualQueryResult({ + data: undefined, + error: new ApolloError({ graphQLErrors: [{ message: "error 2" }] }), + called: true, + loading: false, + networkStatus: NetworkStatus.error, + previousData: undefined, + variables: {}, + }); } - expect(catchFn.mock.calls.length).toBe(1); - expect(catchFn.mock.calls[0].length).toBe(1); - expect(catchFn.mock.calls[0][0]).toBeInstanceOf(ApolloError); - expect(catchFn.mock.calls[0][0].message).toBe("error 2"); + + await expect(takeSnapshot).not.toRerender(); }); it("should render the same error on refetch", async () => { @@ -4328,40 +4854,76 @@ describe("useQuery Hook", () => { ); - const { result } = renderHook( - () => useQuery(query, { notifyOnNetworkStatusChange: true }), - { wrapper } - ); + using _disabledAct = disableActEnvironment(); + const { takeSnapshot, getCurrentSnapshot } = + await renderHookToSnapshotStream( + () => useQuery(query, { notifyOnNetworkStatusChange: true }), + { wrapper } + ); - expect(result.current.loading).toBe(true); - expect(result.current.data).toBe(undefined); - expect(result.current.error).toBe(undefined); + { + const result = await takeSnapshot(); - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); + } + + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: undefined, + error: new ApolloError({ + graphQLErrors: [{ message: "same error" }], + }), + called: true, + loading: false, + networkStatus: NetworkStatus.error, + previousData: undefined, + variables: {}, + }); + } + + await expect(getCurrentSnapshot().refetch()).rejects.toEqual( + new ApolloError({ graphQLErrors: [{ message: "same error" }] }) ); - expect(result.current.data).toBe(undefined); - expect(result.current.error).toBeInstanceOf(ApolloError); - expect(result.current.error!.message).toBe("same error"); + { + const result = await takeSnapshot(); - const catchFn = jest.fn(); - await act(async () => { - await result.current.refetch().catch(catchFn); - }); + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.refetch, + previousData: undefined, + variables: {}, + }); + } - expect(result.current.loading).toBe(false); - expect(result.current.data).toBe(undefined); - expect(result.current.error).toBeInstanceOf(ApolloError); - expect(result.current.error!.message).toBe("same error"); + { + const result = await takeSnapshot(); - expect(catchFn.mock.calls.length).toBe(1); - expect(catchFn.mock.calls[0].length).toBe(1); - expect(catchFn.mock.calls[0][0]).toBeInstanceOf(ApolloError); - expect(catchFn.mock.calls[0][0].message).toBe("same error"); + expect(result).toEqualQueryResult({ + data: undefined, + error: new ApolloError({ + graphQLErrors: [{ message: "same error" }], + }), + called: true, + loading: false, + networkStatus: NetworkStatus.error, + previousData: undefined, + variables: {}, + }); + } + + await expect(takeSnapshot).not.toRerender(); }); it("should render data and errors with refetch", async () => { @@ -4409,53 +4971,90 @@ describe("useQuery Hook", () => { { const result = await takeSnapshot(); - expect(result.loading).toBe(true); - expect(result.data).toBe(undefined); - expect(result.error).toBe(undefined); + + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); } { const result = await takeSnapshot(); - expect(result.loading).toBe(false); - expect(result.data).toBe(undefined); - expect(result.error).toBeInstanceOf(ApolloError); - expect(result.error!.message).toBe("same error"); + + expect(result).toEqualQueryResult({ + data: undefined, + error: new ApolloError({ + graphQLErrors: [{ message: "same error" }], + }), + called: true, + loading: false, + networkStatus: NetworkStatus.error, + previousData: undefined, + variables: {}, + }); } + await getCurrentSnapshot().refetch(); { const result = await takeSnapshot(); - expect(result.loading).toBe(true); - expect(result.data).toBe(undefined); - expect(result.error).toBe(undefined); + + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.refetch, + previousData: undefined, + variables: {}, + }); } { const result = await takeSnapshot(); - expect(result.loading).toBe(false); - expect(result.data).toEqual({ hello: "world" }); - expect(result.error).toBe(undefined); + + expect(result).toEqualQueryResult({ + data: { hello: "world" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); } - const catchFn = jest.fn(); - getCurrentSnapshot().refetch().catch(catchFn); + + await expect(getCurrentSnapshot().refetch()).rejects.toEqual( + new ApolloError({ graphQLErrors: [{ message: "same error" }] }) + ); { const result = await takeSnapshot(); - expect(result.loading).toBe(true); - expect(result.data).toEqual({ hello: "world" }); - expect(result.error).toBe(undefined); + expect(result).toEqualQueryResult({ + data: { hello: "world" }, + called: true, + loading: true, + networkStatus: NetworkStatus.refetch, + previousData: { hello: "world" }, + variables: {}, + }); } { const result = await takeSnapshot(); - expect(result.loading).toBe(false); - // TODO: Is this correct behavior here? - expect(result.data).toEqual({ hello: "world" }); - expect(result.error).toBeInstanceOf(ApolloError); - expect(result.error!.message).toBe("same error"); - } - expect(catchFn.mock.calls.length).toBe(1); - expect(catchFn.mock.calls[0].length).toBe(1); - expect(catchFn.mock.calls[0][0]).toBeInstanceOf(ApolloError); - expect(catchFn.mock.calls[0][0].message).toBe("same error"); + expect(result).toEqualQueryResult({ + // TODO: Is this correct behavior here? + data: { hello: "world" }, + error: new ApolloError({ + graphQLErrors: [{ message: "same error" }], + }), + called: true, + loading: false, + networkStatus: NetworkStatus.error, + previousData: { hello: "world" }, + variables: {}, + }); + } }); it("should call onCompleted when variables change", async () => { @@ -4484,7 +5083,8 @@ describe("useQuery Hook", () => { const onCompleted = jest.fn(); - const { result, rerender } = renderHook( + using _disabledAct = disableActEnvironment(); + const { takeSnapshot, rerender } = await renderHookToSnapshotStream( ({ variables }) => useQuery(query, { variables, onCompleted }), { wrapper: ({ children }) => ( @@ -4496,40 +5096,83 @@ describe("useQuery Hook", () => { } ); - expect(result.current.loading).toBe(true); + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: { first: 1 }, + }); + } + + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: data1, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: { first: 1 }, + }); + } - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); - expect(result.current.data).toEqual(data1); expect(onCompleted).toHaveBeenLastCalledWith(data1); - rerender({ variables: { first: 2 } }); - expect(result.current.loading).toBe(true); + await rerender({ variables: { first: 2 } }); + + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.setVariables, + previousData: data1, + variables: { first: 2 }, + }); + } + + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: data2, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: data1, + variables: { first: 2 }, + }); + } - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); - expect(result.current.data).toEqual(data2); expect(onCompleted).toHaveBeenLastCalledWith(data2); - rerender({ variables: { first: 1 } }); - expect(result.current.loading).toBe(false); - expect(result.current.data).toEqual(data1); - await waitFor( - () => { - expect(onCompleted).toHaveBeenLastCalledWith(data1); - }, - { interval: 1 } - ); + await rerender({ variables: { first: 1 } }); + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: data1, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: data2, + variables: { first: 1 }, + }); + } + + expect(onCompleted).toHaveBeenLastCalledWith(data1); expect(onCompleted).toHaveBeenCalledTimes(3); + + await expect(takeSnapshot).not.toRerender(); }); }); From 6ffb5a34a39657568db7f4e42ed60d8679cb817f Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 16 Jan 2025 17:19:37 -0700 Subject: [PATCH 8/9] Refactor `useQuery` tests to use `toEqualQueryResult` - Part 3 (#12275) --- src/react/hooks/__tests__/useQuery.test.tsx | 5179 +++++++++++-------- src/testing/internal/scenarios/index.ts | 1 + 2 files changed, 3090 insertions(+), 2090 deletions(-) diff --git a/src/react/hooks/__tests__/useQuery.test.tsx b/src/react/hooks/__tests__/useQuery.test.tsx index 2501a16919f..355508a22e2 100644 --- a/src/react/hooks/__tests__/useQuery.test.tsx +++ b/src/react/hooks/__tests__/useQuery.test.tsx @@ -1,4 +1,4 @@ -import React, { Fragment, ReactNode, useEffect, useRef, useState } from "react"; +import React, { Fragment, ReactNode, useEffect, useState } from "react"; import { DocumentNode, GraphQLError, GraphQLFormattedError } from "graphql"; import gql from "graphql-tag"; import { act } from "@testing-library/react"; @@ -7,6 +7,7 @@ import { render, screen, waitFor, renderHook } from "@testing-library/react"; import { ApolloClient, ApolloError, + ApolloQueryResult, FetchPolicy, NetworkStatus, OperationVariables, @@ -31,11 +32,10 @@ import { QueryResult } from "../../types/types"; import { useQuery } from "../useQuery"; import { useMutation } from "../useMutation"; import { setupPaginatedCase, spyOnConsole } from "../../../testing/internal"; -import { useApolloClient } from "../useApolloClient"; import { useLazyQuery } from "../useLazyQuery"; import { mockFetchQuery } from "../../../core/__tests__/ObservableQuery"; import { InvariantError } from "../../../utilities/globals"; -import { Masked, MaskedDocumentNode } from "../../../masking"; +import { Unmasked } from "../../../masking"; import { createRenderStream, renderHookToSnapshotStream, @@ -5196,6 +5196,8 @@ describe("useQuery Hook", () => { { name: "D", position: 4 }, ]; + // TODO: Include an offset variable so that these tests more closely + // resemble a real-world paginated API const mocks = [ { request: { query, variables: { limit: 2 } }, @@ -5217,101 +5219,154 @@ describe("useQuery Hook", () => { ]; it("should fetchMore with updateQuery", async () => { - // TODO: Calling fetchMore with an updateQuery callback is deprecated - using _warnSpy = spyOnConsole("warn"); - const wrapper = ({ children }: any) => ( {children} ); - const { result } = renderHook( - () => useQuery(query, { variables: { limit: 2 } }), - { wrapper } - ); + using _disabledAct = disableActEnvironment(); + const { takeSnapshot, getCurrentSnapshot } = + await renderHookToSnapshotStream( + () => useQuery(query, { variables: { limit: 2 } }), + { wrapper } + ); - expect(result.current.loading).toBe(true); - expect(result.current.networkStatus).toBe(NetworkStatus.loading); - expect(result.current.data).toBe(undefined); + { + const result = await takeSnapshot(); - await waitFor(() => { - expect(result.current.loading).toBe(false); + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: { limit: 2 }, + }); + } + + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: { letters: ab }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: { limit: 2 }, + }); + } + + const fetchMoreResult = await getCurrentSnapshot().fetchMore({ + variables: { limit: 2 }, + updateQuery: (prev, { fetchMoreResult }) => ({ + letters: prev.letters.concat(fetchMoreResult.letters), + }), }); - expect(result.current.networkStatus).toBe(NetworkStatus.ready); - expect(result.current.data).toEqual({ letters: ab }); - await waitFor( - () => - void result.current.fetchMore({ - variables: { limit: 2 }, - updateQuery: (prev, { fetchMoreResult }) => ({ - letters: prev.letters.concat(fetchMoreResult.letters), - }), - }) - ); + expect(fetchMoreResult).toEqualApolloQueryResult({ + data: { letters: cd }, + loading: false, + networkStatus: NetworkStatus.ready, + }); - await waitFor( - () => { - expect(result.current.data).toEqual({ letters: ab.concat(cd) }); - }, - { interval: 1 } - ); - expect(result.current.loading).toBe(false); - expect(result.current.networkStatus).toBe(NetworkStatus.ready); + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: { letters: ab.concat(cd) }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { letters: ab }, + variables: { limit: 2 }, + }); + } + + await expect(takeSnapshot).not.toRerender(); }); it("should fetchMore with updateQuery and notifyOnNetworkStatusChange", async () => { - // TODO: Calling fetchMore with an updateQuery callback is deprecated - using _warnSpy = spyOnConsole("warn"); - const wrapper = ({ children }: any) => ( {children} ); - const { result } = renderHook( - () => - useQuery(query, { - variables: { limit: 2 }, - notifyOnNetworkStatusChange: true, - }), - { wrapper } - ); + using _disabledAct = disableActEnvironment(); + const { takeSnapshot, getCurrentSnapshot } = + await renderHookToSnapshotStream( + () => + useQuery(query, { + variables: { limit: 2 }, + notifyOnNetworkStatusChange: true, + }), + { wrapper } + ); - expect(result.current.loading).toBe(true); - expect(result.current.networkStatus).toBe(NetworkStatus.loading); - expect(result.current.data).toBe(undefined); + { + const result = await takeSnapshot(); - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: { limit: 2 }, + }); + } - expect(result.current.networkStatus).toBe(NetworkStatus.ready); - expect(result.current.data).toEqual({ letters: ab }); + { + const result = await takeSnapshot(); - act( - () => - void result.current.fetchMore({ - variables: { limit: 2 }, - updateQuery: (prev, { fetchMoreResult }) => ({ - letters: prev.letters.concat(fetchMoreResult.letters), - }), - }) - ); + expect(result).toEqualQueryResult({ + data: { letters: ab }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: { limit: 2 }, + }); + } - expect(result.current.loading).toBe(true); - expect(result.current.networkStatus).toBe(NetworkStatus.fetchMore); - expect(result.current.data).toEqual({ letters: ab }); + const fetchMoreResult = await getCurrentSnapshot().fetchMore({ + variables: { limit: 2 }, + updateQuery: (prev, { fetchMoreResult }) => ({ + letters: prev.letters.concat(fetchMoreResult.letters), + }), + }); - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); - expect(result.current.networkStatus).toBe(NetworkStatus.ready); - expect(result.current.data).toEqual({ letters: ab.concat(cd) }); + expect(fetchMoreResult).toEqualApolloQueryResult({ + data: { letters: cd }, + loading: false, + networkStatus: NetworkStatus.ready, + }); + + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: { letters: ab }, + called: true, + loading: true, + networkStatus: NetworkStatus.fetchMore, + previousData: { letters: ab }, + variables: { limit: 2 }, + }); + } + + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: { letters: ab.concat(cd) }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { letters: ab }, + variables: { limit: 2 }, + }); + } + + await expect(takeSnapshot).not.toRerender(); }); it("fetchMore with concatPagination", async () => { @@ -5331,35 +5386,63 @@ describe("useQuery Hook", () => { ); - const { result } = renderHook( - () => useQuery(query, { variables: { limit: 2 } }), - { wrapper } - ); + using _disabledAct = disableActEnvironment(); + const { takeSnapshot, getCurrentSnapshot } = + await renderHookToSnapshotStream( + () => useQuery(query, { variables: { limit: 2 } }), + { wrapper } + ); - expect(result.current.loading).toBe(true); - expect(result.current.networkStatus).toBe(NetworkStatus.loading); - expect(result.current.data).toBe(undefined); + { + const result = await takeSnapshot(); - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); - expect(result.current.networkStatus).toBe(NetworkStatus.ready); - expect(result.current.data).toEqual({ letters: ab }); - await act( - async () => void result.current.fetchMore({ variables: { limit: 2 } }) - ); + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: { limit: 2 }, + }); + } - expect(result.current.loading).toBe(false); - await waitFor( - () => { - expect(result.current.data).toEqual({ letters: ab.concat(cd) }); - }, - { interval: 1 } - ); - expect(result.current.networkStatus).toBe(NetworkStatus.ready); + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: { letters: ab }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: { limit: 2 }, + }); + } + + const fetchMoreResult = await getCurrentSnapshot().fetchMore({ + variables: { limit: 2 }, + }); + + expect(fetchMoreResult).toEqualApolloQueryResult({ + data: { letters: cd }, + loading: false, + networkStatus: NetworkStatus.ready, + }); + + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: { letters: ab.concat(cd) }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { letters: ab }, + variables: { limit: 2 }, + }); + } + + await expect(takeSnapshot).not.toRerender(); }); it("fetchMore with concatPagination and notifyOnNetworkStatusChange", async () => { @@ -5379,41 +5462,80 @@ describe("useQuery Hook", () => { ); - const { result } = renderHook( - () => - useQuery(query, { - variables: { limit: 2 }, - notifyOnNetworkStatusChange: true, - }), - { wrapper } - ); + using _disabledAct = disableActEnvironment(); + const { takeSnapshot, getCurrentSnapshot } = + await renderHookToSnapshotStream( + () => + useQuery(query, { + variables: { limit: 2 }, + notifyOnNetworkStatusChange: true, + }), + { wrapper } + ); - expect(result.current.loading).toBe(true); - expect(result.current.networkStatus).toBe(NetworkStatus.loading); - expect(result.current.data).toBe(undefined); + { + const result = await takeSnapshot(); - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); - expect(result.current.networkStatus).toBe(NetworkStatus.ready); - expect(result.current.data).toEqual({ letters: ab }); + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: { limit: 2 }, + }); + } - act(() => void result.current.fetchMore({ variables: { limit: 2 } })); - expect(result.current.loading).toBe(true); - expect(result.current.networkStatus).toBe(NetworkStatus.fetchMore); - expect(result.current.data).toEqual({ letters: ab }); + { + const result = await takeSnapshot(); - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); - expect(result.current.networkStatus).toBe(NetworkStatus.ready); - expect(result.current.data).toEqual({ letters: ab.concat(cd) }); + expect(result).toEqualQueryResult({ + data: { letters: ab }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: { limit: 2 }, + }); + } + + const fetchMoreResult = await getCurrentSnapshot().fetchMore({ + variables: { limit: 2 }, + }); + + expect(fetchMoreResult).toEqualApolloQueryResult({ + data: { letters: cd }, + loading: false, + networkStatus: NetworkStatus.ready, + }); + + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: { letters: ab }, + called: true, + loading: true, + networkStatus: NetworkStatus.fetchMore, + previousData: { letters: ab }, + variables: { limit: 2 }, + }); + } + + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: { letters: ab.concat(cd) }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { letters: ab }, + variables: { limit: 2 }, + }); + } + + await expect(takeSnapshot).not.toRerender(); }); // https://github.com/apollographql/apollo-client/issues/11965 @@ -5494,23 +5616,33 @@ describe("useQuery Hook", () => { ); { - const { loading, networkStatus, data } = await takeSnapshot(); + const result = await takeSnapshot(); - expect(loading).toBe(true); - expect(networkStatus).toBe(NetworkStatus.loading); - expect(data).toBeUndefined(); + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: { limit: 2 }, + }); } { - const { loading, networkStatus, data } = await takeSnapshot(); + const result = await takeSnapshot(); - expect(loading).toBe(false); - expect(networkStatus).toBe(NetworkStatus.ready); - expect(data).toStrictEqual({ - letters: [ - { __typename: "Letter", letter: "A", position: 1 }, - { __typename: "Letter", letter: "B", position: 2 }, - ], + expect(result).toEqualQueryResult({ + data: { + letters: [ + { __typename: "Letter", letter: "A", position: 1 }, + { __typename: "Letter", letter: "B", position: 2 }, + ], + }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: { limit: 2 }, }); } @@ -5524,35 +5656,54 @@ describe("useQuery Hook", () => { }); { - const { loading, networkStatus, data } = await takeSnapshot(); + const result = await takeSnapshot(); - expect(loading).toBe(true); - expect(networkStatus).toBe(NetworkStatus.fetchMore); - expect(data).toStrictEqual({ - letters: [ - { __typename: "Letter", letter: "A", position: 1 }, - { __typename: "Letter", letter: "B", position: 2 }, - ], + expect(result).toEqualQueryResult({ + data: { + letters: [ + { __typename: "Letter", letter: "A", position: 1 }, + { __typename: "Letter", letter: "B", position: 2 }, + ], + }, + called: true, + loading: true, + networkStatus: NetworkStatus.fetchMore, + previousData: { + letters: [ + { __typename: "Letter", letter: "A", position: 1 }, + { __typename: "Letter", letter: "B", position: 2 }, + ], + }, + variables: { limit: 2 }, }); } { - const { loading, networkStatus, data, observable } = - await takeSnapshot(); + const result = await takeSnapshot(); - expect(loading).toBe(false); - expect(networkStatus).toBe(NetworkStatus.ready); - expect(data).toEqual({ - letters: [ - { __typename: "Letter", letter: "A", position: 1 }, - { __typename: "Letter", letter: "B", position: 2 }, - { __typename: "Letter", letter: "C", position: 3 }, - { __typename: "Letter", letter: "D", position: 4 }, - ], + expect(result).toEqualQueryResult({ + data: { + letters: [ + { __typename: "Letter", letter: "A", position: 1 }, + { __typename: "Letter", letter: "B", position: 2 }, + { __typename: "Letter", letter: "C", position: 3 }, + { __typename: "Letter", letter: "D", position: 4 }, + ], + }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { + letters: [ + { __typename: "Letter", letter: "A", position: 1 }, + { __typename: "Letter", letter: "B", position: 2 }, + ], + }, + variables: { limit: 2 }, }); // Ensure we store the merged result as the last result - expect(observable.getCurrentResult(false).data).toEqual({ + expect(result.observable.getCurrentResult(false).data).toEqual({ letters: [ { __typename: "Letter", letter: "A", position: 1 }, { __typename: "Letter", letter: "B", position: 2 }, @@ -5562,7 +5713,7 @@ describe("useQuery Hook", () => { }); } - await expect(fetchMorePromise).resolves.toStrictEqual({ + await expect(fetchMorePromise).resolves.toEqualApolloQueryResult({ data: { letters: [ { __typename: "Letter", letter: "C", position: 3 }, @@ -5581,34 +5732,57 @@ describe("useQuery Hook", () => { }); { - const { data, loading, networkStatus } = await takeSnapshot(); + const result = await takeSnapshot(); - expect(loading).toBe(true); - expect(networkStatus).toBe(NetworkStatus.fetchMore); - expect(data).toEqual({ - letters: [ - { __typename: "Letter", letter: "A", position: 1 }, - { __typename: "Letter", letter: "B", position: 2 }, - { __typename: "Letter", letter: "C", position: 3 }, - { __typename: "Letter", letter: "D", position: 4 }, - ], + expect(result).toEqualQueryResult({ + data: { + letters: [ + { __typename: "Letter", letter: "A", position: 1 }, + { __typename: "Letter", letter: "B", position: 2 }, + { __typename: "Letter", letter: "C", position: 3 }, + { __typename: "Letter", letter: "D", position: 4 }, + ], + }, + called: true, + loading: true, + networkStatus: NetworkStatus.fetchMore, + previousData: { + letters: [ + { __typename: "Letter", letter: "A", position: 1 }, + { __typename: "Letter", letter: "B", position: 2 }, + { __typename: "Letter", letter: "C", position: 3 }, + { __typename: "Letter", letter: "D", position: 4 }, + ], + }, + variables: { limit: 2 }, }); } { - const { data, loading, networkStatus, observable } = - await takeSnapshot(); + const result = await takeSnapshot(); - expect(loading).toBe(false); - expect(networkStatus).toBe(NetworkStatus.ready); - expect(data).toEqual({ - letters: [ - { __typename: "Letter", letter: "E", position: 5 }, - { __typename: "Letter", letter: "F", position: 6 }, - ], + expect(result).toEqualQueryResult({ + data: { + letters: [ + { __typename: "Letter", letter: "E", position: 5 }, + { __typename: "Letter", letter: "F", position: 6 }, + ], + }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { + letters: [ + { __typename: "Letter", letter: "A", position: 1 }, + { __typename: "Letter", letter: "B", position: 2 }, + { __typename: "Letter", letter: "C", position: 3 }, + { __typename: "Letter", letter: "D", position: 4 }, + ], + }, + variables: { limit: 2 }, }); - expect(observable.getCurrentResult(false).data).toEqual({ + expect(result.observable.getCurrentResult(false).data).toEqual({ letters: [ { __typename: "Letter", letter: "E", position: 5 }, { __typename: "Letter", letter: "F", position: 6 }, @@ -5616,7 +5790,7 @@ describe("useQuery Hook", () => { }); } - await expect(fetchMorePromise).resolves.toStrictEqual({ + await expect(fetchMorePromise).resolves.toEqualApolloQueryResult({ data: { letters: [ { __typename: "Letter", letter: "E", position: 5 }, @@ -5795,18 +5969,39 @@ describe("useQuery Hook", () => { ); - const { result } = renderHook(() => useQuery(GET_COUNTRIES), { wrapper }); + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( + () => useQuery(GET_COUNTRIES), + { wrapper } + ); - expect(result.current.loading).toBe(true); - expect(result.current.data).toBeUndefined(); + { + const result = await takeSnapshot(); - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); - expect(result.current.data).toEqual({ countries }); + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); + } + + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: { countries }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); + } + + await expect(takeSnapshot).not.toRerender(); }); }); @@ -5919,37 +6114,49 @@ describe("useQuery Hook", () => { { const { snapshot } = await renderStream.takeRender(); - expect(snapshot.useQueryResult).toMatchObject({ + expect(snapshot.useQueryResult!).toEqualQueryResult({ data: undefined, + called: true, loading: true, networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: { id: 1 }, }); - expect(snapshot.useLazyQueryResult).toMatchObject({ - called: false, + expect(snapshot.useLazyQueryResult!).toEqualQueryResult({ data: undefined, + error: undefined, + called: false, loading: false, networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: { id: 1 }, }); } { const { snapshot } = await renderStream.takeRender(); - expect(snapshot.useQueryResult).toMatchObject({ + expect(snapshot.useQueryResult!).toEqualQueryResult({ data: undefined, error: new ApolloError({ graphQLErrors: [new GraphQLError("Intentional error")], }), + called: true, loading: false, networkStatus: NetworkStatus.error, + previousData: undefined, + variables: { id: 1 }, }); - expect(snapshot.useLazyQueryResult).toMatchObject({ - called: false, + expect(snapshot.useLazyQueryResult!).toEqualQueryResult({ data: undefined, + error: undefined, + called: false, loading: false, networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: { id: 1 }, }); } @@ -5958,41 +6165,48 @@ describe("useQuery Hook", () => { { const { snapshot } = await renderStream.takeRender(); - expect(snapshot.useQueryResult).toMatchObject({ + expect(snapshot.useQueryResult!).toEqualQueryResult({ data: undefined, error: new ApolloError({ graphQLErrors: [new GraphQLError("Intentional error")], }), + called: true, loading: false, networkStatus: NetworkStatus.error, + previousData: undefined, + variables: { id: 1 }, }); - expect(snapshot.useLazyQueryResult).toMatchObject({ - called: true, + expect(snapshot.useLazyQueryResult!).toEqualQueryResult({ data: undefined, + called: true, loading: true, networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: { id: 1 }, }); } { const { snapshot } = await renderStream.takeRender(); - expect(snapshot.useQueryResult).toMatchObject({ + expect(snapshot.useQueryResult!).toEqualQueryResult({ data: undefined, error: new ApolloError({ graphQLErrors: [new GraphQLError("Intentional error")], }), + called: true, loading: false, networkStatus: NetworkStatus.error, + previousData: undefined, + variables: { id: 1 }, }); // ensure we aren't setting a value on the observable query that contains // the partial result expect( - snapshot.useQueryResult?.observable.getCurrentResult(false) - ).toEqual({ - data: undefined, + snapshot.useQueryResult?.observable.getCurrentResult(false)! + ).toEqualApolloQueryResult({ error: new ApolloError({ graphQLErrors: [new GraphQLError("Intentional error")], }), @@ -6000,13 +6214,18 @@ describe("useQuery Hook", () => { loading: false, networkStatus: NetworkStatus.error, partial: true, - }); + // TODO: Fix ApolloQueryResult type to allow `data` to be an optional property. + // This fails without the type case for now even though the runtime + // code doesn't include a `data` property. + } as unknown as ApolloQueryResult); - expect(snapshot.useLazyQueryResult).toMatchObject({ - called: true, + expect(snapshot.useLazyQueryResult!).toEqualQueryResult({ data: { person: { __typename: "Person", id: 1, lastName: "Doe" } }, + called: true, loading: false, networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: { id: 1 }, }); } @@ -6015,38 +6234,47 @@ describe("useQuery Hook", () => { { const { snapshot } = await renderStream.takeRender(); - expect(snapshot.useQueryResult).toMatchObject({ + expect(snapshot.useQueryResult!).toEqualQueryResult({ data: undefined, + called: true, loading: true, networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: { id: 1 }, }); - expect(snapshot.useLazyQueryResult).toMatchObject({ - called: true, + expect(snapshot.useLazyQueryResult!).toEqualQueryResult({ data: { person: { __typename: "Person", id: 1, lastName: "Doe" } }, + called: true, loading: false, networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: { id: 1 }, }); } { const { snapshot } = await renderStream.takeRender(); - expect(snapshot.useQueryResult).toMatchObject({ + expect(snapshot.useQueryResult!).toEqualQueryResult({ data: undefined, - loading: false, error: new ApolloError({ graphQLErrors: [new GraphQLError("Intentional error")], }), + called: true, + loading: false, networkStatus: NetworkStatus.error, + previousData: undefined, + variables: { id: 1 }, }); // ensure we aren't setting a value on the observable query that contains // the partial result expect( - snapshot.useQueryResult?.observable.getCurrentResult(false) - ).toEqual({ - data: undefined, + snapshot.useQueryResult?.observable.getCurrentResult(false)! + ).toEqualApolloQueryResult({ + // TODO: Fix TypeScript types to allow for `data` to be `undefined` + data: undefined as unknown as Query1, error: new ApolloError({ graphQLErrors: [new GraphQLError("Intentional error")], }), @@ -6056,11 +6284,13 @@ describe("useQuery Hook", () => { partial: true, }); - expect(snapshot.useLazyQueryResult).toMatchObject({ - called: true, + expect(snapshot.useLazyQueryResult!).toEqualQueryResult({ data: { person: { __typename: "Person", id: 1, lastName: "Doe" } }, + called: true, loading: false, networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: { id: 1 }, }); } @@ -6171,37 +6401,49 @@ describe("useQuery Hook", () => { { const { snapshot } = await renderStream.takeRender(); - expect(snapshot.useQueryResult).toMatchObject({ + expect(snapshot.useQueryResult!).toEqualQueryResult({ data: undefined, + called: true, loading: true, networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: { id: 1 }, }); - expect(snapshot.useLazyQueryResult).toMatchObject({ - called: false, + expect(snapshot.useLazyQueryResult!).toEqualQueryResult({ data: undefined, + error: undefined, + called: false, loading: false, networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: { id: 1 }, }); } { const { snapshot } = await renderStream.takeRender(); - expect(snapshot.useQueryResult).toMatchObject({ + expect(snapshot.useQueryResult!).toEqualQueryResult({ data: undefined, error: new ApolloError({ graphQLErrors: [new GraphQLError("Intentional error")], }), + called: true, loading: false, networkStatus: NetworkStatus.error, + previousData: undefined, + variables: { id: 1 }, }); - expect(snapshot.useLazyQueryResult).toMatchObject({ - called: false, + expect(snapshot.useLazyQueryResult!).toEqualQueryResult({ data: undefined, + error: undefined, + called: false, loading: false, networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: { id: 1 }, }); } @@ -6210,20 +6452,25 @@ describe("useQuery Hook", () => { { const { snapshot } = await renderStream.takeRender(); - expect(snapshot.useQueryResult).toMatchObject({ + expect(snapshot.useQueryResult!).toEqualQueryResult({ data: undefined, error: new ApolloError({ graphQLErrors: [new GraphQLError("Intentional error")], }), + called: true, loading: false, networkStatus: NetworkStatus.error, + previousData: undefined, + variables: { id: 1 }, }); - expect(snapshot.useLazyQueryResult).toMatchObject({ - called: true, + expect(snapshot.useLazyQueryResult!).toEqualQueryResult({ data: undefined, + called: true, loading: true, networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: { id: 1 }, }); } @@ -6233,17 +6480,19 @@ describe("useQuery Hook", () => { // We don't see the update from the cache for one more render cycle, hence // why this is still showing the error result even though the result from // the other query has finished and re-rendered. - expect(snapshot.useQueryResult).toMatchObject({ + expect(snapshot.useQueryResult!).toEqualQueryResult({ data: undefined, error: new ApolloError({ graphQLErrors: [new GraphQLError("Intentional error")], }), + called: true, loading: false, networkStatus: NetworkStatus.error, + previousData: undefined, + variables: { id: 1 }, }); - expect(snapshot.useLazyQueryResult).toMatchObject({ - called: true, + expect(snapshot.useLazyQueryResult!).toEqualQueryResult({ data: { person: { __typename: "Person", @@ -6252,15 +6501,18 @@ describe("useQuery Hook", () => { lastName: "Doe", }, }, + called: true, loading: false, networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: { id: 1 }, }); } { const { snapshot } = await renderStream.takeRender(); - expect(snapshot.useQueryResult).toMatchObject({ + expect(snapshot.useQueryResult!).toEqualQueryResult({ data: { person: { __typename: "Person", @@ -6268,12 +6520,14 @@ describe("useQuery Hook", () => { firstName: "John", }, }, + called: true, loading: false, networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: { id: 1 }, }); - expect(snapshot.useLazyQueryResult).toMatchObject({ - called: true, + expect(snapshot.useLazyQueryResult!).toEqualQueryResult({ data: { person: { __typename: "Person", @@ -6282,8 +6536,11 @@ describe("useQuery Hook", () => { lastName: "Doe", }, }, + called: true, loading: false, networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: { id: 1 }, }); } @@ -6394,37 +6651,49 @@ describe("useQuery Hook", () => { { const { snapshot } = await renderStream.takeRender(); - expect(snapshot.useQueryResult).toMatchObject({ + expect(snapshot.useQueryResult!).toEqualQueryResult({ data: undefined, + called: true, loading: true, networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: { id: 1 }, }); - expect(snapshot.useLazyQueryResult).toMatchObject({ - called: false, + expect(snapshot.useLazyQueryResult!).toEqualQueryResult({ data: undefined, + error: undefined, + called: false, loading: false, networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: { id: 1 }, }); } { const { snapshot } = await renderStream.takeRender(); - expect(snapshot.useQueryResult).toMatchObject({ + expect(snapshot.useQueryResult!).toEqualQueryResult({ data: undefined, error: new ApolloError({ graphQLErrors: [new GraphQLError("Intentional error")], }), + called: true, loading: false, networkStatus: NetworkStatus.error, + previousData: undefined, + variables: { id: 1 }, }); - expect(snapshot.useLazyQueryResult).toMatchObject({ - called: false, + expect(snapshot.useLazyQueryResult!).toEqualQueryResult({ data: undefined, + error: undefined, + called: false, loading: false, networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: { id: 1 }, }); } @@ -6433,37 +6702,44 @@ describe("useQuery Hook", () => { { const { snapshot } = await renderStream.takeRender(); - expect(snapshot.useQueryResult).toMatchObject({ + expect(snapshot.useQueryResult!).toEqualQueryResult({ data: undefined, error: new ApolloError({ graphQLErrors: [new GraphQLError("Intentional error")], }), + called: true, loading: false, networkStatus: NetworkStatus.error, + previousData: undefined, + variables: { id: 1 }, }); - expect(snapshot.useLazyQueryResult).toMatchObject({ - called: true, + expect(snapshot.useLazyQueryResult!).toEqualQueryResult({ data: undefined, + called: true, loading: true, networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: { id: 1 }, }); } { const { snapshot } = await renderStream.takeRender(); - expect(snapshot.useQueryResult).toMatchObject({ + expect(snapshot.useQueryResult!).toEqualQueryResult({ data: undefined, error: new ApolloError({ graphQLErrors: [new GraphQLError("Intentional error")], }), + called: true, loading: false, networkStatus: NetworkStatus.error, + previousData: undefined, + variables: { id: 1 }, }); - expect(snapshot.useLazyQueryResult).toMatchObject({ - called: true, + expect(snapshot.useLazyQueryResult!).toEqualQueryResult({ data: { person: { __typename: "Person", @@ -6471,8 +6747,11 @@ describe("useQuery Hook", () => { lastName: "Doe", }, }, + called: true, loading: false, networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: { id: 1 }, }); } @@ -6493,13 +6772,6 @@ describe("useQuery Hook", () => { } `; - using _disabledAct = disableActEnvironment(); - const renderStream = createRenderStream({ - initialSnapshot: { - useQueryResult: null as QueryResult | null, - }, - }); - const client = new ApolloClient({ link: new MockLink([ { @@ -6536,34 +6808,33 @@ describe("useQuery Hook", () => { }), }); - function App() { - const useQueryResult = useQuery(query); - - renderStream.replaceSnapshot({ useQueryResult }); - - return null; - } - - await renderStream.render(, { - wrapper: ({ children }) => ( - {children} - ), - }); + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( + () => useQuery(query), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); { - const { snapshot } = await renderStream.takeRender(); + const result = await takeSnapshot(); - expect(snapshot.useQueryResult).toMatchObject({ + expect(result).toEqualQueryResult({ data: undefined, + called: true, loading: true, networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, }); } { - const { snapshot } = await renderStream.takeRender(); + const result = await takeSnapshot(); - expect(snapshot.useQueryResult).toMatchObject({ + expect(result).toEqualQueryResult({ data: { author: { __typename: "Author", @@ -6571,16 +6842,20 @@ describe("useQuery Hook", () => { name: "Author Lee", post: { __typename: "Post", + id: 1, title: "Title", }, }, }, + called: true, loading: false, networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, }); } - await expect(renderStream).not.toRerender(); + await expect(takeSnapshot).not.toRerender(); }); it("triggers a network request and rerenders with the new result when a mutation causes a partial cache update due to an incomplete merge function result", async () => { @@ -6713,17 +6988,20 @@ describe("useQuery Hook", () => { { const { snapshot } = await renderStream.takeRender(); - expect(snapshot.useQueryResult).toMatchObject({ + expect(snapshot.useQueryResult!).toEqualQueryResult({ data: undefined, + called: true, loading: true, networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, }); } { const { snapshot } = await renderStream.takeRender(); - expect(snapshot.useQueryResult).toMatchObject({ + expect(snapshot.useQueryResult!).toEqualQueryResult({ data: { author: { __typename: "Author", @@ -6731,12 +7009,16 @@ describe("useQuery Hook", () => { name: "Author Lee", post: { __typename: "Post", + id: 1, title: "Title", }, }, }, + called: true, loading: false, networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, }); } @@ -6746,7 +7028,7 @@ describe("useQuery Hook", () => { { const { snapshot } = await renderStream.takeRender(); - expect(snapshot.useQueryResult).toMatchObject({ + expect(snapshot.useQueryResult!).toEqualQueryResult({ data: { author: { __typename: "Author", @@ -6754,19 +7036,23 @@ describe("useQuery Hook", () => { name: "Author Lee", post: { __typename: "Post", + id: 1, title: "Title", }, }, }, + called: true, loading: false, networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, }); } { const { snapshot } = await renderStream.takeRender(); - expect(snapshot.useQueryResult).toMatchObject({ + expect(snapshot.useQueryResult!).toEqualQueryResult({ data: { author: { __typename: "Author", @@ -6778,12 +7064,27 @@ describe("useQuery Hook", () => { name: "Author Lee (refetch)", post: { __typename: "Post", + id: 1, title: "Title", }, }, }, + called: true, loading: false, networkStatus: NetworkStatus.ready, + previousData: { + author: { + __typename: "Author", + id: 1, + name: "Author Lee", + post: { + __typename: "Post", + id: 1, + title: "Title", + }, + }, + }, + variables: {}, }); } @@ -6829,25 +7130,57 @@ describe("useQuery Hook", () => { ); { const result = await takeSnapshot(); - expect(result.loading).toBe(true); - expect(result.data).toBe(undefined); + + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: { id: 1 }, + }); } { const result = await takeSnapshot(); - expect(result.loading).toBe(false); - expect(result.data).toEqual({ hello: "world 1" }); + + expect(result).toEqualQueryResult({ + data: { hello: "world 1" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: { id: 1 }, + }); } + await getCurrentSnapshot().refetch({ id: 2 }); + { const result = await takeSnapshot(); - expect(result.loading).toBe(true); - expect(result.data).toBe(undefined); + + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.setVariables, + previousData: { hello: "world 1" }, + variables: { id: 2 }, + }); } { const result = await takeSnapshot(); - expect(result.loading).toBe(false); - expect(result.data).toEqual({ hello: "world 2" }); + + expect(result).toEqualQueryResult({ + data: { hello: "world 2" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { hello: "world 1" }, + variables: { id: 2 }, + }); } + + await expect(takeSnapshot).not.toRerender(); }); it("refetching after an error", async () => { @@ -6893,46 +7226,96 @@ describe("useQuery Hook", () => { { const result = await takeSnapshot(); - expect(result.loading).toBe(true); - expect(result.data).toBe(undefined); + + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); } { const result = await takeSnapshot(); - expect(result.loading).toBe(false); - expect(result.error).toBe(undefined); - expect(result.data).toEqual({ hello: "world 1" }); + + expect(result).toEqualQueryResult({ + data: { hello: "world 1" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); } - await getCurrentSnapshot() - .refetch() - .catch(() => {}); + await expect(getCurrentSnapshot().refetch()).rejects.toEqual( + new ApolloError({ networkError: new Error("This is an error!") }) + ); + { const result = await takeSnapshot(); - expect(result.loading).toBe(true); - expect(result.error).toBe(undefined); - expect(result.data).toEqual({ hello: "world 1" }); + + expect(result).toEqualQueryResult({ + data: { hello: "world 1" }, + called: true, + loading: true, + networkStatus: NetworkStatus.refetch, + previousData: { hello: "world 1" }, + variables: {}, + }); } { const result = await takeSnapshot(); - expect(result.loading).toBe(false); - expect(result.error).toBeInstanceOf(ApolloError); - expect(result.data).toEqual({ hello: "world 1" }); + + expect(result).toEqualQueryResult({ + data: { hello: "world 1" }, + error: new ApolloError({ + networkError: new Error("This is an error!"), + }), + called: true, + loading: false, + networkStatus: NetworkStatus.error, + previousData: { hello: "world 1" }, + variables: {}, + }); } - await getCurrentSnapshot().refetch(); + await expect( + getCurrentSnapshot().refetch() + ).resolves.toEqualApolloQueryResult({ + data: { hello: "world 2" }, + loading: false, + networkStatus: NetworkStatus.ready, + }); + { const result = await takeSnapshot(); - expect(result.loading).toBe(true); - expect(result.error).toBe(undefined); - expect(result.data).toEqual({ hello: "world 1" }); + + expect(result).toEqualQueryResult({ + data: { hello: "world 1" }, + called: true, + loading: true, + networkStatus: NetworkStatus.refetch, + previousData: { hello: "world 1" }, + variables: {}, + }); } { const result = await takeSnapshot(); - expect(result.loading).toBe(false); - expect(result.error).toBe(undefined); - expect(result.data).toEqual({ hello: "world 2" }); + + expect(result).toEqualQueryResult({ + data: { hello: "world 2" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { hello: "world 1" }, + variables: {}, + }); } + + await expect(takeSnapshot).not.toRerender(); }); describe("refetchWritePolicy", () => { @@ -6992,76 +7375,90 @@ describe("useQuery Hook", () => { ); - const { result } = renderHook( - () => - useQuery(query, { - variables: { min: 0, max: 12 }, - notifyOnNetworkStatusChange: true, - // This is the key line in this test. - refetchWritePolicy: "overwrite", - }), - { wrapper } - ); + using _disabledAct = disableActEnvironment(); + const { takeSnapshot, getCurrentSnapshot } = + await renderHookToSnapshotStream( + () => + useQuery(query, { + variables: { min: 0, max: 12 }, + notifyOnNetworkStatusChange: true, + // This is the key line in this test. + refetchWritePolicy: "overwrite", + }), + { wrapper } + ); - expect(result.current.loading).toBe(true); - expect(result.current.error).toBe(undefined); - expect(result.current.data).toBe(undefined); - expect(typeof result.current.refetch).toBe("function"); + { + const result = await takeSnapshot(); - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); - expect(result.current.error).toBeUndefined(); - expect(result.current.data).toEqual({ primes: [2, 3, 5, 7, 11] }); - expect(mergeParams).toEqual([[void 0, [2, 3, 5, 7, 11]]]); - - const thenFn = jest.fn(); - await act( - async () => - void result.current.refetch({ min: 12, max: 30 }).then(thenFn) - ); + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: { min: 0, max: 12 }, + }); + } - await waitFor( - () => { - expect(result.current.loading).toBe(true); - }, - { interval: 1 } - ); - expect(result.current.error).toBe(undefined); - expect(result.current.data).toEqual({ - // We get the stale data because we configured keyArgs: false. - primes: [2, 3, 5, 7, 11], + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: { primes: [2, 3, 5, 7, 11] }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: { min: 0, max: 12 }, + }); + } + + expect(mergeParams).toEqual([[undefined, [2, 3, 5, 7, 11]]]); + + await expect( + getCurrentSnapshot().refetch({ min: 12, max: 30 }) + ).resolves.toEqualApolloQueryResult({ + data: { primes: [13, 17, 19, 23, 29] }, + loading: false, + networkStatus: NetworkStatus.ready, }); - // This networkStatus is setVariables instead of refetch because we - // called refetch with new variables. - expect(result.current.networkStatus).toBe(NetworkStatus.setVariables); + { + const result = await takeSnapshot(); - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); + expect(result).toEqualQueryResult({ + // We get the stale data because we configured keyArgs: false. + data: { primes: [2, 3, 5, 7, 11] }, + called: true, + loading: true, + // This networkStatus is setVariables instead of refetch because we + // called refetch with new variables. + networkStatus: NetworkStatus.setVariables, + previousData: { primes: [2, 3, 5, 7, 11] }, + variables: { min: 12, max: 30 }, + }); + } + + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: { primes: [13, 17, 19, 23, 29] }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { primes: [2, 3, 5, 7, 11] }, + variables: { min: 12, max: 30 }, + }); + } - expect(result.current.error).toBe(undefined); - expect(result.current.data).toEqual({ primes: [13, 17, 19, 23, 29] }); expect(mergeParams).toEqual([ [undefined, [2, 3, 5, 7, 11]], // Without refetchWritePolicy: "overwrite", this array will be // all 10 primes (2 through 29) together. [undefined, [13, 17, 19, 23, 29]], ]); - - expect(thenFn).toHaveBeenCalledTimes(1); - expect(thenFn).toHaveBeenCalledWith({ - loading: false, - networkStatus: NetworkStatus.ready, - data: { primes: [13, 17, 19, 23, 29] }, - }); }); it('should support explicit "merge"', async () => { @@ -7088,87 +7485,92 @@ describe("useQuery Hook", () => { ); - const { result } = renderHook( - () => - useQuery(query, { - variables: { min: 0, max: 12 }, - notifyOnNetworkStatusChange: true, - // This is the key line in this test. - refetchWritePolicy: "merge", - }), - { wrapper } - ); + using _disabledAct = disableActEnvironment(); + const { takeSnapshot, getCurrentSnapshot } = + await renderHookToSnapshotStream( + () => + useQuery(query, { + variables: { min: 0, max: 12 }, + notifyOnNetworkStatusChange: true, + // This is the key line in this test. + refetchWritePolicy: "merge", + }), + { wrapper } + ); + + { + const result = await takeSnapshot(); - expect(result.current.loading).toBe(true); - expect(result.current.error).toBe(undefined); - expect(result.current.data).toBe(undefined); - expect(typeof result.current.refetch).toBe("function"); + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: { min: 0, max: 12 }, + }); + } - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); - expect(result.current.error).toBeUndefined(); - expect(result.current.data).toEqual({ primes: [2, 3, 5, 7, 11] }); - expect(mergeParams).toEqual([[undefined, [2, 3, 5, 7, 11]]]); + { + const result = await takeSnapshot(); - const thenFn = jest.fn(); - await act( - async () => - void result.current.refetch({ min: 12, max: 30 }).then(thenFn) - ); + expect(result).toEqualQueryResult({ + data: { primes: [2, 3, 5, 7, 11] }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: { min: 0, max: 12 }, + }); + } - await waitFor( - () => { - expect(result.current.loading).toBe(true); - }, - { interval: 1 } - ); - expect(result.current.error).toBe(undefined); - expect(result.current.data).toEqual({ - // We get the stale data because we configured keyArgs: false. - primes: [2, 3, 5, 7, 11], - }); - // This networkStatus is setVariables instead of refetch because we - // called refetch with new variables. - expect(result.current.networkStatus).toBe(NetworkStatus.setVariables); - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); + expect(mergeParams).toEqual([[undefined, [2, 3, 5, 7, 11]]]); - expect(result.current.error).toBe(undefined); - expect(result.current.data).toEqual({ - primes: [2, 3, 5, 7, 11, 13, 17, 19, 23, 29], + await expect( + getCurrentSnapshot().refetch({ min: 12, max: 30 }) + ).resolves.toEqualApolloQueryResult({ + data: { primes: [2, 3, 5, 7, 11, 13, 17, 19, 23, 29] }, + loading: false, + networkStatus: NetworkStatus.ready, }); - expect(mergeParams).toEqual([ - [void 0, [2, 3, 5, 7, 11]], - // This indicates concatenation happened. - [ - [2, 3, 5, 7, 11], - [13, 17, 19, 23, 29], - ], - ]); + + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + // We get the stale data because we configured keyArgs: false. + data: { primes: [2, 3, 5, 7, 11] }, + called: true, + loading: true, + // This networkStatus is setVariables instead of refetch because we + // called refetch with new variables. + networkStatus: NetworkStatus.setVariables, + previousData: { primes: [2, 3, 5, 7, 11] }, + variables: { min: 12, max: 30 }, + }); + } + + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: { primes: [2, 3, 5, 7, 11, 13, 17, 19, 23, 29] }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { primes: [2, 3, 5, 7, 11] }, + variables: { min: 12, max: 30 }, + }); + } + expect(mergeParams).toEqual([ [undefined, [2, 3, 5, 7, 11]], - // Without refetchWritePolicy: "overwrite", this array will be - // all 10 primes (2 through 29) together. + // This indicates concatenation happened. [ [2, 3, 5, 7, 11], [13, 17, 19, 23, 29], ], ]); - - expect(thenFn).toHaveBeenCalledTimes(1); - expect(thenFn).toHaveBeenCalledWith({ - loading: false, - networkStatus: NetworkStatus.ready, - data: { primes: [2, 3, 5, 7, 11, 13, 17, 19, 23, 29] }, - }); }); it('should assume default refetchWritePolicy value is "overwrite"', async () => { @@ -7208,52 +7610,72 @@ describe("useQuery Hook", () => { { const result = await takeSnapshot(); - expect(result.loading).toBe(true); - expect(result.error).toBe(undefined); - expect(result.data).toBe(undefined); - expect(typeof result.refetch).toBe("function"); + + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: { min: 0, max: 12 }, + }); } { const result = await takeSnapshot(); - expect(result.loading).toBe(false); - expect(result.error).toBeUndefined(); - expect(result.data).toEqual({ primes: [2, 3, 5, 7, 11] }); + + expect(result).toEqualQueryResult({ + data: { primes: [2, 3, 5, 7, 11] }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: { min: 0, max: 12 }, + }); expect(mergeParams.shift()).toEqual([void 0, [2, 3, 5, 7, 11]]); } - const thenFn = jest.fn(); - await getCurrentSnapshot().refetch({ min: 12, max: 30 }).then(thenFn); + await expect( + getCurrentSnapshot().refetch({ min: 12, max: 30 }) + ).resolves.toEqualApolloQueryResult({ + data: { primes: [13, 17, 19, 23, 29] }, + loading: false, + networkStatus: NetworkStatus.ready, + }); { const result = await takeSnapshot(); - expect(result.loading).toBe(true); - expect(result.error).toBe(undefined); - expect(result.data).toEqual({ - // We get the stale data because we configured keyArgs: false. - primes: [2, 3, 5, 7, 11], + + expect(result).toEqualQueryResult({ + data: { + // We get the stale data because we configured keyArgs: false. + primes: [2, 3, 5, 7, 11], + }, + called: true, + loading: true, + // This networkStatus is setVariables instead of refetch because we + // called refetch with new variables. + networkStatus: NetworkStatus.setVariables, + previousData: { primes: [2, 3, 5, 7, 11] }, + variables: { min: 12, max: 30 }, }); - // This networkStatus is setVariables instead of refetch because we - // called refetch with new variables. - expect(result.networkStatus).toBe(NetworkStatus.setVariables); } { const result = await takeSnapshot(); - expect(result.loading).toBe(false); - expect(result.error).toBe(undefined); - expect(result.data).toEqual({ primes: [13, 17, 19, 23, 29] }); + + expect(result).toEqualQueryResult({ + data: { primes: [13, 17, 19, 23, 29] }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { primes: [2, 3, 5, 7, 11] }, + variables: { min: 12, max: 30 }, + }); expect(mergeParams.shift()).toEqual( // Without refetchWritePolicy: "overwrite", this array will be // all 10 primes (2 through 29) together. [undefined, [13, 17, 19, 23, 29]] ); - - expect(thenFn).toHaveBeenCalledTimes(1); - expect(thenFn).toHaveBeenCalledWith({ - loading: false, - networkStatus: NetworkStatus.ready, - data: { primes: [13, 17, 19, 23, 29] }, - }); } }); }); @@ -7316,78 +7738,131 @@ describe("useQuery Hook", () => { } }) ); + const client = new ApolloClient({ cache: new InMemoryCache(), link }); + + using _disabledAct = disableActEnvironment(); + const { takeSnapshot, rerender } = await renderHookToSnapshotStream( + ({ id }) => + useQuery(CAR_QUERY_BY_ID, { + variables: { id }, + notifyOnNetworkStatusChange: true, + fetchPolicy: "network-only", + }), + { + initialProps: { id: 1 }, + wrapper: ({ children }) => ( + {children} + ), + } + ); - const hookResponse = jest.fn().mockReturnValue(null); + { + const result = await takeSnapshot(); - function Component({ children, id }: any) { - const result = useQuery(CAR_QUERY_BY_ID, { - variables: { id }, - notifyOnNetworkStatusChange: true, - fetchPolicy: "network-only", + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: { id: 1 }, }); - const client = useApolloClient(); - const hasRefetchedRef = useRef(false); + } - useEffect(() => { - if ( - result.networkStatus === NetworkStatus.ready && - !hasRefetchedRef.current - ) { - void client.reFetchObservableQueries(); - hasRefetchedRef.current = true; - } - }, [result.networkStatus]); + { + const result = await takeSnapshot(); - return children(result); + expect(result).toEqualQueryResult({ + data: { + car: { + __typename: "Car", + make: "Audi", + model: "A4", + }, + }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: { id: 1 }, + }); } - const { rerender } = render( - {hookResponse}, - { - wrapper: ({ children }) => ( - {children} - ), - } - ); + void client.reFetchObservableQueries(); - await waitFor(() => { - // Resolves as soon as reFetchObservableQueries is - // called, but before the result is returned - expect(hookResponse).toHaveBeenCalledTimes(3); - }); + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: { + car: { + __typename: "Car", + make: "Audi", + model: "A4", + }, + }, + called: true, + loading: true, + networkStatus: NetworkStatus.refetch, + previousData: { + car: { + __typename: "Car", + make: "Audi", + model: "A4", + }, + }, + variables: { id: 1 }, + }); + } - rerender({hookResponse}); + // Rerender with new variables before the refetch request completes + await rerender({ id: 2 }); - await waitFor(() => { - // All results are returned - expect(hookResponse).toHaveBeenCalledTimes(5); - }); + { + const result = await takeSnapshot(); - expect(hookResponse.mock.calls.map((call) => call[0].data)).toEqual([ - undefined, - { - car: { - __typename: "Car", - make: "Audi", - model: "A4", + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.setVariables, + previousData: { + car: { + __typename: "Car", + make: "Audi", + model: "A4", + }, }, - }, - { - car: { - __typename: "Car", - make: "Audi", - model: "A4", + variables: { id: 2 }, + }); + } + + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: { + car: { + __typename: "Car", + make: "Audi", + model: "RS8", + }, }, - }, - undefined, - { - car: { - __typename: "Car", - make: "Audi", - model: "RS8", + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { + car: { + __typename: "Car", + make: "Audi", + model: "A4", + }, }, - }, - ]); + variables: { id: 2 }, + }); + } + + await expect(takeSnapshot).not.toRerender(); }); }); @@ -7412,7 +7887,9 @@ describe("useQuery Hook", () => { ); const onCompleted = jest.fn(); - const { result } = renderHook( + + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( () => useQuery(query, { fetchPolicy: "cache-only", @@ -7421,31 +7898,21 @@ describe("useQuery Hook", () => { { wrapper } ); - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); - await waitFor( - () => { - expect(result.current.data).toEqual({ hello: "world" }); - }, - { interval: 1 } - ); - await waitFor( - () => { - expect(onCompleted).toHaveBeenCalledTimes(1); - }, - { interval: 1 } - ); - await waitFor( - () => { - expect(onCompleted).toHaveBeenCalledWith({ hello: "world" }); - }, - { interval: 1 } - ); + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: { hello: "world" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); + expect(onCompleted).toHaveBeenCalledTimes(1); + expect(onCompleted).toHaveBeenCalledWith({ hello: "world" }); + + await expect(takeSnapshot).not.toRerender(); }); it("onCompleted is called once despite state changes", async () => { @@ -7469,34 +7936,61 @@ describe("useQuery Hook", () => { ); const onCompleted = jest.fn(); - const { result, rerender } = renderHook( - () => - useQuery(query, { - onCompleted, - }), + + using _disabledAct = disableActEnvironment(); + const { takeSnapshot, rerender } = await renderHookToSnapshotStream( + () => useQuery(query, { onCompleted }), { wrapper } ); - expect(result.current.loading).toBe(true); - expect(result.current.data).toBe(undefined); + { + const result = await takeSnapshot(); - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); + expect(result).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); + } + + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: { hello: "world" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); + } - expect(result.current.data).toEqual({ hello: "world" }); expect(onCompleted).toHaveBeenCalledTimes(1); expect(onCompleted).toHaveBeenCalledWith({ hello: "world" }); - rerender(); - expect(result.current.loading).toBe(false); - expect(result.current.data).toEqual({ hello: "world" }); + await rerender(undefined); + + { + const result = await takeSnapshot(); + + expect(result).toEqualQueryResult({ + data: { hello: "world" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); + } + expect(onCompleted).toHaveBeenCalledTimes(1); expect(onCompleted).toHaveBeenCalledWith({ hello: "world" }); - expect(onCompleted).toHaveBeenCalledTimes(1); + + await expect(takeSnapshot).not.toRerender(); }); it("should not call onCompleted if skip is true", async () => { @@ -7520,7 +8014,9 @@ describe("useQuery Hook", () => { ); const onCompleted = jest.fn(); - const { result } = renderHook( + + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( () => useQuery(query, { skip: true, @@ -7529,20 +8025,18 @@ describe("useQuery Hook", () => { { wrapper } ); - expect(result.current.loading).toBe(false); - expect(result.current.data).toBe(undefined); + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: undefined, + error: undefined, + called: false, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); expect(onCompleted).toHaveBeenCalledTimes(0); - - await expect( - waitFor( - () => { - expect(onCompleted).toHaveBeenCalledTimes(1); - }, - { interval: 1, timeout: 20 } - ) - ).rejects.toThrow(); - + await expect(takeSnapshot).not.toRerender(); expect(onCompleted).toHaveBeenCalledTimes(0); }); @@ -7567,38 +8061,37 @@ describe("useQuery Hook", () => { ); const onCompleted = jest.fn(); - let updates = 0; - const { result } = renderHook( + + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( () => { - const pendingResult = useQuery(query, { + return useQuery(query, { fetchPolicy: "network-only", onCompleted, }); - updates++; - - return pendingResult; }, { wrapper } ); - expect(result.current.loading).toBe(true); + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); - expect(result.current.data).toEqual({ hello: "world" }); - const previousUpdates = updates; - await expect( - waitFor( - () => { - expect(updates).not.toEqual(previousUpdates); - }, - { interval: 1, timeout: 20 } - ) - ).rejects.toThrow(); + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: { hello: "world" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); + + await expect(takeSnapshot).not.toRerender(); expect(onCompleted).toHaveBeenCalledTimes(1); }); @@ -7625,7 +8118,9 @@ describe("useQuery Hook", () => { const cache = new InMemoryCache(); const onCompleted = jest.fn(); - const { result } = renderHook( + + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( () => useQuery(query, { onCompleted, @@ -7640,37 +8135,51 @@ describe("useQuery Hook", () => { } ); - expect(result.current.loading).toBe(true); + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); + + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: { hello: "world 1" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); - await waitFor( - () => { - expect(result.current.data).toEqual({ hello: "world 1" }); - }, - { interval: 1 } - ); - expect(result.current.loading).toBe(false); expect(onCompleted).toHaveBeenCalledTimes(1); - await waitFor( - () => { - expect(result.current.data).toEqual({ hello: "world 2" }); - }, - { interval: 1 } - ); - expect(result.current.loading).toBe(false); + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: { hello: "world 2" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { hello: "world 1" }, + variables: {}, + }); + expect(onCompleted).toHaveBeenCalledTimes(1); - await waitFor( - () => { - expect(result.current.data).toEqual({ hello: "world 3" }); - }, - { interval: 1 } - ); - expect(result.current.loading).toBe(false); + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: { hello: "world 3" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { hello: "world 2" }, + variables: {}, + }); + expect(onCompleted).toHaveBeenCalledTimes(1); }); it("onCompleted should fire when polling with notifyOnNetworkStatusChange: true", async () => { + jest.useFakeTimers(); const query = gql` { hello @@ -7680,22 +8189,23 @@ describe("useQuery Hook", () => { { request: { query }, result: { data: { hello: "world 1" } }, - delay: 3, + delay: 20, }, { request: { query }, result: { data: { hello: "world 2" } }, - delay: 3, + delay: 20, }, { request: { query }, result: { data: { hello: "world 3" } }, - delay: 3, + delay: 20, }, ]; const cache = new InMemoryCache(); const onCompleted = jest.fn(); + using _disabledAct = disableActEnvironment(); const { takeSnapshot } = await renderHookToSnapshotStream( () => @@ -7714,40 +8224,110 @@ describe("useQuery Hook", () => { ); { - const result = await takeSnapshot(); - expect(result.data).toEqual(undefined); - expect(result.loading).toBe(true); + const promise = takeSnapshot(); + await jest.advanceTimersByTimeAsync(0); + + await expect(promise).resolves.toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); } + + expect(onCompleted).toHaveBeenCalledTimes(0); + jest.advanceTimersByTime(20); + { - const result = await takeSnapshot(); - expect(result.data).toEqual({ hello: "world 1" }); - expect(result.loading).toBe(false); - expect(onCompleted).toHaveBeenCalledTimes(1); + const promise = takeSnapshot(); + await jest.advanceTimersByTimeAsync(0); + + await expect(promise).resolves.toEqualQueryResult({ + data: { hello: "world 1" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); } + + expect(onCompleted).toHaveBeenCalledTimes(1); + // Polling is started with the first request, so we only need to advance + // the timer by 180 (200 poll time - 20 result delay) + jest.advanceTimersByTime(180); + { - const result = await takeSnapshot(); - expect(result.data).toEqual({ hello: "world 1" }); - expect(result.loading).toBe(true); - expect(onCompleted).toHaveBeenCalledTimes(1); + const promise = takeSnapshot(); + await jest.advanceTimersByTimeAsync(0); + + await expect(promise).resolves.toEqualQueryResult({ + data: { hello: "world 1" }, + called: true, + loading: true, + networkStatus: NetworkStatus.poll, + previousData: { hello: "world 1" }, + variables: {}, + }); } + + expect(onCompleted).toHaveBeenCalledTimes(1); + jest.advanceTimersByTime(20); + { - const result = await takeSnapshot(); - expect(result.data).toEqual({ hello: "world 2" }); - expect(result.loading).toBe(false); - expect(onCompleted).toHaveBeenCalledTimes(2); + const promise = takeSnapshot(); + await jest.advanceTimersByTimeAsync(0); + + await expect(promise).resolves.toEqualQueryResult({ + data: { hello: "world 2" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { hello: "world 1" }, + variables: {}, + }); } + + expect(onCompleted).toHaveBeenCalledTimes(2); + jest.advanceTimersByTime(200); + { - const result = await takeSnapshot(); - expect(result.data).toEqual({ hello: "world 2" }); - expect(result.loading).toBe(true); - expect(onCompleted).toHaveBeenCalledTimes(2); + const promise = takeSnapshot(); + await jest.advanceTimersByTimeAsync(0); + + await expect(promise).resolves.toEqualQueryResult({ + data: { hello: "world 2" }, + called: true, + loading: true, + networkStatus: NetworkStatus.poll, + previousData: { hello: "world 2" }, + variables: {}, + }); } + + expect(onCompleted).toHaveBeenCalledTimes(2); + jest.advanceTimersByTime(20); + { - const result = await takeSnapshot(); - expect(result.data).toEqual({ hello: "world 3" }); - expect(result.loading).toBe(false); - expect(onCompleted).toHaveBeenCalledTimes(3); + const promise = takeSnapshot(); + await jest.advanceTimersByTimeAsync(0); + + await expect(promise).resolves.toEqualQueryResult({ + data: { hello: "world 3" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { hello: "world 2" }, + variables: {}, + }); } + + expect(onCompleted).toHaveBeenCalledTimes(3); + + jest.runOnlyPendingTimers(); + jest.useRealTimers(); }); // This test was added for issue https://github.com/apollographql/apollo-client/issues/9794 @@ -7811,51 +8391,66 @@ describe("useQuery Hook", () => { result: { data: { hello: "bar" } }, }, ]; - const link = new MockLink(mocks); - const cache = new InMemoryCache(); + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: new MockLink(mocks), + }); const onCompleted = jest.fn(); - const ChildComponent: React.FC = () => { - const { data, client } = useQuery(query, { onCompleted }); - function refetchQueries() { - void client.refetchQueries({ include: "active" }); - } - function writeQuery() { - client.writeQuery({ query, data: { hello: "baz" } }); + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( + () => useQuery(query, { onCompleted }), + { + wrapper: ({ children }) => ( + {children} + ), } - return ( -
- Data: {data?.hello} - - -
- ); - }; + ); - const ParentComponent: React.FC = () => { - return ( - -
- -
-
- ); - }; + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); - render(); + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: { hello: "foo" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); + expect(onCompleted).toHaveBeenCalledTimes(1); - await screen.findByText("Data: foo"); - await userEvent.click( - screen.getByRole("button", { name: /refetch queries/i }) - ); - expect(onCompleted).toBeCalledTimes(1); - await screen.findByText("Data: bar"); - await userEvent.click( - screen.getByRole("button", { name: /update word/i }) - ); - expect(onCompleted).toBeCalledTimes(1); - await screen.findByText("Data: baz"); - expect(onCompleted).toBeCalledTimes(1); + void client.refetchQueries({ include: "active" }); + + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: { hello: "bar" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { hello: "foo" }, + variables: {}, + }); + expect(onCompleted).toHaveBeenCalledTimes(1); + + client.writeQuery({ query, data: { hello: "baz" } }); + + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: { hello: "baz" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { hello: "bar" }, + variables: {}, + }); + expect(onCompleted).toHaveBeenCalledTimes(1); + + await expect(takeSnapshot).not.toRerender(); }); it("onCompleted should execute on cache writes after initial query execution with notifyOnNetworkStatusChange: true", async () => { @@ -7868,63 +8463,85 @@ describe("useQuery Hook", () => { { request: { query }, result: { data: { hello: "foo" } }, + delay: 20, }, { request: { query }, result: { data: { hello: "bar" } }, + delay: 20, }, ]; - const link = new MockLink(mocks); - const cache = new InMemoryCache(); + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: new MockLink(mocks), + }); const onCompleted = jest.fn(); - const ChildComponent: React.FC = () => { - const { data, client } = useQuery(query, { - onCompleted, - notifyOnNetworkStatusChange: true, - }); - function refetchQueries() { - void client.refetchQueries({ include: "active" }); - } - function writeQuery() { - client.writeQuery({ query, data: { hello: "baz" } }); + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( + () => + useQuery(query, { onCompleted, notifyOnNetworkStatusChange: true }), + { + wrapper: ({ children }) => ( + {children} + ), } - return ( -
- Data: {data?.hello} - - -
- ); - }; + ); - const ParentComponent: React.FC = () => { - return ( - -
- -
-
- ); - }; + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); - render(); + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: { hello: "foo" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); + expect(onCompleted).toHaveBeenCalledTimes(1); - await screen.findByText("Data: foo"); - expect(onCompleted).toBeCalledTimes(1); - await userEvent.click( - screen.getByRole("button", { name: /refetch queries/i }) - ); - // onCompleted increments when refetch occurs since we're hitting the network... - expect(onCompleted).toBeCalledTimes(2); - await screen.findByText("Data: bar"); - await userEvent.click( - screen.getByRole("button", { name: /update word/i }) - ); - // but not on direct cache write, since there's no network request to complete - expect(onCompleted).toBeCalledTimes(2); - await screen.findByText("Data: baz"); - expect(onCompleted).toBeCalledTimes(2); + void client.refetchQueries({ include: "active" }); + + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: { hello: "foo" }, + called: true, + loading: true, + networkStatus: NetworkStatus.refetch, + previousData: { hello: "foo" }, + variables: {}, + }); + expect(onCompleted).toHaveBeenCalledTimes(1); + + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: { hello: "bar" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { hello: "foo" }, + variables: {}, + }); + expect(onCompleted).toHaveBeenCalledTimes(2); + + client.writeQuery({ query, data: { hello: "baz" } }); + + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: { hello: "baz" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { hello: "bar" }, + variables: {}, + }); + expect(onCompleted).toHaveBeenCalledTimes(2); + + await expect(takeSnapshot).not.toRerender(); }); }); @@ -8040,16 +8657,31 @@ describe("useQuery Hook", () => { { const { query } = await takeSnapshot(); - expect(query.loading).toBe(true); + + expect(query).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); } - const mutate = getCurrentSnapshot().mutation[0]; + { const { query } = await takeSnapshot(); - expect(query.loading).toBe(false); - expect(query.loading).toBe(false); - expect(query.data).toEqual(carsData); + + expect(query).toEqualQueryResult({ + data: carsData, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); } + const mutate = getCurrentSnapshot().mutation[0]; void mutate(); { @@ -8065,8 +8697,14 @@ describe("useQuery Hook", () => { ({ query, mutation } = await takeSnapshot()); } expect(mutation[1].loading).toBe(true); - expect(query.loading).toBe(false); - expect(query.data).toEqual(allCarsData); + expect(query).toEqualQueryResult({ + data: allCarsData, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: carsData, + variables: {}, + }); } expect(onError).toHaveBeenCalledTimes(0); @@ -8075,7 +8713,14 @@ describe("useQuery Hook", () => { // The mutation ran and is loading the result. The query stays at // not loading as nothing has changed for the query. expect(mutation[1].loading).toBe(true); - expect(query.loading).toBe(false); + expect(query).toEqualQueryResult({ + data: carsData, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: allCarsData, + variables: {}, + }); } { @@ -8083,8 +8728,14 @@ describe("useQuery Hook", () => { // The mutation has completely finished, leaving the query with access to // the original cache data. expect(mutation[1].loading).toBe(false); - expect(query.loading).toBe(false); - expect(query.data).toEqual(carsData); + expect(query).toEqualQueryResult({ + data: carsData, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: allCarsData, + variables: {}, + }); } expect(onError).toHaveBeenCalledTimes(1); @@ -8370,53 +9021,69 @@ describe("useQuery Hook", () => { {children} ); - const { result } = renderHook( + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( () => useQuery(query, { variables: { id: entityId } }), { wrapper } ); - expect(result.current.loading).toBe(true); - expect(result.current.data).toBe(undefined); - await waitFor( - () => { - expect(result.current.loading).toBe(false); + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: { id: entityId }, + }); + + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: { + clientEntity: { + id: entityId, + title: shortTitle, + titleLength: shortTitle.length, + __typename: "ClientData", + }, }, - { interval: 1 } - ); + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: { id: entityId }, + }); - expect(result.current.data).toEqual({ - clientEntity: { + void client.mutate({ + mutation, + variables: { id: entityId, - title: shortTitle, - titleLength: shortTitle.length, - __typename: "ClientData", + title: longerTitle, }, }); - await act( - async () => - void client.mutate({ - mutation, - variables: { - id: entityId, - title: longerTitle, - }, - }) - ); - - await waitFor( - () => { - expect(result.current.data).toEqual({ - clientEntity: { - id: entityId, - title: longerTitle, - titleLength: longerTitle.length, - __typename: "ClientData", - }, - }); + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: { + clientEntity: { + id: entityId, + title: longerTitle, + titleLength: longerTitle.length, + __typename: "ClientData", + }, }, - { interval: 1 } - ); + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { + clientEntity: { + id: entityId, + title: shortTitle, + titleLength: shortTitle.length, + __typename: "ClientData", + }, + }, + variables: { id: entityId }, + }); + + await expect(takeSnapshot).not.toRerender(); }); }); @@ -8460,25 +9127,43 @@ describe("useQuery Hook", () => { ); - const { result, rerender } = renderHook( + using _disabledAct = disableActEnvironment(); + const { takeSnapshot, rerender } = await renderHookToSnapshotStream( ({ skip }) => useQuery(query, { skip }), { wrapper, initialProps: { skip: true } } ); - expect(result.current.loading).toBe(false); - expect(result.current.data).toBe(undefined); - rerender({ skip: false }); + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: undefined, + error: undefined, + called: false, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); - expect(result.current.loading).toBe(true); - expect(result.current.data).toBe(undefined); + await rerender({ skip: false }); - await waitFor( - () => { - expect(result.current.loading).toBeFalsy(); - }, - { interval: 1 } - ); - expect(result.current.data).toEqual({ hello: "world" }); + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); + + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: { hello: "world" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); + + await expect(takeSnapshot).not.toRerender(); }); it("should not make network requests when `skip` is `true`", async () => { @@ -8496,25 +9181,44 @@ describe("useQuery Hook", () => { {children} ); - const { result, rerender } = renderHook( + using _disabledAct = disableActEnvironment(); + const { takeSnapshot, rerender } = await renderHookToSnapshotStream( ({ skip, variables }) => useQuery(query, { skip, variables }), { wrapper, initialProps: { skip: false, variables: undefined as any } } ); - expect(result.current.loading).toBe(true); - expect(result.current.data).toBe(undefined); + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); - expect(result.current.data).toEqual({ hello: "world" }); + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: { hello: "world" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); + + await rerender({ skip: true, variables: { someVar: true } }); + + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: undefined, + error: undefined, + // TODO: It seems odd to flip this back to false after it was already + // set to true + called: false, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { hello: "world" }, + variables: { someVar: true }, + }); - rerender({ skip: true, variables: { someVar: true } }); - expect(result.current.loading).toBe(false); - expect(result.current.data).toBe(undefined); expect(linkFn).toHaveBeenCalledTimes(1); }); @@ -8550,7 +9254,9 @@ describe("useQuery Hook", () => { result: { data: { hello: "world" } }, }, ]; - const { result, rerender } = renderHook( + + using _disabledAct = disableActEnvironment(); + const { takeSnapshot, rerender } = await renderHookToSnapshotStream( ({ fetchPolicy }) => useQuery(query, { fetchPolicy }), { wrapper: ({ children }) => ( @@ -8560,28 +9266,37 @@ describe("useQuery Hook", () => { } ); - expect(result.current.loading).toBe(false); - expect(result.current.data).toBe(undefined); + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: undefined, + error: undefined, + called: false, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); - await expect( - waitFor( - () => { - expect(result.current.loading).toBe(true); - }, - { interval: 1, timeout: 20 } - ) - ).rejects.toThrow(); + await rerender({ fetchPolicy: "cache-first" }); - rerender({ fetchPolicy: "cache-first" }); - expect(result.current.data).toBe(undefined); + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); - await waitFor( - () => { - expect(result.current.loading).toBeFalsy(); - }, - { interval: 1 } - ); - expect(result.current.data).toEqual({ hello: "world" }); + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: { hello: "world" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); + + await expect(takeSnapshot).not.toRerender(); }); // Amusingly, #8270 thinks this is a bug, but #9101 thinks this is not. @@ -8603,39 +9318,39 @@ describe("useQuery Hook", () => { link, }); - const { result } = renderHook(() => useQuery(query, { skip: true }), { - wrapper: ({ children }) => ( - {children} - ), - }); + using _disabledAct = disableActEnvironment(); + const { takeSnapshot, getCurrentSnapshot } = + await renderHookToSnapshotStream( + () => useQuery(query, { skip: true }), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); - let promise; - await waitFor(async () => { - promise = result.current.refetch(); + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: undefined, + error: undefined, + called: false, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, }); - expect(result.current.loading).toBe(false); - await waitFor( - () => { - expect(result.current.data).toBe(undefined); - }, - { interval: 1 } - ); + const refetchResult = await getCurrentSnapshot().refetch(); - expect(result.current.loading).toBe(false); - await waitFor( - () => { - expect(result.current.data).toBe(undefined); - }, - { interval: 1 } - ); - expect(requestSpy).toHaveBeenCalledTimes(1); - requestSpy.mockRestore(); - await expect(promise).resolves.toEqual({ + expect(refetchResult).toEqualApolloQueryResult({ data: { hello: "world" }, loading: false, - networkStatus: 7, + networkStatus: NetworkStatus.ready, }); + + expect(requestSpy).toHaveBeenCalledTimes(1); + requestSpy.mockRestore(); + + await expect(takeSnapshot).not.toRerender(); }); it("should set correct initialFetchPolicy even if skip:true", async () => { @@ -8659,38 +9374,42 @@ describe("useQuery Hook", () => { const correctInitialFetchPolicy: WatchQueryFetchPolicy = "cache-and-network"; - const { result, rerender } = renderHook< - QueryResult, - { - skip: boolean; - } - >( - ({ skip = true }) => - useQuery(query, { - // Skipping equates to using a fetchPolicy of "standby", but that - // should not mean we revert to standby whenever we want to go back to - // the initial fetchPolicy (e.g. when variables change). - skip, - fetchPolicy: correctInitialFetchPolicy, - }), - { - initialProps: { - skip: true, - }, - wrapper: ({ children }) => ( - {children} - ), - } - ); + using _disabledAct = disableActEnvironment(); + const { takeSnapshot, getCurrentSnapshot, rerender } = + await renderHookToSnapshotStream( + ({ skip }) => + useQuery(query, { + // Skipping equates to using a fetchPolicy of "standby", but that + // should not mean we revert to standby whenever we want to go back to + // the initial fetchPolicy (e.g. when variables change). + skip, + fetchPolicy: correctInitialFetchPolicy, + }), + { + initialProps: { + skip: true, + }, + wrapper: ({ children }) => ( + {children} + ), + } + ); - expect(result.current.loading).toBe(false); - expect(result.current.data).toBeUndefined(); + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: undefined, + error: undefined, + called: false, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); function check( expectedFetchPolicy: WatchQueryFetchPolicy, expectedInitialFetchPolicy: WatchQueryFetchPolicy ) { - const { observable } = result.current; + const { observable } = getCurrentSnapshot(); const { fetchPolicy, initialFetchPolicy } = observable.options; expect(fetchPolicy).toBe(expectedFetchPolicy); @@ -8699,56 +9418,68 @@ describe("useQuery Hook", () => { check("standby", correctInitialFetchPolicy); - rerender({ - skip: false, - }); + await rerender({ skip: false }); - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); - expect(result.current.data).toEqual({ - hello: 1, + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: { hello: 1 }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, }); check(correctInitialFetchPolicy, correctInitialFetchPolicy); const reasons: string[] = []; - const reobservePromise = result.current.observable - .reobserve({ - variables: { - newVar: true, - }, - nextFetchPolicy(currentFetchPolicy, context) { - expect(currentFetchPolicy).toBe("cache-and-network"); - expect(context.initialFetchPolicy).toBe("cache-and-network"); - reasons.push(context.reason); - return currentFetchPolicy; - }, - }) - .then((result) => { - expect(result.loading).toBe(false); - expect(result.data).toEqual({ hello: 2 }); - }); + const result = await getCurrentSnapshot().observable.reobserve({ + variables: { + newVar: true, + }, + nextFetchPolicy(currentFetchPolicy, context) { + expect(currentFetchPolicy).toBe("cache-and-network"); + expect(context.initialFetchPolicy).toBe("cache-and-network"); + reasons.push(context.reason); + return currentFetchPolicy; + }, + }); - expect(result.current.loading).toBe(false); + expect(result).toEqualApolloQueryResult({ + data: { hello: 2 }, + loading: false, + networkStatus: NetworkStatus.ready, + }); + expect(reasons).toEqual(["variables-changed", "after-fetch"]); - await waitFor( - () => { - expect(result.current.data).toEqual({ - hello: 2, - }); - }, - { interval: 1 } - ); + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: { hello: 1 }, + called: true, + loading: true, + networkStatus: NetworkStatus.setVariables, + previousData: { hello: 1 }, + variables: { newVar: true }, + }); - await reobservePromise; + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: { hello: 2 }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { hello: 1 }, + variables: { newVar: true }, + }); - expect(reasons).toEqual(["variables-changed", "after-fetch"]); + await expect(takeSnapshot).not.toRerender(); }); it("should prioritize a `nextFetchPolicy` function over a `fetchPolicy` option when changing variables", async () => { @@ -8789,61 +9520,74 @@ describe("useQuery Hook", () => { expect.any(Number) ); }; - let nextFetchPolicy: WatchQueryOptions< + const nextFetchPolicy: WatchQueryOptions< OperationVariables, any - >["nextFetchPolicy"] = (_, context) => { + >["nextFetchPolicy"] = jest.fn((_, context) => { if (context.reason === "variables-changed") { - return "cache-and-network"; - } else if (context.reason === "after-fetch") { - return "cache-only"; - } - throw new Error("should never happen"); - }; - nextFetchPolicy = jest.fn(nextFetchPolicy); - - const { result, rerender } = renderHook< - QueryResult, - { - variables: { id: number }; - } - >( - ({ variables }) => - useQuery(query, { - fetchPolicy: "network-only", - variables, - notifyOnNetworkStatusChange: true, - nextFetchPolicy, - }), - { - initialProps: { - variables: { id: 1 }, - }, - wrapper: ({ children }) => ( - {children} - ), + return "cache-and-network"; + } else if (context.reason === "after-fetch") { + return "cache-only"; } - ); + throw new Error("should never happen"); + }); + + using _disabledAct = disableActEnvironment(); + const { takeSnapshot, getCurrentSnapshot, rerender } = + await renderHookToSnapshotStream( + ({ variables }) => + useQuery(query, { + fetchPolicy: "network-only", + variables, + notifyOnNetworkStatusChange: true, + nextFetchPolicy, + }), + { + initialProps: { + variables: { id: 1 }, + }, + wrapper: ({ children }) => ( + {children} + ), + } + ); + + await tick(); + // first network request triggers with initial fetchPolicy expectQueryTriggered(1, "network-only"); - await waitFor(() => { - expect(result.current.networkStatus).toBe(NetworkStatus.ready); + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: { id: 1 }, + }); + + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: { hello: "from link" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: { id: 1 }, }); expect(nextFetchPolicy).toHaveBeenCalledTimes(1); expect(nextFetchPolicy).toHaveBeenNthCalledWith( 1, "network-only", - expect.objectContaining({ - reason: "after-fetch", - }) + expect.objectContaining({ reason: "after-fetch" }) ); // `nextFetchPolicy(..., {reason: "after-fetch"})` changed it to // cache-only - expect(result.current.observable.options.fetchPolicy).toBe("cache-only"); + expect(getCurrentSnapshot().observable.options.fetchPolicy).toBe( + "cache-only" + ); - rerender({ + await rerender({ variables: { id: 2 }, }); @@ -8859,8 +9603,23 @@ describe("useQuery Hook", () => { // the return value of `nextFetchPolicy(..., {reason: "variables-changed"})` expectQueryTriggered(2, "cache-and-network"); - await waitFor(() => { - expect(result.current.networkStatus).toBe(NetworkStatus.ready); + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + // TODO: Shouldn't this be undefined? + data: { hello: "from link" }, + called: true, + loading: true, + networkStatus: NetworkStatus.setVariables, + previousData: { hello: "from link" }, + variables: { id: 2 }, + }); + + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: { hello: "from link2" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { hello: "from link" }, + variables: { id: 2 }, }); expect(nextFetchPolicy).toHaveBeenCalledTimes(3); @@ -8873,7 +9632,11 @@ describe("useQuery Hook", () => { ); // `nextFetchPolicy(..., {reason: "after-fetch"})` changed it to // cache-only - expect(result.current.observable.options.fetchPolicy).toBe("cache-only"); + expect(getCurrentSnapshot().observable.options.fetchPolicy).toBe( + "cache-only" + ); + + await expect(takeSnapshot).not.toRerender(); }); }); @@ -8919,22 +9682,29 @@ describe("useQuery Hook", () => { ); - const { result } = renderHook( + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( () => useQuery(carQuery, { variables: { id: 1 } }), { wrapper } ); - expect(result.current.loading).toBe(true); - expect(result.current.data).toBe(undefined); - expect(result.current.error).toBe(undefined); - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); - expect(result.current.data).toEqual(carData); - expect(result.current.error).toBeUndefined(); + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: { id: 1 }, + }); + + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: carData, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: { id: 1 }, + }); expect(consoleSpy.error).toHaveBeenCalled(); expect(consoleSpy.error).toHaveBeenLastCalledWith( @@ -8948,6 +9718,8 @@ describe("useQuery Hook", () => { __typename: "Car", } ); + + await expect(takeSnapshot).not.toRerender(); }); it("should return partial cache data when `returnPartialData` is true", async () => { @@ -9002,7 +9774,8 @@ describe("useQuery Hook", () => { } `; - const { result } = renderHook( + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( () => useQuery(partialQuery, { returnPartialData: true }), { wrapper: ({ children }) => ( @@ -9011,23 +9784,29 @@ describe("useQuery Hook", () => { } ); - expect(result.current.loading).toBe(true); - expect(result.current.data).toEqual({ - cars: [ - { - __typename: "Car", - repairs: [ - { - __typename: "Repair", - date: "2019-05-08", - }, - ], - }, - ], + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: { + cars: [ + { + __typename: "Car", + repairs: [ + { + __typename: "Repair", + date: "2019-05-08", + }, + ], + }, + ], + }, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, }); }); - it("should not return partial cache data when `returnPartialData` is false", () => { + it("should not return partial cache data when `returnPartialData` is false", async () => { const cache = new InMemoryCache(); const client = new ApolloClient({ cache, @@ -9079,7 +9858,8 @@ describe("useQuery Hook", () => { } `; - const { result } = renderHook( + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( () => useQuery(partialQuery, { returnPartialData: false }), { wrapper: ({ children }) => ( @@ -9088,8 +9868,14 @@ describe("useQuery Hook", () => { } ); - expect(result.current.loading).toBe(true); - expect(result.current.data).toBe(undefined); + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); }); it("should not return partial cache data when `returnPartialData` is false and new variables are passed in", async () => { @@ -9144,11 +9930,9 @@ describe("useQuery Hook", () => { }, }); - let setId: any; - const { result } = renderHook( - () => { - const [id, setId1] = React.useState(2); - setId = setId1; + using _disabledAct = disableActEnvironment(); + const { takeSnapshot, rerender } = await renderHookToSnapshotStream( + ({ id }) => { return useQuery(partialQuery, { variables: { id }, returnPartialData: false, @@ -9156,34 +9940,46 @@ describe("useQuery Hook", () => { }); }, { + initialProps: { id: 2 }, wrapper: ({ children }) => ( {children} ), } ); - expect(result.current.loading).toBe(false); - expect(result.current.data).toEqual({ - car: { - __typename: "Car", - id: 2, - make: "Ford", - model: "Pinto", + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: { + car: { + __typename: "Car", + id: 2, + make: "Ford", + model: "Pinto", + }, }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: { id: 2 }, }); - setTimeout(() => { - setId(1); - }); + await rerender({ id: 1 }); - await waitFor( - () => { - expect(result.current.loading).toBe(true); + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.setVariables, + previousData: { + car: { + __typename: "Car", + id: 2, + make: "Ford", + model: "Pinto", + }, }, - { interval: 1 } - ); - - expect(result.current.data).toBe(undefined); + variables: { id: 1 }, + }); }); }); @@ -9227,48 +10023,60 @@ describe("useQuery Hook", () => { ); using _disabledAct = disableActEnvironment(); - const { takeSnapshot } = await renderHookToSnapshotStream( - () => useQuery(query, { notifyOnNetworkStatusChange: true }), - { wrapper } - ); + const { takeSnapshot, getCurrentSnapshot } = + await renderHookToSnapshotStream( + () => useQuery(query, { notifyOnNetworkStatusChange: true }), + { wrapper } + ); - { - const result = await takeSnapshot(); - expect(result.loading).toBe(true); - expect(result.data).toBe(undefined); - expect(result.previousData).toBe(undefined); - } + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); - { - const result = await takeSnapshot(); - expect(result.loading).toBe(false); - expect(result.data).toEqual(data1); - expect(result.previousData).toBe(undefined); + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: data1, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); - await result.refetch(); - } + await getCurrentSnapshot().refetch(); - { - const result = await takeSnapshot(); - expect(result.loading).toBe(true); - expect(result.data).toEqual(data1); - expect(result.previousData).toEqual(data1); - } + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: data1, + called: true, + loading: true, + networkStatus: NetworkStatus.refetch, + previousData: data1, + variables: {}, + }); - { - const result = await takeSnapshot(); - expect(result.loading).toBe(false); - expect(result.data).toEqual(data2); - expect(result.previousData).toEqual(data1); - } + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: data2, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: data1, + variables: {}, + }); + + await expect(takeSnapshot).not.toRerender(); }); it("should persist result.previousData across multiple results", async () => { const query: TypedDocumentNode< { car: { - id: string; + id: number; make: string; + __typename: "Car"; }; }, { @@ -9287,7 +10095,7 @@ describe("useQuery Hook", () => { car: { id: 1, make: "Venturi", - __typename: "Car", + __typename: "Car" as const, }, }; @@ -9295,7 +10103,7 @@ describe("useQuery Hook", () => { car: { id: 2, make: "Wiesmann", - __typename: "Car", + __typename: "Car" as const, }, }; @@ -9303,7 +10111,7 @@ describe("useQuery Hook", () => { car: { id: 3, make: "Beetle", - __typename: "Car", + __typename: "Car" as const, }, }; @@ -9326,49 +10134,72 @@ describe("useQuery Hook", () => { ); - const { result } = renderHook( - () => useQuery(query, { notifyOnNetworkStatusChange: true }), - { wrapper } - ); + using _disabledAct = disableActEnvironment(); + const { takeSnapshot, getCurrentSnapshot } = + await renderHookToSnapshotStream( + () => useQuery(query, { notifyOnNetworkStatusChange: true }), + { wrapper } + ); - expect(result.current.loading).toBe(true); - expect(result.current.data).toBe(undefined); - expect(result.current.previousData).toBe(undefined); + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); + + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: data1, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); + + setTimeout(() => getCurrentSnapshot().refetch()); + + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: data1, + called: true, + loading: true, + networkStatus: NetworkStatus.refetch, + previousData: data1, + variables: {}, + }); + + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: data2, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: data1, + variables: {}, + }); - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); - expect(result.current.data).toEqual(data1); - expect(result.current.previousData).toBe(undefined); + void getCurrentSnapshot().refetch({ vin: "ABCDEFG0123456789" }); - setTimeout(() => result.current.refetch()); - await waitFor( - () => { - expect(result.current.loading).toBe(true); - }, - { interval: 1 } - ); - expect(result.current.data).toEqual(data1); - expect(result.current.previousData).toEqual(data1); + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.setVariables, + previousData: data2, + variables: { vin: "ABCDEFG0123456789" }, + }); - await act( - async () => void result.current.refetch({ vin: "ABCDEFG0123456789" }) - ); - expect(result.current.loading).toBe(true); - expect(result.current.data).toEqual(data1); - expect(result.current.previousData).toEqual(data1); + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: data3, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: data2, + variables: { vin: "ABCDEFG0123456789" }, + }); - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); - expect(result.current.data).toEqual(data3); - expect(result.current.previousData).toEqual(data1); + await expect(takeSnapshot).not.toRerender(); }); it("should persist result.previousData even if query changes", async () => { @@ -9439,213 +10270,125 @@ describe("useQuery Hook", () => { ), }); - const { result } = renderHook( - () => { - const [query, setQuery] = useState(aQuery); - return { - query, - setQuery, - useQueryResult: useQuery(query, { + using _disabledAct = disableActEnvironment(); + const { takeSnapshot, getCurrentSnapshot, rerender } = + await renderHookToSnapshotStream( + ({ query }) => { + return useQuery(query, { fetchPolicy: "cache-and-network", notifyOnNetworkStatusChange: true, - }), - }; - }, - { - wrapper: ({ children }: any) => ( - {children} - ), - } - ); + }); + }, + { + initialProps: { query: aQuery as DocumentNode }, + wrapper: ({ children }: any) => ( + {children} + ), + } + ); - await waitFor( - () => { - const { loading } = result.current.useQueryResult; - expect(loading).toBe(true); - }, - { interval: 1 } - ); - await waitFor( - () => { - const { data } = result.current.useQueryResult; - expect(data).toEqual({ a: "a" }); - }, - { interval: 1 } - ); - await waitFor( - () => { - const { previousData } = result.current.useQueryResult; - expect(previousData).toBeUndefined(); - }, - { interval: 1 } - ); + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); - await waitFor( - () => { - const { loading } = result.current.useQueryResult; - expect(loading).toBe(false); - }, - { interval: 1 } - ); - await waitFor( - () => { - const { data } = result.current.useQueryResult; - expect(data).toEqual({ a: "a" }); - }, - { interval: 1 } - ); - await waitFor( - () => { - const { previousData } = result.current.useQueryResult; - expect(previousData).toBe(undefined); - }, - { interval: 1 } - ); + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: { a: "a" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); - await expect( - await waitFor( - () => { - result.current.setQuery(abQuery); - }, - { interval: 1 } - ) - ); + await rerender({ query: abQuery }); - await waitFor( - () => { - const { loading } = result.current.useQueryResult; - expect(loading).toBe(false); - }, - { interval: 1 } - ); - await waitFor( - () => { - const { data } = result.current.useQueryResult; - expect(data).toEqual({ a: "aa", b: 1 }); - }, - { interval: 1 } - ); - await waitFor( - () => { - const { previousData } = result.current.useQueryResult; - expect(previousData).toEqual({ a: "a" }); - }, - { interval: 1 } - ); + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: { a: "a" }, + variables: {}, + }); - await waitFor( - () => { - const { loading } = result.current.useQueryResult; - expect(loading).toBe(false); - }, - { interval: 1 } - ); - await waitFor( - () => { - const { data } = result.current.useQueryResult; - expect(data).toEqual({ a: "aa", b: 1 }); - }, - { interval: 1 } - ); - await waitFor( - () => { - const { previousData } = result.current.useQueryResult; - expect(previousData).toEqual({ a: "a" }); - }, - { interval: 1 } - ); + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: { a: "aa", b: 1 }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { a: "a" }, + variables: {}, + }); - await waitFor( - () => { - // TODO investigate why do we call `reobserve` in a very quick loop here? - void result.current.useQueryResult.reobserve().then((result) => { - expect(result.loading).toBe(false); - expect(result.data).toEqual({ a: "aaa", b: 2 }); - }); - }, - { interval: 1 } - ); + const result = await getCurrentSnapshot().reobserve(); - await waitFor( - () => { - const { loading } = result.current.useQueryResult; - expect(loading).toBe(false); - }, - { interval: 1 } - ); - await waitFor( - () => { - const { data } = result.current.useQueryResult; - expect(data).toEqual({ a: "aaa", b: 2 }); - }, - { interval: 1 } - ); - await waitFor( - () => { - const { previousData } = result.current.useQueryResult; - expect(previousData).toEqual({ a: "aa", b: 1 }); - }, - { interval: 1 } - ); + expect(result).toEqualApolloQueryResult({ + data: { a: "aaa", b: 2 }, + loading: false, + networkStatus: NetworkStatus.ready, + }); - await waitFor( - () => { - result.current.setQuery(bQuery); - }, - { interval: 1 } - ); + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: { a: "aa", b: 1 }, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: { a: "aa", b: 1 }, + variables: {}, + }); - await waitFor( - () => { - const { loading } = result.current.useQueryResult; - expect(loading).toBe(false); - }, - { interval: 1 } - ); - await waitFor( - () => { - const { data } = result.current.useQueryResult; - expect(data).toEqual({ b: 3 }); - }, - { interval: 1 } - ); - await waitFor( - () => { - const { previousData } = result.current.useQueryResult; - expect(previousData).toEqual({ b: 2 }); - }, - { interval: 1 } - ); - await waitFor( - () => { - const { loading } = result.current.useQueryResult; - expect(loading).toBe(false); - }, - { interval: 1 } - ); - await waitFor( - () => { - const { data } = result.current.useQueryResult; - expect(data).toEqual({ b: 3 }); - }, - { interval: 1 } - ); - await waitFor( - () => { - const { previousData } = result.current.useQueryResult; - expect(previousData).toEqual({ b: 2 }); - }, - { interval: 1 } - ); + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: { a: "aaa", b: 2 }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { a: "aa", b: 1 }, + variables: {}, + }); + + await rerender({ query: bQuery }); + + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: { b: 2 }, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: { a: "aaa", b: 2 }, + variables: {}, + }); + + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: { b: 3 }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { b: 2 }, + variables: {}, + }); + + await expect(takeSnapshot).not.toRerender(); }); + // TODO: Determine if this test is needed or is already covered by other + // tests such as changing variables (since those tests check all returned + // hook properties) it("should be cleared when variables change causes cache miss", async () => { const peopleData = [ - { id: 1, name: "John Smith", gender: "male" }, - { id: 2, name: "Sara Smith", gender: "female" }, - { id: 3, name: "Budd Deey", gender: "nonbinary" }, - { id: 4, name: "Johnny Appleseed", gender: "male" }, - { id: 5, name: "Ada Lovelace", gender: "female" }, + { id: 1, name: "John Smith", gender: "male", __typename: "Person" }, + { id: 2, name: "Sara Smith", gender: "female", __typename: "Person" }, + { id: 3, name: "Budd Deey", gender: "nonbinary", __typename: "Person" }, + { + id: 4, + name: "Johnny Appleseed", + gender: "male", + __typename: "Person", + }, + { id: 5, name: "Ada Lovelace", gender: "female", __typename: "Person" }, ]; const link = new ApolloLink((operation) => { @@ -9668,7 +10411,7 @@ describe("useQuery Hook", () => { type Person = { __typename: string; - id: string; + id: number; name: string; }; @@ -9700,59 +10443,55 @@ describe("useQuery Hook", () => { { wrapper, initialProps: { gender: "all" } } ); - { - const result = await takeSnapshot(); - expect(result.loading).toBe(true); - expect(result.networkStatus).toBe(NetworkStatus.loading); - expect(result.data).toBe(undefined); - } - { - const result = await takeSnapshot(); - expect(result.loading).toBe(false); - expect(result.networkStatus).toBe(NetworkStatus.ready); - expect(result.data).toEqual({ + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: { gender: "all" }, + }); + + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: { people: peopleData.map(({ gender, ...person }) => person), - }); - } + }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: { gender: "all" }, + }); await rerender({ gender: "female" }); - { - const result = await takeSnapshot(); - expect(result.loading).toBe(true); - expect(result.networkStatus).toBe(NetworkStatus.setVariables); - expect(result.data).toBe(undefined); - } + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.setVariables, + previousData: { + people: peopleData.map(({ gender, ...person }) => person), + }, + variables: { gender: "female" }, + }); - { - const result = await takeSnapshot(); - expect(result.loading).toBe(false); - expect(result.networkStatus).toBe(NetworkStatus.ready); - expect(result.data).toEqual({ + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: { people: peopleData .filter((person) => person.gender === "female") .map(({ gender, ...person }) => person), - }); - } - - await rerender({ gender: "nonbinary" }); + }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { + people: peopleData.map(({ gender, ...person }) => person), + }, + variables: { gender: "female" }, + }); - { - const result = await takeSnapshot(); - expect(result.loading).toBe(true); - expect(result.networkStatus).toBe(NetworkStatus.setVariables); - expect(result.data).toBe(undefined); - } - { - const result = await takeSnapshot(); - expect(result.loading).toBe(false); - expect(result.networkStatus).toBe(NetworkStatus.ready); - expect(result.data).toEqual({ - people: peopleData - .filter((person) => person.gender === "nonbinary") - .map(({ gender, ...person }) => person), - }); - } + await expect(takeSnapshot).not.toRerender(); }); }); @@ -9789,53 +10528,51 @@ describe("useQuery Hook", () => { link, }); - const { result } = renderHook(() => useQuery(query), { - wrapper: ({ children }) => ( - {children} - ), - }); + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( + () => useQuery(query), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); - expect(result.current.loading).toBe(true); - expect(result.current.data).toBe(undefined); + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); - await waitFor( - () => { - expect(result.current.data).toEqual({ hello: "world 1" }); - }, - { interval: 1 } - ); + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: { hello: "world 1" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); - await waitFor( - () => { - expect(result.current.data).toEqual({ hello: "world 2" }); - }, - { interval: 1 } - ); + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: { hello: "world 2" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { hello: "world 1" }, + variables: {}, + }); - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); - await waitFor( - () => { - expect(result.current.data).toEqual({ hello: "world 3" }); - }, - { interval: 1 } - ); + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: { hello: "world 3" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { hello: "world 2" }, + variables: {}, + }); }); }); @@ -9876,63 +10613,99 @@ describe("useQuery Hook", () => { {children} ); - const { result, rerender } = renderHook( - ({ canonizeResults }) => - useQuery(query, { - fetchPolicy: "cache-only", - canonizeResults, - }), - { wrapper, initialProps: { canonizeResults: false } } - ); + using _disabledAct = disableActEnvironment(); + const { takeSnapshot, getCurrentSnapshot, rerender } = + await renderHookToSnapshotStream( + ({ canonizeResults }) => + useQuery(query, { + fetchPolicy: "cache-only", + canonizeResults, + }), + { wrapper, initialProps: { canonizeResults: false } } + ); - expect(result.current.loading).toBe(false); - expect(result.current.data).toEqual({ results }); - expect(result.current.data.results.length).toBe(6); - let resultSet = new Set(result.current.data.results); - // Since canonization is not happening, the duplicate 1 results are - // returned as distinct objects. - expect(resultSet.size).toBe(6); - let values: number[] = []; - resultSet.forEach((result: any) => values.push(result.value)); - expect(values).toEqual([0, 1, 1, 2, 3, 5]); - rerender({ canonizeResults: true }); - await waitFor(() => { - results.push({ - __typename: "Result", - value: 8, - }); - // Append another element to the results array, invalidating the - // array itself, triggering another render (below). - cache.writeQuery({ - query, - overwrite: true, - data: { results }, - }); + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: { results }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, }); - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); - await waitFor( - () => { - expect(result.current.data).toEqual({ results }); - }, - { interval: 1 } - ); - expect(result.current.data.results.length).toBe(7); - resultSet = new Set(result.current.data.results); - // Since canonization is happening now, the duplicate 1 results are - // returned as identical (===) objects. - expect(resultSet.size).toBe(6); - values = []; - resultSet.forEach((result: any) => values.push(result.value)); - expect(values).toEqual([0, 1, 2, 3, 5, 8]); + { + const { data } = getCurrentSnapshot(); + const resultSet = new Set<(typeof results)[0]>(data.results); + const values = Array.from(resultSet).map((result) => result.value); + + expect(data.results.length).toBe(6); + // Since canonization is not happening, the duplicate 1 results are + // returned as distinct objects. + expect(resultSet.size).toBe(6); + expect(values).toEqual([0, 1, 1, 2, 3, 5]); + } + + await rerender({ canonizeResults: true }); + + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: { results }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { results }, + variables: {}, + }); + + // Check that canonization takes place immediately + { + const { data } = getCurrentSnapshot(); + const resultSet = new Set<(typeof results)[0]>(data.results); + const values = Array.from(resultSet).map((result) => result.value); + + expect(data.results.length).toBe(6); + // Since canonization is happening now, the duplicate 1 results are + // returned as identical (===) objects. + expect(resultSet.size).toBe(5); + expect(values).toEqual([0, 1, 2, 3, 5]); + } + + const updatedResults = [...results, { __typename: "Result", value: 8 }]; + + // Append another element to the results array, invalidating the + // array itself, triggering another render (below). + cache.writeQuery({ + query, + overwrite: true, + data: { results: updatedResults }, + }); + + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: { results: updatedResults }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { results }, + variables: {}, + }); + + { + const { data } = getCurrentSnapshot(); + const resultSet = new Set<(typeof results)[0]>(data.results); + const values = Array.from(resultSet).map((result) => result.value); + + expect(data.results.length).toBe(7); + // Since canonization is happening now, the duplicate 1 results are + // returned as identical (===) objects. + expect(resultSet.size).toBe(6); + expect(values).toEqual([0, 1, 2, 3, 5, 8]); + } + + await expect(takeSnapshot).not.toRerender(); }); }); + // TODO: Delete this test after PR review since this is a duplicate of the + // previous one describe("canonical cache results", () => { it("can be disabled via useQuery options", async () => { const cache = new InMemoryCache({ @@ -10113,21 +10886,48 @@ describe("useQuery Hook", () => { } ); - expect(result.current.a.loading).toBe(true); - expect(result.current.b.loading).toBe(true); - expect(result.current.a.data).toBe(undefined); - expect(result.current.b.data).toBe(undefined); + expect(result.current.a).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); + expect(result.current.b).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); await waitFor(() => { expect(result.current.a.loading).toBe(false); - }); - await waitFor(() => { expect(result.current.b.loading).toBe(false); }); - expect(result.current.a.data).toEqual(aData); - expect(result.current.b.data).toEqual(bData); + + expect(result.current.a).toEqualQueryResult({ + data: aData, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); + expect(result.current.b).toEqualQueryResult({ + data: bData, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); } + // TODO: Eventually move the "check" code back into these test so we can + // check them render-by-render with renderHookToSnapshotStream it("cache-first for both", () => check("cache-first", "cache-first")); it("cache-first first, cache-and-network second", () => @@ -10155,6 +10955,8 @@ describe("useQuery Hook", () => { }); describe("regression test issue #9204", () => { + // TODO: See if we can rewrite this with renderHookToSnapshotStream and + // check output of hook to ensure its a stable object it("should handle a simple query", async () => { const query = gql` { @@ -10258,14 +11060,28 @@ describe("useQuery Hook", () => { } ); - expect(result.current.loading).toBe(true); - expect(result.current.data).toBeUndefined(); + expect(result.current).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); await waitFor(() => { expect(result.current.loading).toBe(false); }); - expect(result.current.data).toEqual({ hello: "hello 1" }); + expect(result.current).toEqualQueryResult({ + data: { hello: "hello 1" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); + expect(cache.readQuery({ query })).toEqual({ hello: "hello 1" }); act(() => { @@ -10276,14 +11092,28 @@ describe("useQuery Hook", () => { setShow(true); }); - expect(result.current.loading).toBe(true); - expect(result.current.data).toBeUndefined(); - + expect(result.current).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); + await waitFor(() => { expect(result.current.loading).toBe(false); }); - expect(result.current.data).toEqual({ hello: "hello 2" }); + expect(result.current).toEqualQueryResult({ + data: { hello: "hello 2" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); + expect(cache.readQuery({ query })).toEqual({ hello: "hello 2" }); }); }); @@ -10310,14 +11140,25 @@ describe("useQuery Hook", () => { cache: new InMemoryCache(), }); - const { result } = renderHook(() => useQuery(query), { - wrapper: ({ children }) => ( - {children} - ), + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( + () => useQuery(query), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, }); - expect(result.current.loading).toBe(true); - expect(result.current.data).toBe(undefined); setTimeout(() => { link.simulateResult({ result: { @@ -10332,19 +11173,19 @@ describe("useQuery Hook", () => { }); }); - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); - expect(result.current.data).toEqual({ - greeting: { - message: "Hello world", - __typename: "Greeting", + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: { + greeting: { + message: "Hello world", + __typename: "Greeting", + }, }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, }); - expect(result.current).not.toContain({ hasNext: true }); setTimeout(() => { link.simulateResult({ @@ -10366,27 +11207,30 @@ describe("useQuery Hook", () => { }); }); - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); - await waitFor( - () => { - expect(result.current.data).toEqual({ - greeting: { - message: "Hello world", - __typename: "Greeting", - recipient: { - name: "Alice", - __typename: "Person", - }, + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: { + greeting: { + message: "Hello world", + __typename: "Greeting", + recipient: { + name: "Alice", + __typename: "Person", }, - }); + }, }, - { interval: 1 } - ); + called: true, + 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 () => { @@ -10410,14 +11254,25 @@ describe("useQuery Hook", () => { cache: new InMemoryCache(), }); - const { result } = renderHook(() => useQuery(query), { - wrapper: ({ children }) => ( - {children} - ), + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( + () => useQuery(query), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, }); - expect(result.current.loading).toBe(true); - expect(result.current.data).toBe(undefined); setTimeout(() => { link.simulateResult({ result: { @@ -10432,17 +11287,18 @@ describe("useQuery Hook", () => { }); }); - await waitFor( - () => { - expect(result.current.loading).toBe(false); + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: { + greetings: [ + { message: "Hello world", __typename: "Greeting" }, + { message: "Hello again", __typename: "Greeting" }, + ], }, - { interval: 1 } - ); - expect(result.current.data).toEqual({ - greetings: [ - { message: "Hello world", __typename: "Greeting" }, - { message: "Hello again", __typename: "Greeting" }, - ], + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, }); setTimeout(() => { @@ -10465,27 +11321,28 @@ describe("useQuery Hook", () => { }); }); - await waitFor( - () => { - expect(result.current.loading).toBe(false); + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: { + greetings: [ + { + message: "Hello world", + __typename: "Greeting", + recipient: { name: "Alice", __typename: "Person" }, + }, + { message: "Hello again", __typename: "Greeting" }, + ], }, - { interval: 1 } - ); - await waitFor( - () => { - expect(result.current.data).toEqual({ - greetings: [ - { - message: "Hello world", - __typename: "Greeting", - recipient: { name: "Alice", __typename: "Person" }, - }, - { message: "Hello again", __typename: "Greeting" }, - ], - }); + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { + greetings: [ + { message: "Hello world", __typename: "Greeting" }, + { message: "Hello again", __typename: "Greeting" }, + ], }, - { interval: 1 } - ); + variables: {}, + }); setTimeout(() => { link.simulateResult({ @@ -10507,31 +11364,38 @@ describe("useQuery Hook", () => { }); }); - await waitFor( - () => { - expect(result.current.loading).toBe(false); + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: { + greetings: [ + { + message: "Hello world", + __typename: "Greeting", + recipient: { name: "Alice", __typename: "Person" }, + }, + { + message: "Hello again", + __typename: "Greeting", + recipient: { name: "Bob", __typename: "Person" }, + }, + ], }, - { interval: 1 } - ); - await waitFor( - () => { - expect(result.current.data).toEqual({ - greetings: [ - { - message: "Hello world", - __typename: "Greeting", - recipient: { name: "Alice", __typename: "Person" }, - }, - { - message: "Hello again", - __typename: "Greeting", - recipient: { name: "Bob", __typename: "Person" }, - }, - ], - }); + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { + greetings: [ + { + message: "Hello world", + __typename: "Greeting", + recipient: { name: "Alice", __typename: "Person" }, + }, + { message: "Hello again", __typename: "Greeting" }, + ], }, - { interval: 1 } - ); + variables: {}, + }); + + await expect(takeSnapshot).not.toRerender(); }); it("should handle deferred queries in lists, merging arrays", async () => { @@ -10558,14 +11422,25 @@ describe("useQuery Hook", () => { cache: new InMemoryCache(), }); - const { result } = renderHook(() => useQuery(query), { - wrapper: ({ children }) => ( - {children} - ), + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( + () => useQuery(query), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, }); - expect(result.current.loading).toBe(true); - expect(result.current.data).toBe(undefined); setTimeout(() => { link.simulateResult({ result: { @@ -10594,37 +11469,33 @@ describe("useQuery Hook", () => { }); }); - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); - await waitFor( - () => { - expect(result.current.data).toEqual({ - allProducts: [ - { - __typename: "Product", - delivery: { - __typename: "DeliveryEstimates", - }, - id: "apollo-federation", - sku: "federation", + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: { + allProducts: [ + { + __typename: "Product", + delivery: { + __typename: "DeliveryEstimates", }, - { - __typename: "Product", - delivery: { - __typename: "DeliveryEstimates", - }, - id: "apollo-studio", - sku: "studio", + id: "apollo-federation", + sku: "federation", + }, + { + __typename: "Product", + delivery: { + __typename: "DeliveryEstimates", }, - ], - }); + id: "apollo-studio", + sku: "studio", + }, + ], }, - { interval: 1 } - ); + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); setTimeout(() => { link.simulateResult({ @@ -10652,41 +11523,56 @@ describe("useQuery Hook", () => { }); }); - await waitFor( - () => { - expect(result.current.loading).toBe(false); + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + 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", + }, + ], }, - { interval: 1 } - ); - await waitFor( - () => { - expect(result.current.data).toEqual({ - allProducts: [ - { - __typename: "Product", - delivery: { - __typename: "DeliveryEstimates", - estimatedDelivery: "6/25/2021", - fastestDelivery: "6/24/2021", - }, - id: "apollo-federation", - sku: "federation", + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { + allProducts: [ + { + __typename: "Product", + delivery: { + __typename: "DeliveryEstimates", }, - { - __typename: "Product", - delivery: { - __typename: "DeliveryEstimates", - estimatedDelivery: "6/25/2021", - fastestDelivery: "6/24/2021", - }, - id: "apollo-studio", - sku: "studio", + id: "apollo-federation", + sku: "federation", + }, + { + __typename: "Product", + delivery: { + __typename: "DeliveryEstimates", }, - ], - }); + id: "apollo-studio", + sku: "studio", + }, + ], }, - { interval: 1 } - ); + variables: {}, + }); }); it("should handle deferred queries with fetch policy no-cache", async () => { @@ -10710,7 +11596,8 @@ describe("useQuery Hook", () => { cache: new InMemoryCache(), }); - const { result } = renderHook( + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( () => useQuery(query, { fetchPolicy: "no-cache" }), { wrapper: ({ children }) => ( @@ -10719,8 +11606,15 @@ describe("useQuery Hook", () => { } ); - expect(result.current.loading).toBe(true); - expect(result.current.data).toBe(undefined); + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); + setTimeout(() => { link.simulateResult({ result: { @@ -10735,23 +11629,19 @@ describe("useQuery Hook", () => { }); }); - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); - await waitFor( - () => { - expect(result.current.data).toEqual({ - greeting: { - message: "Hello world", - __typename: "Greeting", - }, - }); + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: { + greeting: { + message: "Hello world", + __typename: "Greeting", + }, }, - { interval: 1 } - ); + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); setTimeout(() => { link.simulateResult({ @@ -10773,27 +11663,30 @@ describe("useQuery Hook", () => { }); }); - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); - await waitFor( - () => { - expect(result.current.data).toEqual({ - greeting: { - message: "Hello world", - __typename: "Greeting", - recipient: { - name: "Alice", - __typename: "Person", - }, + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: { + greeting: { + message: "Hello world", + __typename: "Greeting", + recipient: { + name: "Alice", + __typename: "Person", }, - }); + }, }, - { interval: 1 } - ); + called: true, + 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 () => { @@ -10819,14 +11712,25 @@ describe("useQuery Hook", () => { cache: new InMemoryCache(), }); - const { result } = renderHook(() => useQuery(query), { - wrapper: ({ children }) => ( - {children} - ), + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( + () => useQuery(query), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, }); - expect(result.current.loading).toBe(true); - expect(result.current.data).toBe(undefined); setTimeout(() => { link.simulateResult({ result: { @@ -10850,32 +11754,28 @@ describe("useQuery Hook", () => { }); }); - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); - await waitFor( - () => { - expect(result.current.data).toEqual({ - hero: { - heroFriends: [ - { - id: "1000", - name: "Luke Skywalker", - }, - { - id: "1003", - name: "Leia Organa", - }, - ], - name: "R2-D2", - }, - }); + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: { + hero: { + heroFriends: [ + { + id: "1000", + name: "Luke Skywalker", + }, + { + id: "1003", + name: "Leia Organa", + }, + ], + name: "R2-D2", + }, }, - { interval: 1 } - ); + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); setTimeout(() => { link.simulateResult({ @@ -10905,47 +11805,53 @@ describe("useQuery Hook", () => { }); }); - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); - await waitFor( - () => { - expect(result.current.error).toBeInstanceOf(ApolloError); - }, - { interval: 1 } - ); - await waitFor( - () => { - expect(result.current.error!.message).toBe( - "homeWorld for character with ID 1000 could not be fetched." - ); + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: { + hero: { + heroFriends: [ + { + id: "1000", + name: "Luke Skywalker", + }, + { + id: "1003", + name: "Leia Organa", + }, + ], + name: "R2-D2", + }, }, - { interval: 1 } - ); - await waitFor( - () => { - // since default error policy is "none", we do *not* return partial results - expect(result.current.data).toEqual({ - hero: { - heroFriends: [ - { - id: "1000", - name: "Luke Skywalker", - }, - { - id: "1003", - name: "Leia Organa", - }, - ], - name: "R2-D2", + error: new ApolloError({ + graphQLErrors: [ + { + message: + "homeWorld for character with ID 1000 could not be fetched.", + path: ["hero", "heroFriends", 0, "homeWorld"], }, - }); + ], + }), + called: true, + loading: false, + networkStatus: NetworkStatus.error, + previousData: { + hero: { + heroFriends: [ + { + id: "1000", + name: "Luke Skywalker", + }, + { + id: "1003", + name: "Leia Organa", + }, + ], + name: "R2-D2", + }, }, - { interval: 1 } - ); + variables: {}, + }); + + await expect(takeSnapshot).not.toRerender(); }); it('should handle deferred queries with errors returned on the incremental batched result and errorPolicy "all"', async () => { @@ -10971,7 +11877,8 @@ describe("useQuery Hook", () => { cache: new InMemoryCache(), }); - const { result } = renderHook( + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( () => useQuery(query, { errorPolicy: "all" }), { wrapper: ({ children }) => ( @@ -10980,8 +11887,15 @@ describe("useQuery Hook", () => { } ); - expect(result.current.loading).toBe(true); - expect(result.current.data).toBe(undefined); + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); + setTimeout(() => { link.simulateResult({ result: { @@ -11005,32 +11919,28 @@ describe("useQuery Hook", () => { }); }); - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); - await waitFor( - () => { - expect(result.current.data).toEqual({ - hero: { - heroFriends: [ - { - id: "1000", - name: "Luke Skywalker", - }, - { - id: "1003", - name: "Leia Organa", - }, - ], - name: "R2-D2", - }, - }); + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: { + hero: { + name: "R2-D2", + heroFriends: [ + { + id: "1000", + name: "Luke Skywalker", + }, + { + id: "1003", + name: "Leia Organa", + }, + ], + }, }, - { interval: 1 } - ); + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); setTimeout(() => { link.simulateResult({ @@ -11053,82 +11963,81 @@ describe("useQuery Hook", () => { }, }, { - path: ["hero", "heroFriends", 1], - data: { - homeWorld: "Alderaan", - }, - extensions: { - thing1: "foo", - thing2: "bar", - }, + path: ["hero", "heroFriends", 1], + data: { + homeWorld: "Alderaan", + }, + extensions: { + thing1: "foo", + thing2: "bar", + }, + }, + ], + hasNext: false, + }, + }); + }); + + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + 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", + }, + }, + error: new ApolloError({ + graphQLErrors: [ + { + message: + "homeWorld for character with ID 1000 could not be fetched.", + path: ["hero", "heroFriends", 0, "homeWorld"], + }, + ], + }), + errors: [ + { + message: + "homeWorld for character with ID 1000 could not be fetched.", + path: ["hero", "heroFriends", 0, "homeWorld"], + }, + ], + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { + hero: { + heroFriends: [ + { + id: "1000", + name: "Luke Skywalker", + }, + { + id: "1003", + name: "Leia Organa", }, ], - hasNext: false, + name: "R2-D2", }, - }); + }, + variables: {}, }); - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); - await waitFor( - () => { - // @ts-ignore - expect(result.current.label).toBe(undefined); - }, - { interval: 1 } - ); - await waitFor( - () => { - // @ts-ignore - expect(result.current.extensions).toBe(undefined); - }, - { interval: 1 } - ); - await waitFor( - () => { - expect(result.current.error).toBeInstanceOf(ApolloError); - }, - { interval: 1 } - ); - await waitFor( - () => { - expect(result.current.error!.message).toBe( - "homeWorld for character with ID 1000 could not be fetched." - ); - }, - { interval: 1 } - ); - await waitFor( - () => { - // since default error policy is "all", we *do* return partial results - expect(result.current.data).toEqual({ - 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", - }, - }); - }, - { interval: 1 } - ); + 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 () => { @@ -11160,7 +12069,8 @@ describe("useQuery Hook", () => { }, }); - const { result } = renderHook( + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( () => useQuery(query, { fetchPolicy: "cache-and-network" }), { wrapper: ({ children }) => ( @@ -11169,14 +12079,19 @@ describe("useQuery Hook", () => { } ); - expect(result.current.loading).toBe(true); - expect(result.current.networkStatus).toBe(NetworkStatus.loading); - expect(result.current.data).toEqual({ - greeting: { - message: "Hello cached", - __typename: "Greeting", - recipient: { __typename: "Person", name: "Cached Alice" }, + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: { + greeting: { + __typename: "Greeting", + message: "Hello cached", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, }, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, }); link.simulateResult({ @@ -11188,30 +12103,26 @@ describe("useQuery Hook", () => { }, }); - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); - await waitFor( - () => { - expect(result.current.networkStatus).toBe(NetworkStatus.ready); + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: { + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, }, - { interval: 1 } - ); - await waitFor( - () => { - expect(result.current.data).toEqual({ - greeting: { - __typename: "Greeting", - message: "Hello world", - recipient: { __typename: "Person", name: "Cached Alice" }, - }, - }); + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { + greeting: { + __typename: "Greeting", + message: "Hello cached", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, }, - { interval: 1 } - ); + variables: {}, + }); link.simulateResult({ result: { @@ -11228,30 +12139,28 @@ describe("useQuery Hook", () => { }, }); - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); - await waitFor( - () => { - expect(result.current.networkStatus).toBe(NetworkStatus.ready); + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: { + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Alice" }, + }, }, - { interval: 1 } - ); - await waitFor( - () => { - expect(result.current.data).toEqual({ - greeting: { - __typename: "Greeting", - message: "Hello world", - recipient: { __typename: "Person", name: "Alice" }, - }, - }); + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, }, - { interval: 1 } - ); + 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 () => { @@ -11287,7 +12196,8 @@ describe("useQuery Hook", () => { }); } - const { result } = renderHook( + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( () => useQuery(query, { fetchPolicy: "cache-first", @@ -11300,13 +12210,18 @@ describe("useQuery Hook", () => { } ); - expect(result.current.loading).toBe(true); - expect(result.current.networkStatus).toBe(NetworkStatus.loading); - expect(result.current.data).toEqual({ - greeting: { - __typename: "Greeting", - recipient: { __typename: "Person", name: "Cached Alice" }, + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: { + greeting: { + __typename: "Greeting", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, }, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, }); link.simulateResult({ @@ -11318,30 +12233,25 @@ describe("useQuery Hook", () => { }, }); - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); - await waitFor( - () => { - expect(result.current.networkStatus).toBe(NetworkStatus.ready); + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: { + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, }, - { interval: 1 } - ); - await waitFor( - () => { - expect(result.current.data).toEqual({ - greeting: { - __typename: "Greeting", - message: "Hello world", - recipient: { __typename: "Person", name: "Cached Alice" }, - }, - }); + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { + greeting: { + __typename: "Greeting", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, }, - { interval: 1 } - ); + variables: {}, + }); link.simulateResult({ result: { @@ -11358,30 +12268,28 @@ describe("useQuery Hook", () => { }, }); - await waitFor( - () => { - expect(result.current.loading).toBe(false); - }, - { interval: 1 } - ); - await waitFor( - () => { - expect(result.current.networkStatus).toBe(NetworkStatus.ready); + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: { + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Alice" }, + }, }, - { interval: 1 } - ); - await waitFor( - () => { - expect(result.current.data).toEqual({ - greeting: { - __typename: "Greeting", - message: "Hello world", - recipient: { __typename: "Person", name: "Alice" }, - }, - }); + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, }, - { interval: 1 } - ); + variables: {}, + }); + + await expect(takeSnapshot).not.toRerender(); }); }); @@ -11495,26 +12403,31 @@ describe("useQuery Hook", () => { await wait(10); expect(requests).toBe(1); - { - const result = await takeSnapshot(); - expect(result.loading).toBe(true); - expect(result.data).toBeUndefined(); - } + + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); await client.clearStore(); - { - const result = await takeSnapshot(); - expect(result.loading).toBe(false); - expect(result.data).toBeUndefined(); - expect(result.error).toEqual( - new ApolloError({ - networkError: new InvariantError( - "Store reset while query was in flight (not completed in link chain)" - ), - }) - ); - } + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: undefined, + error: new ApolloError({ + networkError: new InvariantError( + "Store reset while query was in flight (not completed in link chain)" + ), + }), + called: true, + loading: false, + networkStatus: NetworkStatus.error, + previousData: undefined, + variables: {}, + }); link.simulateResult({ result: { data: { hello: "Greetings" } } }, true); await expect(takeSnapshot).not.toRerender({ timeout: 50 }); @@ -11555,46 +12468,50 @@ describe("useQuery Hook", () => { {children} ), } - ); - - { - const { loading, data, error } = await takeSnapshot(); - - expect(loading).toBe(true); - expect(data).toBeUndefined(); - expect(error).toBeUndefined(); - } + ); - { - const { loading, data, error } = await takeSnapshot(); + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); - expect(loading).toBe(false); - expect(data).toBeUndefined(); - expect(error).toEqual(new ApolloError({ graphQLErrors: [graphQLError] })); - } + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: undefined, + error: new ApolloError({ graphQLErrors: [graphQLError] }), + called: true, + loading: false, + networkStatus: NetworkStatus.error, + previousData: undefined, + variables: {}, + }); const { refetch } = getCurrentSnapshot(); refetch().catch(() => {}); refetch().catch(() => {}); - { - const { loading, networkStatus, data, error } = await takeSnapshot(); - - expect(loading).toBe(true); - expect(data).toBeUndefined(); - expect(networkStatus).toBe(NetworkStatus.refetch); - expect(error).toBeUndefined(); - } - - { - const { loading, networkStatus, data, error } = await takeSnapshot(); + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.refetch, + previousData: undefined, + variables: {}, + }); - expect(loading).toBe(false); - expect(data).toBeUndefined(); - expect(networkStatus).toBe(NetworkStatus.error); - expect(error).toEqual(new ApolloError({ graphQLErrors: [graphQLError] })); - } + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: undefined, + error: new ApolloError({ graphQLErrors: [graphQLError] }), + called: true, + loading: false, + networkStatus: NetworkStatus.error, + previousData: undefined, + variables: {}, + }); await expect(takeSnapshot).not.toRerender({ timeout: 200 }); }); @@ -11602,6 +12519,7 @@ describe("useQuery Hook", () => { describe("data masking", () => { it("masks queries when dataMasking is `true`", async () => { type UserFieldsFragment = { + __typename: "User"; age: number; } & { " $fragmentName"?: "UserFieldsFragment" }; @@ -11613,7 +12531,7 @@ describe("useQuery Hook", () => { } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; } - const query: MaskedDocumentNode = gql` + const query: TypedDocumentNode> = gql` query MaskedQuery { currentUser { id @@ -11649,16 +12567,13 @@ describe("useQuery Hook", () => { link: new MockLink(mocks), }); - const renderStream = createRenderStream({ - initialSnapshot: { - result: null as QueryResult, never> | null, - }, - }); + const renderStream = + createRenderStream>>(); function App() { const result = useQuery(query); - renderStream.replaceSnapshot({ result }); + renderStream.replaceSnapshot(result); return null; } @@ -11673,27 +12588,41 @@ describe("useQuery Hook", () => { { const { snapshot } = await renderStream.takeRender(); - expect(snapshot.result?.loading).toBe(true); + expect(snapshot).toEqualQueryResult({ + data: undefined, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, + }); } { const { snapshot } = await renderStream.takeRender(); - const { result } = snapshot; - expect(result?.loading).toBe(false); - expect(result?.data).toEqual({ - currentUser: { - __typename: "User", - id: 1, - name: "Test User", + expect(snapshot).toEqualQueryResult({ + data: { + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + }, }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, }); - expect(result?.previousData).toBeUndefined(); } + + await expect(renderStream).not.toRerender(); }); it("does not mask query when dataMasking is `false`", async () => { type UserFieldsFragment = { + __typename: "User"; age: number; } & { " $fragmentName"?: "UserFieldsFragment" }; @@ -11705,7 +12634,11 @@ describe("useQuery Hook", () => { } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; } - const query: TypedDocumentNode = gql` + // We have to use Unmasked here since the default is to preserve types + const query: TypedDocumentNode< + Unmasked, + Record + > = gql` query MaskedQuery { currentUser { id @@ -11741,16 +12674,15 @@ describe("useQuery Hook", () => { link: new MockLink(mocks), }); - const renderStream = createRenderStream({ - initialSnapshot: { - result: null as QueryResult | null, - }, - }); + const renderStream = + createRenderStream< + QueryResult, Record> + >(); function App() { const result = useQuery(query); - renderStream.replaceSnapshot({ result }); + renderStream.replaceSnapshot(result); return null; } @@ -11767,19 +12699,28 @@ describe("useQuery Hook", () => { const { snapshot } = await renderStream.takeRender(); - expect(snapshot.result?.data).toEqual({ - currentUser: { - __typename: "User", - id: 1, - name: "Test User", - age: 30, + expect(snapshot).toEqualQueryResult({ + data: { + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + age: 30, + }, }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, }); - expect(snapshot.result?.previousData).toBeUndefined(); + + await expect(renderStream).not.toRerender(); }); it("does not mask query by default", async () => { type UserFieldsFragment = { + __typename: "User"; age: number; } & { " $fragmentName"?: "UserFieldsFragment" }; @@ -11791,7 +12732,10 @@ describe("useQuery Hook", () => { } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; } - const query: TypedDocumentNode = gql` + const query: TypedDocumentNode< + Unmasked, + Record + > = gql` query MaskedQuery { currentUser { id @@ -11826,16 +12770,15 @@ describe("useQuery Hook", () => { link: new MockLink(mocks), }); - const renderStream = createRenderStream({ - initialSnapshot: { - result: null as QueryResult | null, - }, - }); + const renderStream = + createRenderStream< + QueryResult, Record> + >(); function App() { const result = useQuery(query); - renderStream.replaceSnapshot({ result }); + renderStream.replaceSnapshot(result); return null; } @@ -11852,19 +12795,26 @@ describe("useQuery Hook", () => { const { snapshot } = await renderStream.takeRender(); - expect(snapshot.result?.data).toEqual({ - currentUser: { - __typename: "User", - id: 1, - name: "Test User", - age: 30, + expect(snapshot).toEqualQueryResult({ + data: { + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + age: 30, + }, }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, }); - expect(snapshot.result?.previousData).toBeUndefined(); }); it("masks queries updated by the cache", async () => { type UserFieldsFragment = { + __typename: "User"; age: number; } & { " $fragmentName"?: "UserFieldsFragment" }; @@ -11876,7 +12826,7 @@ describe("useQuery Hook", () => { } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; } - const query: MaskedDocumentNode = gql` + const query: TypedDocumentNode> = gql` query MaskedQuery { currentUser { id @@ -11912,16 +12862,13 @@ describe("useQuery Hook", () => { link: new MockLink(mocks), }); - const renderStream = createRenderStream({ - initialSnapshot: { - result: null as QueryResult, never> | null, - }, - }); + const renderStream = + createRenderStream>>(); function App() { const result = useQuery(query); - renderStream.replaceSnapshot({ result }); + renderStream.replaceSnapshot(result); return null; } @@ -11939,14 +12886,20 @@ describe("useQuery Hook", () => { { const { snapshot } = await renderStream.takeRender(); - expect(snapshot.result?.data).toEqual({ - currentUser: { - __typename: "User", - id: 1, - name: "Test User", + expect(snapshot).toEqualQueryResult({ + data: { + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + }, }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, }); - expect(snapshot.result?.previousData).toBeUndefined(); } client.writeQuery({ @@ -11964,21 +12917,32 @@ describe("useQuery Hook", () => { { const { snapshot } = await renderStream.takeRender(); - expect(snapshot.result?.data).toEqual({ - currentUser: { - __typename: "User", - id: 1, - name: "Test User (updated)", + expect(snapshot).toEqualQueryResult({ + data: { + currentUser: { + __typename: "User", + id: 1, + name: "Test User (updated)", + }, }, - }); - expect(snapshot.result?.previousData).toEqual({ - currentUser: { __typename: "User", id: 1, name: "Test User" }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + }, + }, + variables: {}, }); } }); it("does not rerender when updating field in named fragment", async () => { type UserFieldsFragment = { + __typename: "User"; age: number; } & { " $fragmentName"?: "UserFieldsFragment" }; @@ -11990,7 +12954,7 @@ describe("useQuery Hook", () => { } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; } - const query: MaskedDocumentNode = gql` + const query: TypedDocumentNode> = gql` query MaskedQuery { currentUser { id @@ -12026,16 +12990,13 @@ describe("useQuery Hook", () => { link: new MockLink(mocks), }); - const renderStream = createRenderStream({ - initialSnapshot: { - result: null as QueryResult, never> | null, - }, - }); + const renderStream = + createRenderStream>>(); function App() { const result = useQuery(query); - renderStream.replaceSnapshot({ result }); + renderStream.replaceSnapshot(result); return null; } @@ -12053,14 +13014,20 @@ describe("useQuery Hook", () => { { const { snapshot } = await renderStream.takeRender(); - expect(snapshot.result?.data).toEqual({ - currentUser: { - __typename: "User", - id: 1, - name: "Test User", + expect(snapshot).toEqualQueryResult({ + data: { + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + }, }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, }); - expect(snapshot.result?.previousData).toBeUndefined(); } client.writeQuery({ @@ -12091,6 +13058,7 @@ describe("useQuery Hook", () => { "masks result from cache when using with %s fetch policy", async (fetchPolicy) => { type UserFieldsFragment = { + __typename: "User"; age: number; } & { " $fragmentName"?: "UserFieldsFragment" }; @@ -12102,7 +13070,7 @@ describe("useQuery Hook", () => { } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; } - const query: MaskedDocumentNode = gql` + const query: TypedDocumentNode> = gql` query MaskedQuery { currentUser { id @@ -12150,16 +13118,13 @@ describe("useQuery Hook", () => { }, }); - const renderStream = createRenderStream({ - initialSnapshot: { - result: null as QueryResult, never> | null, - }, - }); + const renderStream = + createRenderStream>>(); function App() { const result = useQuery(query, { fetchPolicy }); - renderStream.replaceSnapshot({ result }); + renderStream.replaceSnapshot(result); return null; } @@ -12173,19 +13138,26 @@ describe("useQuery Hook", () => { const { snapshot } = await renderStream.takeRender(); - expect(snapshot.result?.data).toEqual({ - currentUser: { - __typename: "User", - id: 1, - name: "Test User", + expect(snapshot).toEqualQueryResult({ + data: { + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + }, }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, }); - expect(snapshot.result?.previousData).toBeUndefined(); } ); it("masks cache and network result when using cache-and-network fetch policy", async () => { type UserFieldsFragment = { + __typename: "User"; age: number; } & { " $fragmentName"?: "UserFieldsFragment" }; @@ -12197,7 +13169,7 @@ describe("useQuery Hook", () => { } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; } - const query: MaskedDocumentNode = gql` + const query: TypedDocumentNode> = gql` query MaskedQuery { currentUser { id @@ -12246,16 +13218,13 @@ describe("useQuery Hook", () => { }, }); - const renderStream = createRenderStream({ - initialSnapshot: { - result: null as QueryResult, never> | null, - }, - }); + const renderStream = + createRenderStream>>(); function App() { const result = useQuery(query, { fetchPolicy: "cache-and-network" }); - renderStream.replaceSnapshot({ result }); + renderStream.replaceSnapshot(result); return null; } @@ -12270,32 +13239,44 @@ describe("useQuery Hook", () => { { const { snapshot } = await renderStream.takeRender(); - expect(snapshot.result?.data).toEqual({ - currentUser: { - __typename: "User", - id: 1, - name: "Test User", + expect(snapshot).toEqualQueryResult({ + data: { + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + }, }, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, }); - expect(snapshot.result?.previousData).toBeUndefined(); } { const { snapshot } = await renderStream.takeRender(); - expect(snapshot.result?.data).toEqual({ - currentUser: { - __typename: "User", - id: 1, - name: "Test User (server)", + expect(snapshot).toEqualQueryResult({ + data: { + currentUser: { + __typename: "User", + id: 1, + name: "Test User (server)", + }, }, - }); - expect(snapshot.result?.previousData).toEqual({ - currentUser: { - __typename: "User", - id: 1, - name: "Test User", + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { + currentUser: { + __typename: "User", + id: 1, + name: "Test User", + }, }, + variables: {}, }); } }); @@ -12314,7 +13295,7 @@ describe("useQuery Hook", () => { } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; } - const query: MaskedDocumentNode = gql` + const query: TypedDocumentNode> = gql` query MaskedQuery { currentUser { id @@ -12367,16 +13348,13 @@ describe("useQuery Hook", () => { }); } - const renderStream = createRenderStream({ - initialSnapshot: { - result: null as QueryResult, never> | null, - }, - }); + const renderStream = + createRenderStream>>(); function App() { const result = useQuery(query, { returnPartialData: true }); - renderStream.replaceSnapshot({ result }); + renderStream.replaceSnapshot(result); return null; } @@ -12391,29 +13369,49 @@ describe("useQuery Hook", () => { { const { snapshot } = await renderStream.takeRender(); - expect(snapshot.result?.data).toEqual({ - currentUser: { - __typename: "User", - id: 1, - }, + expect(snapshot).toEqualQueryResult({ + data: { + currentUser: { + __typename: "User", + id: 1, + }, + } as Query, + called: true, + loading: true, + networkStatus: NetworkStatus.loading, + previousData: undefined, + variables: {}, }); } { const { snapshot } = await renderStream.takeRender(); - expect(snapshot.result?.data).toEqual({ - currentUser: { - __typename: "User", - id: 1, - name: "Test User (server)", + expect(snapshot).toEqualQueryResult({ + data: { + currentUser: { + __typename: "User", + id: 1, + name: "Test User (server)", + }, }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { + currentUser: { + __typename: "User", + id: 1, + }, + } as Query, + variables: {}, }); } }); it("masks partial data returned from data on errors with errorPolicy `all`", async () => { type UserFieldsFragment = { + __typename: "User"; age: number; } & { " $fragmentName"?: "UserFieldsFragment" }; @@ -12421,11 +13419,11 @@ describe("useQuery Hook", () => { currentUser: { __typename: "User"; id: number; - name: string; + name: string | null; } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; } - const query: MaskedDocumentNode = gql` + const query: TypedDocumentNode> = gql` query MaskedQuery { currentUser { id @@ -12463,16 +13461,13 @@ describe("useQuery Hook", () => { link: new MockLink(mocks), }); - const renderStream = createRenderStream({ - initialSnapshot: { - result: null as QueryResult, never> | null, - }, - }); + const renderStream = + createRenderStream>>(); function App() { const result = useQuery(query, { errorPolicy: "all" }); - renderStream.replaceSnapshot({ result }); + renderStream.replaceSnapshot(result); return null; } @@ -12489,21 +13484,25 @@ describe("useQuery Hook", () => { { const { snapshot } = await renderStream.takeRender(); - const { result } = snapshot; - expect(result?.data).toEqual({ - currentUser: { - __typename: "User", - id: 1, - name: null, + expect(snapshot).toEqualQueryResult({ + data: { + currentUser: { + __typename: "User", + id: 1, + name: null, + }, }, - }); - - expect(result?.error).toEqual( - new ApolloError({ + error: new ApolloError({ graphQLErrors: [new GraphQLError("Couldn't get name")], - }) - ); + }), + errors: [{ message: "Couldn't get name" }], + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: {}, + }); } }); }); diff --git a/src/testing/internal/scenarios/index.ts b/src/testing/internal/scenarios/index.ts index 5577b16623b..d8b8c6c35cf 100644 --- a/src/testing/internal/scenarios/index.ts +++ b/src/testing/internal/scenarios/index.ts @@ -139,6 +139,7 @@ export function addDelayToMocks[]>( } interface Letter { + __typename: "Letter"; letter: string; position: number; } From eeb5f3c277beedafca282a7f2f9fa7d624f314a6 Mon Sep 17 00:00:00 2001 From: Edward Huang Date: Fri, 17 Jan 2025 10:04:44 -0800 Subject: [PATCH 9/9] docs: set Codesandbox button width (#12279) --- .../development-testing/schema-driven-testing.mdx | 10 +++++++++- docs/source/development-testing/testing.mdx | 10 +++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/docs/source/development-testing/schema-driven-testing.mdx b/docs/source/development-testing/schema-driven-testing.mdx index f69294c0ff6..aa07ea39d65 100644 --- a/docs/source/development-testing/schema-driven-testing.mdx +++ b/docs/source/development-testing/schema-driven-testing.mdx @@ -557,4 +557,12 @@ Please see [this issue](https://github.com/graphql/graphql-js/issues/4062) to tr For a working example that demonstrates how to use both Testing Library and Mock Service Worker to write integration tests with `createTestSchema`, check out this project on CodeSandbox: -[![Edit Testing React Components](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/p/sandbox/github/apollographql/docs-examples/tree/main/apollo-client/v3/schema-driven-testing) + + Edit Testing React Components + diff --git a/docs/source/development-testing/testing.mdx b/docs/source/development-testing/testing.mdx index d32222cb6ab..3453f2559f4 100644 --- a/docs/source/development-testing/testing.mdx +++ b/docs/source/development-testing/testing.mdx @@ -514,4 +514,12 @@ This is necessary because otherwise, the `MockedProvider` component doesn't know For a working example that demonstrates how to test components, check out this project on CodeSandbox: -[![Edit Testing React Components](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/github/apollographql/docs-examples/tree/main/apollo-client/v3/testing-react-components?file=/src/dog.test.js) + + Edit Testing React Components +