diff --git a/.api-reports/api-report-cache.api.md b/.api-reports/api-report-cache.api.md index 72f792334a2..908176a26cf 100644 --- a/.api-reports/api-report-cache.api.md +++ b/.api-reports/api-report-cache.api.md @@ -236,8 +236,12 @@ type CombineIntersection = Exclude>; +// Warning: (ae-forgotten-export) The symbol "IsAny" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "Exact" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "RemoveIndexSignature" needs to be exported by the entry point index.d.ts +// // @public (undocumented) -type ContainsFragmentsRefs = TData extends object ? " $fragmentRefs" extends keyof TData ? true : ContainsFragmentsRefs : false; +type ContainsFragmentsRefs = true extends (IsAny) ? false : TData extends object ? Exact extends Seen ? false : " $fragmentRefs" extends keyof RemoveIndexSignature ? true : ContainsFragmentsRefs> : false; // @public (undocumented) export function createFragmentRegistry(...fragments: DocumentNode[]): FragmentRegistryAPI; @@ -462,6 +466,9 @@ export namespace EntityStore { } } +// @public (undocumented) +type Exact = (x: T) => T; + // @public type ExtractByMatchingTypeNames(value: T): ReactiveVar; -// Warning: (ae-forgotten-export) The symbol "IsAny" 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 "DataMasking" 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 @@ -1033,6 +1039,11 @@ export interface Reference { // @public (undocumented) type RemoveFragmentName = T extends any ? Omit : T; +// @public (undocumented) +type RemoveIndexSignature = { + [K in keyof T as string extends K ? never : number extends K ? never : symbol extends K ? never : K]: T[K]; +}; + // @public (undocumented) type RemoveMaskedMarker = Omit; @@ -1109,7 +1120,7 @@ type Unmasked = true extends IsAny ? TData : TData extends object // @public (undocumented) type UnwrapFragmentRefs = true extends IsAny ? TData : TData extends any ? string extends keyof TData ? TData : keyof TData extends never ? TData : TData extends { " $fragmentRefs"?: infer FragmentRefs; -} ? UnwrapFragmentRefs | RemoveFragmentName[keyof NonNullable]>>>> : TData extends Array ? Array> : TData extends object ? { +} ? UnwrapFragmentRefs | RemoveFragmentName[keyof NonNullable]>>>> : TData extends object ? { [K in keyof TData]: UnwrapFragmentRefs; } : TData : never; diff --git a/.api-reports/api-report-core.api.md b/.api-reports/api-report-core.api.md index cb31ddd4692..81836428979 100644 --- a/.api-reports/api-report-core.api.md +++ b/.api-reports/api-report-core.api.md @@ -479,8 +479,12 @@ type ConcastSourcesIterable = Iterable>; // @public (undocumented) export const concat: typeof ApolloLink.concat; +// Warning: (ae-forgotten-export) The symbol "IsAny" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "Exact" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "RemoveIndexSignature" needs to be exported by the entry point index.d.ts +// // @public (undocumented) -type ContainsFragmentsRefs = TData extends object ? " $fragmentRefs" extends keyof TData ? true : ContainsFragmentsRefs : false; +type ContainsFragmentsRefs = true extends (IsAny) ? false : TData extends object ? Exact extends Seen ? false : " $fragmentRefs" extends keyof RemoveIndexSignature ? true : ContainsFragmentsRefs> : false; // @public (undocumented) export const createHttpLink: (linkOptions?: HttpOptions) => ApolloLink; @@ -787,6 +791,9 @@ namespace EntityStore { // @public export type ErrorPolicy = "none" | "ignore" | "all"; +// @public (undocumented) +type Exact = (x: T) => T; + // @public (undocumented) export const execute: typeof ApolloLink.execute; @@ -1401,7 +1408,6 @@ interface MaskOperationOptions { // @public (undocumented) type MaybeAsync = T | PromiseLike; -// Warning: (ae-forgotten-export) The symbol "IsAny" 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 // @@ -2170,6 +2176,11 @@ export type RefetchWritePolicy = "merge" | "overwrite"; // @public (undocumented) type RemoveFragmentName = T extends any ? Omit : T; +// @public (undocumented) +type RemoveIndexSignature = { + [K in keyof T as string extends K ? never : number extends K ? never : symbol extends K ? never : K]: T[K]; +}; + // @public (undocumented) type RemoveMaskedMarker = Omit; @@ -2406,7 +2417,7 @@ export type Unmasked = true extends IsAny ? TData : TData extends // @public (undocumented) type UnwrapFragmentRefs = true extends IsAny ? TData : TData extends any ? string extends keyof TData ? TData : keyof TData extends never ? TData : TData extends { " $fragmentRefs"?: infer FragmentRefs; -} ? UnwrapFragmentRefs | RemoveFragmentName[keyof NonNullable]>>>> : TData extends Array ? Array> : TData extends object ? { +} ? UnwrapFragmentRefs | RemoveFragmentName[keyof NonNullable]>>>> : TData extends object ? { [K in keyof TData]: UnwrapFragmentRefs; } : TData : never; diff --git a/.api-reports/api-report-masking.api.md b/.api-reports/api-report-masking.api.md index 0d22ad2fe1b..fffd2efb2e1 100644 --- a/.api-reports/api-report-masking.api.md +++ b/.api-reports/api-report-masking.api.md @@ -220,8 +220,12 @@ type CombineIntersection = Exclude>; +// Warning: (ae-forgotten-export) The symbol "IsAny" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "Exact" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "RemoveIndexSignature" needs to be exported by the entry point index.d.ts +// // @public (undocumented) -type ContainsFragmentsRefs = TData extends object ? " $fragmentRefs" extends keyof TData ? true : ContainsFragmentsRefs : false; +type ContainsFragmentsRefs = true extends (IsAny) ? false : TData extends object ? Exact extends Seen ? false : " $fragmentRefs" extends keyof RemoveIndexSignature ? true : ContainsFragmentsRefs> : false; // @public (undocumented) export interface DataMasking { @@ -357,6 +361,9 @@ export const disableWarningsSlot: { // @public (undocumented) type DistributedRequiredExclude = T extends any ? Required extends Required ? Required extends Required ? never : T : T : T; +// @public (undocumented) +type Exact = (x: T) => T; + // @public type ExtractByMatchingTypeNames(data: TData, document: TypedDocume // @internal (undocumented) export function maskOperation(data: TData, document: DocumentNode | TypedDocumentNode, cache: ApolloCache): TData; -// Warning: (ae-forgotten-export) The symbol "IsAny" 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 // @@ -558,6 +564,11 @@ interface Reference { // @public (undocumented) type RemoveFragmentName = T extends any ? Omit : T; +// @public (undocumented) +type RemoveIndexSignature = { + [K in keyof T as string extends K ? never : number extends K ? never : symbol extends K ? never : K]: T[K]; +}; + // @public (undocumented) type RemoveMaskedMarker = Omit; @@ -608,7 +619,7 @@ export type Unmasked = true extends IsAny ? TData : TData extends // @public (undocumented) type UnwrapFragmentRefs = true extends IsAny ? TData : TData extends any ? string extends keyof TData ? TData : keyof TData extends never ? TData : TData extends { " $fragmentRefs"?: infer FragmentRefs; -} ? UnwrapFragmentRefs | RemoveFragmentName[keyof NonNullable]>>>> : TData extends Array ? Array> : TData extends object ? { +} ? UnwrapFragmentRefs | RemoveFragmentName[keyof NonNullable]>>>> : TData extends object ? { [K in keyof TData]: UnwrapFragmentRefs; } : TData : never; diff --git a/.api-reports/api-report-react.api.md b/.api-reports/api-report-react.api.md index 8373acf4521..16e002d9106 100644 --- a/.api-reports/api-report-react.api.md +++ b/.api-reports/api-report-react.api.md @@ -577,8 +577,12 @@ class Concast extends Observable { // @public (undocumented) type ConcastSourcesIterable = Iterable>; +// Warning: (ae-forgotten-export) The symbol "IsAny" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "Exact" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "RemoveIndexSignature" needs to be exported by the entry point index.d.ts +// // @public (undocumented) -type ContainsFragmentsRefs = TData extends object ? " $fragmentRefs" extends keyof TData ? true : ContainsFragmentsRefs : false; +type ContainsFragmentsRefs = true extends (IsAny) ? false : TData extends object ? Exact extends Seen ? false : " $fragmentRefs" extends keyof RemoveIndexSignature ? true : ContainsFragmentsRefs> : false; // @public (undocumented) export interface Context extends Record { @@ -776,6 +780,9 @@ export { DocumentType_2 as DocumentType } // @public type ErrorPolicy = "none" | "ignore" | "all"; +// @public (undocumented) +type Exact = (x: T) => T; + // Warning: (ae-forgotten-export) The symbol "ExecutionPatchResultBase" needs to be exported by the entry point index.d.ts // // @public (undocumented) @@ -1146,7 +1153,6 @@ interface MaskOperationOptions { // @public (undocumented) type MaybeAsync = T | PromiseLike; -// Warning: (ae-forgotten-export) The symbol "IsAny" 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 "DataMasking" 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 @@ -1955,6 +1961,11 @@ type RefetchWritePolicy = "merge" | "overwrite"; // @public (undocumented) type RemoveFragmentName = T extends any ? Omit : T; +// @public (undocumented) +type RemoveIndexSignature = { + [K in keyof T as string extends K ? never : number extends K ? never : symbol extends K ? never : K]: T[K]; +}; + // @public (undocumented) type RemoveMaskedMarker = Omit; @@ -2202,7 +2213,7 @@ type Unmasked = true extends IsAny ? TData : TData extends object // @public (undocumented) type UnwrapFragmentRefs = true extends IsAny ? TData : TData extends any ? string extends keyof TData ? TData : keyof TData extends never ? TData : TData extends { " $fragmentRefs"?: infer FragmentRefs; -} ? UnwrapFragmentRefs | RemoveFragmentName[keyof NonNullable]>>>> : TData extends Array ? Array> : TData extends object ? { +} ? UnwrapFragmentRefs | RemoveFragmentName[keyof NonNullable]>>>> : TData extends object ? { [K in keyof TData]: UnwrapFragmentRefs; } : TData : never; diff --git a/.api-reports/api-report-react_components.api.md b/.api-reports/api-report-react_components.api.md index c9ea4d9100a..051470cb154 100644 --- a/.api-reports/api-report-react_components.api.md +++ b/.api-reports/api-report-react_components.api.md @@ -523,8 +523,12 @@ class Concast extends Observable { // @public (undocumented) type ConcastSourcesIterable = Iterable>; +// Warning: (ae-forgotten-export) The symbol "IsAny" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "Exact" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "RemoveIndexSignature" needs to be exported by the entry point index.d.ts +// // @public (undocumented) -type ContainsFragmentsRefs = TData extends object ? " $fragmentRefs" extends keyof TData ? true : ContainsFragmentsRefs : false; +type ContainsFragmentsRefs = true extends (IsAny) ? false : TData extends object ? Exact extends Seen ? false : " $fragmentRefs" extends keyof RemoveIndexSignature ? true : ContainsFragmentsRefs> : false; // @public (undocumented) interface DataMasking { @@ -708,6 +712,9 @@ interface DocumentTransformOptions { // @public type ErrorPolicy = "none" | "ignore" | "all"; +// @public (undocumented) +type Exact = (x: T) => T; + // Warning: (ae-forgotten-export) The symbol "ExecutionPatchResultBase" needs to be exported by the entry point index.d.ts // // @public (undocumented) @@ -1009,7 +1016,6 @@ interface MaskOperationOptions { // @public (undocumented) type MaybeAsync = T | PromiseLike; -// Warning: (ae-forgotten-export) The symbol "IsAny" 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 "DataMasking" 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 @@ -1737,6 +1743,11 @@ type RefetchWritePolicy = "merge" | "overwrite"; // @public (undocumented) type RemoveFragmentName = T extends any ? Omit : T; +// @public (undocumented) +type RemoveIndexSignature = { + [K in keyof T as string extends K ? never : number extends K ? never : symbol extends K ? never : K]: T[K]; +}; + // @public (undocumented) type RemoveMaskedMarker = Omit; @@ -1935,7 +1946,7 @@ type Unmasked = true extends IsAny ? TData : TData extends object // @public (undocumented) type UnwrapFragmentRefs = true extends IsAny ? TData : TData extends any ? string extends keyof TData ? TData : keyof TData extends never ? TData : TData extends { " $fragmentRefs"?: infer FragmentRefs; -} ? UnwrapFragmentRefs | RemoveFragmentName[keyof NonNullable]>>>> : TData extends Array ? Array> : TData extends object ? { +} ? UnwrapFragmentRefs | RemoveFragmentName[keyof NonNullable]>>>> : TData extends object ? { [K in keyof TData]: UnwrapFragmentRefs; } : TData : never; diff --git a/.api-reports/api-report-react_context.api.md b/.api-reports/api-report-react_context.api.md index c9d716e731e..703cfb4825d 100644 --- a/.api-reports/api-report-react_context.api.md +++ b/.api-reports/api-report-react_context.api.md @@ -517,8 +517,12 @@ class Concast extends Observable { // @public (undocumented) type ConcastSourcesIterable = Iterable>; +// Warning: (ae-forgotten-export) The symbol "IsAny" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "Exact" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "RemoveIndexSignature" needs to be exported by the entry point index.d.ts +// // @public (undocumented) -type ContainsFragmentsRefs = TData extends object ? " $fragmentRefs" extends keyof TData ? true : ContainsFragmentsRefs : false; +type ContainsFragmentsRefs = true extends (IsAny) ? false : TData extends object ? Exact extends Seen ? false : " $fragmentRefs" extends keyof RemoveIndexSignature ? true : ContainsFragmentsRefs> : false; // @public (undocumented) interface DataMasking { @@ -702,6 +706,9 @@ interface DocumentTransformOptions { // @public type ErrorPolicy = "none" | "ignore" | "all"; +// @public (undocumented) +type Exact = (x: T) => T; + // Warning: (ae-forgotten-export) The symbol "ExecutionPatchResultBase" needs to be exported by the entry point index.d.ts // // @public (undocumented) @@ -1006,7 +1013,6 @@ interface MaskOperationOptions { // @public (undocumented) type MaybeAsync = T | PromiseLike; -// Warning: (ae-forgotten-export) The symbol "IsAny" 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 "DataMasking" 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 @@ -1665,6 +1671,11 @@ type RefetchWritePolicy = "merge" | "overwrite"; // @public (undocumented) type RemoveFragmentName = T extends any ? Omit : T; +// @public (undocumented) +type RemoveIndexSignature = { + [K in keyof T as string extends K ? never : number extends K ? never : symbol extends K ? never : K]: T[K]; +}; + // @public (undocumented) type RemoveMaskedMarker = Omit; @@ -1855,7 +1866,7 @@ type Unmasked = true extends IsAny ? TData : TData extends object // @public (undocumented) type UnwrapFragmentRefs = true extends IsAny ? TData : TData extends any ? string extends keyof TData ? TData : keyof TData extends never ? TData : TData extends { " $fragmentRefs"?: infer FragmentRefs; -} ? UnwrapFragmentRefs | RemoveFragmentName[keyof NonNullable]>>>> : TData extends Array ? Array> : TData extends object ? { +} ? UnwrapFragmentRefs | RemoveFragmentName[keyof NonNullable]>>>> : TData extends object ? { [K in keyof TData]: UnwrapFragmentRefs; } : TData : never; diff --git a/.api-reports/api-report-react_hoc.api.md b/.api-reports/api-report-react_hoc.api.md index 7579701495f..3f867350519 100644 --- a/.api-reports/api-report-react_hoc.api.md +++ b/.api-reports/api-report-react_hoc.api.md @@ -506,8 +506,12 @@ class Concast extends Observable { // @public (undocumented) type ConcastSourcesIterable = Iterable>; +// Warning: (ae-forgotten-export) The symbol "IsAny" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "Exact" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "RemoveIndexSignature" needs to be exported by the entry point index.d.ts +// // @public (undocumented) -type ContainsFragmentsRefs = TData extends object ? " $fragmentRefs" extends keyof TData ? true : ContainsFragmentsRefs : false; +type ContainsFragmentsRefs = true extends (IsAny) ? false : TData extends object ? Exact extends Seen ? false : " $fragmentRefs" extends keyof RemoveIndexSignature ? true : ContainsFragmentsRefs> : false; // @public (undocumented) interface DataMasking { @@ -700,6 +704,9 @@ interface DocumentTransformOptions { // @public type ErrorPolicy = "none" | "ignore" | "all"; +// @public (undocumented) +type Exact = (x: T) => T; + // Warning: (ae-forgotten-export) The symbol "ExecutionPatchResultBase" needs to be exported by the entry point index.d.ts // // @public (undocumented) @@ -1013,7 +1020,6 @@ interface MaskOperationOptions { // @public (undocumented) type MaybeAsync = T | PromiseLike; -// Warning: (ae-forgotten-export) The symbol "IsAny" 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 "DataMasking" 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 @@ -1694,6 +1700,11 @@ type RefetchWritePolicy = "merge" | "overwrite"; // @public (undocumented) type RemoveFragmentName = T extends any ? Omit : T; +// @public (undocumented) +type RemoveIndexSignature = { + [K in keyof T as string extends K ? never : number extends K ? never : symbol extends K ? never : K]: T[K]; +}; + // @public (undocumented) type RemoveMaskedMarker = Omit; @@ -1859,7 +1870,7 @@ type Unmasked = true extends IsAny ? TData : TData extends object // @public (undocumented) type UnwrapFragmentRefs = true extends IsAny ? TData : TData extends any ? string extends keyof TData ? TData : keyof TData extends never ? TData : TData extends { " $fragmentRefs"?: infer FragmentRefs; -} ? UnwrapFragmentRefs | RemoveFragmentName[keyof NonNullable]>>>> : TData extends Array ? Array> : TData extends object ? { +} ? UnwrapFragmentRefs | RemoveFragmentName[keyof NonNullable]>>>> : TData extends object ? { [K in keyof TData]: UnwrapFragmentRefs; } : TData : never; diff --git a/.api-reports/api-report-react_hooks.api.md b/.api-reports/api-report-react_hooks.api.md index b639caed65d..b252b2a672f 100644 --- a/.api-reports/api-report-react_hooks.api.md +++ b/.api-reports/api-report-react_hooks.api.md @@ -546,8 +546,12 @@ class Concast extends Observable { // @public (undocumented) type ConcastSourcesIterable = Iterable>; +// Warning: (ae-forgotten-export) The symbol "IsAny" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "Exact" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "RemoveIndexSignature" needs to be exported by the entry point index.d.ts +// // @public (undocumented) -type ContainsFragmentsRefs = TData extends object ? " $fragmentRefs" extends keyof TData ? true : ContainsFragmentsRefs : false; +type ContainsFragmentsRefs = true extends (IsAny) ? false : TData extends object ? Exact extends Seen ? false : " $fragmentRefs" extends keyof RemoveIndexSignature ? true : ContainsFragmentsRefs> : false; // @public (undocumented) interface DataMasking { @@ -731,6 +735,9 @@ interface DocumentTransformOptions { // @public type ErrorPolicy = "none" | "ignore" | "all"; +// @public (undocumented) +type Exact = (x: T) => T; + // Warning: (ae-forgotten-export) The symbol "ExecutionPatchResultBase" needs to be exported by the entry point index.d.ts // // @public (undocumented) @@ -1095,7 +1102,6 @@ interface MaskOperationOptions { // @public (undocumented) type MaybeAsync = T | PromiseLike; -// Warning: (ae-forgotten-export) The symbol "IsAny" 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 "DataMasking" 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 @@ -1814,6 +1820,11 @@ type RefetchWritePolicy = "merge" | "overwrite"; // @public (undocumented) type RemoveFragmentName = T extends any ? Omit : T; +// @public (undocumented) +type RemoveIndexSignature = { + [K in keyof T as string extends K ? never : number extends K ? never : symbol extends K ? never : K]: T[K]; +}; + // @public (undocumented) type RemoveMaskedMarker = Omit; @@ -2025,7 +2036,7 @@ type Unmasked = true extends IsAny ? TData : TData extends object // @public (undocumented) type UnwrapFragmentRefs = true extends IsAny ? TData : TData extends any ? string extends keyof TData ? TData : keyof TData extends never ? TData : TData extends { " $fragmentRefs"?: infer FragmentRefs; -} ? UnwrapFragmentRefs | RemoveFragmentName[keyof NonNullable]>>>> : TData extends Array ? Array> : TData extends object ? { +} ? UnwrapFragmentRefs | RemoveFragmentName[keyof NonNullable]>>>> : TData extends object ? { [K in keyof TData]: UnwrapFragmentRefs; } : TData : never; diff --git a/.api-reports/api-report-react_internal.api.md b/.api-reports/api-report-react_internal.api.md index f5587b3de51..3e43a1dafbb 100644 --- a/.api-reports/api-report-react_internal.api.md +++ b/.api-reports/api-report-react_internal.api.md @@ -525,8 +525,12 @@ class Concast extends Observable { // @public (undocumented) type ConcastSourcesIterable = Iterable>; +// Warning: (ae-forgotten-export) The symbol "IsAny" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "Exact" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "RemoveIndexSignature" needs to be exported by the entry point index.d.ts +// // @public (undocumented) -type ContainsFragmentsRefs = TData extends object ? " $fragmentRefs" extends keyof TData ? true : ContainsFragmentsRefs : false; +type ContainsFragmentsRefs = true extends (IsAny) ? false : TData extends object ? Exact extends Seen ? false : " $fragmentRefs" extends keyof RemoveIndexSignature ? true : ContainsFragmentsRefs> : false; // Warning: (ae-forgotten-export) The symbol "PreloadQueryFunction" needs to be exported by the entry point index.d.ts // @@ -715,6 +719,9 @@ interface DocumentTransformOptions { // @public type ErrorPolicy = "none" | "ignore" | "all"; +// @public (undocumented) +type Exact = (x: T) => T; + // Warning: (ae-forgotten-export) The symbol "ExecutionPatchResultBase" needs to be exported by the entry point index.d.ts // // @public (undocumented) @@ -1105,7 +1112,6 @@ interface MaskOperationOptions { // @public (undocumented) type MaybeAsync = T | PromiseLike; -// Warning: (ae-forgotten-export) The symbol "IsAny" 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 "DataMasking" 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 @@ -1867,6 +1873,11 @@ interface RejectedPromise extends Promise { // @public (undocumented) type RemoveFragmentName = T extends any ? Omit : T; +// @public (undocumented) +type RemoveIndexSignature = { + [K in keyof T as string extends K ? never : number extends K ? never : symbol extends K ? never : K]: T[K]; +}; + // @public (undocumented) type RemoveMaskedMarker = Omit; @@ -2077,7 +2088,7 @@ type Unmasked = true extends IsAny ? TData : TData extends object // @public (undocumented) type UnwrapFragmentRefs = true extends IsAny ? TData : TData extends any ? string extends keyof TData ? TData : keyof TData extends never ? TData : TData extends { " $fragmentRefs"?: infer FragmentRefs; -} ? UnwrapFragmentRefs | RemoveFragmentName[keyof NonNullable]>>>> : TData extends Array ? Array> : TData extends object ? { +} ? UnwrapFragmentRefs | RemoveFragmentName[keyof NonNullable]>>>> : TData extends object ? { [K in keyof TData]: UnwrapFragmentRefs; } : TData : never; diff --git a/.api-reports/api-report-react_ssr.api.md b/.api-reports/api-report-react_ssr.api.md index cb40f4d1f28..d0560e817dd 100644 --- a/.api-reports/api-report-react_ssr.api.md +++ b/.api-reports/api-report-react_ssr.api.md @@ -486,8 +486,12 @@ class Concast extends Observable { // @public (undocumented) type ConcastSourcesIterable = Iterable>; +// Warning: (ae-forgotten-export) The symbol "IsAny" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "Exact" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "RemoveIndexSignature" needs to be exported by the entry point index.d.ts +// // @public (undocumented) -type ContainsFragmentsRefs = TData extends object ? " $fragmentRefs" extends keyof TData ? true : ContainsFragmentsRefs : false; +type ContainsFragmentsRefs = true extends (IsAny) ? false : TData extends object ? Exact extends Seen ? false : " $fragmentRefs" extends keyof RemoveIndexSignature ? true : ContainsFragmentsRefs> : false; // @public (undocumented) interface DataMasking { @@ -671,6 +675,9 @@ interface DocumentTransformOptions { // @public type ErrorPolicy = "none" | "ignore" | "all"; +// @public (undocumented) +type Exact = (x: T) => T; + // Warning: (ae-forgotten-export) The symbol "ExecutionPatchResultBase" needs to be exported by the entry point index.d.ts // // @public (undocumented) @@ -991,7 +998,6 @@ interface MaskOperationOptions { // @public (undocumented) type MaybeAsync = T | PromiseLike; -// Warning: (ae-forgotten-export) The symbol "IsAny" 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 "DataMasking" 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 @@ -1650,6 +1656,11 @@ type RefetchWritePolicy = "merge" | "overwrite"; // @public (undocumented) type RemoveFragmentName = T extends any ? Omit : T; +// @public (undocumented) +type RemoveIndexSignature = { + [K in keyof T as string extends K ? never : number extends K ? never : symbol extends K ? never : K]: T[K]; +}; + // @public (undocumented) type RemoveMaskedMarker = Omit; @@ -1840,7 +1851,7 @@ type Unmasked = true extends IsAny ? TData : TData extends object // @public (undocumented) type UnwrapFragmentRefs = true extends IsAny ? TData : TData extends any ? string extends keyof TData ? TData : keyof TData extends never ? TData : TData extends { " $fragmentRefs"?: infer FragmentRefs; -} ? UnwrapFragmentRefs | RemoveFragmentName[keyof NonNullable]>>>> : TData extends Array ? Array> : TData extends object ? { +} ? UnwrapFragmentRefs | RemoveFragmentName[keyof NonNullable]>>>> : TData extends object ? { [K in keyof TData]: UnwrapFragmentRefs; } : TData : never; diff --git a/.api-reports/api-report-testing.api.md b/.api-reports/api-report-testing.api.md index 00f01de37dd..0c7b64d8eee 100644 --- a/.api-reports/api-report-testing.api.md +++ b/.api-reports/api-report-testing.api.md @@ -476,8 +476,12 @@ class Concast extends Observable { // @public (undocumented) type ConcastSourcesIterable = Iterable>; +// Warning: (ae-forgotten-export) The symbol "IsAny" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "Exact" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "RemoveIndexSignature" needs to be exported by the entry point index.d.ts +// // @public (undocumented) -type ContainsFragmentsRefs = TData extends object ? " $fragmentRefs" extends keyof TData ? true : ContainsFragmentsRefs : false; +type ContainsFragmentsRefs = true extends (IsAny) ? false : TData extends object ? Exact extends Seen ? false : " $fragmentRefs" extends keyof RemoveIndexSignature ? true : ContainsFragmentsRefs> : false; // @internal (undocumented) type CovariantUnaryFunction = { @@ -672,6 +676,9 @@ interface DocumentTransformOptions { // @public type ErrorPolicy = "none" | "ignore" | "all"; +// @public (undocumented) +type Exact = (x: T) => T; + // Warning: (ae-forgotten-export) The symbol "ExecutionPatchResultBase" needs to be exported by the entry point index.d.ts // // @public (undocumented) @@ -980,7 +987,6 @@ interface MaskOperationOptions { // @public (undocumented) type MaybeAsync = T | PromiseLike; -// Warning: (ae-forgotten-export) The symbol "IsAny" 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 "DataMasking" 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 @@ -1715,6 +1721,11 @@ type RefetchWritePolicy = "merge" | "overwrite"; // @public (undocumented) type RemoveFragmentName = T extends any ? Omit : T; +// @public (undocumented) +type RemoveIndexSignature = { + [K in keyof T as string extends K ? never : number extends K ? never : symbol extends K ? never : K]: T[K]; +}; + // @public (undocumented) type RemoveMaskedMarker = Omit; @@ -1891,7 +1902,7 @@ type Unmasked = true extends IsAny ? TData : TData extends object // @public (undocumented) type UnwrapFragmentRefs = true extends IsAny ? TData : TData extends any ? string extends keyof TData ? TData : keyof TData extends never ? TData : TData extends { " $fragmentRefs"?: infer FragmentRefs; -} ? UnwrapFragmentRefs | RemoveFragmentName[keyof NonNullable]>>>> : TData extends Array ? Array> : TData extends object ? { +} ? UnwrapFragmentRefs | RemoveFragmentName[keyof NonNullable]>>>> : TData extends object ? { [K in keyof TData]: UnwrapFragmentRefs; } : TData : never; diff --git a/.api-reports/api-report-testing_core.api.md b/.api-reports/api-report-testing_core.api.md index 1606498c68a..d46a9f55d1f 100644 --- a/.api-reports/api-report-testing_core.api.md +++ b/.api-reports/api-report-testing_core.api.md @@ -475,8 +475,12 @@ class Concast extends Observable { // @public (undocumented) type ConcastSourcesIterable = Iterable>; +// Warning: (ae-forgotten-export) The symbol "IsAny" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "Exact" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "RemoveIndexSignature" needs to be exported by the entry point index.d.ts +// // @public (undocumented) -type ContainsFragmentsRefs = TData extends object ? " $fragmentRefs" extends keyof TData ? true : ContainsFragmentsRefs : false; +type ContainsFragmentsRefs = true extends (IsAny) ? false : TData extends object ? Exact extends Seen ? false : " $fragmentRefs" extends keyof RemoveIndexSignature ? true : ContainsFragmentsRefs> : false; // @internal (undocumented) type CovariantUnaryFunction = { @@ -671,6 +675,9 @@ interface DocumentTransformOptions { // @public type ErrorPolicy = "none" | "ignore" | "all"; +// @public (undocumented) +type Exact = (x: T) => T; + // Warning: (ae-forgotten-export) The symbol "ExecutionPatchResultBase" needs to be exported by the entry point index.d.ts // // @public (undocumented) @@ -979,7 +986,6 @@ interface MaskOperationOptions { // @public (undocumented) type MaybeAsync = T | PromiseLike; -// Warning: (ae-forgotten-export) The symbol "IsAny" 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 "DataMasking" 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 @@ -1672,6 +1678,11 @@ type RefetchWritePolicy = "merge" | "overwrite"; // @public (undocumented) type RemoveFragmentName = T extends any ? Omit : T; +// @public (undocumented) +type RemoveIndexSignature = { + [K in keyof T as string extends K ? never : number extends K ? never : symbol extends K ? never : K]: T[K]; +}; + // @public (undocumented) type RemoveMaskedMarker = Omit; @@ -1848,7 +1859,7 @@ type Unmasked = true extends IsAny ? TData : TData extends object // @public (undocumented) type UnwrapFragmentRefs = true extends IsAny ? TData : TData extends any ? string extends keyof TData ? TData : keyof TData extends never ? TData : TData extends { " $fragmentRefs"?: infer FragmentRefs; -} ? UnwrapFragmentRefs | RemoveFragmentName[keyof NonNullable]>>>> : TData extends Array ? Array> : TData extends object ? { +} ? UnwrapFragmentRefs | RemoveFragmentName[keyof NonNullable]>>>> : TData extends object ? { [K in keyof TData]: UnwrapFragmentRefs; } : TData : never; diff --git a/.api-reports/api-report-utilities.api.md b/.api-reports/api-report-utilities.api.md index 76b0f621592..a09ee2ed138 100644 --- a/.api-reports/api-report-utilities.api.md +++ b/.api-reports/api-report-utilities.api.md @@ -606,8 +606,11 @@ export type ConcastSourcesIterable = Iterable>; // @public (undocumented) export function concatPagination(keyArgs?: KeyArgs): FieldPolicy; +// Warning: (ae-forgotten-export) The symbol "IsAny" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "Exact" needs to be exported by the entry point index.d.ts +// // @public (undocumented) -type ContainsFragmentsRefs = TData extends object ? " $fragmentRefs" extends keyof TData ? true : ContainsFragmentsRefs : false; +type ContainsFragmentsRefs = true extends (IsAny) ? false : TData extends object ? Exact extends Seen ? false : " $fragmentRefs" extends keyof RemoveIndexSignature ? true : ContainsFragmentsRefs> : false; // @public (undocumented) export function createFragmentMap(fragments?: FragmentDefinitionNode[]): FragmentMap; @@ -979,6 +982,9 @@ namespace EntityStore { // @public type ErrorPolicy = "none" | "ignore" | "all"; +// @public (undocumented) +type Exact = (x: T) => T; + // Warning: (ae-forgotten-export) The symbol "ExecutionPatchResultBase" needs to be exported by the entry point index.d.ts // // @public (undocumented) @@ -1689,7 +1695,6 @@ type MaybeAsync = T | PromiseLike; // @public (undocumented) export function maybeDeepFreeze(obj: T): T; -// Warning: (ae-forgotten-export) The symbol "IsAny" 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 "DataMasking" 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 @@ -2519,6 +2524,11 @@ export type RemoveFragmentSpreadConfig = RemoveNodeConfig; // @public (undocumented) export function removeFragmentSpreadFromDocument(config: RemoveFragmentSpreadConfig[], doc: DocumentNode): DocumentNode | null; +// @public (undocumented) +export type RemoveIndexSignature = { + [K in keyof T as string extends K ? never : number extends K ? never : symbol extends K ? never : K]: T[K]; +}; + // @public (undocumented) type RemoveMaskedMarker = Omit; @@ -2770,7 +2780,7 @@ type Unmasked = true extends IsAny ? TData : TData extends object // @public (undocumented) type UnwrapFragmentRefs = true extends IsAny ? TData : TData extends any ? string extends keyof TData ? TData : keyof TData extends never ? TData : TData extends { " $fragmentRefs"?: infer FragmentRefs; -} ? UnwrapFragmentRefs | RemoveFragmentName[keyof NonNullable]>>>> : TData extends Array ? Array> : TData extends object ? { +} ? UnwrapFragmentRefs | RemoveFragmentName[keyof NonNullable]>>>> : TData extends object ? { [K in keyof TData]: UnwrapFragmentRefs; } : TData : never; diff --git a/.api-reports/api-report.api.md b/.api-reports/api-report.api.md index 3a4bac0f7a6..850d6e603b9 100644 --- a/.api-reports/api-report.api.md +++ b/.api-reports/api-report.api.md @@ -576,8 +576,12 @@ type ConcastSourcesIterable = Iterable>; // @public (undocumented) export const concat: typeof ApolloLink.concat; +// Warning: (ae-forgotten-export) The symbol "IsAny" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "Exact" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "RemoveIndexSignature" needs to be exported by the entry point index.d.ts +// // @public (undocumented) -type ContainsFragmentsRefs = TData extends object ? " $fragmentRefs" extends keyof TData ? true : ContainsFragmentsRefs : false; +type ContainsFragmentsRefs = true extends (IsAny) ? false : TData extends object ? Exact extends Seen ? false : " $fragmentRefs" extends keyof RemoveIndexSignature ? true : ContainsFragmentsRefs> : false; // @public (undocumented) export const createHttpLink: (linkOptions?: HttpOptions) => ApolloLink; @@ -900,6 +904,9 @@ namespace EntityStore { // @public export type ErrorPolicy = "none" | "ignore" | "all"; +// @public (undocumented) +type Exact = (x: T) => T; + // @public (undocumented) export const execute: typeof ApolloLink.execute; @@ -1582,7 +1589,6 @@ interface MaskOperationOptions { // @public (undocumented) type MaybeAsync = T | PromiseLike; -// Warning: (ae-forgotten-export) The symbol "IsAny" 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 // @@ -2552,6 +2558,11 @@ export type RefetchWritePolicy = "merge" | "overwrite"; // @public (undocumented) type RemoveFragmentName = T extends any ? Omit : T; +// @public (undocumented) +type RemoveIndexSignature = { + [K in keyof T as string extends K ? never : number extends K ? never : symbol extends K ? never : K]: T[K]; +}; + // @public (undocumented) type RemoveMaskedMarker = Omit; @@ -2871,7 +2882,7 @@ export type Unmasked = true extends IsAny ? TData : TData extends // @public (undocumented) type UnwrapFragmentRefs = true extends IsAny ? TData : TData extends any ? string extends keyof TData ? TData : keyof TData extends never ? TData : TData extends { " $fragmentRefs"?: infer FragmentRefs; -} ? UnwrapFragmentRefs | RemoveFragmentName[keyof NonNullable]>>>> : TData extends Array ? Array> : TData extends object ? { +} ? UnwrapFragmentRefs | RemoveFragmentName[keyof NonNullable]>>>> : TData extends object ? { [K in keyof TData]: UnwrapFragmentRefs; } : TData : never; diff --git a/.github/ISSUE_TEMPLATE/question-discussion.md b/.github/ISSUE_TEMPLATE/question-discussion.md index efb11c7a9e9..be195b9cd84 100644 --- a/.github/ISSUE_TEMPLATE/question-discussion.md +++ b/.github/ISSUE_TEMPLATE/question-discussion.md @@ -5,7 +5,6 @@ about: Questions / discussions are best posted in our community forums or StackO Need help or want to talk all things Apollo Client? Issues here are reserved for bugs, but one of the following resources should help: -* Apollo Discord server: https://discord.gg/graphos * Apollo GraphQL community forums: https://community.apollographql.com * StackOverflow (`apollo-client` tag): https://stackoverflow.com/questions/tagged/apollo-client * Apollo Feature Request repo: https://github.com/apollographql/apollo-feature-requests diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml index 5cc733c8cd7..2cb19258de2 100644 --- a/.github/workflows/lock.yml +++ b/.github/workflows/lock.yml @@ -25,7 +25,7 @@ jobs: issue-comment: > This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs. - For general questions, we recommend using [StackOverflow](https://stackoverflow.com/questions/tagged/apollo-client) or our [discord server](https://discord.gg/graphos). + For general questions, we recommend using our [Community Forum](https://community.apollographql.com/) or [Stack Overflow](https://stackoverflow.com/questions/tagged/apollo-client). pr-inactive-days: "30" exclude-any-pr-labels: "discussion" diff --git a/.github/workflows/scheduled-test-canary.yml b/.github/workflows/scheduled-test-canary.yml new file mode 100644 index 00000000000..42d5442d948 --- /dev/null +++ b/.github/workflows/scheduled-test-canary.yml @@ -0,0 +1,38 @@ +# a GitHub Action that once a day runs all tests from `main` and `release-*` branches +# with the latest `canary` and `experimental` release of `react` and `react-dom` +name: Scheduled React Canary Test +on: + schedule: + - cron: "0 0 * * *" + workflow_dispatch: + inputs: + branches: + description: "Branches to test" + required: true + default: '["main", "release-3.13", "release-4.0"]' + tags: + description: "React and React-DOM versions" + required: true + default: '["canary", "experimental"]' +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + tag: ${{ fromJson(github.event_name == 'workflow_dispatch' && inputs.tags || '["canary", "experimental"]') }} + branch: ${{ fromJson(github.event_name == 'workflow_dispatch' && inputs.branches || '["main", "release-3.13", "release-4.0"]') }} + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ matrix.branch }} + - uses: actions/setup-node@v4 + with: + node-version: 22.x + - uses: bahmutov/npm-install@v1 + - run: | + npm install react@${{ matrix.tag }} react-dom@${{ matrix.tag }} + # tests can be flaky, this runs only once a day and we want to minimize false negatives - retry up to three times + - run: | + node -e 'console.log("\n\nReact %s, React-DOM %s\n\n", require("react").version, require("react-dom").version)' + parallel --line-buffer -j 1 --retries 3 'npm test -- --logHeapUsage --selectProjects ' ::: 'ReactDOM 19' diff --git a/.semgrepignore b/.semgrepignore index 3031b723ab1..5b3c586139a 100644 --- a/.semgrepignore +++ b/.semgrepignore @@ -11,3 +11,4 @@ dist/ # custom paths __tests__/ ./docs/source/data/subscriptions.mdx +./docs/source/development-testing/developer-tooling.mdx \ No newline at end of file diff --git a/.size-limits.json b/.size-limits.json index 54bce409ebd..54621796c0c 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 41613, - "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 34349 + "dist/apollo-client.min.cjs": 41639, + "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 34381 } diff --git a/CHANGELOG.md b/CHANGELOG.md index 173e739fd1e..adc45c38443 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,23 @@ # @apollo/client +## 3.12.4 + +### Patch Changes + +- [#12236](https://github.com/apollographql/apollo-client/pull/12236) [`4334d30`](https://github.com/apollographql/apollo-client/commit/4334d30cc3fbedb4f736eff196c49a9f20a46704) Thanks [@charpeni](https://github.com/charpeni)! - Fix an issue with `refetchQueries` where comparing `DocumentNode`s internally by references could lead to an unknown query, even though the `DocumentNode` was indeed an active query—with a different reference. + +## 3.12.3 + +### Patch Changes + +- [#12214](https://github.com/apollographql/apollo-client/pull/12214) [`8bfee88`](https://github.com/apollographql/apollo-client/commit/8bfee88102dd071ea5836f7267f30ca082671b2b) Thanks [@phryneas](https://github.com/phryneas)! - Data masking: prevent infinite recursion of `ContainsFragmentsRefs` type + +- [#12204](https://github.com/apollographql/apollo-client/pull/12204) [`851deb0`](https://github.com/apollographql/apollo-client/commit/851deb06f42eb255b4839c2b88430f991943ae0f) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Fix `Unmasked` unwrapping tuple types into an array of their subtypes. + +- [#12204](https://github.com/apollographql/apollo-client/pull/12204) [`851deb0`](https://github.com/apollographql/apollo-client/commit/851deb06f42eb255b4839c2b88430f991943ae0f) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Ensure `MaybeMasked` does not try and unwrap types that contain index signatures. + +- [#12204](https://github.com/apollographql/apollo-client/pull/12204) [`851deb0`](https://github.com/apollographql/apollo-client/commit/851deb06f42eb255b4839c2b88430f991943ae0f) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Ensure `MaybeMasked` does not try to unwrap the type as `Unmasked` if the type contains `any`. + ## 3.12.2 ### Patch Changes diff --git a/README.md b/README.md index 49c33c2fd06..d10076c425f 100644 --- a/README.md +++ b/README.md @@ -5,13 +5,13 @@

Apollo Client

-[![npm version](https://badge.fury.io/js/%40apollo%2Fclient.svg)](https://badge.fury.io/js/%40apollo%2Fclient) [![Build Status](https://circleci.com/gh/apollographql/apollo-client.svg?style=svg)](https://circleci.com/gh/apollographql/apollo-client) [![Join the community](https://img.shields.io/discourse/status?label=Join%20the%20community&server=https%3A%2F%2Fcommunity.apollographql.com)](https://community.apollographql.com) [![Join our Discord server](https://img.shields.io/discord/1022972389463687228.svg?color=7389D8&labelColor=6A7EC2&logo=discord&logoColor=ffffff&style=flat-square)](https://discord.gg/graphos) +[![npm version](https://badge.fury.io/js/%40apollo%2Fclient.svg)](https://badge.fury.io/js/%40apollo%2Fclient) [![Build Status](https://circleci.com/gh/apollographql/apollo-client.svg?style=svg)](https://circleci.com/gh/apollographql/apollo-client) [![Join the community](https://img.shields.io/discourse/status?label=Join%20the%20community&server=https%3A%2F%2Fcommunity.apollographql.com)](https://community.apollographql.com) --- -**Announcement:** +**Announcement:** Join 1000+ engineers at GraphQL Summit for talks, workshops, and office hours, Oct 8-10 in NYC. [Get your pass here ->](https://summit.graphql.com/?utm_campaign=github_federation_readme) --- diff --git a/ROADMAP.md b/ROADMAP.md index 9840dd9dfd1..2d96bae7d14 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,6 +1,6 @@ # 🔮 Apollo Client Ecosystem Roadmap -**Last updated: 2024-11-04** +**Last updated: 2024-12-11** For up to date release notes, refer to the project's [Changelog](https://github.com/apollographql/apollo-client/blob/main/CHANGELOG.md). @@ -17,42 +17,41 @@ For up to date release notes, refer to the project's [Changelog](https://github. ### Apollo Client -#### [3.12.0](https://github.com/apollographql/apollo-client/milestone/42) - November 18, 2024 -_Release candidate - November 11, 2024_ +#### [3.13.0](https://github.com/apollographql/apollo-client/milestone/42) - January 13, 2024 +_Release candidate - January 7th_ -- Data masking +- `useSuspenseFragment` #### Upcoming features -- Leaner client (under alternate entry point) -- Better types for `useQuery`/`useMutation`/`useSubscription` -- Introduce `useSuspenseFragment` that will suspend when the data is not yet loaded (experimental) +- Deprecations and preparations for 4.0 #### 4.0 -- `Release 4.0` will be our next major release of the Client and is still in early planning. See Github [4.0 Milestone](https://github.com/apollographql/apollo-client/milestone/31) for more details. +- `Release 4.0` will be our next major release of the Client and is still in planning. See Github [4.0 Milestone](https://github.com/apollographql/apollo-client/milestone/31) for more details. ### GraphQL Testing Library - New documentation - Subscription support (waiting for MSW WebSocket support to land) +_These changes will take longer than anticipated due to prioritization on Apollo Client 4.0_ + ### VSCode Extension -- Bug fixes and long-requested features -- Apollo Client Devtools integration +_No outstanding work_ ### GraphQL Tag -- Started 3.0 milestone planning +- `Release 3.0` will be our next major release of `graphql-tag` and is still in planning. See Github [3.0 Milestone](https://github.com/apollographql/graphql-tag/milestone/3) for more details. ### Apollo Client DevTools -- Ongoing work with fixing error messages shown in devtools -- Add a memory panel to monitor Apollo Client devtools internal caches -- Connectors debugger +_These changes will take longer than anticipated due to prioritization on Apollo Client 4.0_ -### Apollo Client NextJS +### Apollo Client React Framework Integrations - New/more robust documentation - Support for `@defer` in RSC +- Support for Apollo Client Streaming in TanStack Router +- Support for Apollo Client Streaming in React Router 7 diff --git a/docs/source/_redirects b/docs/source/_redirects deleted file mode 100644 index 689c408a089..00000000000 --- a/docs/source/_redirects +++ /dev/null @@ -1,108 +0,0 @@ -# Redirect all 3.0 beta docs to root -/v3.0-beta/* /docs/react/:splat - -# Redirect 2.x docs to v2 -/v2.4/* /docs/react/v2/ -/v2.5/* /docs/react/v2/ -/v2.6/* /docs/react/v2/:splat - -# Split out pagination article -/data/pagination/ /docs/react/pagination/overview/ - -# Remove 'Recompose patterns' article -/development-testing/recompose/ /docs/react/ - -# Client 3.0 changes -/api/apollo-client/ /docs/react/api/core/ApolloClient/ -/api/react-hooks/ /docs/react/api/react/hooks/ -/api/react-testing/ /docs/react/api/react/testing/ -/api/react-components/ /docs/react/api/react/components/ -/api/react-hoc/ /docs/react/api/react/hoc/ -/api/react-ssr/ /docs/react/api/react/ssr/ -/api/react-common/ /docs/react/api/react/hooks/ - -# Apollo Client Information Architecture refresh -# https://github.com/apollographql/apollo-client/pull/5321 -/features/error-handling/ /docs/react/data/error-handling/ -/advanced/fragments/ /docs/react/data/fragments/ -/essentials/mutations/ /docs/react/data/mutations/ -/features/pagination/ /docs/react/data/pagination/ -/essentials/queries/ /docs/react/data/queries/ -/advanced/subscriptions/ /docs/react/data/subscriptions/ -/recipes/client-schema-mocking/ /docs/react/development-testing/client-schema-mocking/ -/features/developer-tooling/ /docs/react/development-testing/developer-tooling/ -/recipes/recompose/ /docs/react/ -/recipes/static-typing/ /docs/react/development-testing/static-typing/ -/recipes/testing/ /docs/react/development-testing/testing/ -/essentials/get-started/ /docs/react/get-started/ -/integrations/ /docs/react/integrations/integrations/ -/recipes/meteor/ /docs/react/integrations/meteor/ -/recipes/react-native/ /docs/react/integrations/react-native/ -/recipes/webpack/ /docs/react/integrations/webpack/ -/advanced/caching/ /docs/react/caching/cache-configuration/ -/essentials/local-state/ /docs/react/data/local-state/ -/advanced/boost-migration/ /docs/react/migrating/boost-migration/ -/hooks-migration/ /docs/react/migrating/hooks-migration/ -/recipes/authentication/ /docs/react/networking/authentication/ -/advanced/network-layer/ /docs/react/networking/network-layer/ -/recipes/babel/ /docs/react/performance/babel/ -/features/optimistic-ui/ /docs/react/performance/optimistic-ui/ -/recipes/performance/ /docs/react/performance/performance/ -/features/server-side-rendering/ /docs/react/performance/server-side-rendering/ - -# React Apollo 2.0 - Basics -# https://github.com/apollographql/apollo-client/pull/3097 -/basics/setup.html /docs/react/get-started/ -/essentials/get-started.html /docs/react/get-started/ -/basics/queries.html /docs/react/data/queries/ -/essentials/queries.html /docs/react/data/queries/ -/basics/mutations.html /docs/react/data/mutations/ -/essentials/mutations.html /docs/react/data/mutations/ -/basics/network-layer.html /docs/react/networking/network-layer/ -/advanced/network-layer.html /docs/react/networking/network-layer/ -/basics/caching.html /docs/react/caching/cache-configuration/ -/advanced/caching.html /docs/react/caching/cache-configuration/ - -# React Apollo 2.0 - Features -# https://github.com/apollographql/apollo-client/pull/3097 -/features/caching.html /docs/react/caching/cache-configuration/ -/advanced/caching.html /docs/react/caching/cache-configuration/ -/features/cache-updates.html /docs/react/caching/cache-configuration/ -/advanced/caching.html /docs/react/caching/cache-configuration/ -/features/fragments.html /docs/react/data/fragments/ -/advanced/fragments.html /docs/react/data/fragments/ -/features/subscriptions.html /docs/react/data/subscriptions/ -/advanced/subscriptions.html /docs/react/data/subscriptions/ -/features/react-native.html /docs/react/integrations/react-native/ -/recipes/react-native.html /docs/react/integrations/react-native/ -/features/static-typing.html /docs/react/development-testing/static-typing/ -/recipes/static-typing.html /docs/react/development-testing/static-typing/ -/features/error-handling.html /docs/react/data/error-handling/ - -# React Apollo 2.0 - Recipes -# https://github.com/apollographql/apollo-client/pull/3097 -/recipes/query-splitting.html /docs/react/performance/performance/ -/features/performance.html#query-splitting /docs/react/performance/performance/ -/recipes/pagination.html /docs/react/data/pagination/ -/features/pagination.html /docs/react/data/pagination/ -/recipes/prefetching.html /docs/react/performance/performance/ -/features/performance.html#prefetching /docs/react/performance/performance/ -/recipes/server-side-rendering.html /docs/react/performance/server-side-rendering/ -/features/server-side-rendering.html /docs/react/performance/server-side-rendering/ -/recipes/fragment-matching.html /docs/react/data/fragments/ -/advanced/fragments.html /docs/react/data/fragments/ - -# Ported from old _config.yml file -/essentials/get-started.html#api /docs/react/get-started/ -/api/react-apollo.html /docs/react/get-started/ -/essentials/queries.html#api /docs/react/data/queries/#options -docs/react/api/react-apollo.html#graphql-query-options /docs/react/data/queries/#options -/basics/mutations.html#api /docs/react/basics/mutations.html -/recipes/simple-example.html /docs/react/get-started/ -docs/react/essentials/get-started.html /docs/react/get-started/ -/api/apollo-client.html#FetchPolicy /docs/react/api/core/ApolloClient/#example-defaultoptions-object -docs/react/api/react-apollo.html#graphql-config-options-fetchPolicy /docs/react/api/core/ApolloClient/#example-defaultoptions-object -/api/apollo-client.html#ErrorPolicy /docs/react/api/core/ApolloClient/#example-defaultoptions-object -docs/react/api/react-apollo.html#graphql-config-options-errorPolicy /docs/react/api/core/ApolloClient/#example-defaultoptions-object -/features/performance.html /docs/react/performance/performance/ -/recipes/performance.html /docs/react/performance/performance/ diff --git a/docs/source/assets/devtools/vscode-panel.png b/docs/source/assets/devtools/vscode-panel.png new file mode 100644 index 00000000000..d330d1e74a3 Binary files /dev/null and b/docs/source/assets/devtools/vscode-panel.png differ diff --git a/docs/source/assets/devtools/vscode-setting.png b/docs/source/assets/devtools/vscode-setting.png new file mode 100644 index 00000000000..1d2db0fc272 Binary files /dev/null and b/docs/source/assets/devtools/vscode-setting.png differ diff --git a/docs/source/data/fragments.mdx b/docs/source/data/fragments.mdx index 3a9627fe64c..0faf4fcca73 100644 --- a/docs/source/data/fragments.mdx +++ b/docs/source/data/fragments.mdx @@ -1156,10 +1156,12 @@ const config: CodegenConfig = { // ... // disables the incompatible GraphQL Codegen fragment masking feature fragmentMasking: false, - inlineFragmentTypes: "mask", customDirectives: { apolloUnmask: true } + }, + config: { + inlineFragmentTypes: "mask", } } } diff --git a/docs/source/development-testing/developer-tooling.md b/docs/source/development-testing/developer-tooling.mdx similarity index 56% rename from docs/source/development-testing/developer-tooling.md rename to docs/source/development-testing/developer-tooling.mdx index 2ef169783f2..7a43af36713 100644 --- a/docs/source/development-testing/developer-tooling.md +++ b/docs/source/development-testing/developer-tooling.mdx @@ -29,7 +29,7 @@ The Apollo Client Devtools appear as an "Apollo" tab in your web browser's Inspe - **Mutation inspector:** View active mutations and their variables, and re-run individual mutations. - **Cache inspector:** Visualize the Apollo Client cache and search it by field name and/or value. -![Apollo Client Devtools](../assets/devtools/apollo-client-devtools/ac-browser-devtools-3.png) +Apollo Client Devtools ### Installation @@ -40,3 +40,38 @@ You can install the extension via the webstores for [Chrome](https://chrome.goog While your app is in dev mode, the Apollo Client Devtools will appear as an "Apollo" tab in your web browser inspector. To enable the devtools in your app in production, pass `connectToDevTools: true` to the `ApolloClient` constructor in your app. Pass `connectToDevTools: false` if want to manually disable this functionality. Find more information about contributing and debugging on the [Apollo Client Devtools GitHub page](https://github.com/apollographql/apollo-client-devtools). + +## Apollo Client Devtools in VS Code + +The Apollo VSCode extension ships with an instance of the Apollo Client Devtools. +You can use it to remotely debug your client, which makes it possible to also debug React Native and node applications. + +The following sections walk through how to install and integrate with the extension. + + +This feature is currently released as "experimental" - please try it out and give us feedback in our [GitHub issues](https://github.com/apollographql/vscode-graphql/issues)! + + +* Install the Apollo VS Code extension: [start installation](vscode:extension/apollographql.vscode-apollo) | [marketplace page](https://marketplace.visualstudio.com/items?itemName=apollographql.vscode-apollo) +* Set the "Apollographql > Dev Tools: Show Panel" setting to "detect" or "always" in the VS code settings dialog. +A screenshot of the VS Code settings dialog focusing on the 'Show Panel' option +* In your code base, install the `@apollo/client-devtools-vscode` package: +```sh +npm install @apollo/client-devtools-vscode +``` +* After initializing your `ApolloClient` instance, call `connectApolloClientToVSCodeDevTools` with your client instance. +```js +import { connectApolloClientToVSCodeDevTools } from "@apollo/client-devtools-vscode"; + +const client = new ApolloClient({ /* ... */ }); + +// we recommend wrapping this statement in a check for e.g. process.env.NODE_ENV === "development" +const devtoolsRegistration = connectApolloClientToVSCodeDevTools( + client, + // the default port of the VSCode DevTools is 7095 + "ws://localhost:7095", +); +``` +* Open the "Apollo Client DevTools" panel in VS Code. +* Start your application. It should automatically connect to the DevTools. +Apollo Client Devtools in a VS Code panel diff --git a/docs/source/integrations/react-native.md b/docs/source/integrations/react-native.md index 3990bdffde4..78ea0eb8897 100644 --- a/docs/source/integrations/react-native.md +++ b/docs/source/integrations/react-native.md @@ -38,7 +38,15 @@ For more information on setting up Apollo Client, see [Getting started](../get-s ## Apollo Client Devtools -#### 1. Using [React Native Debugger](https://github.com/jhen0409/react-native-debugger) +#### 1. Using the VS Code [Apollo GraphQL extension](https://marketplace.visualstudio.com/items?itemName=apollographql.vscode-apollo) + +Apollo Client Devtools in a VS Code panel + +The Apollo GraphQL VSCode extension comes with the Apollo Client Devtools bundled, and these can be used with React Native. + +See [Developer tools - Apollo Client Devtools in VS Code](../development-testing/developer-tooling/#apollo-client-devtools-in-vs-code) for setup instructions. + +#### 2. Using [React Native Debugger](https://github.com/jhen0409/react-native-debugger) The React Native Debugger supports the [Apollo Client Devtools](../development-testing/developer-tooling/#apollo-client-devtools): @@ -46,7 +54,7 @@ The React Native Debugger supports the [Apollo Client Devtools](../development-t 2. Enable "Debug JS Remotely" in your app. 3. If you don't see the Developer Tools panel or the Apollo tab is missing from it, toggle the Developer Tools by right-clicking anywhere and selecting **Toggle Developer Tools**. -#### 2. Using [Flipper](https://fbflipper.com/) +#### 3. Using [Flipper](https://fbflipper.com/) A community plugin called [React Native Apollo devtools](https://github.com/razorpay/react-native-apollo-devtools) is available for Flipper, which supports viewing cache data. diff --git a/package-lock.json b/package-lock.json index f568baca25f..6503dfb9530 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@apollo/client", - "version": "3.12.2", + "version": "3.12.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@apollo/client", - "version": "3.12.2", + "version": "3.12.4", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index e509e80e8c6..7538bd3a651 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/client", - "version": "3.12.2", + "version": "3.12.4", "description": "A fully-featured caching GraphQL client.", "private": true, "keywords": [ diff --git a/src/__tests__/ApolloClient.ts b/src/__tests__/ApolloClient.ts index e147de233a4..b7c1e8eb39a 100644 --- a/src/__tests__/ApolloClient.ts +++ b/src/__tests__/ApolloClient.ts @@ -15,7 +15,6 @@ import { Observable } from "../utilities"; import { ApolloLink, FetchResult } from "../link/core"; import { HttpLink } from "../link/http"; import { createFragmentRegistry, InMemoryCache } from "../cache"; -import { itAsync } from "../testing"; import { ObservableStream, spyOnConsole } from "../testing/internal"; import { TypedDocumentNode } from "@graphql-typed-document-node/core"; import { invariant } from "../utilities/globals"; @@ -1205,235 +1204,181 @@ describe("ApolloClient", () => { result.data?.people.friends[0].id; }); - itAsync( - "with a replacement of nested array (wq)", - (resolve, reject) => { - let count = 0; - const client = newClient(); - const observable = client.watchQuery({ query }); - const subscription = observable.subscribe({ - next(nextResult) { - ++count; - if (count === 1) { - expect(nextResult.data).toEqual(data); - expect(observable.getCurrentResult().data).toEqual(data); - - const readData = client.readQuery({ query }); - expect(readData).toEqual(data); - - // modify readData and writeQuery - const bestFriends = readData!.people.friends.filter( - (x) => x.type === "best" - ); - // this should re call next - client.writeQuery({ - query, - data: { - people: { - id: 1, - friends: bestFriends, - __typename: "Person", - }, - }, - }); - } else if (count === 2) { - const expectation = { - people: { - id: 1, - friends: [bestFriend], - __typename: "Person", - }, - }; - expect(nextResult.data).toEqual(expectation); - expect(client.readQuery({ query })).toEqual( - expectation - ); - subscription.unsubscribe(); - resolve(); - } + it("with a replacement of nested array (wq)", async () => { + const client = newClient(); + const observable = client.watchQuery({ query }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitMatchedValue({ data }); + expect(observable.getCurrentResult().data).toEqual(data); + + const readData = client.readQuery({ query }); + expect(readData).toEqual(data); + + // modify readData and writeQuery + const bestFriends = readData!.people.friends.filter( + (x) => x.type === "best" + ); + // this should re call next + client.writeQuery({ + query, + data: { + people: { + id: 1, + friends: bestFriends, + __typename: "Person", }, - }); - } - ); + }, + }); - itAsync( - "with a value change inside a nested array (wq)", - (resolve, reject) => { - let count = 0; - const client = newClient(); - const observable = client.watchQuery({ query }); - observable.subscribe({ - next: (nextResult) => { - count++; - if (count === 1) { - expect(nextResult.data).toEqual(data); - expect(observable.getCurrentResult().data).toEqual(data); - - const readData = client.readQuery({ query }); - expect(readData).toEqual(data); - - // modify readData and writeQuery - const friends = readData!.people.friends.slice(); - friends[0] = { ...friends[0], type: "okayest" }; - friends[1] = { ...friends[1], type: "okayest" }; - - // this should re call next - client.writeQuery({ - query, - data: { - people: { - id: 1, - friends, - __typename: "Person", - }, - }, - }); - - setTimeout(() => { - if (count === 1) - reject( - new Error( - "writeFragment did not re-call observable with next value" - ) - ); - }, 250); - } + const expectation = { + people: { + id: 1, + friends: [bestFriend], + __typename: "Person", + }, + }; - if (count === 2) { - const expectation0 = { - ...bestFriend, - type: "okayest", - }; - const expectation1 = { - ...badFriend, - type: "okayest", - }; - const nextFriends = nextResult.data!.people.friends; - expect(nextFriends[0]).toEqual(expectation0); - expect(nextFriends[1]).toEqual(expectation1); - - const readFriends = client.readQuery({ query })!.people - .friends; - expect(readFriends[0]).toEqual(expectation0); - expect(readFriends[1]).toEqual(expectation1); - resolve(); - } + await expect(stream).toEmitMatchedValue({ data: expectation }); + expect(client.readQuery({ query })).toEqual(expectation); + }); + + it("with a value change inside a nested array (wq)", async () => { + const client = newClient(); + const observable = client.watchQuery({ query }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitMatchedValue({ data }); + + expect(observable.getCurrentResult().data).toEqual(data); + + const readData = client.readQuery({ query }); + expect(readData).toEqual(data); + + // modify readData and writeQuery + const friends = readData!.people.friends.slice(); + friends[0] = { ...friends[0], type: "okayest" }; + friends[1] = { ...friends[1], type: "okayest" }; + + // this should re call next + client.writeQuery({ + query, + data: { + people: { + id: 1, + friends, + __typename: "Person", }, - }); - } - ); + }, + }); + + const expectation0 = { + ...bestFriend, + type: "okayest", + }; + const expectation1 = { + ...badFriend, + type: "okayest", + }; + + const nextResult = await stream.takeNext(); + const nextFriends = nextResult.data!.people.friends; + + expect(nextFriends[0]).toEqual(expectation0); + expect(nextFriends[1]).toEqual(expectation1); + + const readFriends = client.readQuery({ query })!.people.friends; + expect(readFriends[0]).toEqual(expectation0); + expect(readFriends[1]).toEqual(expectation1); + }); }); + describe("using writeFragment", () => { - itAsync( - "with a replacement of nested array (wf)", - (resolve, reject) => { - let count = 0; - const client = newClient(); - const observable = client.watchQuery({ query }); - observable.subscribe({ - next: (result) => { - count++; - if (count === 1) { - expect(result.data).toEqual(data); - expect(observable.getCurrentResult().data).toEqual(data); - const bestFriends = result.data!.people.friends.filter( - (x) => x.type === "best" - ); - // this should re call next - client.writeFragment({ - id: `Person${result.data!.people.id}`, - fragment: gql` - fragment bestFriends on Person { - friends { - id - } - } - `, - data: { - friends: bestFriends, - __typename: "Person", - }, - }); - - setTimeout(() => { - if (count === 1) - reject( - new Error( - "writeFragment did not re-call observable with next value" - ) - ); - }, 50); - } + it("with a replacement of nested array (wf)", async () => { + const client = newClient(); + const observable = client.watchQuery({ query }); + const stream = new ObservableStream(observable); - if (count === 2) { - expect(result.data!.people.friends).toEqual([bestFriend]); - resolve(); + { + const result = await stream.takeNext(); + + expect(result.data).toEqual(data); + expect(observable.getCurrentResult().data).toEqual(data); + + const bestFriends = result.data!.people.friends.filter( + (x) => x.type === "best" + ); + + // this should re call next + client.writeFragment({ + id: `Person${result.data!.people.id}`, + fragment: gql` + fragment bestFriends on Person { + friends { + id + } } + `, + data: { + friends: bestFriends, + __typename: "Person", }, }); } - ); - itAsync( - "with a value change inside a nested array (wf)", - (resolve, reject) => { - let count = 0; - const client = newClient(); - const observable = client.watchQuery({ query }); - observable.subscribe({ - next: (result) => { - count++; - if (count === 1) { - expect(result.data).toEqual(data); - expect(observable.getCurrentResult().data).toEqual(data); - const friends = result.data!.people.friends; - - // this should re call next - client.writeFragment({ - id: `Person${result.data!.people.id}`, - fragment: gql` - fragment bestFriends on Person { - friends { - id - type - } - } - `, - data: { - friends: [ - { ...friends[0], type: "okayest" }, - { ...friends[1], type: "okayest" }, - ], - __typename: "Person", - }, - }); - - setTimeout(() => { - if (count === 1) - reject( - new Error( - "writeFragment did not re-call observable with next value" - ) - ); - }, 50); - } + { + const result = await stream.takeNext(); + expect(result.data!.people.friends).toEqual([bestFriend]); + } + }); + + it("with a value change inside a nested array (wf)", async () => { + const client = newClient(); + const observable = client.watchQuery({ query }); + const stream = new ObservableStream(observable); - if (count === 2) { - const nextFriends = result.data!.people.friends; - expect(nextFriends[0]).toEqual({ - ...bestFriend, - type: "okayest", - }); - expect(nextFriends[1]).toEqual({ - ...badFriend, - type: "okayest", - }); - resolve(); + { + const result = await stream.takeNext(); + + expect(result.data).toEqual(data); + expect(observable.getCurrentResult().data).toEqual(data); + const friends = result.data!.people.friends; + + // this should re call next + client.writeFragment({ + id: `Person${result.data!.people.id}`, + fragment: gql` + fragment bestFriends on Person { + friends { + id + type + } } + `, + data: { + friends: [ + { ...friends[0], type: "okayest" }, + { ...friends[1], type: "okayest" }, + ], + __typename: "Person", }, }); } - ); + + { + const result = await stream.takeNext(); + const nextFriends = result.data!.people.friends; + + expect(nextFriends[0]).toEqual({ + ...bestFriend, + type: "okayest", + }); + expect(nextFriends[1]).toEqual({ + ...badFriend, + type: "okayest", + }); + } + }); }); }); }); @@ -2804,69 +2749,63 @@ describe("ApolloClient", () => { invariantDebugSpy.mockRestore(); }); - itAsync( - "should catch refetchQueries error when not caught explicitly", - (resolve, reject) => { - const linkFn = jest - .fn( - () => - new Observable((observer) => { - setTimeout(() => { - observer.error(new Error("refetch failed")); - }); - }) - ) - .mockImplementationOnce(() => { - setTimeout(refetchQueries); - return Observable.of(); - }); - - const client = new ApolloClient({ - link: new ApolloLink(linkFn), - cache: new InMemoryCache(), + it("should catch refetchQueries error when not caught explicitly", (done) => { + expect.assertions(2); + const linkFn = jest + .fn( + () => + new Observable((observer) => { + setTimeout(() => { + observer.error(new Error("refetch failed")); + }); + }) + ) + .mockImplementationOnce(() => { + setTimeout(refetchQueries); + return Observable.of(); }); - const query = gql` - query someData { - foo { - bar - } + const client = new ApolloClient({ + link: new ApolloLink(linkFn), + cache: new InMemoryCache(), + }); + + const query = gql` + query someData { + foo { + bar } - `; + } + `; - const observable = client.watchQuery({ - query, - fetchPolicy: "network-only", - }); + const observable = client.watchQuery({ + query, + fetchPolicy: "network-only", + }); - observable.subscribe({}); + observable.subscribe({}); - function refetchQueries() { - const result = client.refetchQueries({ - include: "all", - }); + function refetchQueries() { + const result = client.refetchQueries({ + include: "all", + }); - result.queries[0].subscribe({ - error() { - setTimeout(() => { - try { - expect(invariantDebugSpy).toHaveBeenCalledTimes(1); - expect(invariantDebugSpy).toHaveBeenCalledWith( - "In client.refetchQueries, Promise.all promise rejected with error %o", - new ApolloError({ - networkError: new Error("refetch failed"), - }) - ); - resolve(); - } catch (err) { - reject(err); - } - }); - }, - }); - } + result.queries[0].subscribe({ + error() { + setTimeout(() => { + expect(invariantDebugSpy).toHaveBeenCalledTimes(1); + expect(invariantDebugSpy).toHaveBeenCalledWith( + "In client.refetchQueries, Promise.all promise rejected with error %o", + new ApolloError({ + networkError: new Error("refetch failed"), + }) + ); + done(); + }); + }, + }); } - ); + }); }); describe.skip("type tests", () => { diff --git a/src/__tests__/client.ts b/src/__tests__/client.ts index 686d1c078ee..181e7b8d373 100644 --- a/src/__tests__/client.ts +++ b/src/__tests__/client.ts @@ -36,7 +36,7 @@ import { } from "../cache"; import { ApolloError } from "../errors"; -import { itAsync, mockSingleLink, MockLink, wait } from "../testing"; +import { mockSingleLink, MockLink, wait } from "../testing"; import { ObservableStream, spyOnConsole } from "../testing/internal"; import { waitFor } from "@testing-library/react"; @@ -106,36 +106,33 @@ describe("client", () => { ); }); - itAsync( - "should allow for a single query to take place", - (resolve, reject) => { - const query = gql` - query people { - allPeople(first: 1) { - people { - name - __typename - } + it("should allow for a single query to take place", async () => { + const query = gql` + query people { + allPeople(first: 1) { + people { + name __typename } + __typename } - `; + } + `; - const data = { - allPeople: { - people: [ - { - name: "Luke Skywalker", - __typename: "Person", - }, - ], - __typename: "People", - }, - }; + const data = { + allPeople: { + people: [ + { + name: "Luke Skywalker", + __typename: "Person", + }, + ], + __typename: "People", + }, + }; - return clientRoundtrip(resolve, reject, query, { data }); - } - ); + await clientRoundtrip(query, { data }); + }); it("should allow a single query with an apollo-link enabled network interface", async () => { const query = gql` @@ -176,137 +173,132 @@ describe("client", () => { expect(actualResult.data).toEqual(data); }); - itAsync( - "should allow for a single query with complex default variables to take place", - (resolve, reject) => { - const query = gql` - query stuff( - $test: Input = { key1: ["value", "value2"], key2: { key3: 4 } } - ) { - allStuff(test: $test) { - people { - name - } + it("should allow for a single query with complex default variables to take place", async () => { + const query = gql` + query stuff( + $test: Input = { key1: ["value", "value2"], key2: { key3: 4 } } + ) { + allStuff(test: $test) { + people { + name } } - `; + } + `; - const result = { - allStuff: { - people: [ - { - name: "Luke Skywalker", - }, - { - name: "Jabba The Hutt", - }, - ], - }, - }; + const result = { + allStuff: { + people: [ + { + name: "Luke Skywalker", + }, + { + name: "Jabba The Hutt", + }, + ], + }, + }; - const variables = { - test: { key1: ["value", "value2"], key2: { key3: 4 } }, - }; + const variables = { + test: { key1: ["value", "value2"], key2: { key3: 4 } }, + }; - const link = mockSingleLink({ - request: { query, variables }, - result: { data: result }, - }).setOnError(reject); + const link = mockSingleLink({ + request: { query, variables }, + result: { data: result }, + }); - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ addTypename: false }), - }); + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ addTypename: false }), + }); - const basic = client.query({ query, variables }).then((actualResult) => { - expect(actualResult.data).toEqual(result); - }); + { + const actualResult = await client.query({ query, variables }); - const withDefault = client.query({ query }).then((actualResult) => { - expect(actualResult.data).toEqual(result); - }); + expect(actualResult.data).toEqual(result); + } - return Promise.all([basic, withDefault]).then(resolve, reject); + { + const actualResult = await client.query({ query }); + + expect(actualResult.data).toEqual(result); } - ); + }); - itAsync( - "should allow for a single query with default values that get overridden with variables", - (resolve, reject) => { - const query = gql` - query people($first: Int = 1) { - allPeople(first: $first) { - people { - name - } + it("should allow for a single query with default values that get overridden with variables", async () => { + const query = gql` + query people($first: Int = 1) { + allPeople(first: $first) { + people { + name } } - `; + } + `; - const variables = { first: 1 }; - const override = { first: 2 }; + const variables = { first: 1 }; + const override = { first: 2 }; - const result = { - allPeople: { - people: [ - { - name: "Luke Skywalker", - }, - ], - }, - }; + const result = { + allPeople: { + people: [ + { + name: "Luke Skywalker", + }, + ], + }, + }; - const overriddenResult = { - allPeople: { - people: [ - { - name: "Luke Skywalker", - }, - { - name: "Jabba The Hutt", - }, - ], - }, - }; + const overriddenResult = { + allPeople: { + people: [ + { + name: "Luke Skywalker", + }, + { + name: "Jabba The Hutt", + }, + ], + }, + }; - const link = mockSingleLink( - { - request: { query, variables }, - result: { data: result }, - }, - { - request: { query, variables: override }, - result: { data: overriddenResult }, - } - ).setOnError(reject); + const link = mockSingleLink( + { + request: { query, variables }, + result: { data: result }, + }, + { + request: { query, variables: override }, + result: { data: overriddenResult }, + } + ); - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ addTypename: false }), - }); + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ addTypename: false }), + }); - const basic = client.query({ query, variables }).then((actualResult) => { - expect(actualResult.data).toEqual(result); - }); + { + const actualResult = await client.query({ query, variables }); - const withDefault = client.query({ query }).then((actualResult) => { - return expect(actualResult.data).toEqual(result); - }); + expect(actualResult.data).toEqual(result); + } - const withOverride = client - .query({ query, variables: override }) - .then((actualResult) => { - return expect(actualResult.data).toEqual(overriddenResult); - }); + { + const actualResult = await client.query({ query }); - return Promise.all([basic, withDefault, withOverride]).then( - resolve, - reject - ); + expect(actualResult.data).toEqual(result); + } + + { + const actualResult = await client.query({ query, variables: override }); + + expect(actualResult.data).toEqual(overriddenResult); } - ); + }); - itAsync("should allow fragments on root query", (resolve, reject) => { + it("should allow fragments on root query", async () => { const query = gql` query { ...QueryFragment @@ -330,42 +322,39 @@ describe("client", () => { __typename: "Query", }; - return clientRoundtrip(resolve, reject, query, { data }, null); + return clientRoundtrip(query, { data }, null); }); - itAsync( - "should allow fragments on root query with ifm", - (resolve, reject) => { - const query = gql` - query { - ...QueryFragment - } + it("should allow fragments on root query with ifm", async () => { + const query = gql` + query { + ...QueryFragment + } - fragment QueryFragment on Query { - records { - id - name - __typename - } + fragment QueryFragment on Query { + records { + id + name __typename } - `; + __typename + } + `; - const data = { - records: [ - { id: 1, name: "One", __typename: "Record" }, - { id: 2, name: "Two", __typename: "Record" }, - ], - __typename: "Query", - }; + const data = { + records: [ + { id: 1, name: "One", __typename: "Record" }, + { id: 2, name: "Two", __typename: "Record" }, + ], + __typename: "Query", + }; - return clientRoundtrip(resolve, reject, query, { data }, null, { - Query: ["Record"], - }); - } - ); + await clientRoundtrip(query, { data }, null, { + Query: ["Record"], + }); + }); - itAsync("should merge fragments on root query", (resolve, reject) => { + it("should merge fragments on root query", async () => { // The fragment should be used after the selected fields for the query. // Otherwise, the results aren't merged. // see: https://github.com/apollographql/apollo-client/issues/1479 @@ -395,12 +384,12 @@ describe("client", () => { __typename: "Query", }; - return clientRoundtrip(resolve, reject, query, { data }, null, { + await clientRoundtrip(query, { data }, null, { Query: ["Record"], }); }); - itAsync("store can be rehydrated from the server", (resolve, reject) => { + it("store can be rehydrated from the server", async () => { const query = gql` query people { allPeople(first: 1) { @@ -424,7 +413,7 @@ describe("client", () => { const link = mockSingleLink({ request: { query }, result: { data }, - }).setOnError(reject); + }); const initialState: any = { data: { @@ -450,269 +439,238 @@ describe("client", () => { ), }); - return client - .query({ query }) - .then((result) => { - expect(result.data).toEqual(data); - expect(finalState.data).toEqual( - (client.cache as InMemoryCache).extract() - ); - }) - .then(resolve, reject); + const result = await client.query({ query }); + + expect(result.data).toEqual(data); + expect(finalState.data).toEqual((client.cache as InMemoryCache).extract()); }); - itAsync( - "store can be rehydrated from the server using the shadow method", - (resolve, reject) => { - const query = gql` - query people { - allPeople(first: 1) { - people { - name - } + it("store can be rehydrated from the server using the shadow method", async () => { + const query = gql` + query people { + allPeople(first: 1) { + people { + name } } - `; + } + `; - const data = { - allPeople: { - people: [ - { - name: "Luke Skywalker", - }, - ], - }, - }; + const data = { + allPeople: { + people: [ + { + name: "Luke Skywalker", + }, + ], + }, + }; - const link = mockSingleLink({ - request: { query }, - result: { data }, - }).setOnError(reject); + const link = mockSingleLink({ + request: { query }, + result: { data }, + }); - const initialState: any = { - data: { - ROOT_QUERY: { - 'allPeople({"first":1})': { - people: [ - { - name: "Luke Skywalker", - }, - ], - }, + const initialState: any = { + data: { + ROOT_QUERY: { + 'allPeople({"first":1})': { + people: [ + { + name: "Luke Skywalker", + }, + ], }, - optimistic: [], }, - }; + optimistic: [], + }, + }; - const finalState = assign({}, initialState, {}); + const finalState = assign({}, initialState, {}); - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ addTypename: false }).restore( - initialState.data - ), - }); + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ addTypename: false }).restore( + initialState.data + ), + }); - return client - .query({ query }) - .then((result) => { - expect(result.data).toEqual(data); - expect(finalState.data).toEqual(client.extract()); - }) - .then(resolve, reject); - } - ); + const result = await client.query({ query }); - itAsync( - "stores shadow of restore returns the same result as accessing the method directly on the cache", - (resolve, reject) => { - const query = gql` - query people { - allPeople(first: 1) { - people { - name - } - } - } - `; + expect(result.data).toEqual(data); + expect(finalState.data).toEqual(client.extract()); + }); - const data = { - allPeople: { - people: [ - { - name: "Luke Skywalker", - }, - ], - }, - }; - - const link = mockSingleLink({ - request: { query }, - result: { data }, - }).setOnError(reject); + it("stores shadow of restore returns the same result as accessing the method directly on the cache", async () => { + const query = gql` + query people { + allPeople(first: 1) { + people { + name + } + } + } + `; - const initialState: any = { - data: { - 'ROOT_QUERY.allPeople({"first":"1"}).people.0': { + const data = { + allPeople: { + people: [ + { name: "Luke Skywalker", }, - 'ROOT_QUERY.allPeople({"first":1})': { - people: [ - { - type: "id", - generated: true, - id: 'ROOT_QUERY.allPeople({"first":"1"}).people.0', - }, - ], - }, - ROOT_QUERY: { - 'allPeople({"first":1})': { + ], + }, + }; + + const link = mockSingleLink({ + request: { query }, + result: { data }, + }); + + const initialState: any = { + data: { + 'ROOT_QUERY.allPeople({"first":"1"}).people.0': { + name: "Luke Skywalker", + }, + 'ROOT_QUERY.allPeople({"first":1})': { + people: [ + { type: "id", - id: 'ROOT_QUERY.allPeople({"first":1})', generated: true, + id: 'ROOT_QUERY.allPeople({"first":"1"}).people.0', }, + ], + }, + ROOT_QUERY: { + 'allPeople({"first":1})': { + type: "id", + id: 'ROOT_QUERY.allPeople({"first":1})', + generated: true, }, - optimistic: [], }, - }; - - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ addTypename: false }).restore( - initialState.data - ), - }); + optimistic: [], + }, + }; - expect(client.restore(initialState.data)).toEqual( - client.cache.restore(initialState.data) - ); + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ addTypename: false }).restore( + initialState.data + ), + }); - resolve(); - } - ); + expect(client.restore(initialState.data)).toEqual( + client.cache.restore(initialState.data) + ); + }); - itAsync( - "should return errors correctly for a single query", - (resolve, reject) => { - const query = gql` - query people { - allPeople(first: 1) { - people { - name - } + it("should return errors correctly for a single query", async () => { + const query = gql` + query people { + allPeople(first: 1) { + people { + name } } - `; + } + `; - const errors: GraphQLError[] = [ - new GraphQLError( - "Syntax Error GraphQL request (8:9) Expected Name, found EOF" - ), - ]; + const errors: GraphQLError[] = [ + new GraphQLError( + "Syntax Error GraphQL request (8:9) Expected Name, found EOF" + ), + ]; - const link = mockSingleLink({ - request: { query }, - result: { errors }, - }).setOnError(reject); + const link = mockSingleLink({ + request: { query }, + result: { errors }, + }); - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ addTypename: false }), - }); + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ addTypename: false }), + }); - return client - .query({ query }) - .catch((error: ApolloError) => { - expect(error.graphQLErrors).toEqual(errors); - }) - .then(resolve, reject); - } - ); + await expect(client.query({ query })).rejects.toEqual( + expect.objectContaining({ graphQLErrors: errors }) + ); + }); - itAsync( - "should return GraphQL errors correctly for a single query with an apollo-link enabled network interface", - (resolve, reject) => { - const query = gql` - query people { - allPeople(first: 1) { - people { - name - } + it("should return GraphQL errors correctly for a single query with an apollo-link enabled network interface", async () => { + const query = gql` + query people { + allPeople(first: 1) { + people { + name } } - `; + } + `; - const data = { - allPeople: { - people: [ - { - name: "Luke Skywalker", - }, - ], - }, - }; + const data = { + allPeople: { + people: [ + { + name: "Luke Skywalker", + }, + ], + }, + }; - const errors: GraphQLError[] = [ - new GraphQLError( - "Syntax Error GraphQL request (8:9) Expected Name, found EOF" - ), - ]; + const errors: GraphQLError[] = [ + new GraphQLError( + "Syntax Error GraphQL request (8:9) Expected Name, found EOF" + ), + ]; - const link = ApolloLink.from([ - () => { - return new Observable((observer) => { - observer.next({ data, errors }); - }); - }, - ]); + const link = ApolloLink.from([ + () => { + return new Observable((observer) => { + observer.next({ data, errors }); + }); + }, + ]); - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ addTypename: false }), - }); + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ addTypename: false }), + }); - client.query({ query }).catch((error: ApolloError) => { - expect(error.graphQLErrors).toEqual(errors); - resolve(); - }); - } - ); + await expect(client.query({ query })).rejects.toEqual( + expect.objectContaining({ graphQLErrors: errors }) + ); + }); - itAsync( - "should pass a network error correctly on a query with apollo-link network interface", - (resolve, reject) => { - const query = gql` - query people { - allPeople(first: 1) { - people { - name - } + it("should pass a network error correctly on a query with apollo-link network interface", async () => { + const query = gql` + query people { + allPeople(first: 1) { + people { + name } } - `; + } + `; - const networkError = new Error("Some kind of network error."); + const networkError = new Error("Some kind of network error."); - const link = ApolloLink.from([ - () => { - return new Observable((_) => { - throw networkError; - }); - }, - ]); + const link = ApolloLink.from([ + () => { + return new Observable((_) => { + throw networkError; + }); + }, + ]); - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ addTypename: false }), - }); + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ addTypename: false }), + }); - client.query({ query }).catch((error: ApolloError) => { - expect(error.networkError).toBeDefined(); - expect(error.networkError!.message).toEqual(networkError.message); - resolve(); - }); - } - ); + await expect(client.query({ query })).rejects.toThrow( + new ApolloError({ networkError }) + ); + }); it("should not warn when receiving multiple results from apollo-link network interface", () => { const query = gql` @@ -747,117 +705,22 @@ describe("client", () => { }); }); - itAsync.skip( - "should surface errors in observer.next as uncaught", - (resolve, reject) => { - const expectedError = new Error("this error should not reach the store"); - const listeners = process.listeners("uncaughtException"); - const oldHandler = listeners[listeners.length - 1]; - const handleUncaught = (e: Error) => { - console.log(e); - process.removeListener("uncaughtException", handleUncaught); - if (typeof oldHandler === "function") - process.addListener("uncaughtException", oldHandler); - if (e === expectedError) { - resolve(); - } else { - reject(e); - } - }; - process.removeListener("uncaughtException", oldHandler); - process.addListener("uncaughtException", handleUncaught); - - const query = gql` - query people { - allPeople(first: 1) { - people { - name - } - } - } - `; - - const data = { - allPeople: { - people: [ - { - name: "Luke Skywalker", - }, - ], - }, - }; - - const link = mockSingleLink({ - request: { query }, - result: { data }, - }).setOnError(reject); - - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ addTypename: false }), - }); - - const handle = client.watchQuery({ query }); - - handle.subscribe({ - next() { - throw expectedError; - }, - }); - } - ); - - itAsync.skip( - "should surfaces errors in observer.error as uncaught", - (resolve, reject) => { - const expectedError = new Error("this error should not reach the store"); - const listeners = process.listeners("uncaughtException"); - const oldHandler = listeners[listeners.length - 1]; - const handleUncaught = (e: Error) => { - process.removeListener("uncaughtException", handleUncaught); + it.skip("should surface errors in observer.next as uncaught", async () => { + const expectedError = new Error("this error should not reach the store"); + const listeners = process.listeners("uncaughtException"); + const oldHandler = listeners[listeners.length - 1]; + const handleUncaught = (e: Error) => { + console.log(e); + process.removeListener("uncaughtException", handleUncaught); + if (typeof oldHandler === "function") process.addListener("uncaughtException", oldHandler); - if (e === expectedError) { - resolve(); - } else { - reject(e); - } - }; - process.removeListener("uncaughtException", oldHandler); - process.addListener("uncaughtException", handleUncaught); - - const query = gql` - query people { - allPeople(first: 1) { - people { - name - } - } - } - `; - - const link = mockSingleLink({ - request: { query }, - result: {}, - }).setOnError(reject); - - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ addTypename: false }), - }); - - const handle = client.watchQuery({ query }); - handle.subscribe({ - next() { - reject(new Error("did not expect next to be called")); - }, - error() { - throw expectedError; - }, - }); - } - ); + if (e !== expectedError) { + throw e; + } + }; + process.removeListener("uncaughtException", oldHandler); + process.addListener("uncaughtException", handleUncaught); - itAsync("should allow for subscribing to a request", (resolve, reject) => { const query = gql` query people { allPeople(first: 1) { @@ -881,7 +744,7 @@ describe("client", () => { const link = mockSingleLink({ request: { query }, result: { data }, - }).setOnError(reject); + }); const client = new ApolloClient({ link, @@ -891,37 +754,118 @@ describe("client", () => { const handle = client.watchQuery({ query }); handle.subscribe({ - next(result) { - expect(result.data).toEqual(data); - resolve(); + next() { + throw expectedError; }, }); }); - itAsync("should be able to transform queries", (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } + it.skip("should surfaces errors in observer.error as uncaught", async () => { + const expectedError = new Error("this error should not reach the store"); + const listeners = process.listeners("uncaughtException"); + const oldHandler = listeners[listeners.length - 1]; + const handleUncaught = (e: Error) => { + process.removeListener("uncaughtException", handleUncaught); + process.addListener("uncaughtException", oldHandler); + if (e !== expectedError) { + throw e; } - `; - const transformedQuery = gql` - query { - author { - firstName - lastName - __typename + }; + process.removeListener("uncaughtException", oldHandler); + process.addListener("uncaughtException", handleUncaught); + + const query = gql` + query people { + allPeople(first: 1) { + people { + name + } } } `; - const result = { - author: { - firstName: "John", - lastName: "Smith", - }, + const link = mockSingleLink({ + request: { query }, + result: {}, + }); + + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ addTypename: false }), + }); + + const handle = client.watchQuery({ query }); + handle.subscribe({ + next() { + throw new Error("did not expect next to be called"); + }, + error() { + throw expectedError; + }, + }); + }); + + it("should allow for subscribing to a request", async () => { + const query = gql` + query people { + allPeople(first: 1) { + people { + name + } + } + } + `; + + const data = { + allPeople: { + people: [ + { + name: "Luke Skywalker", + }, + ], + }, + }; + + const link = mockSingleLink({ + request: { query }, + result: { data }, + }); + + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ addTypename: false }), + }); + + const handle = client.watchQuery({ query }); + const stream = new ObservableStream(handle); + + await expect(stream).toEmitMatchedValue({ data }); + }); + + it("should be able to transform queries", async () => { + const query = gql` + query { + author { + firstName + lastName + } + } + `; + const transformedQuery = gql` + query { + author { + firstName + lastName + __typename + } + } + `; + + const result = { + author: { + firstName: "John", + lastName: "Smith", + }, }; const transformedResult = { author: { @@ -941,79 +885,73 @@ describe("client", () => { result: { data: transformedResult }, }, false - ).setOnError(reject); + ); const client = new ApolloClient({ link, cache: new InMemoryCache({ addTypename: true }), }); - return client - .query({ query }) - .then((actualResult) => { - expect(actualResult.data).toEqual(transformedResult); - }) - .then(resolve, reject); + const actualResult = await client.query({ query }); + + expect(actualResult.data).toEqual(transformedResult); }); - itAsync( - "should be able to transform queries on network-only fetches", - (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } + it("should be able to transform queries on network-only fetches", async () => { + const query = gql` + query { + author { + firstName + lastName } - `; - const transformedQuery = gql` - query { - author { - firstName - lastName - __typename - } + } + `; + const transformedQuery = gql` + query { + author { + firstName + lastName + __typename } - `; - const result = { - author: { - firstName: "John", - lastName: "Smith", - }, - }; - const transformedResult = { - author: { - firstName: "John", - lastName: "Smith", - __typename: "Author", - }, - }; - const link = mockSingleLink( - { - request: { query }, - result: { data: result }, - }, - { - request: { query: transformedQuery }, - result: { data: transformedResult }, - }, - false - ).setOnError(reject); + } + `; + const result = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; + const transformedResult = { + author: { + firstName: "John", + lastName: "Smith", + __typename: "Author", + }, + }; + const link = mockSingleLink( + { + request: { query }, + result: { data: result }, + }, + { + request: { query: transformedQuery }, + result: { data: transformedResult }, + }, + false + ); - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ addTypename: true }), - }); + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ addTypename: true }), + }); - return client - .query({ fetchPolicy: "network-only", query }) - .then((actualResult) => { - expect(actualResult.data).toEqual(transformedResult); - }) - .then(resolve, reject); - } - ); + const actualResult = await client.query({ + fetchPolicy: "network-only", + query, + }); + + expect(actualResult.data).toEqual(transformedResult); + }); it("removes @client fields from the query before it reaches the link", async () => { const result: { current: Operation | undefined } = { @@ -1063,7 +1001,7 @@ describe("client", () => { expect(print(result.current!.query)).toEqual(print(transformedQuery)); }); - itAsync("should handle named fragments on mutations", (resolve, reject) => { + it("should handle named fragments on mutations", async () => { const mutation = gql` mutation { starAuthor(id: 12) { @@ -1091,117 +1029,64 @@ describe("client", () => { const link = mockSingleLink({ request: { query: mutation }, result: { data: result }, - }).setOnError(reject); + }); const client = new ApolloClient({ link, cache: new InMemoryCache({ addTypename: false }), }); - return client - .mutate({ mutation }) - .then((actualResult) => { - expect(actualResult.data).toEqual(result); - }) - .then(resolve, reject); - }); - - itAsync( - "should be able to handle named fragments on network-only queries", - (resolve, reject) => { - const query = gql` - fragment authorDetails on Author { - firstName - lastName - } - - query { - author { - __typename - ...authorDetails - } - } - `; - const result = { - author: { - __typename: "Author", - firstName: "John", - lastName: "Smith", - }, - }; - - const link = mockSingleLink({ - request: { query }, - result: { data: result }, - }).setOnError(reject); + const actualResult = await client.mutate({ mutation }); - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ addTypename: false }), - }); + expect(actualResult.data).toEqual(result); + }); - return client - .query({ fetchPolicy: "network-only", query }) - .then((actualResult) => { - expect(actualResult.data).toEqual(result); - }) - .then(resolve, reject); - } - ); + it("should be able to handle named fragments on network-only queries", async () => { + const query = gql` + fragment authorDetails on Author { + firstName + lastName + } - itAsync( - "should be able to handle named fragments with multiple fragments", - (resolve, reject) => { - const query = gql` - query { - author { - __typename - ...authorDetails - ...moreDetails - } + query { + author { + __typename + ...authorDetails } + } + `; + const result = { + author: { + __typename: "Author", + firstName: "John", + lastName: "Smith", + }, + }; - fragment authorDetails on Author { - firstName - lastName - } + const link = mockSingleLink({ + request: { query }, + result: { data: result }, + }); - fragment moreDetails on Author { - address - } - `; - const result = { - author: { - __typename: "Author", - firstName: "John", - lastName: "Smith", - address: "1337 10th St.", - }, - }; + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ addTypename: false }), + }); - const link = mockSingleLink({ - request: { query }, - result: { data: result }, - }).setOnError(reject); - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ addTypename: false }), - }); + const actualResult = await client.query({ + fetchPolicy: "network-only", + query, + }); - return client - .query({ query }) - .then((actualResult) => { - expect(actualResult.data).toEqual(result); - }) - .then(resolve, reject); - } - ); + expect(actualResult.data).toEqual(result); + }); - itAsync("should be able to handle named fragments", (resolve, reject) => { + it("should be able to handle named fragments with multiple fragments", async () => { const query = gql` query { author { __typename ...authorDetails + ...moreDetails } } @@ -1209,240 +1094,251 @@ describe("client", () => { firstName lastName } + + fragment moreDetails on Author { + address + } `; const result = { author: { __typename: "Author", firstName: "John", lastName: "Smith", + address: "1337 10th St.", }, }; const link = mockSingleLink({ request: { query }, result: { data: result }, - }).setOnError(reject); + }); const client = new ApolloClient({ link, cache: new InMemoryCache({ addTypename: false }), }); - return client - .query({ query }) - .then((actualResult) => { - expect(actualResult.data).toEqual(result); - }) - .then(resolve, reject); - }); + const actualResult = await client.query({ query }); - itAsync( - "should be able to handle inlined fragments on an Interface type", - (resolve, reject) => { - const query = gql` - query items { - items { - ...ItemFragment - __typename - } - } + expect(actualResult.data).toEqual(result); + }); - fragment ItemFragment on Item { - id - __typename - ... on ColorItem { - color - __typename - } + it("should be able to handle named fragments", async () => { + const query = gql` + query { + author { + __typename + ...authorDetails } - `; - const result = { - items: [ - { - __typename: "ColorItem", - id: "27tlpoPeXm6odAxj3paGQP", - color: "red", - }, - { - __typename: "MonochromeItem", - id: "1t3iFLsHBm4c4RjOMdMgOO", - }, - ], - }; + } - const link = mockSingleLink({ - request: { query }, - result: { data: result }, - }).setOnError(reject); - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ - possibleTypes: { - Item: ["ColorItem", "MonochromeItem"], - }, - }), - }); - return client - .query({ query }) - .then((actualResult: any) => { - expect(actualResult.data).toEqual(result); - }) - .then(resolve, reject); - } - ); + fragment authorDetails on Author { + firstName + lastName + } + `; + const result = { + author: { + __typename: "Author", + firstName: "John", + lastName: "Smith", + }, + }; - itAsync( - "should be able to handle inlined fragments on an Interface type with introspection fragment matcher", - (resolve, reject) => { - const query = gql` - query items { - items { - ...ItemFragment - __typename - } - } + const link = mockSingleLink({ + request: { query }, + result: { data: result }, + }); + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ addTypename: false }), + }); - fragment ItemFragment on Item { - id - ... on ColorItem { - color - __typename - } + const actualResult = await client.query({ query }); + + expect(actualResult.data).toEqual(result); + }); + + it("should be able to handle inlined fragments on an Interface type", async () => { + const query = gql` + query items { + items { + ...ItemFragment __typename } - `; - const result = { - items: [ - { - __typename: "ColorItem", - id: "27tlpoPeXm6odAxj3paGQP", - color: "red", - }, - { - __typename: "MonochromeItem", - id: "1t3iFLsHBm4c4RjOMdMgOO", - }, - ], - }; + } - const link = mockSingleLink({ - request: { query }, - result: { data: result }, - }).setOnError(reject); + fragment ItemFragment on Item { + id + __typename + ... on ColorItem { + color + __typename + } + } + `; + const result = { + items: [ + { + __typename: "ColorItem", + id: "27tlpoPeXm6odAxj3paGQP", + color: "red", + }, + { + __typename: "MonochromeItem", + id: "1t3iFLsHBm4c4RjOMdMgOO", + }, + ], + }; - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ - possibleTypes: { - Item: ["ColorItem", "MonochromeItem"], - }, - }), - }); + const link = mockSingleLink({ + request: { query }, + result: { data: result }, + }); + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ + possibleTypes: { + Item: ["ColorItem", "MonochromeItem"], + }, + }), + }); + const actualResult = await client.query({ query }); - return client - .query({ query }) - .then((actualResult) => { - expect(actualResult.data).toEqual(result); - }) - .then(resolve, reject); - } - ); + expect(actualResult.data).toEqual(result); + }); - itAsync( - "should call updateQueries and update after mutation on query with inlined fragments on an Interface type", - (resolve, reject) => { - const query = gql` - query items { - items { - ...ItemFragment - __typename - } + it("should be able to handle inlined fragments on an Interface type with introspection fragment matcher", async () => { + const query = gql` + query items { + items { + ...ItemFragment + __typename } + } - fragment ItemFragment on Item { - id - ... on ColorItem { - color - __typename - } + fragment ItemFragment on Item { + id + ... on ColorItem { + color __typename } - `; - const result = { - items: [ - { - __typename: "ColorItem", - id: "27tlpoPeXm6odAxj3paGQP", - color: "red", - }, - { - __typename: "MonochromeItem", - id: "1t3iFLsHBm4c4RjOMdMgOO", - }, - ], - }; + __typename + } + `; + const result = { + items: [ + { + __typename: "ColorItem", + id: "27tlpoPeXm6odAxj3paGQP", + color: "red", + }, + { + __typename: "MonochromeItem", + id: "1t3iFLsHBm4c4RjOMdMgOO", + }, + ], + }; - const mutation = gql` - mutation myMutationName { - fortuneCookie + const link = mockSingleLink({ + request: { query }, + result: { data: result }, + }); + + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ + possibleTypes: { + Item: ["ColorItem", "MonochromeItem"], + }, + }), + }); + + const actualResult = await client.query({ query }); + + expect(actualResult.data).toEqual(result); + }); + + it("should call updateQueries and update after mutation on query with inlined fragments on an Interface type", async () => { + const query = gql` + query items { + items { + ...ItemFragment + __typename } - `; - const mutationResult = { - fortuneCookie: "The waiter spit in your food", - }; + } - const link = mockSingleLink( + fragment ItemFragment on Item { + id + ... on ColorItem { + color + __typename + } + __typename + } + `; + const result = { + items: [ { - request: { query }, - result: { data: result }, + __typename: "ColorItem", + id: "27tlpoPeXm6odAxj3paGQP", + color: "red", }, { - request: { query: mutation }, - result: { data: mutationResult }, - } - ).setOnError(reject); + __typename: "MonochromeItem", + id: "1t3iFLsHBm4c4RjOMdMgOO", + }, + ], + }; - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ - possibleTypes: { - Item: ["ColorItem", "MonochromeItem"], - }, - }), - }); + const mutation = gql` + mutation myMutationName { + fortuneCookie + } + `; + const mutationResult = { + fortuneCookie: "The waiter spit in your food", + }; - const queryUpdaterSpy = jest.fn(); - const queryUpdater = (prev: any) => { - queryUpdaterSpy(); - return prev; - }; - const updateQueries = { - items: queryUpdater, - }; + const link = mockSingleLink( + { + request: { query }, + result: { data: result }, + }, + { + request: { query: mutation }, + result: { data: mutationResult }, + } + ); - const updateSpy = jest.fn(); + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ + possibleTypes: { + Item: ["ColorItem", "MonochromeItem"], + }, + }), + }); - const obs = client.watchQuery({ query }); + const queryUpdaterSpy = jest.fn(); + const queryUpdater = (prev: any) => { + queryUpdaterSpy(); + return prev; + }; + const updateQueries = { + items: queryUpdater, + }; - const sub = obs.subscribe({ - next() { - client - .mutate({ mutation, updateQueries, update: updateSpy }) - .then(() => { - expect(queryUpdaterSpy).toBeCalled(); - expect(updateSpy).toBeCalled(); - sub.unsubscribe(); - resolve(); - }) - .catch((err) => { - reject(err); - }); - }, - error(err) { - reject(err); - }, - }); - } - ); + const updateSpy = jest.fn(); + + const obs = client.watchQuery({ query }); + const stream = new ObservableStream(obs); + + await expect(stream).toEmitNext(); + await client.mutate({ mutation, updateQueries, update: updateSpy }); + + expect(queryUpdaterSpy).toBeCalled(); + expect(updateSpy).toBeCalled(); + }); it("should send operationName along with the query to the server", () => { const query = gql` @@ -1494,61 +1390,7 @@ describe("client", () => { }); }); - itAsync( - "does not deduplicate queries if option is set to false", - (resolve, reject) => { - const queryDoc = gql` - query { - author { - name - } - } - `; - const data = { - author: { - name: "Jonas", - }, - }; - const data2 = { - author: { - name: "Dhaivat", - }, - }; - - // we have two responses for identical queries, and both should be requested. - // the second one should make it through to the network interface. - const link = mockSingleLink( - { - request: { query: queryDoc }, - result: { data }, - delay: 10, - }, - { - request: { query: queryDoc }, - result: { data: data2 }, - } - ).setOnError(reject); - - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ addTypename: false }), - queryDeduplication: false, - }); - - const q1 = client.query({ query: queryDoc }); - const q2 = client.query({ query: queryDoc }); - - // if deduplication happened, result2.data will equal data. - return Promise.all([q1, q2]) - .then(([result1, result2]) => { - expect(result1.data).toEqual(data); - expect(result2.data).toEqual(data2); - }) - .then(resolve, reject); - } - ); - - itAsync("deduplicates queries by default", (resolve, reject) => { + it("does not deduplicate queries if option is set to false", async () => { const queryDoc = gql` query { author { @@ -1567,8 +1409,8 @@ describe("client", () => { }, }; - // we have two responses for identical queries, but only the first should be requested. - // the second one should never make it through to the network interface. + // we have two responses for identical queries, and both should be requested. + // the second one should make it through to the network interface. const link = mockSingleLink( { request: { query: queryDoc }, @@ -1579,24 +1421,25 @@ describe("client", () => { request: { query: queryDoc }, result: { data: data2 }, } - ).setOnError(reject); + ); + const client = new ApolloClient({ link, cache: new InMemoryCache({ addTypename: false }), + queryDeduplication: false, }); const q1 = client.query({ query: queryDoc }); const q2 = client.query({ query: queryDoc }); - // if deduplication didn't happen, result.data will equal data2. - return Promise.all([q1, q2]) - .then(([result1, result2]) => { - expect(result1.data).toEqual(result2.data); - }) - .then(resolve, reject); + // if deduplication happened, result2.data will equal data. + const [result1, result2] = await Promise.all([q1, q2]); + + expect(result1.data).toEqual(data); + expect(result2.data).toEqual(data2); }); - it("deduplicates queries if query context.queryDeduplication is set to true", () => { + it("deduplicates queries by default", async () => { const queryDoc = gql` query { author { @@ -1631,24 +1474,70 @@ describe("client", () => { const client = new ApolloClient({ link, cache: new InMemoryCache({ addTypename: false }), - queryDeduplication: false, }); - // Both queries need to be deduplicated, otherwise only one gets tracked - const q1 = client.query({ - query: queryDoc, - context: { queryDeduplication: true }, - }); - const q2 = client.query({ - query: queryDoc, - context: { queryDeduplication: true }, - }); + const q1 = client.query({ query: queryDoc }); + const q2 = client.query({ query: queryDoc }); - // if deduplication happened, result2.data will equal data. - return Promise.all([q1, q2]).then(([result1, result2]) => { - expect(result1.data).toEqual(data); - expect(result2.data).toEqual(data); - }); + // if deduplication didn't happen, result.data will equal data2. + const [result1, result2] = await Promise.all([q1, q2]); + + expect(result1.data).toEqual(result2.data); + }); + + it("deduplicates queries if query context.queryDeduplication is set to true", () => { + const queryDoc = gql` + query { + author { + name + } + } + `; + const data = { + author: { + name: "Jonas", + }, + }; + const data2 = { + author: { + name: "Dhaivat", + }, + }; + + // we have two responses for identical queries, but only the first should be requested. + // the second one should never make it through to the network interface. + const link = mockSingleLink( + { + request: { query: queryDoc }, + result: { data }, + delay: 10, + }, + { + request: { query: queryDoc }, + result: { data: data2 }, + } + ); + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ addTypename: false }), + queryDeduplication: false, + }); + + // Both queries need to be deduplicated, otherwise only one gets tracked + const q1 = client.query({ + query: queryDoc, + context: { queryDeduplication: true }, + }); + const q2 = client.query({ + query: queryDoc, + context: { queryDeduplication: true }, + }); + + // if deduplication happened, result2.data will equal data. + return Promise.all([q1, q2]).then(([result1, result2]) => { + expect(result1.data).toEqual(data); + expect(result2.data).toEqual(data); + }); }); it("does not deduplicate queries if query context.queryDeduplication is set to false", () => { @@ -1702,53 +1591,53 @@ describe("client", () => { }); }); - itAsync( - "unsubscribes from deduplicated observables only once", - (resolve, reject) => { - const document: DocumentNode = gql` - query test1($x: String) { - test(x: $x) - } - `; + it("unsubscribes from deduplicated observables only once", async () => { + const document: DocumentNode = gql` + query test1($x: String) { + test(x: $x) + } + `; - const variables1 = { x: "Hello World" }; - const variables2 = { x: "Hello World" }; + const variables1 = { x: "Hello World" }; + const variables2 = { x: "Hello World" }; - let unsubscribed = false; + let unsubscribeCount = 0; - const client = new ApolloClient({ - link: new ApolloLink(() => { - return new Observable((observer) => { - observer.complete(); - return () => { - unsubscribed = true; - setTimeout(resolve, 0); - }; - }); - }), - cache: new InMemoryCache(), - }); + const client = new ApolloClient({ + link: new ApolloLink(() => { + return new Observable((observer) => { + observer.complete(); + return () => { + unsubscribeCount++; + }; + }); + }), + cache: new InMemoryCache(), + }); - const sub1 = client - .watchQuery({ - query: document, - variables: variables1, - }) - .subscribe({}); + const sub1 = client + .watchQuery({ + query: document, + variables: variables1, + }) + .subscribe({}); - const sub2 = client - .watchQuery({ - query: document, - variables: variables2, - }) - .subscribe({}); + const sub2 = client + .watchQuery({ + query: document, + variables: variables2, + }) + .subscribe({}); - sub1.unsubscribe(); - expect(unsubscribed).toBe(false); + sub1.unsubscribe(); + // cleanup happens async + expect(unsubscribeCount).toBe(0); - sub2.unsubscribe(); - } - ); + sub2.unsubscribe(); + + await wait(0); + expect(unsubscribeCount).toBe(1); + }); describe("deprecated options", () => { const query = gql` @@ -1801,11 +1690,11 @@ describe("client", () => { }, }; - itAsync("for internal store", (resolve, reject) => { + it("for internal store", async () => { const link = mockSingleLink({ request: { query }, result: { data }, - }).setOnError(reject); + }); const client = new ApolloClient({ link, @@ -1816,16 +1705,13 @@ describe("client", () => { }), }); - return client - .query({ query }) - .then((result) => { - expect(result.data).toEqual(data); - expect((client.cache as InMemoryCache).extract()["1"]).toEqual({ - id: "1", - name: "Luke Skywalker", - }); - }) - .then(resolve, reject); + const result = await client.query({ query }); + + expect(result.data).toEqual(data); + expect((client.cache as InMemoryCache).extract()["1"]).toEqual({ + id: "1", + name: "Luke Skywalker", + }); }); }); @@ -1855,15 +1741,6 @@ describe("client", () => { "to receive multiple results from the cache and the network, or consider " + "using a different fetchPolicy, such as cache-first or network-only."; - function checkCacheAndNetworkError(callback: () => any) { - try { - callback(); - throw new Error("not reached"); - } catch (thrown) { - expect((thrown as Error).message).toBe(cacheAndNetworkError); - } - } - // Test that cache-and-network can only be used on watchQuery, not query. it("warns when used with client.query", () => { const client = new ApolloClient({ @@ -1871,12 +1748,12 @@ describe("client", () => { cache: new InMemoryCache(), }); - checkCacheAndNetworkError(() => - client.query({ + expect(() => { + void client.query({ query, fetchPolicy: "cache-and-network" as FetchPolicy, - }) - ); + }); + }).toThrow(new Error(cacheAndNetworkError)); }); it("warns when used with client.query with defaultOptions", () => { @@ -1890,14 +1767,15 @@ describe("client", () => { }, }); - checkCacheAndNetworkError(() => - client.query({ - query, - // This undefined value should be ignored in favor of - // defaultOptions.query.fetchPolicy. - fetchPolicy: void 0, - }) - ); + expect( + () => + void client.query({ + query, + // This undefined value should be ignored in favor of + // defaultOptions.query.fetchPolicy. + fetchPolicy: void 0, + }) + ).toThrow(new Error(cacheAndNetworkError)); }); it("fetches from cache first, then network", async () => { @@ -1950,7 +1828,7 @@ describe("client", () => { await expect(stream).not.toEmitAnything(); }); - itAsync("fails if network request fails", (resolve, reject) => { + it("fails if network request fails", async () => { const link = mockSingleLink(); // no queries = no replies. const client = new ApolloClient({ link, @@ -1961,59 +1839,42 @@ describe("client", () => { query, fetchPolicy: "cache-and-network", }); + const stream = new ObservableStream(obs); - obs.subscribe({ - error: (e) => { - if (!/No more mocked responses/.test(e.message)) { - reject(e); - } else { - resolve(); - } - }, - }); + const error = await stream.takeError(); + + expect(error.message).toMatch(/No more mocked responses/); }); - itAsync( - "fetches from cache first, then network and does not have an unhandled error", - (resolve, reject) => { - const link = mockSingleLink({ - request: { query }, - result: { errors: [{ message: "network failure" }] }, - }).setOnError(reject); + it("fetches from cache first, then network and does not have an unhandled error", async () => { + const link = mockSingleLink({ + request: { query }, + result: { errors: [{ message: "network failure" }] }, + }); - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ addTypename: false }), - }); + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ addTypename: false }), + }); - client.writeQuery({ query, data: initialData }); + client.writeQuery({ query, data: initialData }); - const obs = client.watchQuery({ - query, - fetchPolicy: "cache-and-network", - }); - let shouldFail = true; - process.once("unhandledRejection", (rejection) => { - if (shouldFail) reject("promise had an unhandledRejection"); - }); - let count = 0; - obs.subscribe({ - next: (result) => { - expect(result.data).toEqual(initialData); - expect(result.loading).toBe(true); - count++; - }, - error: (e) => { - expect(e.message).toMatch(/network failure/); - expect(count).toBe(1); // make sure next was called. - setTimeout(() => { - shouldFail = false; - resolve(); - }, 0); - }, - }); - } - ); + const obs = client.watchQuery({ + query, + fetchPolicy: "cache-and-network", + }); + const stream = new ObservableStream(obs); + + await expect(stream).toEmitValue({ + loading: true, + data: initialData, + networkStatus: 1, + }); + + const error = await stream.takeError(); + + expect(error.message).toMatch(/network failure/); + }); }); describe("standby queries", () => { @@ -2096,7 +1957,7 @@ describe("client", () => { }, }; - function makeLink(reject: (reason: any) => any) { + function makeLink() { return mockSingleLink( { request: { query }, @@ -2106,31 +1967,26 @@ describe("client", () => { request: { query }, result: { data: secondFetch }, } - ).setOnError(reject); + ); } - itAsync("forces the query to rerun", (resolve, reject) => { + it("forces the query to rerun", async () => { const client = new ApolloClient({ - link: makeLink(reject), + link: makeLink(), cache: new InMemoryCache({ addTypename: false }), }); // Run a query first to initialize the store - return ( - client - .query({ query }) - // then query for real - .then(() => client.query({ query, fetchPolicy: "network-only" })) - .then((result) => { - expect(result.data).toEqual({ myNumber: { n: 2 } }); - }) - .then(resolve, reject) - ); + await client.query({ query }); + // then query for real + const result = await client.query({ query, fetchPolicy: "network-only" }); + + expect(result.data).toEqual({ myNumber: { n: 2 } }); }); - itAsync("can be disabled with ssrMode", (resolve, reject) => { + it("can be disabled with ssrMode", async () => { const client = new ApolloClient({ - link: makeLink(reject), + link: makeLink(), ssrMode: true, cache: new InMemoryCache({ addTypename: false }), }); @@ -2138,185 +1994,76 @@ describe("client", () => { const options: QueryOptions = { query, fetchPolicy: "network-only" }; // Run a query first to initialize the store - return ( - client - .query({ query }) - // then query for real - .then(() => client.query(options)) - .then((result) => { - expect(result.data).toEqual({ myNumber: { n: 1 } }); - // Test that options weren't mutated, issue #339 - expect(options).toEqual({ - query, - fetchPolicy: "network-only", - }); - }) - .then(resolve, reject) - ); - }); + await client.query({ query }); + // then query for real + const result = await client.query(options); - itAsync( - "can temporarily be disabled with ssrForceFetchDelay", - (resolve, reject) => { - const client = new ApolloClient({ - link: makeLink(reject), - ssrForceFetchDelay: 100, - cache: new InMemoryCache({ addTypename: false }), - }); - - // Run a query first to initialize the store - return ( - client - .query({ query }) - // then query for real - .then(() => { - return client.query({ query, fetchPolicy: "network-only" }); - }) - .then(async (result) => { - expect(result.data).toEqual({ myNumber: { n: 1 } }); - await new Promise((resolve) => setTimeout(resolve, 100)); - return client.query({ query, fetchPolicy: "network-only" }); - }) - .then((result) => { - expect(result.data).toEqual({ myNumber: { n: 2 } }); - }) - .then(resolve, reject) - ); - } - ); - }); + expect(result.data).toEqual({ myNumber: { n: 1 } }); + // Test that options weren't mutated, issue #339 + expect(options).toEqual({ + query, + fetchPolicy: "network-only", + }); + }); - itAsync( - "should pass a network error correctly on a mutation", - (resolve, reject) => { - const mutation = gql` - mutation { - person { - firstName - lastName - } - } - `; - const data = { - person: { - firstName: "John", - lastName: "Smith", - }, - }; - const networkError = new Error("Some kind of network error."); + it("can temporarily be disabled with ssrForceFetchDelay", async () => { const client = new ApolloClient({ - link: mockSingleLink({ - request: { query: mutation }, - result: { data }, - error: networkError, - }), + link: makeLink(), + ssrForceFetchDelay: 100, cache: new InMemoryCache({ addTypename: false }), }); - client - .mutate({ mutation }) - .then((_) => { - reject(new Error("Returned a result when it should not have.")); - }) - .catch((error: ApolloError) => { - expect(error.networkError).toBeDefined(); - expect(error.networkError!.message).toBe(networkError.message); - resolve(); + // Run a query first to initialize the store + await client.query({ query }); + // then query for real + { + const result = await client.query({ + query, + fetchPolicy: "network-only", }); - } - ); - itAsync( - "should pass a GraphQL error correctly on a mutation", - (resolve, reject) => { - const mutation = gql` - mutation { - newPerson { - person { - firstName - lastName - } - } - } - `; - const data = { - person: { - firstName: "John", - lastName: "Smith", - }, - }; - const errors = [new Error("Some kind of GraphQL error.")]; - const client = new ApolloClient({ - link: mockSingleLink({ - request: { query: mutation }, - result: { data, errors }, - }).setOnError(reject), - cache: new InMemoryCache({ addTypename: false }), - }); - client - .mutate({ mutation }) - .then((_) => { - reject(new Error("Returned a result when it should not have.")); - }) - .catch((error: ApolloError) => { - expect(error.graphQLErrors).toBeDefined(); - expect(error.graphQLErrors.length).toBe(1); - expect(error.graphQLErrors[0].message).toBe(errors[0].message); - resolve(); - }); - } - ); + expect(result.data).toEqual({ myNumber: { n: 1 } }); + } - itAsync( - "should allow errors to be returned from a mutation", - (resolve, reject) => { - const mutation = gql` - mutation { - newPerson { - person { - firstName - lastName - } - } + await wait(100); + + const result = await client.query({ query, fetchPolicy: "network-only" }); + + expect(result.data).toEqual({ myNumber: { n: 2 } }); + }); + }); + + it("should pass a network error correctly on a mutation", async () => { + const mutation = gql` + mutation { + person { + firstName + lastName } - `; - const data = { - person: { - firstName: "John", - lastName: "Smith", - }, - }; - const errors = [new Error("Some kind of GraphQL error.")]; - const client = new ApolloClient({ - link: mockSingleLink({ - request: { query: mutation }, - result: { - errors, - data: { - newPerson: data, - }, - }, - }).setOnError(reject), - cache: new InMemoryCache({ addTypename: false }), - }); - client - .mutate({ mutation, errorPolicy: "all" }) - .then((result) => { - expect(result.errors).toBeDefined(); - expect(result.errors!.length).toBe(1); - expect(result.errors![0].message).toBe(errors[0].message); - expect(result.data).toEqual({ - newPerson: data, - }); - resolve(); - }) - .catch((error: ApolloError) => { - throw error; - }); - } - ); + } + `; + const data = { + person: { + firstName: "John", + lastName: "Smith", + }, + }; + const networkError = new Error("Some kind of network error."); + const client = new ApolloClient({ + link: mockSingleLink({ + request: { query: mutation }, + result: { data }, + error: networkError, + }), + cache: new InMemoryCache({ addTypename: false }), + }); + + await expect(client.mutate({ mutation })).rejects.toThrow( + new ApolloError({ networkError }) + ); + }); - itAsync("should strip errors on a mutation if ignored", (resolve, reject) => { + it("should pass a GraphQL error correctly on a mutation", async () => { const mutation = gql` mutation { newPerson { @@ -2328,11 +2075,9 @@ describe("client", () => { } `; const data = { - newPerson: { - person: { - firstName: "John", - lastName: "Smith", - }, + person: { + firstName: "John", + lastName: "Smith", }, }; const errors = [new Error("Some kind of GraphQL error.")]; @@ -2340,80 +2085,144 @@ describe("client", () => { link: mockSingleLink({ request: { query: mutation }, result: { data, errors }, - }).setOnError(reject), + }), cache: new InMemoryCache({ addTypename: false }), }); - client - .mutate({ mutation, errorPolicy: "ignore" }) - .then((result) => { - expect(result.errors).toBeUndefined(); - expect(result.data).toEqual(data); - resolve(); - }) - .catch((error: ApolloError) => { - throw error; - }); + + await expect(client.mutate({ mutation })).rejects.toEqual( + expect.objectContaining({ graphQLErrors: errors }) + ); }); - itAsync( - "should rollback optimistic after mutation got a GraphQL error", - (resolve, reject) => { - const mutation = gql` - mutation { - newPerson { - person { - firstName - lastName - } - } - } - `; - const data = { - newPerson: { - person: { - firstName: "John", - lastName: "Smith", + it("should allow errors to be returned from a mutation", async () => { + const mutation = gql` + mutation { + newPerson { + person { + firstName + lastName + } + } + } + `; + const data = { + person: { + firstName: "John", + lastName: "Smith", + }, + }; + const errors = [new Error("Some kind of GraphQL error.")]; + const client = new ApolloClient({ + link: mockSingleLink({ + request: { query: mutation }, + result: { + errors, + data: { + newPerson: data, }, }, - }; - const errors = [new Error("Some kind of GraphQL error.")]; - const client = new ApolloClient({ - link: mockSingleLink({ - request: { query: mutation }, - result: { data, errors }, - }).setOnError(reject), - cache: new InMemoryCache({ addTypename: false }), - }); - const mutatePromise = client.mutate({ - mutation, - optimisticResponse: { - newPerson: { - person: { - firstName: "John*", - lastName: "Smith*", - }, - }, + }), + cache: new InMemoryCache({ addTypename: false }), + }); + + const result = await client.mutate({ mutation, errorPolicy: "all" }); + + expect(result.errors).toBeDefined(); + expect(result.errors!.length).toBe(1); + expect(result.errors![0].message).toBe(errors[0].message); + expect(result.data).toEqual({ + newPerson: data, + }); + }); + + it("should strip errors on a mutation if ignored", async () => { + const mutation = gql` + mutation { + newPerson { + person { + firstName + lastName + } + } + } + `; + const data = { + newPerson: { + person: { + firstName: "John", + lastName: "Smith", }, - }); + }, + }; + const errors = [new Error("Some kind of GraphQL error.")]; + const client = new ApolloClient({ + link: mockSingleLink({ + request: { query: mutation }, + result: { data, errors }, + }), + cache: new InMemoryCache({ addTypename: false }), + }); - { - const { data, optimisticData } = client.cache as any; - expect(optimisticData).not.toBe(data); - expect(optimisticData.parent).toBe(data.stump); - expect(optimisticData.parent.parent).toBe(data); + const result = await client.mutate({ mutation, errorPolicy: "ignore" }); + + expect(result.errors).toBeUndefined(); + expect(result.data).toEqual(data); + }); + + it("should rollback optimistic after mutation got a GraphQL error", async () => { + const mutation = gql` + mutation { + newPerson { + person { + firstName + lastName + } + } } + `; + const data = { + newPerson: { + person: { + firstName: "John", + lastName: "Smith", + }, + }, + }; + const errors = [new Error("Some kind of GraphQL error.")]; + const client = new ApolloClient({ + link: mockSingleLink({ + request: { query: mutation }, + result: { data, errors }, + }), + cache: new InMemoryCache({ addTypename: false }), + }); + const mutatePromise = client.mutate({ + mutation, + optimisticResponse: { + newPerson: { + person: { + firstName: "John*", + lastName: "Smith*", + }, + }, + }, + }); - mutatePromise - .then((_) => { - reject(new Error("Returned a result when it should not have.")); - }) - .catch((_: ApolloError) => { - const { data, optimisticData } = client.cache as any; - expect(optimisticData).toBe(data.stump); - resolve(); - }); + { + const { data, optimisticData } = client.cache as any; + expect(optimisticData).not.toBe(data); + expect(optimisticData.parent).toBe(data.stump); + expect(optimisticData.parent.parent).toBe(data); } - ); + + await expect(mutatePromise).rejects.toThrow(); + + { + const { data, optimisticData } = client.cache as any; + + expect(optimisticData).toBe(data.stump); + } + }); it("has a clearStore method which calls QueryManager", async () => { const client = new ApolloClient({ @@ -2526,100 +2335,83 @@ describe("client", () => { expect(count).toEqual(2); }); - itAsync( - "invokes onResetStore callbacks before notifying queries during resetStore call", - async (resolve, reject) => { - const delay = (time: number) => new Promise((r) => setTimeout(r, time)); + it("invokes onResetStore callbacks before notifying queries during resetStore call", async () => { + const delay = (time: number) => new Promise((r) => setTimeout(r, time)); - const query = gql` - query { - author { - firstName - lastName - } + const query = gql` + query { + author { + firstName + lastName } - `; + } + `; - const data = { - author: { - __typename: "Author", - firstName: "John", - lastName: "Smith", - }, - }; + const data = { + author: { + __typename: "Author", + firstName: "John", + lastName: "Smith", + }, + }; - const data2 = { - author: { - __typename: "Author", - firstName: "Joe", - lastName: "Joe", - }, - }; + const data2 = { + author: { + __typename: "Author", + firstName: "Joe", + lastName: "Joe", + }, + }; - const link = ApolloLink.from([ - new ApolloLink( - () => - new Observable((observer) => { - observer.next({ data }); - observer.complete(); - return; - }) - ), - ]); + const link = ApolloLink.from([ + new ApolloLink( + () => + new Observable((observer) => { + observer.next({ data }); + observer.complete(); + return; + }) + ), + ]); - const client = new ApolloClient({ - link, - cache: new InMemoryCache(), - }); + const client = new ApolloClient({ + link, + cache: new InMemoryCache(), + }); - let count = 0; - const onResetStoreOne = jest.fn(async () => { - expect(count).toEqual(0); - await delay(10).then(() => count++); - expect(count).toEqual(1); - }); + let count = 0; + const onResetStoreOne = jest.fn(async () => { + expect(count).toEqual(0); + await delay(10).then(() => count++); + expect(count).toEqual(1); + }); - const onResetStoreTwo = jest.fn(async () => { - expect(count).toEqual(0); - await delay(11).then(() => count++); - expect(count).toEqual(2); - expect(client.readQuery({ query })).toBe(null); - client.cache.writeQuery({ query, data: data2 }); - }); + const onResetStoreTwo = jest.fn(async () => { + expect(count).toEqual(0); + await delay(11).then(() => count++); + expect(count).toEqual(2); + expect(client.readQuery({ query })).toBe(null); + client.cache.writeQuery({ query, data: data2 }); + }); - client.onResetStore(onResetStoreOne); - client.onResetStore(onResetStoreTwo); + client.onResetStore(onResetStoreOne); + client.onResetStore(onResetStoreTwo); - let called = false; - const next = jest.fn((d) => { - if (called) { - expect(onResetStoreOne).toHaveBeenCalled(); - } else { - expect(d.data).toEqual(data); - called = true; - } - }); + const observable = client.watchQuery({ + query, + notifyOnNetworkStatusChange: false, + }); + const stream = new ObservableStream(observable); - client - .watchQuery({ - query, - notifyOnNetworkStatusChange: false, - }) - .subscribe({ - next, - error: reject, - complete: reject, - }); + expect(count).toBe(0); + await client.resetStore(); + expect(count).toBe(2); - expect(count).toEqual(0); - await client.resetStore(); - expect(count).toEqual(2); - //watchQuery should only receive data twice - expect(next).toHaveBeenCalledTimes(2); + await expect(stream).toEmitMatchedValue({ data }); + await expect(stream).toEmitNext(); - resolve(); - } - ); + expect(onResetStoreOne).toHaveBeenCalled(); + }); it("has a reFetchObservableQueries method which calls QueryManager", async () => { const client = new ApolloClient({ @@ -2690,845 +2482,758 @@ describe("client", () => { expect(spy).toHaveBeenCalled(); }); - itAsync( - "should propagate errors from network interface to observers", - (resolve, reject) => { - const link = ApolloLink.from([ - () => - new Observable((x) => { - x.error(new Error("Uh oh!")); - return; - }), - ]); - - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ addTypename: false }), - }); + it("should propagate errors from network interface to observers", async () => { + const link = ApolloLink.from([ + () => + new Observable((x) => { + x.error(new Error("Uh oh!")); + return; + }), + ]); - const handle = client.watchQuery({ - query: gql` - query { - a - b - c - } - `, - }); + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ addTypename: false }), + }); - handle.subscribe({ - error(error) { - expect(error.message).toBe("Uh oh!"); - resolve(); - }, - }); - } - ); - - itAsync( - "should be able to refetch after there was a network error", - (resolve, reject) => { - const query: DocumentNode = gql` - query somethingelse { - allPeople(first: 1) { - people { - name - } - } + const handle = client.watchQuery({ + query: gql` + query { + a + b + c } - `; + `, + }); - const data = { allPeople: { people: [{ name: "Luke Skywalker" }] } }; - const dataTwo = { allPeople: { people: [{ name: "Princess Leia" }] } }; - const link = mockSingleLink( - { request: { query }, result: { data } }, - { request: { query }, error: new Error("This is an error!") }, - { request: { query }, result: { data: dataTwo } } - ); - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ addTypename: false }), - }); + const stream = new ObservableStream(handle); - let count = 0; - const noop = () => null; + const error = await stream.takeError(); - const observable = client.watchQuery({ - query, - notifyOnNetworkStatusChange: true, - }); + expect(error.message).toBe("Uh oh!"); + }); - let subscription: any = null; - - const observerOptions = { - next(result: any) { - try { - switch (count++) { - case 0: - if (!result.data!.allPeople) { - reject("Should have data by this point"); - break; - } - // First result is loaded, run a refetch to get the second result - // which is an error. - expect(result.loading).toBeFalsy(); - expect(result.networkStatus).toBe(7); - expect(result.data!.allPeople).toEqual(data.allPeople); - setTimeout(() => { - observable.refetch().then(() => { - reject("Expected error value on first refetch."); - }, noop); - }, 0); - break; - case 1: - // Waiting for the second result to load - expect(result.loading).toBeTruthy(); - expect(result.networkStatus).toBe(4); - break; - // case 2 is handled by the error callback - case 3: - expect(result.loading).toBeTruthy(); - expect(result.networkStatus).toBe(4); - expect(result.errors).toBeFalsy(); - break; - case 4: - // Third result's data is loaded - expect(result.loading).toBeFalsy(); - expect(result.networkStatus).toBe(7); - expect(result.errors).toBeFalsy(); - if (!result.data) { - reject("Should have data by this point"); - break; - } - expect(result.data.allPeople).toEqual(dataTwo.allPeople); - resolve(); - break; - default: - throw new Error("Unexpected fall through"); - } - } catch (e) { - reject(e); + it("should be able to refetch after there was a network error", async () => { + const query: DocumentNode = gql` + query somethingelse { + allPeople(first: 1) { + people { + name } - }, - error(error: Error) { - expect(count++).toBe(2); - expect(error.message).toBe("This is an error!"); - - subscription.unsubscribe(); - - const lastError = observable.getLastError(); - expect(lastError).toBeInstanceOf(ApolloError); - expect(lastError!.networkError).toEqual((error as any).networkError); - - const lastResult = observable.getLastResult(); - expect(lastResult).toBeTruthy(); - expect(lastResult!.loading).toBe(false); - expect(lastResult!.networkStatus).toBe(8); - - observable.resetLastResults(); - subscription = observable.subscribe(observerOptions); - - // The error arrived, run a refetch to get the third result - // which should now contain valid data. - setTimeout(() => { - observable.refetch().catch(() => { - reject("Expected good data on second refetch."); - }); - }, 0); - }, - }; - - subscription = observable.subscribe(observerOptions); - } - ); - - itAsync("should throw a GraphQL error", (resolve, reject) => { - const query = gql` - query { - posts { - foo - __typename } } `; - const errors: GraphQLError[] = [ - new GraphQLError('Cannot query field "foo" on type "Post".'), - ]; - const link = mockSingleLink({ - request: { query }, - result: { errors }, - }).setOnError(reject); + + const data = { allPeople: { people: [{ name: "Luke Skywalker" }] } }; + const dataTwo = { allPeople: { people: [{ name: "Princess Leia" }] } }; + const link = mockSingleLink( + { request: { query }, result: { data } }, + { request: { query }, error: new Error("This is an error!") }, + { request: { query }, result: { data: dataTwo } } + ); + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ addTypename: false }), + }); + + const observable = client.watchQuery({ + query, + notifyOnNetworkStatusChange: true, + }); + + let stream = new ObservableStream(observable); + + await expect(stream).toEmitValue({ + loading: false, + networkStatus: NetworkStatus.ready, + data, + }); + + await wait(0); + await expect(observable.refetch()).rejects.toThrow(); + + await expect(stream).toEmitValue({ + loading: true, + networkStatus: NetworkStatus.refetch, + data, + }); + + const error = await stream.takeError(); + + expect(error.message).toBe("This is an error!"); + + stream.unsubscribe(); + + const lastError = observable.getLastError(); + expect(lastError).toBeInstanceOf(ApolloError); + expect(lastError!.networkError).toEqual((error as any).networkError); + + const lastResult = observable.getLastResult(); + expect(lastResult).toBeTruthy(); + expect(lastResult!.loading).toBe(false); + expect(lastResult!.networkStatus).toBe(8); + + observable.resetLastResults(); + stream = new ObservableStream(observable); + + await expect(stream).toEmitValue({ + loading: false, + networkStatus: NetworkStatus.ready, + data, + }); + + await wait(0); + await expect(observable.refetch()).resolves.toBeTruthy(); + + await expect(stream).toEmitValue({ + loading: true, + networkStatus: NetworkStatus.refetch, + data, + }); + + await expect(stream).toEmitValue({ + loading: false, + networkStatus: NetworkStatus.ready, + errors: undefined, + data: dataTwo, + }); + + await expect(stream).not.toEmitAnything(); + }); + + it("should throw a GraphQL error", async () => { + const query = gql` + query { + posts { + foo + __typename + } + } + `; + const errors: GraphQLError[] = [ + new GraphQLError('Cannot query field "foo" on type "Post".'), + ]; + const link = mockSingleLink({ + request: { query }, + result: { errors }, + }); const client = new ApolloClient({ link, cache: new InMemoryCache(), }); - return client - .query({ query }) - .catch((err) => { - expect(err.message).toBe('Cannot query field "foo" on type "Post".'); - }) - .then(resolve, reject); + await expect(client.query({ query })).rejects.toThrow( + 'Cannot query field "foo" on type "Post".' + ); }); it("should warn if server returns wrong data", async () => { using _consoleSpies = spyOnConsole.takeSnapshots("error"); - await new Promise((resolve, reject) => { - const query = gql` - query { - todos { - id - name - description - __typename - } + const query = gql` + query { + todos { + id + name + description + __typename } - `; - const result = { - data: { - todos: [ - { - id: "1", - name: "Todo 1", - price: 100, - __typename: "Todo", - }, - ], - }, - }; - - const link = mockSingleLink({ - request: { query }, - result, - }).setOnError(reject); - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ - // Passing an empty map enables the warning: - possibleTypes: {}, - }), - }); + } + `; + const result = { + data: { + todos: [ + { + id: "1", + name: "Todo 1", + price: 100, + __typename: "Todo", + }, + ], + }, + }; - return client - .query({ query }) - .then(({ data }) => { - expect(data).toEqual(result.data); - }) - .then(resolve, reject); + const link = mockSingleLink({ + request: { query }, + result, + }); + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ + // Passing an empty map enables the warning: + possibleTypes: {}, + }), }); + + const { data } = await client.query({ query }); + + expect(data).toEqual(result.data); }); - itAsync( - "runs a query with the connection directive and writes it to the store key defined in the directive", - (resolve, reject) => { - const query = gql` - { - books(skip: 0, limit: 2) @connection(key: "abc") { - name - } + it("runs a query with the connection directive and writes it to the store key defined in the directive", async () => { + const query = gql` + { + books(skip: 0, limit: 2) @connection(key: "abc") { + name } - `; + } + `; - const transformedQuery = gql` - { - books(skip: 0, limit: 2) { - name - __typename - } + const transformedQuery = gql` + { + books(skip: 0, limit: 2) { + name + __typename } - `; + } + `; - const result = { - books: [ - { - name: "abcd", - __typename: "Book", - }, - ], - }; + const result = { + books: [ + { + name: "abcd", + __typename: "Book", + }, + ], + }; - const link = mockSingleLink({ - request: { query: transformedQuery }, - result: { data: result }, - }).setOnError(reject); + const link = mockSingleLink({ + request: { query: transformedQuery }, + result: { data: result }, + }); - const client = new ApolloClient({ - link, - cache: new InMemoryCache(), - }); + const client = new ApolloClient({ + link, + cache: new InMemoryCache(), + }); - return client - .query({ query }) - .then((actualResult) => { - expect(actualResult.data).toEqual(result); - }) - .then(resolve, reject); - } - ); + const actualResult = await client.query({ query }); - itAsync( - "runs query with cache field policy analogous to @connection", - (resolve, reject) => { - const query = gql` - { - books(skip: 0, limit: 2) { - name - } + expect(actualResult.data).toEqual(result); + }); + + it("runs query with cache field policy analogous to @connection", async () => { + const query = gql` + { + books(skip: 0, limit: 2) { + name } - `; + } + `; - const transformedQuery = gql` - { - books(skip: 0, limit: 2) { - name - __typename - } + const transformedQuery = gql` + { + books(skip: 0, limit: 2) { + name + __typename } - `; + } + `; - const result = { - books: [ - { - name: "abcd", - __typename: "Book", - }, - ], - }; + const result = { + books: [ + { + name: "abcd", + __typename: "Book", + }, + ], + }; - const link = mockSingleLink({ - request: { query: transformedQuery }, - result: { data: result }, - }).setOnError(reject); + const link = mockSingleLink({ + request: { query: transformedQuery }, + result: { data: result }, + }); - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ - typePolicies: { - Query: { - fields: { - books: { - keyArgs: () => "abc", - }, + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ + typePolicies: { + Query: { + fields: { + books: { + keyArgs: () => "abc", }, }, }, - }), - }); + }, + }), + }); - return client - .query({ query }) - .then((actualResult) => { - expect(actualResult.data).toEqual(result); - }) - .then(resolve, reject); - } - ); + const actualResult = await client.query({ query }); - itAsync( - "should remove the connection directive before the link is sent", - (resolve, reject) => { - const query = gql` - { - books(skip: 0, limit: 2) @connection(key: "books") { - name - } + expect(actualResult.data).toEqual(result); + }); + + it("should remove the connection directive before the link is sent", async () => { + const query = gql` + { + books(skip: 0, limit: 2) @connection(key: "books") { + name } - `; + } + `; - const transformedQuery = gql` - { - books(skip: 0, limit: 2) { - name - __typename - } + const transformedQuery = gql` + { + books(skip: 0, limit: 2) { + name + __typename } - `; + } + `; - const result = { - books: [ - { - name: "abcd", - __typename: "Book", - }, - ], - }; + const result = { + books: [ + { + name: "abcd", + __typename: "Book", + }, + ], + }; - const link = mockSingleLink({ - request: { query: transformedQuery }, - result: { data: result }, - }).setOnError(reject); + const link = mockSingleLink({ + request: { query: transformedQuery }, + result: { data: result }, + }); - const client = new ApolloClient({ - link, - cache: new InMemoryCache(), - }); + const client = new ApolloClient({ + link, + cache: new InMemoryCache(), + }); - return client - .query({ query }) - .then((actualResult) => { - expect(actualResult.data).toEqual(result); - }) - .then(resolve, reject); - } - ); + const actualResult = await client.query({ query }); + + expect(actualResult.data).toEqual(result); + }); }); describe("@connection", () => { - itAsync( - "should run a query with the @connection directive and write the result to the store key defined in the directive", - (resolve, reject) => { - const query = gql` - { - books(skip: 0, limit: 2) @connection(key: "abc") { - name - } + it("should run a query with the @connection directive and write the result to the store key defined in the directive", async () => { + const query = gql` + { + books(skip: 0, limit: 2) @connection(key: "abc") { + name } - `; + } + `; - const transformedQuery = gql` - { - books(skip: 0, limit: 2) { - name - __typename - } + const transformedQuery = gql` + { + books(skip: 0, limit: 2) { + name + __typename } - `; + } + `; - const result = { - books: [ - { - name: "abcd", - __typename: "Book", - }, - ], - }; + const result = { + books: [ + { + name: "abcd", + __typename: "Book", + }, + ], + }; - const link = mockSingleLink({ - request: { query: transformedQuery }, - result: { data: result }, - }).setOnError(reject); + const link = mockSingleLink({ + request: { query: transformedQuery }, + result: { data: result }, + }); - const client = new ApolloClient({ - link, - cache: new InMemoryCache(), - }); + const client = new ApolloClient({ + link, + cache: new InMemoryCache(), + }); + + const actualResult = await client.query({ query }); - return client - .query({ query }) - .then((actualResult) => { - expect(actualResult.data).toEqual(result); - expect((client.cache as InMemoryCache).extract()).toMatchSnapshot(); - }) - .then(resolve, reject); - } - ); + expect(actualResult.data).toEqual(result); + expect((client.cache as InMemoryCache).extract()).toMatchSnapshot(); + }); - itAsync( - "should run a query with the connection directive and filter arguments and write the result to the correct store key", - (resolve, reject) => { - const query = gql` - query books($order: string) { - books(skip: 0, limit: 2, order: $order) - @connection(key: "abc", filter: ["order"]) { - name - } + it("should run a query with the connection directive and filter arguments and write the result to the correct store key", async () => { + const query = gql` + query books($order: string) { + books(skip: 0, limit: 2, order: $order) + @connection(key: "abc", filter: ["order"]) { + name } - `; - const transformedQuery = gql` - query books($order: string) { - books(skip: 0, limit: 2, order: $order) { - name - __typename - } + } + `; + const transformedQuery = gql` + query books($order: string) { + books(skip: 0, limit: 2, order: $order) { + name + __typename } - `; + } + `; - const result = { - books: [ - { - name: "abcd", - __typename: "Book", - }, - ], - }; + const result = { + books: [ + { + name: "abcd", + __typename: "Book", + }, + ], + }; - const variables = { order: "popularity" }; + const variables = { order: "popularity" }; - const link = mockSingleLink({ - request: { query: transformedQuery, variables }, - result: { data: result }, - }).setOnError(reject); + const link = mockSingleLink({ + request: { query: transformedQuery, variables }, + result: { data: result }, + }); - const client = new ApolloClient({ - link, - cache: new InMemoryCache(), - }); + const client = new ApolloClient({ + link, + cache: new InMemoryCache(), + }); - return client - .query({ query, variables }) - .then((actualResult) => { - expect(actualResult.data).toEqual(result); - expect((client.cache as InMemoryCache).extract()).toMatchSnapshot(); - }) - .then(resolve, reject); - } - ); + const actualResult = await client.query({ query, variables }); - itAsync( - "should support cache field policies that filter key arguments", - (resolve, reject) => { - const query = gql` - query books($order: string) { - books(skip: 0, limit: 2, order: $order) { - name - } + expect(actualResult.data).toEqual(result); + expect((client.cache as InMemoryCache).extract()).toMatchSnapshot(); + }); + + it("should support cache field policies that filter key arguments", async () => { + const query = gql` + query books($order: string) { + books(skip: 0, limit: 2, order: $order) { + name } - `; - const transformedQuery = gql` - query books($order: string) { - books(skip: 0, limit: 2, order: $order) { - name - __typename - } + } + `; + const transformedQuery = gql` + query books($order: string) { + books(skip: 0, limit: 2, order: $order) { + name + __typename } - `; - - const result = { - books: [ - { - name: "abcd", - __typename: "Book", - }, - ], - }; + } + `; - const variables = { order: "popularity" }; + const result = { + books: [ + { + name: "abcd", + __typename: "Book", + }, + ], + }; - const link = mockSingleLink({ - request: { query: transformedQuery, variables }, - result: { data: result }, - }).setOnError(reject); + const variables = { order: "popularity" }; - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ - typePolicies: { - Query: { - fields: { - books: { - keyArgs: ["order"], - }, - }, - }, - }, - }), - }); + const link = mockSingleLink({ + request: { query: transformedQuery, variables }, + result: { data: result }, + }); - return client - .query({ query, variables }) - .then((actualResult) => { - expect(actualResult.data).toEqual(result); - expect((client.cache as InMemoryCache).extract()).toMatchSnapshot(); - }) - .then(resolve, reject); - } - ); - - itAsync( - "should broadcast changes for reactive variables", - async (resolve, reject) => { - const aVar = makeVar(123); - const bVar = makeVar("asdf"); - const cache: InMemoryCache = new InMemoryCache({ + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ typePolicies: { Query: { fields: { - a() { - return aVar(); - }, - b() { - return bVar(); + books: { + keyArgs: ["order"], }, }, }, }, - }); + }), + }); - const client = new ApolloClient({ cache }); + const actualResult = await client.query({ query, variables }); - const obsQueries = new Set>(); - const subs = new Set(); - function watch( - query: DocumentNode, - fetchPolicy: WatchQueryFetchPolicy = "cache-first" - ): any[] { - const results: any[] = []; - const obsQuery = client.watchQuery({ - query, - fetchPolicy, - }); - obsQueries.add(obsQuery); - subs.add( - obsQuery.subscribe({ - next(result) { - results.push(result.data); - }, - }) - ); - return results; - } + expect(actualResult.data).toEqual(result); + expect((client.cache as InMemoryCache).extract()).toMatchSnapshot(); + }); - const aResults = watch(gql` - { - a - } - `); - const bResults = watch(gql` - { - b - } - `); - const abResults = watch(gql` - { - a - b - } - `); + it("should broadcast changes for reactive variables", async () => { + const aVar = makeVar(123); + const bVar = makeVar("asdf"); + const cache: InMemoryCache = new InMemoryCache({ + typePolicies: { + Query: { + fields: { + a() { + return aVar(); + }, + b() { + return bVar(); + }, + }, + }, + }, + }); - await wait(); + const client = new ApolloClient({ cache }); - function checkLastResult( - results: any[], - expectedData: Record - ) { - const lastResult = results[results.length - 1]; - expect(lastResult).toEqual(expectedData); - return lastResult; - } - - checkLastResult(aResults, { a: 123 }); - const bAsdf = checkLastResult(bResults, { b: "asdf" }); - checkLastResult(abResults, { a: 123, b: "asdf" }); - - aVar(aVar() + 111); - await wait(); - - const a234 = checkLastResult(aResults, { a: 234 }); - expect(checkLastResult(bResults, { b: "asdf" })).toBe(bAsdf); - checkLastResult(abResults, { a: 234, b: "asdf" }); - - bVar(bVar().toUpperCase()); - await wait(); - - expect(checkLastResult(aResults, { a: 234 })).toBe(a234); - checkLastResult(bResults, { b: "ASDF" }); - checkLastResult(abResults, { a: 234, b: "ASDF" }); - - aVar(aVar() + 222); - bVar("oyez"); - await wait(); - - const a456 = checkLastResult(aResults, { a: 456 }); - const bOyez = checkLastResult(bResults, { b: "oyez" }); - const a456bOyez = checkLastResult(abResults, { a: 456, b: "oyez" }); - - // Since the ObservableQuery skips results that are the same as the - // previous result, and nothing is actually changing about the - // ROOT_QUERY.a field, clear previous results to give the invalidated - // results a chance to be delivered. - obsQueries.forEach((obsQuery) => obsQuery.resetLastResults()); - await wait(); - // Verify that resetting previous results did not trigger the delivery - // of any new results, by itself. - expect(checkLastResult(aResults, a456)).toBe(a456); - expect(checkLastResult(bResults, bOyez)).toBe(bOyez); - expect(checkLastResult(abResults, a456bOyez)).toBe(a456bOyez); - - // Now invalidate the ROOT_QUERY.a field. - client.cache.evict({ fieldName: "a" }); - await wait(); - - expect(checkLastResult(aResults, a456)).toBe(a456); - expect(checkLastResult(bResults, bOyez)).toBe(bOyez); - expect(checkLastResult(abResults, a456bOyez)).toBe(a456bOyez); - - const cQuery = gql` - { - c - } - `; - // Passing cache-only as the fetchPolicy allows the { c: "see" } - // result to be delivered even though networkStatus is still loading. - const cResults = watch(cQuery, "cache-only"); - - // Now try writing directly to the cache, rather than calling - // client.writeQuery. - client.cache.writeQuery({ - query: cQuery, - data: { - c: "see", - }, + const obsQueries = new Set>(); + const subs = new Set(); + function watch( + query: DocumentNode, + fetchPolicy: WatchQueryFetchPolicy = "cache-first" + ): any[] { + const results: any[] = []; + const obsQuery = client.watchQuery({ + query, + fetchPolicy, }); - await wait(); - - checkLastResult(aResults, a456); - checkLastResult(bResults, bOyez); - checkLastResult(abResults, a456bOyez); - checkLastResult(cResults, { c: "see" }); - - cache.modify({ - fields: { - c(value) { - expect(value).toBe("see"); - return "saw"; + obsQueries.add(obsQuery); + subs.add( + obsQuery.subscribe({ + next(result) { + results.push(result.data); }, - }, - }); - await wait(); + }) + ); + return results; + } - checkLastResult(aResults, a456); - checkLastResult(bResults, bOyez); - checkLastResult(abResults, a456bOyez); - checkLastResult(cResults, { c: "saw" }); + const aResults = watch(gql` + { + a + } + `); + const bResults = watch(gql` + { + b + } + `); + const abResults = watch(gql` + { + a + b + } + `); - client.cache.evict({ fieldName: "c" }); - await wait(); + await wait(); - checkLastResult(aResults, a456); - checkLastResult(bResults, bOyez); - checkLastResult(abResults, a456bOyez); - expect(checkLastResult(cResults, {})); + function checkLastResult( + results: any[], + expectedData: Record + ) { + const lastResult = results[results.length - 1]; + expect(lastResult).toEqual(expectedData); + return lastResult; + } - expect(aResults).toEqual([{ a: 123 }, { a: 234 }, { a: 456 }]); + checkLastResult(aResults, { a: 123 }); + const bAsdf = checkLastResult(bResults, { b: "asdf" }); + checkLastResult(abResults, { a: 123, b: "asdf" }); + + aVar(aVar() + 111); + await wait(); + + const a234 = checkLastResult(aResults, { a: 234 }); + expect(checkLastResult(bResults, { b: "asdf" })).toBe(bAsdf); + checkLastResult(abResults, { a: 234, b: "asdf" }); + + bVar(bVar().toUpperCase()); + await wait(); + + expect(checkLastResult(aResults, { a: 234 })).toBe(a234); + checkLastResult(bResults, { b: "ASDF" }); + checkLastResult(abResults, { a: 234, b: "ASDF" }); + + aVar(aVar() + 222); + bVar("oyez"); + await wait(); + + const a456 = checkLastResult(aResults, { a: 456 }); + const bOyez = checkLastResult(bResults, { b: "oyez" }); + const a456bOyez = checkLastResult(abResults, { a: 456, b: "oyez" }); + + // Since the ObservableQuery skips results that are the same as the + // previous result, and nothing is actually changing about the + // ROOT_QUERY.a field, clear previous results to give the invalidated + // results a chance to be delivered. + obsQueries.forEach((obsQuery) => obsQuery.resetLastResults()); + await wait(); + // Verify that resetting previous results did not trigger the delivery + // of any new results, by itself. + expect(checkLastResult(aResults, a456)).toBe(a456); + expect(checkLastResult(bResults, bOyez)).toBe(bOyez); + expect(checkLastResult(abResults, a456bOyez)).toBe(a456bOyez); + + // Now invalidate the ROOT_QUERY.a field. + client.cache.evict({ fieldName: "a" }); + await wait(); + + expect(checkLastResult(aResults, a456)).toBe(a456); + expect(checkLastResult(bResults, bOyez)).toBe(bOyez); + expect(checkLastResult(abResults, a456bOyez)).toBe(a456bOyez); + + const cQuery = gql` + { + c + } + `; + // Passing cache-only as the fetchPolicy allows the { c: "see" } + // result to be delivered even though networkStatus is still loading. + const cResults = watch(cQuery, "cache-only"); + + // Now try writing directly to the cache, rather than calling + // client.writeQuery. + client.cache.writeQuery({ + query: cQuery, + data: { + c: "see", + }, + }); + await wait(); - expect(bResults).toEqual([{ b: "asdf" }, { b: "ASDF" }, { b: "oyez" }]); + checkLastResult(aResults, a456); + checkLastResult(bResults, bOyez); + checkLastResult(abResults, a456bOyez); + checkLastResult(cResults, { c: "see" }); - expect(abResults).toEqual([ - { a: 123, b: "asdf" }, - { a: 234, b: "asdf" }, - { a: 234, b: "ASDF" }, - { a: 456, b: "oyez" }, - ]); + cache.modify({ + fields: { + c(value) { + expect(value).toBe("see"); + return "saw"; + }, + }, + }); + await wait(); - expect(cResults).toEqual([{}, { c: "see" }, { c: "saw" }, {}]); + checkLastResult(aResults, a456); + checkLastResult(bResults, bOyez); + checkLastResult(abResults, a456bOyez); + checkLastResult(cResults, { c: "saw" }); - subs.forEach((sub) => sub.unsubscribe()); + client.cache.evict({ fieldName: "c" }); + await wait(); - resolve(); - } - ); + checkLastResult(aResults, a456); + checkLastResult(bResults, bOyez); + checkLastResult(abResults, a456bOyez); + expect(checkLastResult(cResults, {})); + + expect(aResults).toEqual([{ a: 123 }, { a: 234 }, { a: 456 }]); + + expect(bResults).toEqual([{ b: "asdf" }, { b: "ASDF" }, { b: "oyez" }]); + + expect(abResults).toEqual([ + { a: 123, b: "asdf" }, + { a: 234, b: "asdf" }, + { a: 234, b: "ASDF" }, + { a: 456, b: "oyez" }, + ]); + + expect(cResults).toEqual([{}, { c: "see" }, { c: "saw" }, {}]); + + subs.forEach((sub) => sub.unsubscribe()); + }); function wait(time = 10) { return new Promise((resolve) => setTimeout(resolve, time)); } - itAsync( - "should call forgetCache for reactive vars when stopped", - async (resolve, reject) => { - const aVar = makeVar(123); - const bVar = makeVar("asdf"); - const aSpy = jest.spyOn(aVar, "forgetCache"); - const bSpy = jest.spyOn(bVar, "forgetCache"); - const cache: InMemoryCache = new InMemoryCache({ - typePolicies: { - Query: { - fields: { - a() { - return aVar(); - }, - b() { - return bVar(); - }, + it("should call forgetCache for reactive vars when stopped", async () => { + const aVar = makeVar(123); + const bVar = makeVar("asdf"); + const aSpy = jest.spyOn(aVar, "forgetCache"); + const bSpy = jest.spyOn(bVar, "forgetCache"); + const cache: InMemoryCache = new InMemoryCache({ + typePolicies: { + Query: { + fields: { + a() { + return aVar(); + }, + b() { + return bVar(); }, }, }, - }); - - const client = new ApolloClient({ cache }); + }, + }); - const obsQueries = new Set>(); - const subs = new Set(); - function watch( - query: DocumentNode, - fetchPolicy: WatchQueryFetchPolicy = "cache-first" - ): any[] { - const results: any[] = []; - const obsQuery = client.watchQuery({ - query, - fetchPolicy, - }); - obsQueries.add(obsQuery); - subs.add( - obsQuery.subscribe({ - next(result) { - results.push(result.data); - }, - }) - ); - return results; - } + const client = new ApolloClient({ cache }); - const aQuery = gql` - { - a - } - `; - const bQuery = gql` - { - b - } - `; - const abQuery = gql` - { - a - b - } - `; + const obsQueries = new Set>(); + const subs = new Set(); + function watch( + query: DocumentNode, + fetchPolicy: WatchQueryFetchPolicy = "cache-first" + ): any[] { + const results: any[] = []; + const obsQuery = client.watchQuery({ + query, + fetchPolicy, + }); + obsQueries.add(obsQuery); + subs.add( + obsQuery.subscribe({ + next(result) { + results.push(result.data); + }, + }) + ); + return results; + } - const aResults = watch(aQuery); - const bResults = watch(bQuery); + const aQuery = gql` + { + a + } + `; + const bQuery = gql` + { + b + } + `; + const abQuery = gql` + { + a + b + } + `; - expect(cache["watches"].size).toBe(2); + const aResults = watch(aQuery); + const bResults = watch(bQuery); - expect(aResults).toEqual([]); - expect(bResults).toEqual([]); + expect(cache["watches"].size).toBe(2); - expect(aSpy).not.toBeCalled(); - expect(bSpy).not.toBeCalled(); + expect(aResults).toEqual([]); + expect(bResults).toEqual([]); - subs.forEach((sub) => sub.unsubscribe()); + expect(aSpy).not.toBeCalled(); + expect(bSpy).not.toBeCalled(); - expect(aSpy).toBeCalledTimes(1); - expect(aSpy).toBeCalledWith(cache); - expect(bSpy).toBeCalledTimes(1); - expect(bSpy).toBeCalledWith(cache); + subs.forEach((sub) => sub.unsubscribe()); - expect(aResults).toEqual([]); - expect(bResults).toEqual([]); + expect(aSpy).toBeCalledTimes(1); + expect(aSpy).toBeCalledWith(cache); + expect(bSpy).toBeCalledTimes(1); + expect(bSpy).toBeCalledWith(cache); - expect(cache["watches"].size).toBe(0); - const abResults = watch(abQuery); - expect(abResults).toEqual([]); - expect(cache["watches"].size).toBe(1); + expect(aResults).toEqual([]); + expect(bResults).toEqual([]); - await wait(); + expect(cache["watches"].size).toBe(0); + const abResults = watch(abQuery); + expect(abResults).toEqual([]); + expect(cache["watches"].size).toBe(1); - expect(aResults).toEqual([]); - expect(bResults).toEqual([]); - expect(abResults).toEqual([{ a: 123, b: "asdf" }]); + await wait(); - client.stop(); + expect(aResults).toEqual([]); + expect(bResults).toEqual([]); + expect(abResults).toEqual([{ a: 123, b: "asdf" }]); - await wait(); + client.stop(); - expect(aSpy).toBeCalledTimes(2); - expect(aSpy).toBeCalledWith(cache); - expect(bSpy).toBeCalledTimes(2); - expect(bSpy).toBeCalledWith(cache); + await wait(); - resolve(); - } - ); + expect(aSpy).toBeCalledTimes(2); + expect(aSpy).toBeCalledWith(cache); + expect(bSpy).toBeCalledTimes(2); + expect(bSpy).toBeCalledWith(cache); + }); describe("default settings", () => { const query = gql` @@ -3777,12 +3482,12 @@ describe("@connection", () => { await expect(stream).not.toEmitAnything(); }); - itAsync("allows setting default options for query", (resolve, reject) => { + it("allows setting default options for query", async () => { const errors = [{ message: "failure", name: "failure" }]; const link = mockSingleLink({ request: { query }, result: { errors }, - }).setOnError(reject); + }); const client = new ApolloClient({ link, cache: new InMemoryCache({ addTypename: false }), @@ -3791,55 +3496,46 @@ describe("@connection", () => { }, }); - return client - .query({ query }) - .then((result) => { - expect(result.errors).toEqual(errors); - }) - .then(resolve, reject); + const result = await client.query({ query }); + + expect(result.errors).toEqual(errors); }); - itAsync( - "allows setting default options for mutation", - (resolve, reject) => { - const mutation = gql` - mutation upVote($id: ID!) { - upvote(id: $id) { - success - } + it("allows setting default options for mutation", async () => { + const mutation = gql` + mutation upVote($id: ID!) { + upvote(id: $id) { + success } - `; - - const data = { - upvote: { success: true }, - }; - - const link = mockSingleLink({ - request: { query: mutation, variables: { id: 1 } }, - result: { data }, - }).setOnError(reject); - - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ addTypename: false }), - defaultOptions: { - mutate: { variables: { id: 1 } }, - }, - }); + } + `; - return client - .mutate({ - mutation, - // This undefined value should be ignored in favor of - // defaultOptions.mutate.variables. - variables: void 0, - }) - .then((result) => { - expect(result.data).toEqual(data); - }) - .then(resolve, reject); - } - ); + const data = { + upvote: { success: true }, + }; + + const link = mockSingleLink({ + request: { query: mutation, variables: { id: 1 } }, + result: { data }, + }); + + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ addTypename: false }), + defaultOptions: { + mutate: { variables: { id: 1 } }, + }, + }); + + const result = await client.mutate({ + mutation, + // This undefined value should be ignored in favor of + // defaultOptions.mutate.variables. + variables: void 0, + }); + + expect(result.data).toEqual(data); + }); }); }); @@ -6351,8 +6047,6 @@ describe("custom document transforms", () => { }); function clientRoundtrip( - resolve: (result: any) => any, - reject: (reason: any) => any, query: DocumentNode, data: FormattedExecutionResult, variables?: any, @@ -6361,7 +6055,7 @@ function clientRoundtrip( const link = mockSingleLink({ request: { query: cloneDeep(query) }, result: data, - }).setOnError(reject); + }); const client = new ApolloClient({ link, @@ -6370,10 +6064,7 @@ function clientRoundtrip( }), }); - return client - .query({ query, variables }) - .then((result) => { - expect(result.data).toEqual(data.data); - }) - .then(resolve, reject); + return client.query({ query, variables }).then((result) => { + expect(result.data).toEqual(data.data); + }); } diff --git a/src/__tests__/local-state/general.ts b/src/__tests__/local-state/general.ts index 0d65993e821..c1f570e85ac 100644 --- a/src/__tests__/local-state/general.ts +++ b/src/__tests__/local-state/general.ts @@ -17,8 +17,7 @@ import { ApolloLink } from "../../link/core"; import { Operation } from "../../link/core"; import { ApolloClient } from "../../core"; import { ApolloCache, InMemoryCache } from "../../cache"; -import { itAsync } from "../../testing"; -import { spyOnConsole } from "../../testing/internal"; +import { ObservableStream, spyOnConsole } from "../../testing/internal"; describe("General functionality", () => { it("should not impact normal non-@client use", () => { @@ -279,57 +278,43 @@ describe("Cache manipulation", () => { }); }); - itAsync( - "should be able to write to the cache with a local mutation and have " + - "things rerender automatically", - (resolve, reject) => { - const query = gql` - { - field @client - } - `; + it("should be able to write to the cache with a local mutation and have things rerender automatically", async () => { + const query = gql` + { + field @client + } + `; - const mutation = gql` - mutation start { - start @client - } - `; + const mutation = gql` + mutation start { + start @client + } + `; - const resolvers = { - Query: { - field: () => 0, - }, - Mutation: { - start: (_1: any, _2: any, { cache }: { cache: InMemoryCache }) => { - cache.writeQuery({ query, data: { field: 1 } }); - return { start: true }; - }, + const resolvers = { + Query: { + field: () => 0, + }, + Mutation: { + start: (_1: any, _2: any, { cache }: { cache: InMemoryCache }) => { + cache.writeQuery({ query, data: { field: 1 } }); + return { start: true }; }, - }; + }, + }; - const client = new ApolloClient({ - cache: new InMemoryCache(), - link: ApolloLink.empty(), - resolvers, - }); + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: ApolloLink.empty(), + resolvers, + }); - let count = 0; - client.watchQuery({ query }).subscribe({ - next: ({ data }) => { - count++; - if (count === 1) { - expect({ ...data }).toMatchObject({ field: 0 }); - client.mutate({ mutation }); - } + const stream = new ObservableStream(client.watchQuery({ query })); - if (count === 2) { - expect({ ...data }).toMatchObject({ field: 1 }); - resolve(); - } - }, - }); - } - ); + await expect(stream).toEmitMatchedValue({ data: { field: 0 } }); + await client.mutate({ mutation }); + await expect(stream).toEmitMatchedValue({ data: { field: 1 } }); + }); it("should support writing to the cache with a local mutation using variables", () => { const query = gql` @@ -381,376 +366,352 @@ describe("Cache manipulation", () => { }); }); - itAsync( - "should read @client fields from cache on refetch (#4741)", - (resolve, reject) => { - const query = gql` - query FetchInitialData { - serverData { - id - title - } - selectedItemId @client + it("should read @client fields from cache on refetch (#4741)", async () => { + const query = gql` + query FetchInitialData { + serverData { + id + title } - `; + selectedItemId @client + } + `; - const mutation = gql` - mutation Select { - select(itemId: $id) @client - } - `; + const mutation = gql` + mutation Select { + select(itemId: $id) @client + } + `; - const serverData = { - __typename: "ServerData", - id: 123, - title: "Oyez and Onoz", - }; + const serverData = { + __typename: "ServerData", + id: 123, + title: "Oyez and Onoz", + }; - let selectedItemId = -1; - const client = new ApolloClient({ - cache: new InMemoryCache(), - link: new ApolloLink(() => Observable.of({ data: { serverData } })), - resolvers: { - Query: { - selectedItemId() { - return selectedItemId; - }, + let selectedItemId = -1; + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: new ApolloLink(() => Observable.of({ data: { serverData } })), + resolvers: { + Query: { + selectedItemId() { + return selectedItemId; }, - Mutation: { - select(_, { itemId }) { - selectedItemId = itemId; - }, + }, + Mutation: { + select(_, { itemId }) { + selectedItemId = itemId; }, }, - }); + }, + }); - client.watchQuery({ query }).subscribe({ - next(result) { - expect(result).toEqual({ - data: { - serverData, - selectedItemId, - }, - loading: false, - networkStatus: 7, - }); + const stream = new ObservableStream(client.watchQuery({ query })); - if (selectedItemId !== 123) { - client.mutate({ - mutation, - variables: { - id: 123, - }, - refetchQueries: ["FetchInitialData"], - }); - } else { - resolve(); - } - }, - }); - } - ); + await expect(stream).toEmitValue({ + data: { + serverData, + selectedItemId: -1, + }, + loading: false, + networkStatus: 7, + }); - itAsync( - "should rerun @client(always: true) fields on entity update", - (resolve, reject) => { - const query = gql` - query GetClientData($id: ID) { - clientEntity(id: $id) @client(always: true) { - id - title - titleLength @client(always: true) - } - } - `; + await client.mutate({ + mutation, + variables: { id: 123 }, + refetchQueries: ["FetchInitialData"], + }); - const mutation = gql` - mutation AddOrUpdate { - addOrUpdate(id: $id, title: $title) @client - } - `; + await expect(stream).toEmitValue({ + data: { + serverData, + selectedItemId: 123, + }, + loading: false, + networkStatus: 7, + }); + }); - const fragment = gql` - fragment ClientDataFragment on ClientData { + it("should rerun @client(always: true) fields on entity update", async () => { + const query = gql` + query GetClientData($id: ID) { + clientEntity(id: $id) @client(always: true) { id title + titleLength @client(always: true) } - `; - const client = new ApolloClient({ - cache: new InMemoryCache(), - link: new ApolloLink(() => Observable.of({ data: {} })), - resolvers: { - ClientData: { - titleLength(data) { - return data.title.length; - }, + } + `; + + const mutation = gql` + mutation AddOrUpdate { + addOrUpdate(id: $id, title: $title) @client + } + `; + + const fragment = gql` + fragment ClientDataFragment on ClientData { + id + title + } + `; + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: new ApolloLink(() => Observable.of({ data: {} })), + resolvers: { + ClientData: { + titleLength(data) { + return data.title.length; }, - Query: { - clientEntity(_root, { id }, { cache }) { - return cache.readFragment({ - id: cache.identify({ id, __typename: "ClientData" }), - fragment, - }); - }, + }, + Query: { + clientEntity(_root, { id }, { cache }) { + return cache.readFragment({ + id: cache.identify({ id, __typename: "ClientData" }), + fragment, + }); }, - Mutation: { - addOrUpdate(_root, { id, title }, { cache }) { - return cache.writeFragment({ - id: cache.identify({ id, __typename: "ClientData" }), - fragment, - data: { id, title, __typename: "ClientData" }, - }); - }, + }, + Mutation: { + addOrUpdate(_root, { id, title }, { cache }) { + return cache.writeFragment({ + id: cache.identify({ id, __typename: "ClientData" }), + fragment, + data: { id, title, __typename: "ClientData" }, + }); }, }, - }); + }, + }); - const entityId = 1; - const shortTitle = "Short"; - const longerTitle = "A little longer"; - client.mutate({ - mutation, - variables: { - id: entityId, - title: shortTitle, - }, + const entityId = 1; + const shortTitle = "Short"; + const longerTitle = "A little longer"; + await client.mutate({ + mutation, + variables: { + id: entityId, + title: shortTitle, + }, + }); + const stream = new ObservableStream( + client.watchQuery({ query, variables: { id: entityId } }) + ); + + { + const result = await stream.takeNext(); + + expect(result.data.clientEntity).toEqual({ + id: entityId, + title: shortTitle, + titleLength: shortTitle.length, + __typename: "ClientData", }); - let mutated = false; - client.watchQuery({ query, variables: { id: entityId } }).subscribe({ - next(result) { - if (!mutated) { - expect(result.data.clientEntity).toEqual({ - id: entityId, - title: shortTitle, - titleLength: shortTitle.length, - __typename: "ClientData", - }); - client.mutate({ - mutation, - variables: { - id: entityId, - title: longerTitle, - }, - }); - mutated = true; - } else if (mutated) { - expect(result.data.clientEntity).toEqual({ - id: entityId, - title: longerTitle, - titleLength: longerTitle.length, - __typename: "ClientData", - }); - resolve(); - } - }, + } + + await client.mutate({ + mutation, + variables: { + id: entityId, + title: longerTitle, + }, + }); + + { + const result = await stream.takeNext(); + + expect(result.data.clientEntity).toEqual({ + id: entityId, + title: longerTitle, + titleLength: longerTitle.length, + __typename: "ClientData", }); } - ); + + await expect(stream).not.toEmitAnything(); + }); }); describe("Sample apps", () => { - itAsync( - "should support a simple counter app using local state", - (resolve, reject) => { - const query = gql` - query GetCount { - count @client - lastCount # stored in db on server - } - `; + it("should support a simple counter app using local state", async () => { + const query = gql` + query GetCount { + count @client + lastCount # stored in db on server + } + `; - const increment = gql` - mutation Increment($amount: Int = 1) { - increment(amount: $amount) @client - } - `; + const increment = gql` + mutation Increment($amount: Int = 1) { + increment(amount: $amount) @client + } + `; - const decrement = gql` - mutation Decrement($amount: Int = 1) { - decrement(amount: $amount) @client - } - `; + const decrement = gql` + mutation Decrement($amount: Int = 1) { + decrement(amount: $amount) @client + } + `; - const link = new ApolloLink((operation) => { - expect(operation.operationName).toBe("GetCount"); - return Observable.of({ data: { lastCount: 1 } }); - }); + const link = new ApolloLink((operation) => { + expect(operation.operationName).toBe("GetCount"); + return Observable.of({ data: { lastCount: 1 } }); + }); - const client = new ApolloClient({ - link, - cache: new InMemoryCache(), - resolvers: {}, - }); + const client = new ApolloClient({ + link, + cache: new InMemoryCache(), + resolvers: {}, + }); - const update = ( - query: DocumentNode, - updater: (data: { count: number }, variables: { amount: number }) => any - ) => { - return ( - _result: {}, - variables: { amount: number }, - { cache }: { cache: ApolloCache } - ): null => { - const read = client.readQuery<{ count: number }>({ - query, - variables, - }); - if (read) { - const data = updater(read, variables); - cache.writeQuery({ query, variables, data }); - } else { - throw new Error("readQuery returned a falsy value"); - } - return null; - }; + const update = ( + query: DocumentNode, + updater: (data: { count: number }, variables: { amount: number }) => any + ) => { + return ( + _result: {}, + variables: { amount: number }, + { cache }: { cache: ApolloCache } + ): null => { + const read = client.readQuery<{ count: number }>({ + query, + variables, + }); + if (read) { + const data = updater(read, variables); + cache.writeQuery({ query, variables, data }); + } else { + throw new Error("readQuery returned a falsy value"); + } + return null; }; + }; - const resolvers = { - Query: { - count: () => 0, - }, - Mutation: { - increment: update(query, ({ count, ...rest }, { amount }) => ({ - ...rest, - count: count + amount, - })), - decrement: update(query, ({ count, ...rest }, { amount }) => ({ - ...rest, - count: count - amount, - })), - }, - }; + const resolvers = { + Query: { + count: () => 0, + }, + Mutation: { + increment: update(query, ({ count, ...rest }, { amount }) => ({ + ...rest, + count: count + amount, + })), + decrement: update(query, ({ count, ...rest }, { amount }) => ({ + ...rest, + count: count - amount, + })), + }, + }; - client.addResolvers(resolvers); - - let count = 0; - client.watchQuery({ query }).subscribe({ - next: ({ data }) => { - count++; - if (count === 1) { - try { - expect({ ...data }).toMatchObject({ count: 0, lastCount: 1 }); - } catch (e) { - reject(e); - } - client.mutate({ mutation: increment, variables: { amount: 2 } }); - } + client.addResolvers(resolvers); + const stream = new ObservableStream(client.watchQuery({ query })); - if (count === 2) { - try { - expect({ ...data }).toMatchObject({ count: 2, lastCount: 1 }); - } catch (e) { - reject(e); - } - client.mutate({ mutation: decrement, variables: { amount: 1 } }); - } - if (count === 3) { - try { - expect({ ...data }).toMatchObject({ count: 1, lastCount: 1 }); - } catch (e) { - reject(e); - } - resolve(); - } - }, - error: (e) => reject(e), - complete: reject, - }); - } - ); + await expect(stream).toEmitMatchedValue({ + data: { count: 0, lastCount: 1 }, + }); - itAsync( - "should support a simple todo app using local state", - (resolve, reject) => { - const query = gql` - query GetTasks { - todos @client { - message - title - } - } - `; + await client.mutate({ mutation: increment, variables: { amount: 2 } }); - const mutation = gql` - mutation AddTodo($message: String, $title: String) { - addTodo(message: $message, title: $title) @client - } - `; + await expect(stream).toEmitMatchedValue({ + data: { count: 2, lastCount: 1 }, + }); - const client = new ApolloClient({ - link: ApolloLink.empty(), - cache: new InMemoryCache(), - resolvers: {}, - }); + await client.mutate({ mutation: decrement, variables: { amount: 1 } }); + + await expect(stream).toEmitMatchedValue({ + data: { count: 1, lastCount: 1 }, + }); + }); - interface Todo { - title: string; - message: string; - __typename: string; + it("should support a simple todo app using local state", async () => { + const query = gql` + query GetTasks { + todos @client { + message + title + } } + `; - const update = ( - query: DocumentNode, - updater: (todos: any, variables: Todo) => any - ) => { - return ( - _result: {}, - variables: Todo, - { cache }: { cache: ApolloCache } - ): null => { - const data = updater( - client.readQuery({ query, variables }), - variables - ); - cache.writeQuery({ query, variables, data }); - return null; - }; - }; + const mutation = gql` + mutation AddTodo($message: String, $title: String) { + addTodo(message: $message, title: $title) @client + } + `; - const resolvers = { - Query: { - todos: () => [], - }, - Mutation: { - addTodo: update(query, ({ todos }, { title, message }: Todo) => ({ - todos: todos.concat([{ message, title, __typename: "Todo" }]), - })), - }, + const client = new ApolloClient({ + link: ApolloLink.empty(), + cache: new InMemoryCache(), + resolvers: {}, + }); + + interface Todo { + title: string; + message: string; + __typename: string; + } + + const update = ( + query: DocumentNode, + updater: (todos: any, variables: Todo) => any + ) => { + return ( + _result: {}, + variables: Todo, + { cache }: { cache: ApolloCache } + ): null => { + const data = updater(client.readQuery({ query, variables }), variables); + cache.writeQuery({ query, variables, data }); + return null; }; + }; - client.addResolvers(resolvers); - - let count = 0; - client.watchQuery({ query }).subscribe({ - next: ({ data }: any) => { - count++; - if (count === 1) { - expect({ ...data }).toMatchObject({ todos: [] }); - client.mutate({ - mutation, - variables: { - title: "Apollo Client 2.0", - message: "ship it", - }, - }); - } else if (count === 2) { - expect(data.todos.map((x: Todo) => ({ ...x }))).toMatchObject([ - { - title: "Apollo Client 2.0", - message: "ship it", - __typename: "Todo", - }, - ]); - resolve(); - } + const resolvers = { + Query: { + todos: () => [], + }, + Mutation: { + addTodo: update(query, ({ todos }, { title, message }: Todo) => ({ + todos: todos.concat([{ message, title, __typename: "Todo" }]), + })), + }, + }; + + client.addResolvers(resolvers); + const stream = new ObservableStream(client.watchQuery({ query })); + + { + const { data } = await stream.takeNext(); + + expect(data).toEqual({ todos: [] }); + } + + await client.mutate({ + mutation, + variables: { + title: "Apollo Client 2.0", + message: "ship it", + }, + }); + + { + const { data } = await stream.takeNext(); + + expect(data.todos).toEqual([ + { + title: "Apollo Client 2.0", + message: "ship it", + __typename: "Todo", }, - }); + ]); } - ); + }); }); describe("Combining client and server state/operations", () => { - itAsync("should merge remote and local state", (resolve, reject) => { + it("should merge remote and local state", async () => { const query = gql` query list { list(name: "my list") { @@ -815,462 +776,403 @@ describe("Combining client and server state/operations", () => { const observer = client.watchQuery({ query }); - let count = 0; - observer.subscribe({ - next: (response) => { - if (count === 0) { - const initial = { ...data }; - initial.list.items = initial.list.items.map((x) => ({ - ...x, - isSelected: false, - })); - expect(response.data).toMatchObject(initial); - } - if (count === 1) { - expect((response.data as any).list.items[0].isSelected).toBe(true); - expect((response.data as any).list.items[1].isSelected).toBe(false); - resolve(); + const stream = new ObservableStream(observer); + + { + const response = await stream.takeNext(); + const initial = { ...data }; + initial.list.items = initial.list.items.map((x) => ({ + ...x, + isSelected: false, + })); + + expect(response.data).toMatchObject(initial); + } + + await client.mutate({ + mutation: gql` + mutation SelectItem($id: Int!) { + toggleItem(id: $id) @client } - count++; - }, - error: reject, + `, + variables: { id: 1 }, }); - const variables = { id: 1 }; - const mutation = gql` - mutation SelectItem($id: Int!) { - toggleItem(id: $id) @client - } - `; - // After initial result, toggle the state of one of the items - setTimeout(() => { - client.mutate({ mutation, variables }); - }, 10); + + { + const response = await stream.takeNext(); + + expect((response.data as any).list.items[0].isSelected).toBe(true); + expect((response.data as any).list.items[1].isSelected).toBe(false); + } }); - itAsync( - "query resolves with loading: false if subsequent responses contain the same data", - (resolve, reject) => { - const request = { - query: gql` - query people($id: Int) { - people(id: $id) { - id - name - } + it("query resolves with loading: false if subsequent responses contain the same data", async () => { + const request = { + query: gql` + query people($id: Int) { + people(id: $id) { + id + name } - `, - variables: { - id: 1, - }, - notifyOnNetworkStatusChange: true, - }; + } + `, + variables: { + id: 1, + }, + notifyOnNetworkStatusChange: true, + }; - const PersonType = new GraphQLObjectType({ - name: "Person", - fields: { - id: { type: GraphQLID }, - name: { type: GraphQLString }, - }, - }); + const PersonType = new GraphQLObjectType({ + name: "Person", + fields: { + id: { type: GraphQLID }, + name: { type: GraphQLString }, + }, + }); - const peopleData = [ - { id: 1, name: "John Smith" }, - { id: 2, name: "Sara Smith" }, - { id: 3, name: "Budd Deey" }, - ]; - - const QueryType = new GraphQLObjectType({ - name: "Query", - fields: { - people: { - type: PersonType, - args: { - id: { - type: GraphQLInt, - }, - }, - resolve: (_, { id }) => { - return peopleData; + const peopleData = [ + { id: 1, name: "John Smith" }, + { id: 2, name: "Sara Smith" }, + { id: 3, name: "Budd Deey" }, + ]; + + const QueryType = new GraphQLObjectType({ + name: "Query", + fields: { + people: { + type: PersonType, + args: { + id: { + type: GraphQLInt, }, }, + resolve: (_, { id }) => { + return peopleData; + }, }, - }); + }, + }); - const schema = new GraphQLSchema({ query: QueryType }); - - const link = new ApolloLink((operation) => { - // @ts-ignore - return new Observable(async (observer) => { - const { query, operationName, variables } = operation; - try { - const result = await graphql({ - schema, - source: print(query), - variableValues: variables, - operationName, - }); - observer.next(result); - observer.complete(); - } catch (err) { - observer.error(err); - } - }); + const schema = new GraphQLSchema({ query: QueryType }); + + const link = new ApolloLink((operation) => { + // @ts-ignore + return new Observable(async (observer) => { + const { query, operationName, variables } = operation; + try { + const result = await graphql({ + schema, + source: print(query), + variableValues: variables, + operationName, + }); + observer.next(result); + observer.complete(); + } catch (err) { + observer.error(err); + } }); + }); - const client = new ApolloClient({ - cache: new InMemoryCache(), - link, - }); + const client = new ApolloClient({ + cache: new InMemoryCache(), + link, + }); - const observer = client.watchQuery(request); + const observable = client.watchQuery(request); + const stream = new ObservableStream(observable); - let count = 0; - observer.subscribe({ - next: ({ loading, data }) => { - if (count === 0) expect(loading).toBe(false); - if (count === 1) expect(loading).toBe(true); - if (count === 2) { - expect(loading).toBe(false); - resolve(); - } - count++; - }, - error: reject, - }); + await expect(stream).toEmitMatchedValue({ loading: false }); - setTimeout(() => { - observer.refetch({ - id: 2, - }); - }, 1); - } - ); + await observable.refetch({ id: 2 }); - itAsync( - "should correctly propagate an error from a client resolver", - async (resolve, reject) => { - const data = { - list: { - __typename: "List", - items: [ - { __typename: "ListItem", id: 1, name: "first", isDone: true }, - { __typename: "ListItem", id: 2, name: "second", isDone: false }, - ], - }, - }; + await expect(stream).toEmitMatchedValue({ loading: true }); + await expect(stream).toEmitMatchedValue({ loading: false }); + }); - const link = new ApolloLink(() => Observable.of({ data })); + it("should correctly propagate an error from a client resolver", async () => { + const data = { + list: { + __typename: "List", + items: [ + { __typename: "ListItem", id: 1, name: "first", isDone: true }, + { __typename: "ListItem", id: 2, name: "second", isDone: false }, + ], + }, + }; - const client = new ApolloClient({ - cache: new InMemoryCache(), - link, - resolvers: { - Query: { - hasBeenIllegallyTouched: (_, _v, _c) => { - throw new Error("Illegal Query Operation Occurred"); - }, - }, + const link = new ApolloLink(() => Observable.of({ data })); - Mutation: { - touchIllegally: (_, _v, _c) => { - throw new Error("Illegal Mutation Operation Occurred"); - }, + const client = new ApolloClient({ + cache: new InMemoryCache(), + link, + resolvers: { + Query: { + hasBeenIllegallyTouched: (_, _v, _c) => { + throw new Error("Illegal Query Operation Occurred"); }, }, - }); - const variables = { id: 1 }; - const query = gql` - query hasBeenIllegallyTouched($id: Int!) { - hasBeenIllegallyTouched(id: $id) @client - } - `; - const mutation = gql` - mutation SelectItem($id: Int!) { - touchIllegally(id: $id) @client - } - `; + Mutation: { + touchIllegally: (_, _v, _c) => { + throw new Error("Illegal Mutation Operation Occurred"); + }, + }, + }, + }); - try { - await client.query({ query, variables }); - reject("Should have thrown!"); - } catch (e) { - // Test Passed! - expect(() => { - throw e; - }).toThrowErrorMatchingSnapshot(); + const variables = { id: 1 }; + const query = gql` + query hasBeenIllegallyTouched($id: Int!) { + hasBeenIllegallyTouched(id: $id) @client } - - try { - await client.mutate({ mutation, variables }); - reject("Should have thrown!"); - } catch (e) { - // Test Passed! - expect(() => { - throw e; - }).toThrowErrorMatchingSnapshot(); + `; + const mutation = gql` + mutation SelectItem($id: Int!) { + touchIllegally(id: $id) @client } + `; - resolve(); - } - ); + await expect( + client.query({ query, variables }) + ).rejects.toThrowErrorMatchingSnapshot(); + + await expect( + client.mutate({ mutation, variables }) + ).rejects.toThrowErrorMatchingSnapshot(); + }); it("should handle a simple query with both server and client fields", async () => { using _consoleSpies = spyOnConsole.takeSnapshots("error"); - await new Promise((resolve, reject) => { - const query = gql` - query GetCount { - count @client - lastCount - } - `; - const cache = new InMemoryCache(); + const query = gql` + query GetCount { + count @client + lastCount + } + `; + const cache = new InMemoryCache(); - const link = new ApolloLink((operation) => { - expect(operation.operationName).toBe("GetCount"); - return Observable.of({ data: { lastCount: 1 } }); - }); + const link = new ApolloLink((operation) => { + expect(operation.operationName).toBe("GetCount"); + return Observable.of({ data: { lastCount: 1 } }); + }); - const client = new ApolloClient({ - cache, - link, - resolvers: {}, - }); + const client = new ApolloClient({ + cache, + link, + resolvers: {}, + }); - cache.writeQuery({ - query, - data: { - count: 0, - }, - }); + cache.writeQuery({ + query, + data: { + count: 0, + }, + }); - client.watchQuery({ query }).subscribe({ - next: ({ data }) => { - expect({ ...data }).toMatchObject({ count: 0, lastCount: 1 }); - resolve(); - }, - }); + const stream = new ObservableStream(client.watchQuery({ query })); + + await expect(stream).toEmitMatchedValue({ + data: { count: 0, lastCount: 1 }, }); }); it("should support nested querying of both server and client fields", async () => { using _consoleSpies = spyOnConsole.takeSnapshots("error"); - await new Promise((resolve, reject) => { - const query = gql` - query GetUser { - user { - firstName @client - lastName - } + const query = gql` + query GetUser { + user { + firstName @client + lastName } - `; - - const cache = new InMemoryCache(); - const link = new ApolloLink((operation) => { - expect(operation.operationName).toBe("GetUser"); - return Observable.of({ - data: { - user: { - __typename: "User", - // We need an id (or a keyFields policy) because, if the User - // object is not identifiable, the call to cache.writeQuery - // below will simply replace the existing data rather than - // merging the new data with the existing data. - id: 123, - lastName: "Doe", - }, - }, - }); - }); - - const client = new ApolloClient({ - cache, - link, - resolvers: {}, - }); + } + `; - cache.writeQuery({ - query, + const cache = new InMemoryCache(); + const link = new ApolloLink((operation) => { + expect(operation.operationName).toBe("GetUser"); + return Observable.of({ data: { user: { __typename: "User", + // We need an id (or a keyFields policy) because, if the User + // object is not identifiable, the call to cache.writeQuery + // below will simply replace the existing data rather than + // merging the new data with the existing data. id: 123, - firstName: "John", + lastName: "Doe", }, }, }); + }); - client.watchQuery({ query }).subscribe({ - next({ data }: any) { - const { user } = data; - try { - expect(user).toMatchObject({ - firstName: "John", - lastName: "Doe", - __typename: "User", - }); - } catch (e) { - reject(e); - } - resolve(); + const client = new ApolloClient({ + cache, + link, + resolvers: {}, + }); + + cache.writeQuery({ + query, + data: { + user: { + __typename: "User", + id: 123, + firstName: "John", }, - }); + }, + }); + + const stream = new ObservableStream(client.watchQuery({ query })); + + await expect(stream).toEmitMatchedValue({ + data: { + user: { + firstName: "John", + lastName: "Doe", + __typename: "User", + }, + }, }); }); - itAsync( - "should combine both server and client mutations", - (resolve, reject) => { - const query = gql` - query SampleQuery { - count @client - user { - firstName - } + it("should combine both server and client mutations", async () => { + const query = gql` + query SampleQuery { + count @client + user { + firstName } - `; + } + `; - const mutation = gql` - mutation SampleMutation { - incrementCount @client - updateUser(firstName: "Harry") { - firstName - } + const mutation = gql` + mutation SampleMutation { + incrementCount @client + updateUser(firstName: "Harry") { + firstName } - `; + } + `; - const counterQuery = gql` - { - count @client - } - `; + const counterQuery = gql` + { + count @client + } + `; - const userQuery = gql` - { - user { - firstName - } + const userQuery = gql` + { + user { + firstName } - `; + } + `; - let watchCount = 0; - const link = new ApolloLink((operation: Operation): Observable<{}> => { - if (operation.operationName === "SampleQuery") { - return Observable.of({ - data: { user: { __typename: "User", firstName: "John" } }, - }); - } - if (operation.operationName === "SampleMutation") { - return Observable.of({ - data: { updateUser: { __typename: "User", firstName: "Harry" } }, - }); - } + const link = new ApolloLink((operation: Operation): Observable<{}> => { + if (operation.operationName === "SampleQuery") { return Observable.of({ - errors: [new Error(`Unknown operation ${operation.operationName}`)], + data: { user: { __typename: "User", firstName: "John" } }, }); + } + if (operation.operationName === "SampleMutation") { + return Observable.of({ + data: { updateUser: { __typename: "User", firstName: "Harry" } }, + }); + } + return Observable.of({ + errors: [new Error(`Unknown operation ${operation.operationName}`)], }); + }); - const cache = new InMemoryCache(); - const client = new ApolloClient({ - cache, - link, - resolvers: { - Mutation: { - incrementCount: (_, __, { cache }) => { - const { count } = cache.readQuery({ query: counterQuery }); - const data = { count: count + 1 }; - cache.writeQuery({ - query: counterQuery, - data, - }); - return null; - }, + const cache = new InMemoryCache(); + const client = new ApolloClient({ + cache, + link, + resolvers: { + Mutation: { + incrementCount: (_, __, { cache }) => { + const { count } = cache.readQuery({ query: counterQuery }); + const data = { count: count + 1 }; + cache.writeQuery({ + query: counterQuery, + data, + }); + return null; }, }, - }); + }, + }); - cache.writeQuery({ - query: counterQuery, - data: { - count: 0, - }, - }); + cache.writeQuery({ + query: counterQuery, + data: { + count: 0, + }, + }); - client.watchQuery({ query }).subscribe({ - next: ({ data }: any) => { - if (watchCount === 0) { - expect(data.count).toEqual(0); - expect({ ...data.user }).toMatchObject({ - __typename: "User", - firstName: "John", - }); - watchCount += 1; - client.mutate({ - mutation, - update(proxy, { data: { updateUser } }) { - proxy.writeQuery({ - query: userQuery, - data: { - user: { ...updateUser }, - }, - }); - }, - }); - } else { - expect(data.count).toEqual(1); - expect({ ...data.user }).toMatchObject({ - __typename: "User", - firstName: "Harry", - }); - resolve(); - } - }, - }); - } - ); + const stream = new ObservableStream(client.watchQuery({ query })); - itAsync( - "handles server errors when root data property is null", - (resolve, reject) => { - const query = gql` - query GetUser { - user { - firstName @client - lastName - } - } - `; + await expect(stream).toEmitMatchedValue({ + data: { + count: 0, + user: { __typename: "User", firstName: "John" }, + }, + }); - const cache = new InMemoryCache(); - const link = new ApolloLink((operation) => { - return Observable.of({ - data: null, - errors: [ - new GraphQLError("something went wrong", { - extensions: { - code: "INTERNAL_SERVER_ERROR", - }, - path: ["user"], - }), - ], + await client.mutate({ + mutation, + update(proxy, { data: { updateUser } }) { + proxy.writeQuery({ + query: userQuery, + data: { + user: { ...updateUser }, + }, }); - }); + }, + }); - const client = new ApolloClient({ - cache, - link, - resolvers: {}, - }); + await expect(stream).toEmitMatchedValue({ + data: { + count: 1, + user: { __typename: "User", firstName: "Harry" }, + }, + }); + }); - client.watchQuery({ query }).subscribe({ - error(error) { - expect(error.message).toEqual("something went wrong"); - resolve(); - }, - next() { - reject(); - }, + it("handles server errors when root data property is null", async () => { + const query = gql` + query GetUser { + user { + firstName @client + lastName + } + } + `; + + const cache = new InMemoryCache(); + const link = new ApolloLink((operation) => { + return Observable.of({ + data: null, + errors: [ + new GraphQLError("something went wrong", { + extensions: { + code: "INTERNAL_SERVER_ERROR", + }, + path: ["user"], + }), + ], }); - } - ); + }); + + const client = new ApolloClient({ + cache, + link, + resolvers: {}, + }); + + const stream = new ObservableStream(client.watchQuery({ query })); + + await expect(stream).toEmitError("something went wrong"); + }); }); diff --git a/src/__tests__/local-state/resolvers.ts b/src/__tests__/local-state/resolvers.ts index b305a3ed7d8..b1941cd80c7 100644 --- a/src/__tests__/local-state/resolvers.ts +++ b/src/__tests__/local-state/resolvers.ts @@ -1,27 +1,19 @@ import gql from "graphql-tag"; import { DocumentNode, ExecutionResult } from "graphql"; -import { assign } from "lodash"; import { LocalState } from "../../core/LocalState"; -import { - ApolloClient, - ApolloQueryResult, - Resolvers, - WatchQueryOptions, -} from "../../core"; +import { ApolloClient, ApolloQueryResult, Resolvers } from "../../core"; import { InMemoryCache, isReference } from "../../cache"; -import { Observable, Observer } from "../../utilities"; +import { Observable } from "../../utilities"; import { ApolloLink } from "../../link/core"; -import { itAsync } from "../../testing"; import mockQueryManager from "../../testing/core/mocking/mockQueryManager"; -import wrap from "../../testing/core/wrap"; +import { ObservableStream } from "../../testing/internal"; // Helper method that sets up a mockQueryManager and then passes on the // results to an observer. -const assertWithObserver = ({ - reject, +const setupTestWithResolvers = ({ resolvers, query, serverQuery, @@ -30,10 +22,8 @@ const assertWithObserver = ({ serverResult, error, delay, - observer, }: { - reject: (reason: any) => any; - resolvers?: Resolvers; + resolvers: Resolvers; query: DocumentNode; serverQuery?: DocumentNode; variables?: object; @@ -41,7 +31,6 @@ const assertWithObserver = ({ error?: Error; serverResult?: ExecutionResult; delay?: number; - observer: Observer>; }) => { const queryManager = mockQueryManager({ request: { query: serverQuery || query, variables }, @@ -50,22 +39,15 @@ const assertWithObserver = ({ delay, }); - if (resolvers) { - queryManager.getLocalState().addResolvers(resolvers); - } - - const finalOptions = assign( - { query, variables }, - queryOptions - ) as WatchQueryOptions; - return queryManager.watchQuery(finalOptions).subscribe({ - next: wrap(reject, observer.next!), - error: observer.error, - }); + queryManager.getLocalState().addResolvers(resolvers); + + return new ObservableStream( + queryManager.watchQuery({ query, variables, ...queryOptions }) + ); }; describe("Basic resolver capabilities", () => { - itAsync("should run resolvers for @client queries", (resolve, reject) => { + it("should run resolvers for @client queries", async () => { const query = gql` query Test { foo @client { @@ -80,234 +62,183 @@ describe("Basic resolver capabilities", () => { }, }; - assertWithObserver({ - reject, + const stream = setupTestWithResolvers({ resolvers, query }); + + await expect(stream).toEmitMatchedValue({ data: { foo: { bar: true } } }); + }); + + it("should handle queries with a mix of @client and server fields", async () => { + const query = gql` + query Mixed { + foo @client { + bar + } + bar { + baz + } + } + `; + + const serverQuery = gql` + query Mixed { + bar { + baz + } + } + `; + + const resolvers = { + Query: { + foo: () => ({ bar: true }), + }, + }; + + const stream = setupTestWithResolvers({ resolvers, query, - observer: { - next({ data }) { - try { - expect(data).toEqual({ foo: { bar: true } }); - } catch (error) { - reject(error); - } - resolve(); - }, + serverQuery, + serverResult: { data: { bar: { baz: true } } }, + }); + + await expect(stream).toEmitMatchedValue({ + data: { + foo: { bar: true }, + bar: { baz: true }, }, }); }); - itAsync( - "should handle queries with a mix of @client and server fields", - (resolve, reject) => { - const query = gql` - query Mixed { - foo @client { - bar - } - bar { - baz - } - } - `; + it("should handle a mix of @client fields with fragments and server fields", async () => { + const query = gql` + fragment client on ClientData { + bar + __typename + } - const serverQuery = gql` - query Mixed { - bar { - baz - } + query Mixed { + foo @client { + ...client } - `; - - const resolvers = { - Query: { - foo: () => ({ bar: true }), - }, - }; - - assertWithObserver({ - reject, - resolvers, - query, - serverQuery, - serverResult: { data: { bar: { baz: true } } }, - observer: { - next({ data }) { - try { - expect(data).toEqual({ foo: { bar: true }, bar: { baz: true } }); - } catch (error) { - reject(error); - } - resolve(); - }, - }, - }); - } - ); - - itAsync( - "should handle a mix of @client fields with fragments and server fields", - (resolve, reject) => { - const query = gql` - fragment client on ClientData { - bar - __typename + bar { + baz } + } + `; - query Mixed { - foo @client { - ...client - } - bar { - baz - } + const serverQuery = gql` + query Mixed { + bar { + baz } - `; + } + `; - const serverQuery = gql` - query Mixed { - bar { - baz - } - } - `; + const resolvers = { + Query: { + foo: () => ({ bar: true, __typename: "ClientData" }), + }, + }; - const resolvers = { - Query: { - foo: () => ({ bar: true, __typename: "ClientData" }), - }, - }; - - assertWithObserver({ - reject, - resolvers, - query, - serverQuery, - serverResult: { data: { bar: { baz: true, __typename: "Bar" } } }, - observer: { - next({ data }) { - try { - expect(data).toEqual({ - foo: { bar: true, __typename: "ClientData" }, - bar: { baz: true }, - }); - } catch (error) { - reject(error); - } - resolve(); - }, - }, - }); - } - ); + const stream = setupTestWithResolvers({ + resolvers, + query, + serverQuery, + serverResult: { data: { bar: { baz: true, __typename: "Bar" } } }, + }); - itAsync( - "should handle @client fields inside fragments", - (resolve, reject) => { - const query = gql` - fragment Foo on Foo { - bar - ...Foo2 - } - fragment Foo2 on Foo { - __typename - baz @client + await expect(stream).toEmitMatchedValue({ + data: { + foo: { bar: true, __typename: "ClientData" }, + bar: { baz: true }, + }, + }); + }); + + it("should handle @client fields inside fragments", async () => { + const query = gql` + fragment Foo on Foo { + bar + ...Foo2 + } + fragment Foo2 on Foo { + __typename + baz @client + } + query Mixed { + foo { + ...Foo } - query Mixed { - foo { - ...Foo - } - bar { - baz - } + bar { + baz } - `; + } + `; - const serverQuery = gql` - fragment Foo on Foo { - bar + const serverQuery = gql` + fragment Foo on Foo { + bar + } + query Mixed { + foo { + ...Foo } - query Mixed { - foo { - ...Foo - } - bar { - baz - } + bar { + baz } - `; + } + `; - const resolvers = { - Foo: { - baz: () => false, - }, - }; - - assertWithObserver({ - reject, - resolvers, - query, - serverQuery, - serverResult: { - data: { foo: { bar: true, __typename: `Foo` }, bar: { baz: true } }, - }, - observer: { - next({ data }) { - try { - expect(data).toEqual({ - foo: { bar: true, baz: false, __typename: "Foo" }, - bar: { baz: true }, - }); - } catch (error) { - reject(error); - } - resolve(); - }, - }, - }); - } - ); + const resolvers = { + Foo: { + baz: () => false, + }, + }; - itAsync( - "should have access to query variables when running @client resolvers", - (resolve, reject) => { - const query = gql` - query WithVariables($id: ID!) { - foo @client { - bar(id: $id) - } + const stream = setupTestWithResolvers({ + resolvers, + query, + serverQuery, + serverResult: { + data: { foo: { bar: true, __typename: `Foo` }, bar: { baz: true } }, + }, + }); + + await expect(stream).toEmitMatchedValue({ + data: { + foo: { bar: true, baz: false, __typename: "Foo" }, + bar: { baz: true }, + }, + }); + }); + + it("should have access to query variables when running @client resolvers", async () => { + const query = gql` + query WithVariables($id: ID!) { + foo @client { + bar(id: $id) } - `; + } + `; - const resolvers = { - Query: { - foo: () => ({ __typename: "Foo" }), - }, - Foo: { - bar: (_data: any, { id }: { id: number }) => id, - }, - }; - - assertWithObserver({ - reject, - resolvers, - query, - variables: { id: 1 }, - observer: { - next({ data }) { - try { - expect(data).toEqual({ foo: { bar: 1 } }); - } catch (error) { - reject(error); - } - resolve(); - }, - }, - }); - } - ); + const resolvers = { + Query: { + foo: () => ({ __typename: "Foo" }), + }, + Foo: { + bar: (_data: any, { id }: { id: number }) => id, + }, + }; + + const stream = setupTestWithResolvers({ + resolvers, + query, + variables: { id: 1 }, + }); + + await expect(stream).toEmitMatchedValue({ data: { foo: { bar: 1 } } }); + }); - itAsync("should pass context to @client resolvers", (resolve, reject) => { + it("should pass context to @client resolvers", async () => { const query = gql` query WithContext { foo @client { @@ -325,127 +256,99 @@ describe("Basic resolver capabilities", () => { }, }; - assertWithObserver({ - reject, + const stream = setupTestWithResolvers({ resolvers, query, queryOptions: { context: { id: 1 } }, - observer: { - next({ data }) { - try { - expect(data).toEqual({ foo: { bar: 1 } }); - } catch (error) { - reject(error); - } - resolve(); - }, - }, }); + + await expect(stream).toEmitMatchedValue({ data: { foo: { bar: 1 } } }); }); - itAsync( - "should combine local @client resolver results with server results, for " + - "the same field", - (resolve, reject) => { - const query = gql` - query author { - author { - name - stats { - totalPosts - postsToday @client - } + it("should combine local @client resolver results with server results, for the same field", async () => { + const query = gql` + query author { + author { + name + stats { + totalPosts + postsToday @client } } - `; + } + `; - const serverQuery = gql` - query author { - author { - name - stats { - totalPosts - } + const serverQuery = gql` + query author { + author { + name + stats { + totalPosts } } - `; + } + `; - const resolvers = { - Stats: { - postsToday: () => 10, - }, - }; - - assertWithObserver({ - reject, - resolvers, - query, - serverQuery, - serverResult: { - data: { - author: { - name: "John Smith", - stats: { - totalPosts: 100, - __typename: "Stats", - }, - __typename: "Author", + const resolvers = { + Stats: { + postsToday: () => 10, + }, + }; + + const stream = setupTestWithResolvers({ + resolvers, + query, + serverQuery, + serverResult: { + data: { + author: { + name: "John Smith", + stats: { + totalPosts: 100, + __typename: "Stats", }, + __typename: "Author", }, }, - observer: { - next({ data }) { - try { - expect(data).toEqual({ - author: { - name: "John Smith", - stats: { - totalPosts: 100, - postsToday: 10, - }, - }, - }); - } catch (error) { - reject(error); - } - resolve(); + }, + }); + + await expect(stream).toEmitMatchedValue({ + data: { + author: { + name: "John Smith", + stats: { + totalPosts: 100, + postsToday: 10, }, }, - }); - } - ); + }, + }); + }); - itAsync( - "should handle resolvers that work with booleans properly", - (resolve, reject) => { - const query = gql` - query CartDetails { - isInCart @client - } - `; + it("should handle resolvers that work with booleans properly", async () => { + const query = gql` + query CartDetails { + isInCart @client + } + `; - const cache = new InMemoryCache(); - cache.writeQuery({ query, data: { isInCart: true } }); + const cache = new InMemoryCache(); + cache.writeQuery({ query, data: { isInCart: true } }); - const client = new ApolloClient({ - cache, - resolvers: { - Query: { - isInCart: () => false, - }, + const client = new ApolloClient({ + cache, + resolvers: { + Query: { + isInCart: () => false, }, - }); + }, + }); - return client - .query({ query, fetchPolicy: "network-only" }) - .then(({ data }: any) => { - expect({ ...data }).toMatchObject({ - isInCart: false, - }); - resolve(); - }); - } - ); + const { data } = await client.query({ query, fetchPolicy: "network-only" }); + + expect(data).toMatchObject({ isInCart: false }); + }); it("should handle nested asynchronous @client resolvers (issue #4841)", () => { const query = gql` @@ -569,57 +472,47 @@ describe("Basic resolver capabilities", () => { ]); }); - itAsync( - "should not run resolvers without @client directive (issue #9571)", - (resolve, reject) => { - const query = gql` - query Mixed { - foo @client { - bar - } - bar { - baz - } + it("should not run resolvers without @client directive (issue #9571)", async () => { + const query = gql` + query Mixed { + foo @client { + bar + } + bar { + baz } - `; + } + `; - const serverQuery = gql` - query Mixed { - bar { - baz - } + const serverQuery = gql` + query Mixed { + bar { + baz } - `; + } + `; - const barResolver = jest.fn(() => ({ __typename: `Bar`, baz: false })); + const barResolver = jest.fn(() => ({ __typename: `Bar`, baz: false })); - const resolvers = { - Query: { - foo: () => ({ __typename: `Foo`, bar: true }), - bar: barResolver, - }, - }; - - assertWithObserver({ - reject, - resolvers, - query, - serverQuery, - serverResult: { data: { bar: { baz: true } } }, - observer: { - next({ data }) { - try { - expect(data).toEqual({ foo: { bar: true }, bar: { baz: true } }); - expect(barResolver).not.toHaveBeenCalled(); - } catch (error) { - reject(error); - } - resolve(); - }, - }, - }); - } - ); + const resolvers = { + Query: { + foo: () => ({ __typename: `Foo`, bar: true }), + bar: barResolver, + }, + }; + + const stream = setupTestWithResolvers({ + resolvers, + query, + serverQuery, + serverResult: { data: { bar: { baz: true } } }, + }); + + await expect(stream).toEmitMatchedValue({ + data: { foo: { bar: true }, bar: { baz: true } }, + }); + expect(barResolver).not.toHaveBeenCalled(); + }); }); describe("Writing cache data from resolvers", () => { @@ -777,440 +670,394 @@ describe("Writing cache data from resolvers", () => { }); describe("Resolving field aliases", () => { - itAsync( - "should run resolvers for missing client queries with aliased field", - (resolve, reject) => { - // expect.assertions(1); - const query = gql` - query Aliased { - foo @client { - bar - } - baz: bar { - foo - } + it("should run resolvers for missing client queries with aliased field", async () => { + const query = gql` + query Aliased { + foo @client { + bar } - `; + baz: bar { + foo + } + } + `; - const link = new ApolloLink(() => - // Each link is responsible for implementing their own aliasing so it - // returns baz not bar - Observable.of({ data: { baz: { foo: true, __typename: "Baz" } } }) - ); + const link = new ApolloLink(() => + // Each link is responsible for implementing their own aliasing so it + // returns baz not bar + Observable.of({ data: { baz: { foo: true, __typename: "Baz" } } }) + ); - const client = new ApolloClient({ - cache: new InMemoryCache(), - link, - resolvers: { - Query: { - foo: () => ({ bar: true, __typename: "Foo" }), - }, + const client = new ApolloClient({ + cache: new InMemoryCache(), + link, + resolvers: { + Query: { + foo: () => ({ bar: true, __typename: "Foo" }), }, - }); + }, + }); - client.query({ query }).then(({ data }) => { - try { - expect(data).toEqual({ - foo: { bar: true, __typename: "Foo" }, - baz: { foo: true, __typename: "Baz" }, - }); - } catch (e) { - reject(e); - return; - } - resolve(); - }, reject); - } - ); + const { data } = await client.query({ query }); - itAsync( - "should run resolvers for client queries when aliases are in use on " + - "the @client-tagged node", - (resolve, reject) => { - const aliasedQuery = gql` - query Test { - fie: foo @client { - bar - } - } - `; - - const client = new ApolloClient({ - cache: new InMemoryCache(), - link: ApolloLink.empty(), - resolvers: { - Query: { - foo: () => ({ bar: true, __typename: "Foo" }), - fie: () => { - reject( - "Called the resolver using the alias' name, instead of " + - "the correct resolver name." - ); - }, - }, - }, - }); + expect(data).toEqual({ + foo: { bar: true, __typename: "Foo" }, + baz: { foo: true, __typename: "Baz" }, + }); + }); - client.query({ query: aliasedQuery }).then(({ data }) => { - expect(data).toEqual({ fie: { bar: true, __typename: "Foo" } }); - resolve(); - }, reject); - } - ); + it("should run resolvers for client queries when aliases are in use on the @client-tagged node", async () => { + const aliasedQuery = gql` + query Test { + fie: foo @client { + bar + } + } + `; - itAsync( - "should respect aliases for *nested fields* on the @client-tagged node", - (resolve, reject) => { - const aliasedQuery = gql` - query Test { - fie: foo @client { - fum: bar - } - baz: bar { - foo - } + const fie = jest.fn(); + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: ApolloLink.empty(), + resolvers: { + Query: { + foo: () => ({ bar: true, __typename: "Foo" }), + fie, + }, + }, + }); + + const { data } = await client.query({ query: aliasedQuery }); + + expect(data).toEqual({ fie: { bar: true, __typename: "Foo" } }); + expect(fie).not.toHaveBeenCalled(); + }); + + it("should respect aliases for *nested fields* on the @client-tagged node", async () => { + const aliasedQuery = gql` + query Test { + fie: foo @client { + fum: bar } - `; + baz: bar { + foo + } + } + `; - const link = new ApolloLink(() => - Observable.of({ data: { baz: { foo: true, __typename: "Baz" } } }) - ); + const link = new ApolloLink(() => + Observable.of({ data: { baz: { foo: true, __typename: "Baz" } } }) + ); - const client = new ApolloClient({ - cache: new InMemoryCache(), - link, - resolvers: { - Query: { - foo: () => ({ bar: true, __typename: "Foo" }), - fie: () => { - reject( - "Called the resolver using the alias' name, instead of " + - "the correct resolver name." - ); - }, - }, + const fie = jest.fn(); + const client = new ApolloClient({ + cache: new InMemoryCache(), + link, + resolvers: { + Query: { + foo: () => ({ bar: true, __typename: "Foo" }), + fie, }, - }); + }, + }); - client.query({ query: aliasedQuery }).then(({ data }) => { - expect(data).toEqual({ - fie: { fum: true, __typename: "Foo" }, - baz: { foo: true, __typename: "Baz" }, - }); - resolve(); - }, reject); - } - ); + const { data } = await client.query({ query: aliasedQuery }); + + expect(data).toEqual({ + fie: { fum: true, __typename: "Foo" }, + baz: { foo: true, __typename: "Baz" }, + }); + expect(fie).not.toHaveBeenCalled(); + }); - it( - "should pull initialized values for aliased fields tagged with @client " + - "from the cache", - () => { - const query = gql` + it("should pull initialized values for aliased fields tagged with @client from the cache", async () => { + const query = gql` + { + fie: foo @client { + bar + } + } + `; + + const cache = new InMemoryCache(); + const client = new ApolloClient({ + cache, + link: ApolloLink.empty(), + resolvers: {}, + }); + + cache.writeQuery({ + query: gql` { - fie: foo @client { + foo { bar } } - `; + `, + data: { + foo: { + bar: "yo", + __typename: "Foo", + }, + }, + }); - const cache = new InMemoryCache(); - const client = new ApolloClient({ - cache, - link: ApolloLink.empty(), - resolvers: {}, - }); + const { data } = await client.query({ query }); - cache.writeQuery({ - query: gql` - { - foo { - bar - } - } - `, + expect({ ...data }).toMatchObject({ + fie: { bar: "yo", __typename: "Foo" }, + }); + }); + + it("should resolve @client fields using local resolvers and not have their value overridden when a fragment is loaded", async () => { + const query = gql` + fragment LaunchDetails on Launch { + id + __typename + } + query Launch { + launch { + isInCart @client + ...LaunchDetails + } + } + `; + + const link = new ApolloLink(() => + Observable.of({ data: { - foo: { - bar: "yo", - __typename: "Foo", + launch: { + id: 1, + __typename: "Launch", }, }, - }); + }) + ); - return client.query({ query }).then(({ data }) => { - expect({ ...data }).toMatchObject({ - fie: { bar: "yo", __typename: "Foo" }, - }); - }); - } - ); + const client = new ApolloClient({ + cache: new InMemoryCache(), + link, + resolvers: { + Launch: { + isInCart() { + return true; + }, + }, + }, + }); - it( - "should resolve @client fields using local resolvers and not have " + - "their value overridden when a fragment is loaded", - () => { - const query = gql` - fragment LaunchDetails on Launch { - id - __typename - } - query Launch { + client.writeQuery({ + query: gql` + { launch { - isInCart @client - ...LaunchDetails + isInCart } } - `; - - const link = new ApolloLink(() => - Observable.of({ - data: { - launch: { - id: 1, - __typename: "Launch", - }, - }, - }) - ); - - const client = new ApolloClient({ - cache: new InMemoryCache(), - link, - resolvers: { - Launch: { - isInCart() { - return true; - }, - }, + `, + data: { + launch: { + isInCart: false, + __typename: "Launch", }, - }); + }, + }); - client.writeQuery({ - query: gql` - { - launch { - isInCart - } - } - `, - data: { - launch: { - isInCart: false, - __typename: "Launch", - }, - }, - }); + { + const { data } = await client.query({ query }); + // `isInCart` resolver is fired, returning `true` (which is then + // stored in the cache). + expect(data.launch.isInCart).toBe(true); + } - return client - .query({ query }) - .then(({ data }) => { - // `isInCart` resolver is fired, returning `true` (which is then - // stored in the cache). - expect(data.launch.isInCart).toBe(true); - }) - .then(() => { - client.query({ query }).then(({ data }) => { - // When the same query fires again, `isInCart` should be pulled from - // the cache and have a value of `true`. - expect(data.launch.isInCart).toBe(true); - }); - }); + { + const { data } = await client.query({ query }); + // When the same query fires again, `isInCart` should be pulled from + // the cache and have a value of `true`. + expect(data.launch.isInCart).toBe(true); } - ); + }); }); describe("Force local resolvers", () => { - it( - "should force the running of local resolvers marked with " + - "`@client(always: true)` when using `ApolloClient.query`", - async () => { - const query = gql` - query Author { - author { - name - isLoggedIn @client(always: true) - } + it("should force the running of local resolvers marked with `@client(always: true)` when using `ApolloClient.query`", async () => { + const query = gql` + query Author { + author { + name + isLoggedIn @client(always: true) } - `; + } + `; - const cache = new InMemoryCache(); - const client = new ApolloClient({ - cache, - link: ApolloLink.empty(), - resolvers: {}, - }); + const cache = new InMemoryCache(); + const client = new ApolloClient({ + cache, + link: ApolloLink.empty(), + resolvers: {}, + }); + + cache.writeQuery({ + query, + data: { + author: { + name: "John Smith", + isLoggedIn: false, + __typename: "Author", + }, + }, + }); + + // When the resolver isn't defined, there isn't anything to force, so + // make sure the query resolves from the cache properly. + const { data: data1 } = await client.query({ query }); + expect(data1.author.isLoggedIn).toEqual(false); + + client.addResolvers({ + Author: { + isLoggedIn() { + return true; + }, + }, + }); + + // A resolver is defined, so make sure it's forced, and the result + // resolves properly as a combination of cache and local resolver + // data. + const { data: data2 } = await client.query({ query }); + expect(data2.author.isLoggedIn).toEqual(true); + }); + + it("should avoid running forced resolvers a second time when loading results over the network (so not from the cache)", async () => { + const query = gql` + query Author { + author { + name + isLoggedIn @client(always: true) + } + } + `; - cache.writeQuery({ - query, + const link = new ApolloLink(() => + Observable.of({ data: { author: { name: "John Smith", - isLoggedIn: false, __typename: "Author", }, }, - }); - - // When the resolver isn't defined, there isn't anything to force, so - // make sure the query resolves from the cache properly. - const { data: data1 } = await client.query({ query }); - expect(data1.author.isLoggedIn).toEqual(false); + }) + ); - client.addResolvers({ + let count = 0; + const client = new ApolloClient({ + cache: new InMemoryCache(), + link, + resolvers: { Author: { isLoggedIn() { + count += 1; return true; }, }, - }); + }, + }); - // A resolver is defined, so make sure it's forced, and the result - // resolves properly as a combination of cache and local resolver - // data. - const { data: data2 } = await client.query({ query }); - expect(data2.author.isLoggedIn).toEqual(true); - } - ); + const { data } = await client.query({ query }); + expect(data.author.isLoggedIn).toEqual(true); + expect(count).toEqual(1); + }); - it( - "should avoid running forced resolvers a second time when " + - "loading results over the network (so not from the cache)", - async () => { - const query = gql` - query Author { - author { - name - isLoggedIn @client(always: true) - } - } - `; - - const link = new ApolloLink(() => - Observable.of({ - data: { - author: { - name: "John Smith", - __typename: "Author", - }, - }, - }) - ); + it("should only force resolvers for fields marked with `@client(always: true)`, not all `@client` fields", async () => { + const query = gql` + query UserDetails { + name @client + isLoggedIn @client(always: true) + } + `; - let count = 0; - const client = new ApolloClient({ - cache: new InMemoryCache(), - link, - resolvers: { - Author: { - isLoggedIn() { - count += 1; - return true; - }, + let nameCount = 0; + let isLoggedInCount = 0; + const client = new ApolloClient({ + cache: new InMemoryCache(), + resolvers: { + Query: { + name() { + nameCount += 1; + return "John Smith"; + }, + isLoggedIn() { + isLoggedInCount += 1; + return true; }, }, - }); + }, + }); - const { data } = await client.query({ query }); - expect(data.author.isLoggedIn).toEqual(true); - expect(count).toEqual(1); - } - ); + await client.query({ query }); + expect(nameCount).toEqual(1); + expect(isLoggedInCount).toEqual(1); - it( - "should only force resolvers for fields marked with " + - "`@client(always: true)`, not all `@client` fields", - async () => { - const query = gql` - query UserDetails { - name @client - isLoggedIn @client(always: true) - } - `; - - let nameCount = 0; - let isLoggedInCount = 0; - const client = new ApolloClient({ - cache: new InMemoryCache(), - resolvers: { - Query: { - name() { - nameCount += 1; - return "John Smith"; - }, - isLoggedIn() { - isLoggedInCount += 1; - return true; - }, + // On the next request, `name` will be loaded from the cache only, + // whereas `isLoggedIn` will be loaded from the cache then overwritten + // by running its forced local resolver. + await client.query({ query }); + expect(nameCount).toEqual(1); + expect(isLoggedInCount).toEqual(2); + }); + + it("should force the running of local resolvers marked with `@client(always: true)` when using `ApolloClient.watchQuery`", async () => { + const query = gql` + query IsUserLoggedIn { + isUserLoggedIn @client(always: true) + } + `; + + const queryNoForce = gql` + query IsUserLoggedIn { + isUserLoggedIn @client + } + `; + + let callCount = 0; + const client = new ApolloClient({ + cache: new InMemoryCache(), + resolvers: { + Query: { + isUserLoggedIn() { + callCount += 1; + return true; }, }, - }); + }, + }); - await client.query({ query }); - expect(nameCount).toEqual(1); - expect(isLoggedInCount).toEqual(1); + { + const stream = new ObservableStream(client.watchQuery({ query })); - // On the next request, `name` will be loaded from the cache only, - // whereas `isLoggedIn` will be loaded from the cache then overwritten - // by running its forced local resolver. - await client.query({ query }); - expect(nameCount).toEqual(1); - expect(isLoggedInCount).toEqual(2); + await expect(stream).toEmitNext(); + expect(callCount).toBe(1); } - ); - itAsync( - "should force the running of local resolvers marked with " + - "`@client(always: true)` when using `ApolloClient.watchQuery`", - (resolve, reject) => { - const query = gql` - query IsUserLoggedIn { - isUserLoggedIn @client(always: true) - } - `; - - const queryNoForce = gql` - query IsUserLoggedIn { - isUserLoggedIn @client - } - `; - - let callCount = 0; - const client = new ApolloClient({ - cache: new InMemoryCache(), - resolvers: { - Query: { - isUserLoggedIn() { - callCount += 1; - return true; - }, - }, - }, - }); + { + const stream = new ObservableStream(client.watchQuery({ query })); - client.watchQuery({ query }).subscribe({ - next() { - expect(callCount).toBe(1); + await expect(stream).toEmitNext(); + expect(callCount).toBe(2); + } - client.watchQuery({ query }).subscribe({ - next() { - expect(callCount).toBe(2); + { + const stream = new ObservableStream( + client.watchQuery({ query: queryNoForce }) + ); - client.watchQuery({ query: queryNoForce }).subscribe({ - next() { - // Result is loaded from the cache since the resolver - // isn't being forced. - expect(callCount).toBe(2); - resolve(); - }, - }); - }, - }); - }, - }); + await expect(stream).toEmitNext(); + // Result is loaded from the cache since the resolver + // isn't being forced. + expect(callCount).toBe(2); } - ); + }); - it("should allow client-only virtual resolvers (#4731)", function () { + it("should allow client-only virtual resolvers (#4731)", async () => { const query = gql` query UserData { userData @client { @@ -1241,21 +1088,21 @@ describe("Force local resolvers", () => { }, }); - return client.query({ query }).then((result) => { - expect(result.data).toEqual({ - userData: { - __typename: "User", - firstName: "Ben", - lastName: "Newman", - fullName: "Ben Newman", - }, - }); + const result = await client.query({ query }); + + expect(result.data).toEqual({ + userData: { + __typename: "User", + firstName: "Ben", + lastName: "Newman", + fullName: "Ben Newman", + }, }); }); }); describe("Async resolvers", () => { - itAsync("should support async @client resolvers", async (resolve, reject) => { + it("should support async @client resolvers", async () => { const query = gql` query Member { isLoggedIn @client @@ -1276,64 +1123,61 @@ describe("Async resolvers", () => { const { data: { isLoggedIn }, } = await client.query({ query })!; + expect(isLoggedIn).toBe(true); - return resolve(); }); - itAsync( - "should support async @client resolvers mixed with remotely resolved data", - async (resolve, reject) => { - const query = gql` - query Member { - member { - name - sessionCount @client - isLoggedIn @client - } + it("should support async @client resolvers mixed with remotely resolved data", async () => { + const query = gql` + query Member { + member { + name + sessionCount @client + isLoggedIn @client } - `; - - const testMember = { - name: "John Smithsonian", - isLoggedIn: true, - sessionCount: 10, - }; - - const link = new ApolloLink(() => - Observable.of({ - data: { - member: { - name: testMember.name, - __typename: "Member", - }, + } + `; + + const testMember = { + name: "John Smithsonian", + isLoggedIn: true, + sessionCount: 10, + }; + + const link = new ApolloLink(() => + Observable.of({ + data: { + member: { + name: testMember.name, + __typename: "Member", }, - }) - ); + }, + }) + ); - const client = new ApolloClient({ - cache: new InMemoryCache(), - link, - resolvers: { - Member: { - isLoggedIn() { - return Promise.resolve(testMember.isLoggedIn); - }, - sessionCount() { - return testMember.sessionCount; - }, + const client = new ApolloClient({ + cache: new InMemoryCache(), + link, + resolvers: { + Member: { + isLoggedIn() { + return Promise.resolve(testMember.isLoggedIn); + }, + sessionCount() { + return testMember.sessionCount; }, }, - }); + }, + }); - const { - data: { member }, - } = await client.query({ query })!; - expect(member.name).toBe(testMember.name); - expect(member.isLoggedIn).toBe(testMember.isLoggedIn); - expect(member.sessionCount).toBe(testMember.sessionCount); - return resolve(); - } - ); + const { + data: { member }, + } = await client.query({ query })!; + + expect(member.name).toBe(testMember.name); + expect(member.isLoggedIn).toBe(testMember.isLoggedIn); + expect(member.sessionCount).toBe(testMember.sessionCount); + }); }); describe("LocalState helpers", () => { diff --git a/src/cache/inmemory/__tests__/fragmentMatcher.ts b/src/cache/inmemory/__tests__/fragmentMatcher.ts index 29bce194c2d..6823819226b 100644 --- a/src/cache/inmemory/__tests__/fragmentMatcher.ts +++ b/src/cache/inmemory/__tests__/fragmentMatcher.ts @@ -1,6 +1,5 @@ import gql from "graphql-tag"; -import { itAsync } from "../../../testing"; import { InMemoryCache } from "../inMemoryCache"; import { visit, FragmentDefinitionNode } from "graphql"; import { hasOwn } from "../helpers"; @@ -242,7 +241,7 @@ describe("policies.fragmentMatches", () => { console.warn = warn; }); - itAsync("can infer fuzzy subtypes heuristically", (resolve, reject) => { + it("can infer fuzzy subtypes heuristically", async () => { const cache = new InMemoryCache({ possibleTypes: { A: ["B", "C"], @@ -279,7 +278,7 @@ describe("policies.fragmentMatches", () => { FragmentDefinition(frag) { function check(typename: string, result: boolean) { if (result !== cache.policies.fragmentMatches(frag, typename)) { - reject( + throw new Error( `fragment ${frag.name.value} should${ result ? "" : " not" } have matched typename ${typename}` @@ -577,7 +576,5 @@ describe("policies.fragmentMatches", () => { }, }).size ).toBe("ABCDEF".length); - - resolve(); }); }); diff --git a/src/cache/inmemory/__tests__/writeToStore.ts b/src/cache/inmemory/__tests__/writeToStore.ts index 8c8bf1c5b48..f147f5d49d4 100644 --- a/src/cache/inmemory/__tests__/writeToStore.ts +++ b/src/cache/inmemory/__tests__/writeToStore.ts @@ -19,7 +19,6 @@ import { cloneDeep, getMainDefinition, } from "../../../utilities"; -import { itAsync } from "../../../testing/core"; import { StoreWriter } from "../writeToStore"; import { defaultNormalizedCacheFactory, writeQueryToStore } from "./helpers"; import { InMemoryCache } from "../inMemoryCache"; @@ -1860,137 +1859,132 @@ describe("writing to the store", () => { expect(cache.extract()).toMatchSnapshot(); }); - itAsync( - "should allow a union of objects of a different type, when overwriting a generated id with a real id", - (resolve, reject) => { - const dataWithPlaceholder = { - author: { - hello: "Foo", - __typename: "Placeholder", - }, - }; - const dataWithAuthor = { - author: { - firstName: "John", - lastName: "Smith", - id: "129", - __typename: "Author", - }, - }; - const query = gql` - query { - author { - ... on Author { - firstName - lastName - id - __typename - } - ... on Placeholder { - hello - __typename - } + it("should allow a union of objects of a different type, when overwriting a generated id with a real id", async () => { + const dataWithPlaceholder = { + author: { + hello: "Foo", + __typename: "Placeholder", + }, + }; + const dataWithAuthor = { + author: { + firstName: "John", + lastName: "Smith", + id: "129", + __typename: "Author", + }, + }; + const query = gql` + query { + author { + ... on Author { + firstName + lastName + id + __typename + } + ... on Placeholder { + hello + __typename } } - `; + } + `; - let mergeCount = 0; - const cache = new InMemoryCache({ - typePolicies: { - Query: { - fields: { - author: { - merge(existing, incoming, { isReference, readField }) { - switch (++mergeCount) { - case 1: - expect(existing).toBeUndefined(); - expect(isReference(incoming)).toBe(false); - expect(incoming).toEqual(dataWithPlaceholder.author); - break; - case 2: - expect(existing).toEqual(dataWithPlaceholder.author); - expect(isReference(incoming)).toBe(true); - expect(readField("__typename", incoming)).toBe("Author"); - break; - case 3: - expect(isReference(existing)).toBe(true); - expect(readField("__typename", existing)).toBe("Author"); - expect(incoming).toEqual(dataWithPlaceholder.author); - break; - default: - reject("unreached"); - } - return incoming; - }, + let mergeCount = 0; + const cache = new InMemoryCache({ + typePolicies: { + Query: { + fields: { + author: { + merge(existing, incoming, { isReference, readField }) { + switch (++mergeCount) { + case 1: + expect(existing).toBeUndefined(); + expect(isReference(incoming)).toBe(false); + expect(incoming).toEqual(dataWithPlaceholder.author); + break; + case 2: + expect(existing).toEqual(dataWithPlaceholder.author); + expect(isReference(incoming)).toBe(true); + expect(readField("__typename", incoming)).toBe("Author"); + break; + case 3: + expect(isReference(existing)).toBe(true); + expect(readField("__typename", existing)).toBe("Author"); + expect(incoming).toEqual(dataWithPlaceholder.author); + break; + default: + throw new Error("unreached"); + } + return incoming; }, }, }, }, - }); + }, + }); - // write the first object, without an ID, placeholder - cache.writeQuery({ - query, - data: dataWithPlaceholder, - }); + // write the first object, without an ID, placeholder + cache.writeQuery({ + query, + data: dataWithPlaceholder, + }); - expect(cache.extract()).toEqual({ - ROOT_QUERY: { - __typename: "Query", - author: { - hello: "Foo", - __typename: "Placeholder", - }, + expect(cache.extract()).toEqual({ + ROOT_QUERY: { + __typename: "Query", + author: { + hello: "Foo", + __typename: "Placeholder", }, - }); + }, + }); - // replace with another one of different type with ID - cache.writeQuery({ - query, - data: dataWithAuthor, - }); + // replace with another one of different type with ID + cache.writeQuery({ + query, + data: dataWithAuthor, + }); - expect(cache.extract()).toEqual({ - "Author:129": { - firstName: "John", - lastName: "Smith", - id: "129", - __typename: "Author", - }, - ROOT_QUERY: { - __typename: "Query", - author: makeReference("Author:129"), - }, - }); + expect(cache.extract()).toEqual({ + "Author:129": { + firstName: "John", + lastName: "Smith", + id: "129", + __typename: "Author", + }, + ROOT_QUERY: { + __typename: "Query", + author: makeReference("Author:129"), + }, + }); - // and go back to the original: - cache.writeQuery({ - query, - data: dataWithPlaceholder, - }); + // and go back to the original: + cache.writeQuery({ + query, + data: dataWithPlaceholder, + }); - // Author__129 will remain in the store, - // but will not be referenced by any of the fields, - // hence we combine, and in that very order - expect(cache.extract()).toEqual({ - "Author:129": { - firstName: "John", - lastName: "Smith", - id: "129", - __typename: "Author", - }, - ROOT_QUERY: { - __typename: "Query", - author: { - hello: "Foo", - __typename: "Placeholder", - }, + // Author__129 will remain in the store, + // but will not be referenced by any of the fields, + // hence we combine, and in that very order + expect(cache.extract()).toEqual({ + "Author:129": { + firstName: "John", + lastName: "Smith", + id: "129", + __typename: "Author", + }, + ROOT_QUERY: { + __typename: "Query", + author: { + hello: "Foo", + __typename: "Placeholder", }, - }); - - resolve(); - } - ); + }, + }); + }); it("does not swallow errors other than field errors", () => { const query = gql` @@ -2888,29 +2882,28 @@ describe("writing to the store", () => { expect(mergeCounts).toEqual({ first: 1, second: 1, third: 1, fourth: 1 }); }); - itAsync( - "should allow silencing broadcast of cache updates", - function (resolve, reject) { - const cache = new InMemoryCache({ - typePolicies: { - Counter: { - // Counter is a singleton, but we want to be able to test - // writing to it with writeFragment, so it needs to have an ID. - keyFields: [], - }, + it("should allow silencing broadcast of cache updates", async () => { + const cache = new InMemoryCache({ + typePolicies: { + Counter: { + // Counter is a singleton, but we want to be able to test + // writing to it with writeFragment, so it needs to have an ID. + keyFields: [], }, - }); + }, + }); - const query = gql` - query { - counter { - count - } + const query = gql` + query { + counter { + count } - `; + } + `; - const results: number[] = []; + const results: number[] = []; + const promise = new Promise((resolve) => { cache.watch({ query, optimistic: true, @@ -2925,101 +2918,103 @@ describe("writing to the store", () => { resolve(); }, }); + }); - let count = 0; - - cache.writeQuery({ - query, - data: { - counter: { - __typename: "Counter", - count: ++count, - }, - }, - broadcast: false, - }); + let count = 0; - expect(cache.extract()).toEqual({ - ROOT_QUERY: { - __typename: "Query", - counter: { __ref: "Counter:{}" }, - }, - "Counter:{}": { + cache.writeQuery({ + query, + data: { + counter: { __typename: "Counter", - count: 1, + count: ++count, }, - }); + }, + broadcast: false, + }); + + expect(cache.extract()).toEqual({ + ROOT_QUERY: { + __typename: "Query", + counter: { __ref: "Counter:{}" }, + }, + "Counter:{}": { + __typename: "Counter", + count: 1, + }, + }); + + expect(results).toEqual([]); + + const counterId = cache.identify({ + __typename: "Counter", + })!; + + cache.writeFragment({ + id: counterId, + fragment: gql` + fragment Count on Counter { + count + } + `, + data: { + count: ++count, + }, + broadcast: false, + }); - expect(results).toEqual([]); + const counterMeta = { + extraRootIds: ["Counter:{}"], + }; - const counterId = cache.identify({ + expect(cache.extract()).toEqual({ + __META: counterMeta, + ROOT_QUERY: { + __typename: "Query", + counter: { __ref: "Counter:{}" }, + }, + "Counter:{}": { __typename: "Counter", - })!; + count: 2, + }, + }); - cache.writeFragment({ + expect(results).toEqual([]); + + expect( + cache.evict({ id: counterId, - fragment: gql` - fragment Count on Counter { - count - } - `, - data: { - count: ++count, - }, + fieldName: "count", broadcast: false, - }); - - const counterMeta = { - extraRootIds: ["Counter:{}"], - }; - - expect(cache.extract()).toEqual({ - __META: counterMeta, - ROOT_QUERY: { - __typename: "Query", - counter: { __ref: "Counter:{}" }, - }, - "Counter:{}": { - __typename: "Counter", - count: 2, - }, - }); + }) + ).toBe(true); - expect(results).toEqual([]); + expect(cache.extract()).toEqual({ + __META: counterMeta, + ROOT_QUERY: { + __typename: "Query", + counter: { __ref: "Counter:{}" }, + }, + "Counter:{}": { + __typename: "Counter", + }, + }); - expect( - cache.evict({ - id: counterId, - fieldName: "count", - broadcast: false, - }) - ).toBe(true); + expect(results).toEqual([]); - expect(cache.extract()).toEqual({ - __META: counterMeta, - ROOT_QUERY: { - __typename: "Query", - counter: { __ref: "Counter:{}" }, - }, - "Counter:{}": { + // Only this write should trigger a broadcast. + cache.writeQuery({ + query, + data: { + counter: { __typename: "Counter", + count: 3, }, - }); - - expect(results).toEqual([]); + }, + }); - // Only this write should trigger a broadcast. - cache.writeQuery({ - query, - data: { - counter: { - __typename: "Counter", - count: 3, - }, - }, - }); - } - ); + await promise; + }); it("writeFragment should be able to infer ROOT_QUERY", () => { const cache = new InMemoryCache(); diff --git a/src/core/QueryManager.ts b/src/core/QueryManager.ts index e61e123c5f2..066dc137de9 100644 --- a/src/core/QueryManager.ts +++ b/src/core/QueryManager.ts @@ -899,15 +899,19 @@ export class QueryManager { include: InternalRefetchQueriesInclude = "active" ) { const queries = new Map>(); - const queryNamesAndDocs = new Map(); + const queryNames = new Map(); + const queryNamesAndQueryStrings = new Map(); const legacyQueryOptions = new Set(); if (Array.isArray(include)) { include.forEach((desc) => { if (typeof desc === "string") { - queryNamesAndDocs.set(desc, false); + queryNames.set(desc, desc); + queryNamesAndQueryStrings.set(desc, false); } else if (isDocumentNode(desc)) { - queryNamesAndDocs.set(this.transform(desc), false); + const queryString = print(this.transform(desc)); + queryNames.set(queryString, getOperationName(desc)); + queryNamesAndQueryStrings.set(queryString, false); } else if (isNonNullObject(desc) && desc.query) { legacyQueryOptions.add(desc); } @@ -935,12 +939,12 @@ export class QueryManager { if ( include === "active" || - (queryName && queryNamesAndDocs.has(queryName)) || - (document && queryNamesAndDocs.has(document)) + (queryName && queryNamesAndQueryStrings.has(queryName)) || + (document && queryNamesAndQueryStrings.has(print(document))) ) { queries.set(queryId, oq); - if (queryName) queryNamesAndDocs.set(queryName, true); - if (document) queryNamesAndDocs.set(document, true); + if (queryName) queryNamesAndQueryStrings.set(queryName, true); + if (document) queryNamesAndQueryStrings.set(print(document), true); } } }); @@ -969,15 +973,21 @@ export class QueryManager { }); } - if (__DEV__ && queryNamesAndDocs.size) { - queryNamesAndDocs.forEach((included, nameOrDoc) => { + if (__DEV__ && queryNamesAndQueryStrings.size) { + queryNamesAndQueryStrings.forEach((included, nameOrQueryString) => { if (!included) { - invariant.warn( - typeof nameOrDoc === "string" ? - `Unknown query named "%s" requested in refetchQueries options.include array` - : `Unknown query %o requested in refetchQueries options.include array`, - nameOrDoc - ); + const queryName = queryNames.get(nameOrQueryString); + + if (queryName) { + invariant.warn( + `Unknown query named "%s" requested in refetchQueries options.include array`, + queryName + ); + } else { + invariant.warn( + `Unknown anonymous query requested in refetchQueries options.include array` + ); + } } }); } diff --git a/src/core/__tests__/QueryManager/index.ts b/src/core/__tests__/QueryManager/index.ts index 5d6d9592bcc..1edd4e2c2f1 100644 --- a/src/core/__tests__/QueryManager/index.ts +++ b/src/core/__tests__/QueryManager/index.ts @@ -46,7 +46,7 @@ import wrap from "../../../testing/core/wrap"; import observableToPromise, { observableToPromiseAndSubscription, } from "../../../testing/core/observableToPromise"; -import { itAsync, wait } from "../../../testing/core"; +import { itAsync } from "../../../testing/core"; import { ApolloClient } from "../../../core"; import { mockFetchQuery } from "../ObservableQuery"; import { Concast, print } from "../../../utilities"; @@ -5156,6 +5156,151 @@ describe("QueryManager", () => { } ); + itAsync( + "should ignore (with warning) a document node in refetchQueries that has no active subscriptions", + (resolve, reject) => { + const mutation = gql` + mutation changeAuthorName { + changeAuthorName(newName: "Jack Smith") { + firstName + lastName + } + } + `; + const mutationData = { + changeAuthorName: { + firstName: "Jack", + lastName: "Smith", + }, + }; + const query = gql` + query getAuthors { + author { + firstName + lastName + } + } + `; + const data = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; + const secondReqData = { + author: { + firstName: "Jane", + lastName: "Johnson", + }, + }; + const queryManager = mockQueryManager( + { + request: { query }, + result: { data }, + }, + { + request: { query }, + result: { data: secondReqData }, + }, + { + request: { query: mutation }, + result: { data: mutationData }, + } + ); + + const observable = queryManager.watchQuery({ query }); + return observableToPromise({ observable }, (result) => { + expect(result.data).toEqual(data); + }) + .then(() => { + // The subscription has been stopped already + return queryManager.mutate({ + mutation, + refetchQueries: [query], + }); + }) + .then(() => { + expect(consoleWarnSpy).toHaveBeenLastCalledWith( + 'Unknown query named "%s" requested in refetchQueries options.include array', + "getAuthors" + ); + }) + .then(resolve, reject); + } + ); + + itAsync( + "should ignore (with warning) a document node containing an anonymous query in refetchQueries that has no active subscriptions", + (resolve, reject) => { + const mutation = gql` + mutation changeAuthorName { + changeAuthorName(newName: "Jack Smith") { + firstName + lastName + } + } + `; + const mutationData = { + changeAuthorName: { + firstName: "Jack", + lastName: "Smith", + }, + }; + const query = gql` + query { + author { + firstName + lastName + } + } + `; + const data = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; + const secondReqData = { + author: { + firstName: "Jane", + lastName: "Johnson", + }, + }; + const queryManager = mockQueryManager( + { + request: { query }, + result: { data }, + }, + { + request: { query }, + result: { data: secondReqData }, + }, + { + request: { query: mutation }, + result: { data: mutationData }, + } + ); + + const observable = queryManager.watchQuery({ query }); + return observableToPromise({ observable }, (result) => { + expect(result.data).toEqual(data); + }) + .then(() => { + // The subscription has been stopped already + return queryManager.mutate({ + mutation, + refetchQueries: [query], + }); + }) + .then(() => { + expect(consoleWarnSpy).toHaveBeenLastCalledWith( + "Unknown anonymous query requested in refetchQueries options.include array" + ); + }) + .then(resolve, reject); + } + ); + it("also works with a query document and variables", async () => { const mutation = gql` mutation changeAuthorName($id: ID!) { @@ -5228,12 +5373,157 @@ describe("QueryManager", () => { ); expect(observable.getCurrentResult().data).toEqual(secondReqData); - await wait(10); + await expect(stream).not.toEmitAnything(); + }); - queryManager["queries"].forEach((_, queryId) => { - expect(queryId).not.toContain("legacyOneTimeQuery"); + it("also works with a query document node", async () => { + const mutation = gql` + mutation changeAuthorName($id: ID!) { + changeAuthorName(newName: "Jack Smith", id: $id) { + firstName + lastName + } + } + `; + const mutationData = { + changeAuthorName: { + firstName: "Jack", + lastName: "Smith", + }, + }; + const query = gql` + query getAuthors($id: ID!) { + author(id: $id) { + firstName + lastName + } + } + `; + const data = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; + const secondReqData = { + author: { + firstName: "Jane", + lastName: "Johnson", + }, + }; + + const variables = { id: "1234" }; + const mutationVariables = { id: "2345" }; + const queryManager = mockQueryManager( + { + request: { query, variables }, + result: { data }, + delay: 10, + }, + { + request: { query, variables }, + result: { data: secondReqData }, + delay: 100, + }, + { + request: { query: mutation, variables: mutationVariables }, + result: { data: mutationData }, + delay: 10, + } + ); + const observable = queryManager.watchQuery({ query, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitMatchedValue({ data }); + + await queryManager.mutate({ + mutation, + variables: mutationVariables, + refetchQueries: [query], }); + await expect(stream).toEmitMatchedValue( + { data: secondReqData }, + { timeout: 150 } + ); + expect(observable.getCurrentResult().data).toEqual(secondReqData); + + await expect(stream).not.toEmitAnything(); + }); + + it("also works with different references of a same query document node", async () => { + const mutation = gql` + mutation changeAuthorName($id: ID!) { + changeAuthorName(newName: "Jack Smith", id: $id) { + firstName + lastName + } + } + `; + const mutationData = { + changeAuthorName: { + firstName: "Jack", + lastName: "Smith", + }, + }; + const query = gql` + query getAuthors($id: ID!) { + author(id: $id) { + firstName + lastName + } + } + `; + const data = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; + const secondReqData = { + author: { + firstName: "Jane", + lastName: "Johnson", + }, + }; + + const variables = { id: "1234" }; + const mutationVariables = { id: "2345" }; + const queryManager = mockQueryManager( + { + request: { query, variables }, + result: { data }, + delay: 10, + }, + { + request: { query, variables }, + result: { data: secondReqData }, + delay: 100, + }, + { + request: { query: mutation, variables: mutationVariables }, + result: { data: mutationData }, + delay: 10, + } + ); + const observable = queryManager.watchQuery({ query, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitMatchedValue({ data }); + + await queryManager.mutate({ + mutation, + variables: mutationVariables, + // spread the query into a new object to simulate multiple instances + refetchQueries: [{ ...query }], + }); + + await expect(stream).toEmitMatchedValue( + { data: secondReqData }, + { timeout: 150 } + ); + expect(observable.getCurrentResult().data).toEqual(secondReqData); + await expect(stream).not.toEmitAnything(); }); diff --git a/src/core/__tests__/QueryManager/links.ts b/src/core/__tests__/QueryManager/links.ts index 53d3b22f6bb..de43efdf8b9 100644 --- a/src/core/__tests__/QueryManager/links.ts +++ b/src/core/__tests__/QueryManager/links.ts @@ -10,7 +10,7 @@ import { ApolloLink } from "../../../link/core"; import { InMemoryCache } from "../../../cache/inmemory/inMemoryCache"; // mocks -import { itAsync, MockSubscriptionLink } from "../../../testing/core"; +import { MockSubscriptionLink } from "../../../testing/core"; // core import { QueryManager } from "../../QueryManager"; @@ -18,308 +18,277 @@ import { NextLink, Operation, Reference } from "../../../core"; import { getDefaultOptionsForQueryManagerTests } from "../../../testing/core/mocking/mockQueryManager"; describe("Link interactions", () => { - itAsync( - "includes the cache on the context for eviction links", - (resolve, reject) => { - const query = gql` - query CachedLuke { - people_one(id: 1) { + it("includes the cache on the context for eviction links", (done) => { + const query = gql` + query CachedLuke { + people_one(id: 1) { + name + friends { name - friends { - name - } } } - `; + } + `; - const initialData = { - people_one: { - name: "Luke Skywalker", - friends: [{ name: "Leia Skywalker" }], - }, - }; - - const evictionLink = (operation: Operation, forward: NextLink) => { - const { cache } = operation.getContext(); - expect(cache).toBeDefined(); - return forward(operation).map((result) => { - setTimeout(() => { - const cacheResult = cache.read({ query }); - expect(cacheResult).toEqual(initialData); - expect(cacheResult).toEqual(result.data); - if (count === 1) { - resolve(); - } - }, 10); - return result; - }); - }; - - const mockLink = new MockSubscriptionLink(); - const link = ApolloLink.from([evictionLink, mockLink]); - const queryManager = new QueryManager( - getDefaultOptionsForQueryManagerTests({ - cache: new InMemoryCache({ addTypename: false }), - link, - }) - ); - - const observable = queryManager.watchQuery({ - query, - variables: {}, - }); + const initialData = { + people_one: { + name: "Luke Skywalker", + friends: [{ name: "Leia Skywalker" }], + }, + }; - let count = 0; - observable.subscribe({ - next: (result) => { - count++; - }, - error: (e) => { - console.error(e); - }, + const evictionLink = (operation: Operation, forward: NextLink) => { + const { cache } = operation.getContext(); + expect(cache).toBeDefined(); + return forward(operation).map((result) => { + setTimeout(() => { + const cacheResult = cache.read({ query }); + expect(cacheResult).toEqual(initialData); + expect(cacheResult).toEqual(result.data); + if (count === 1) { + done(); + } + }, 10); + return result; }); + }; + + const mockLink = new MockSubscriptionLink(); + const link = ApolloLink.from([evictionLink, mockLink]); + const queryManager = new QueryManager( + getDefaultOptionsForQueryManagerTests({ + cache: new InMemoryCache({ addTypename: false }), + link, + }) + ); + + const observable = queryManager.watchQuery({ + query, + variables: {}, + }); - // fire off first result - mockLink.simulateResult({ result: { data: initialData } }); - } - ); - itAsync( - "cleans up all links on the final unsubscribe from watchQuery", - (resolve, reject) => { - const query = gql` - query WatchedLuke { - people_one(id: 1) { + let count = 0; + observable.subscribe({ + next: (result) => { + count++; + }, + error: (e) => { + console.error(e); + }, + }); + + // fire off first result + mockLink.simulateResult({ result: { data: initialData } }); + }); + + it("cleans up all links on the final unsubscribe from watchQuery", (done) => { + const query = gql` + query WatchedLuke { + people_one(id: 1) { + name + friends { name - friends { - name - } } } - `; + } + `; - const initialData = { - people_one: { - name: "Luke Skywalker", - friends: [{ name: "Leia Skywalker" }], - }, - }; - - const link = new MockSubscriptionLink(); - const queryManager = new QueryManager( - getDefaultOptionsForQueryManagerTests({ - cache: new InMemoryCache({ addTypename: false }), - link, - }) - ); - - const observable = queryManager.watchQuery({ - query, - variables: {}, - }); + const initialData = { + people_one: { + name: "Luke Skywalker", + friends: [{ name: "Leia Skywalker" }], + }, + }; - let count = 0; - let four: ObservableSubscription; - // first watch - const one = observable.subscribe((result) => count++); - // second watch - const two = observable.subscribe((result) => count++); - // third watch (to be unsubscribed) - const three = observable.subscribe((result) => { - count++; - three.unsubscribe(); - // fourth watch - four = observable.subscribe((x) => count++); - }); + const link = new MockSubscriptionLink(); + const queryManager = new QueryManager( + getDefaultOptionsForQueryManagerTests({ + cache: new InMemoryCache({ addTypename: false }), + link, + }) + ); - // fire off first result - link.simulateResult({ result: { data: initialData } }); - setTimeout(() => { - one.unsubscribe(); - - link.simulateResult({ - result: { - data: { - people_one: { - name: "Luke Skywalker", - friends: [{ name: "R2D2" }], - }, + const observable = queryManager.watchQuery({ + query, + variables: {}, + }); + + let count = 0; + let four: ObservableSubscription; + // first watch + const one = observable.subscribe((result) => count++); + // second watch + const two = observable.subscribe((result) => count++); + // third watch (to be unsubscribed) + const three = observable.subscribe((result) => { + count++; + three.unsubscribe(); + // fourth watch + four = observable.subscribe((x) => count++); + }); + + // fire off first result + link.simulateResult({ result: { data: initialData } }); + setTimeout(() => { + one.unsubscribe(); + + link.simulateResult({ + result: { + data: { + people_one: { + name: "Luke Skywalker", + friends: [{ name: "R2D2" }], }, }, - }); - setTimeout(() => { - four.unsubscribe(); - // final unsubscribe should be called now - two.unsubscribe(); - }, 10); + }, + }); + setTimeout(() => { + four.unsubscribe(); + // final unsubscribe should be called now + two.unsubscribe(); }, 10); + }, 10); - link.onUnsubscribe(() => { - expect(count).toEqual(6); - resolve(); - }); - } - ); - itAsync( - "cleans up all links on the final unsubscribe from watchQuery [error]", - (resolve, reject) => { - const query = gql` - query WatchedLuke { - people_one(id: 1) { + link.onUnsubscribe(() => { + expect(count).toEqual(6); + done(); + }); + }); + + it("cleans up all links on the final unsubscribe from watchQuery [error]", (done) => { + const query = gql` + query WatchedLuke { + people_one(id: 1) { + name + friends { name - friends { - name - } } } - `; + } + `; - const initialData = { - people_one: { - name: "Luke Skywalker", - friends: [{ name: "Leia Skywalker" }], - }, - }; - - const link = new MockSubscriptionLink(); - const queryManager = new QueryManager( - getDefaultOptionsForQueryManagerTests({ - cache: new InMemoryCache({ addTypename: false }), - link, - }) - ); - - const observable = queryManager.watchQuery({ - query, - variables: {}, - }); + const initialData = { + people_one: { + name: "Luke Skywalker", + friends: [{ name: "Leia Skywalker" }], + }, + }; - let count = 0; - let four: ObservableSubscription; - // first watch - const one = observable.subscribe((result) => count++); - // second watch - observable.subscribe({ - next: () => count++, - error: () => { - count = 0; - }, - }); - // third watch (to be unsubscribed) - const three = observable.subscribe((result) => { - count++; - three.unsubscribe(); - // fourth watch - four = observable.subscribe((x) => count++); - }); + const link = new MockSubscriptionLink(); + const queryManager = new QueryManager( + getDefaultOptionsForQueryManagerTests({ + cache: new InMemoryCache({ addTypename: false }), + link, + }) + ); - // fire off first result - link.simulateResult({ result: { data: initialData } }); - setTimeout(() => { - one.unsubscribe(); - four.unsubscribe(); + const observable = queryManager.watchQuery({ + query, + variables: {}, + }); - // final unsubscribe should be called now - // since errors clean up subscriptions - link.simulateResult({ error: new Error("dang") }); + let count = 0; + let four: ObservableSubscription; + // first watch + const one = observable.subscribe((result) => count++); + // second watch + observable.subscribe({ + next: () => count++, + error: () => { + count = 0; + }, + }); + // third watch (to be unsubscribed) + const three = observable.subscribe((result) => { + count++; + three.unsubscribe(); + // fourth watch + four = observable.subscribe((x) => count++); + }); - setTimeout(() => { - expect(count).toEqual(0); - resolve(); - }, 10); + // fire off first result + link.simulateResult({ result: { data: initialData } }); + setTimeout(() => { + one.unsubscribe(); + four.unsubscribe(); + + // final unsubscribe should be called now + // since errors clean up subscriptions + link.simulateResult({ error: new Error("dang") }); + + setTimeout(() => { + expect(count).toEqual(0); + done(); }, 10); + }, 10); - link.onUnsubscribe(() => { - expect(count).toEqual(4); - }); - } - ); - itAsync( - "includes the cache on the context for mutations", - (resolve, reject) => { - const mutation = gql` - mutation UpdateLuke { - people_one(id: 1) { + link.onUnsubscribe(() => { + expect(count).toEqual(4); + }); + }); + + it("includes the cache on the context for mutations", (done) => { + const mutation = gql` + mutation UpdateLuke { + people_one(id: 1) { + name + friends { name - friends { - name - } } } - `; + } + `; - const initialData = { - people_one: { - name: "Luke Skywalker", - friends: [{ name: "Leia Skywalker" }], - }, - }; - - const evictionLink = (operation: Operation, forward: NextLink) => { - const { cache } = operation.getContext(); - expect(cache).toBeDefined(); - resolve(); - return forward(operation); - }; - - const mockLink = new MockSubscriptionLink(); - const link = ApolloLink.from([evictionLink, mockLink]); - const queryManager = new QueryManager( - getDefaultOptionsForQueryManagerTests({ - cache: new InMemoryCache({ addTypename: false }), - link, - }) - ); - - queryManager.mutate({ mutation }); - - // fire off first result - mockLink.simulateResult({ result: { data: initialData } }); - } - ); - - itAsync( - "includes passed context in the context for mutations", - (resolve, reject) => { - const mutation = gql` - mutation UpdateLuke { - people_one(id: 1) { + const evictionLink = (operation: Operation, forward: NextLink) => { + const { cache } = operation.getContext(); + expect(cache).toBeDefined(); + done(); + return forward(operation); + }; + + const mockLink = new MockSubscriptionLink(); + const link = ApolloLink.from([evictionLink, mockLink]); + const queryManager = new QueryManager( + getDefaultOptionsForQueryManagerTests({ + cache: new InMemoryCache({ addTypename: false }), + link, + }) + ); + + void queryManager.mutate({ mutation }); + }); + + it("includes passed context in the context for mutations", (done) => { + const mutation = gql` + mutation UpdateLuke { + people_one(id: 1) { + name + friends { name - friends { - name - } } } - `; + } + `; + + const evictionLink = (operation: Operation, forward: NextLink) => { + const { planet } = operation.getContext(); + expect(planet).toBe("Tatooine"); + done(); + return forward(operation); + }; + + const mockLink = new MockSubscriptionLink(); + const link = ApolloLink.from([evictionLink, mockLink]); + const queryManager = new QueryManager( + getDefaultOptionsForQueryManagerTests({ + cache: new InMemoryCache({ addTypename: false }), + link, + }) + ); + + void queryManager.mutate({ mutation, context: { planet: "Tatooine" } }); + }); - const initialData = { - people_one: { - name: "Luke Skywalker", - friends: [{ name: "Leia Skywalker" }], - }, - }; - - const evictionLink = (operation: Operation, forward: NextLink) => { - const { planet } = operation.getContext(); - expect(planet).toBe("Tatooine"); - resolve(); - return forward(operation); - }; - - const mockLink = new MockSubscriptionLink(); - const link = ApolloLink.from([evictionLink, mockLink]); - const queryManager = new QueryManager( - getDefaultOptionsForQueryManagerTests({ - cache: new InMemoryCache({ addTypename: false }), - link, - }) - ); - - queryManager.mutate({ mutation, context: { planet: "Tatooine" } }); - - // fire off first result - mockLink.simulateResult({ result: { data: initialData } }); - } - ); it("includes getCacheKey function on the context for cache resolvers", async () => { const query = gql` { diff --git a/src/core/__tests__/fetchPolicies.ts b/src/core/__tests__/fetchPolicies.ts index 0208b6982c5..8042f2712b4 100644 --- a/src/core/__tests__/fetchPolicies.ts +++ b/src/core/__tests__/fetchPolicies.ts @@ -4,7 +4,7 @@ import { ApolloClient, NetworkStatus } from "../../core"; import { ApolloLink } from "../../link/core"; import { InMemoryCache } from "../../cache"; import { Observable } from "../../utilities"; -import { itAsync, mockSingleLink } from "../../testing"; +import { mockSingleLink } from "../../testing"; import { TypedDocumentNode } from "@graphql-typed-document-node/core"; import { WatchQueryFetchPolicy, WatchQueryOptions } from "../watchQueryOptions"; import { ApolloQueryResult } from "../types"; @@ -56,7 +56,7 @@ const mutationResult = { const merged = { author: { ...result.author, firstName: "James" } }; -const createLink = (reject: (reason: any) => any) => +const createLink = () => mockSingleLink( { request: { query }, @@ -66,7 +66,7 @@ const createLink = (reject: (reason: any) => any) => request: { query }, result: { data: result }, } - ).setOnError(reject); + ); const createFailureLink = () => mockSingleLink( @@ -80,7 +80,7 @@ const createFailureLink = () => } ); -const createMutationLink = (reject: (reason: any) => any) => +const createMutationLink = () => // fetch the data mockSingleLink( { @@ -95,41 +95,35 @@ const createMutationLink = (reject: (reason: any) => any) => request: { query }, result: { data: merged }, } - ).setOnError(reject); + ); describe("network-only", () => { - itAsync( - "requests from the network even if already in cache", - (resolve, reject) => { - let called = 0; - const inspector = new ApolloLink((operation, forward) => { + it("requests from the network even if already in cache", async () => { + let called = 0; + const inspector = new ApolloLink((operation, forward) => { + called++; + return forward(operation).map((result) => { called++; - return forward(operation).map((result) => { - called++; - return result; - }); + return result; }); + }); - const client = new ApolloClient({ - link: inspector.concat(createLink(reject)), - cache: new InMemoryCache({ addTypename: false }), - }); + const client = new ApolloClient({ + link: inspector.concat(createLink()), + cache: new InMemoryCache({ addTypename: false }), + }); - return client - .query({ query }) - .then(() => - client - .query({ fetchPolicy: "network-only", query }) - .then((actualResult) => { - expect(actualResult.data).toEqual(result); - expect(called).toBe(4); - }) - ) - .then(resolve, reject); - } - ); + await client.query({ query }); + const actualResult = await client.query({ + fetchPolicy: "network-only", + query, + }); + + expect(actualResult.data).toEqual(result); + expect(called).toBe(4); + }); - itAsync("saves data to the cache on success", (resolve, reject) => { + it("saves data to the cache on success", async () => { let called = 0; const inspector = new ApolloLink((operation, forward) => { called++; @@ -140,22 +134,18 @@ describe("network-only", () => { }); const client = new ApolloClient({ - link: inspector.concat(createLink(reject)), + link: inspector.concat(createLink()), cache: new InMemoryCache({ addTypename: false }), }); - return client - .query({ query, fetchPolicy: "network-only" }) - .then(() => - client.query({ query }).then((actualResult) => { - expect(actualResult.data).toEqual(result); - expect(called).toBe(2); - }) - ) - .then(resolve, reject); + await client.query({ query, fetchPolicy: "network-only" }); + const actualResult = await client.query({ query }); + + expect(actualResult.data).toEqual(result); + expect(called).toBe(2); }); - itAsync("does not save data to the cache on failure", (resolve, reject) => { + it("does not save data to the cache on failure", async () => { let called = 0; const inspector = new ApolloLink((operation, forward) => { called++; @@ -171,24 +161,20 @@ describe("network-only", () => { }); let didFail = false; - return client - .query({ query, fetchPolicy: "network-only" }) - .catch((e) => { - expect(e.message).toMatch("query failed"); - didFail = true; - }) - .then(() => - client.query({ query }).then((actualResult) => { - expect(actualResult.data).toEqual(result); - // the first error doesn't call .map on the inspector - expect(called).toBe(3); - expect(didFail).toBe(true); - }) - ) - .then(resolve, reject); + await client.query({ query, fetchPolicy: "network-only" }).catch((e) => { + expect(e.message).toMatch("query failed"); + didFail = true; + }); + + const actualResult = await client.query({ query }); + + expect(actualResult.data).toEqual(result); + // the first error doesn't call .map on the inspector + expect(called).toBe(3); + expect(didFail).toBe(true); }); - itAsync("updates the cache on a mutation", (resolve, reject) => { + it("updates the cache on a mutation", async () => { const inspector = new ApolloLink((operation, forward) => { return forward(operation).map((result) => { return result; @@ -196,28 +182,23 @@ describe("network-only", () => { }); const client = new ApolloClient({ - link: inspector.concat(createMutationLink(reject)), + link: inspector.concat(createMutationLink()), cache: new InMemoryCache({ addTypename: false }), }); - return client - .query({ query }) - .then(() => - // XXX currently only no-cache is supported as a fetchPolicy - // this mainly serves to ensure the cache is updated correctly - client.mutate({ mutation, variables }) - ) - .then(() => { - return client.query({ query }).then((actualResult) => { - expect(actualResult.data).toEqual(merged); - }); - }) - .then(resolve, reject); + await client.query({ query }); + // XXX currently only no-cache is supported as a fetchPolicy + // this mainly serves to ensure the cache is updated correctly + await client.mutate({ mutation, variables }); + + const actualResult = await client.query({ query }); + + expect(actualResult.data).toEqual(merged); }); }); describe("no-cache", () => { - itAsync("requests from the network when not in cache", (resolve, reject) => { + it("requests from the network when not in cache", async () => { let called = 0; const inspector = new ApolloLink((operation, forward) => { called++; @@ -228,81 +209,62 @@ describe("no-cache", () => { }); const client = new ApolloClient({ - link: inspector.concat(createLink(reject)), + link: inspector.concat(createLink()), cache: new InMemoryCache({ addTypename: false }), }); - return client - .query({ fetchPolicy: "no-cache", query }) - .then((actualResult) => { - expect(actualResult.data).toEqual(result); - expect(called).toBe(2); - }) - .then(resolve, reject); + const actualResult = await client.query({ fetchPolicy: "no-cache", query }); + + expect(actualResult.data).toEqual(result); + expect(called).toBe(2); }); - itAsync( - "requests from the network even if already in cache", - (resolve, reject) => { - let called = 0; - const inspector = new ApolloLink((operation, forward) => { + it("requests from the network even if already in cache", async () => { + let called = 0; + const inspector = new ApolloLink((operation, forward) => { + called++; + return forward(operation).map((result) => { called++; - return forward(operation).map((result) => { - called++; - return result; - }); + return result; }); + }); - const client = new ApolloClient({ - link: inspector.concat(createLink(reject)), - cache: new InMemoryCache({ addTypename: false }), - }); + const client = new ApolloClient({ + link: inspector.concat(createLink()), + cache: new InMemoryCache({ addTypename: false }), + }); - return client - .query({ query }) - .then(() => - client - .query({ fetchPolicy: "no-cache", query }) - .then((actualResult) => { - expect(actualResult.data).toEqual(result); - expect(called).toBe(4); - }) - ) - .then(resolve, reject); - } - ); + await client.query({ query }); + const actualResult = await client.query({ fetchPolicy: "no-cache", query }); - itAsync( - "does not save the data to the cache on success", - (resolve, reject) => { - let called = 0; - const inspector = new ApolloLink((operation, forward) => { + expect(actualResult.data).toEqual(result); + expect(called).toBe(4); + }); + + it("does not save the data to the cache on success", async () => { + let called = 0; + const inspector = new ApolloLink((operation, forward) => { + called++; + return forward(operation).map((result) => { called++; - return forward(operation).map((result) => { - called++; - return result; - }); + return result; }); + }); - const client = new ApolloClient({ - link: inspector.concat(createLink(reject)), - cache: new InMemoryCache({ addTypename: false }), - }); + const client = new ApolloClient({ + link: inspector.concat(createLink()), + cache: new InMemoryCache({ addTypename: false }), + }); - return client - .query({ query, fetchPolicy: "no-cache" }) - .then(() => - client.query({ query }).then((actualResult) => { - expect(actualResult.data).toEqual(result); - // the second query couldn't read anything from the cache - expect(called).toBe(4); - }) - ) - .then(resolve, reject); - } - ); + await client.query({ query, fetchPolicy: "no-cache" }); + const actualResult = await client.query({ query }); - itAsync("does not save data to the cache on failure", (resolve, reject) => { + expect(actualResult.data).toEqual(result); + // the second query couldn't read anything from the cache + expect(called).toBe(4); + }); + + it("does not save data to the cache on failure", async () => { let called = 0; const inspector = new ApolloLink((operation, forward) => { called++; @@ -318,24 +280,20 @@ describe("no-cache", () => { }); let didFail = false; - return client - .query({ query, fetchPolicy: "no-cache" }) - .catch((e) => { - expect(e.message).toMatch("query failed"); - didFail = true; - }) - .then(() => - client.query({ query }).then((actualResult) => { - expect(actualResult.data).toEqual(result); - // the first error doesn't call .map on the inspector - expect(called).toBe(3); - expect(didFail).toBe(true); - }) - ) - .then(resolve, reject); + await client.query({ query, fetchPolicy: "no-cache" }).catch((e) => { + expect(e.message).toMatch("query failed"); + didFail = true; + }); + + const actualResult = await client.query({ query }); + + expect(actualResult.data).toEqual(result); + // the first error doesn't call .map on the inspector + expect(called).toBe(3); + expect(didFail).toBe(true); }); - itAsync("does not update the cache on a mutation", (resolve, reject) => { + it("does not update the cache on a mutation", async () => { const inspector = new ApolloLink((operation, forward) => { return forward(operation).map((result) => { return result; @@ -343,59 +301,46 @@ describe("no-cache", () => { }); const client = new ApolloClient({ - link: inspector.concat(createMutationLink(reject)), + link: inspector.concat(createMutationLink()), cache: new InMemoryCache({ addTypename: false }), }); - return client - .query({ query }) - .then(() => - client.mutate({ mutation, variables, fetchPolicy: "no-cache" }) - ) - .then(() => { - return client.query({ query }).then((actualResult) => { - expect(actualResult.data).toEqual(result); - }); - }) - .then(resolve, reject); + await client.query({ query }); + await client.mutate({ mutation, variables, fetchPolicy: "no-cache" }); + const actualResult = await client.query({ query }); + + expect(actualResult.data).toEqual(result); }); describe("when notifyOnNetworkStatusChange is set", () => { - itAsync( - "does not save the data to the cache on success", - (resolve, reject) => { - let called = 0; - const inspector = new ApolloLink((operation, forward) => { + it("does not save the data to the cache on success", async () => { + let called = 0; + const inspector = new ApolloLink((operation, forward) => { + called++; + return forward(operation).map((result) => { called++; - return forward(operation).map((result) => { - called++; - return result; - }); + return result; }); + }); - const client = new ApolloClient({ - link: inspector.concat(createLink(reject)), - cache: new InMemoryCache({ addTypename: false }), - }); + const client = new ApolloClient({ + link: inspector.concat(createLink()), + cache: new InMemoryCache({ addTypename: false }), + }); - return client - .query({ - query, - fetchPolicy: "no-cache", - notifyOnNetworkStatusChange: true, - }) - .then(() => - client.query({ query }).then((actualResult) => { - expect(actualResult.data).toEqual(result); - // the second query couldn't read anything from the cache - expect(called).toBe(4); - }) - ) - .then(resolve, reject); - } - ); + await client.query({ + query, + fetchPolicy: "no-cache", + notifyOnNetworkStatusChange: true, + }); + const actualResult = await client.query({ query }); + + expect(actualResult.data).toEqual(result); + // the second query couldn't read anything from the cache + expect(called).toBe(4); + }); - itAsync("does not save data to the cache on failure", (resolve, reject) => { + it("does not save data to the cache on failure", async () => { let called = 0; const inspector = new ApolloLink((operation, forward) => { called++; @@ -411,7 +356,7 @@ describe("no-cache", () => { }); let didFail = false; - return client + await client .query({ query, fetchPolicy: "no-cache", @@ -420,16 +365,14 @@ describe("no-cache", () => { .catch((e) => { expect(e.message).toMatch("query failed"); didFail = true; - }) - .then(() => - client.query({ query }).then((actualResult) => { - expect(actualResult.data).toEqual(result); - // the first error doesn't call .map on the inspector - expect(called).toBe(3); - expect(didFail).toBe(true); - }) - ) - .then(resolve, reject); + }); + + const actualResult = await client.query({ query }); + + expect(actualResult.data).toEqual(result); + // the first error doesn't call .map on the inspector + expect(called).toBe(3); + expect(didFail).toBe(true); }); it("gives appropriate networkStatus for watched queries", async () => { @@ -543,11 +486,7 @@ describe("cache-first", () => { results.push(result); return result; }); - }).concat( - createMutationLink((error) => { - throw error; - }) - ), + }).concat(createMutationLink()), cache: new InMemoryCache(), }); diff --git a/src/link/batch-http/__tests__/batchHttpLink.ts b/src/link/batch-http/__tests__/batchHttpLink.ts index 1209b0414c1..033e97a62ac 100644 --- a/src/link/batch-http/__tests__/batchHttpLink.ts +++ b/src/link/batch-http/__tests__/batchHttpLink.ts @@ -10,8 +10,8 @@ import { Observer, } from "../../../utilities/observables/Observable"; import { BatchHttpLink } from "../batchHttpLink"; -import { itAsync } from "../../../testing"; import { FetchResult } from "../../core"; +import { ObservableStream } from "../../../testing/internal"; const sampleQuery = gql` query SampleQuery { @@ -29,22 +29,6 @@ const sampleMutation = gql` } `; -function makeCallback( - resolve: () => void, - reject: (error: Error) => void, - callback: (...args: TArgs) => any -) { - return function () { - try { - // @ts-expect-error - callback.apply(this, arguments); - resolve(); - } catch (error) { - reject(error as Error); - } - } as typeof callback; -} - describe("BatchHttpLink", () => { beforeAll(() => { jest.resetModules(); @@ -76,7 +60,7 @@ describe("BatchHttpLink", () => { expect(() => new BatchHttpLink()).not.toThrow(); }); - itAsync("handles batched requests", (resolve, reject) => { + it("handles batched requests", (done) => { const clientAwareness = { name: "Some Client Name", version: "1.0.1", @@ -91,45 +75,37 @@ describe("BatchHttpLink", () => { let nextCalls = 0; let completions = 0; const next = (expectedData: any) => (data: any) => { - try { - expect(data).toEqual(expectedData); - nextCalls++; - } catch (error) { - reject(error); - } + expect(data).toEqual(expectedData); + nextCalls++; }; const complete = () => { - try { - const calls = fetchMock.calls("begin:/batch"); - expect(calls.length).toBe(1); - expect(nextCalls).toBe(2); + const calls = fetchMock.calls("begin:/batch"); + expect(calls.length).toBe(1); + expect(nextCalls).toBe(2); - const options: any = fetchMock.lastOptions("begin:/batch"); - expect(options.credentials).toEqual("two"); + const options: any = fetchMock.lastOptions("begin:/batch"); + expect(options.credentials).toEqual("two"); - const { headers } = options; - expect(headers["apollographql-client-name"]).toBeDefined(); - expect(headers["apollographql-client-name"]).toEqual( - clientAwareness.name - ); - expect(headers["apollographql-client-version"]).toBeDefined(); - expect(headers["apollographql-client-version"]).toEqual( - clientAwareness.version - ); + const { headers } = options; + expect(headers["apollographql-client-name"]).toBeDefined(); + expect(headers["apollographql-client-name"]).toEqual( + clientAwareness.name + ); + expect(headers["apollographql-client-version"]).toBeDefined(); + expect(headers["apollographql-client-version"]).toEqual( + clientAwareness.version + ); - completions++; + completions++; - if (completions === 2) { - resolve(); - } - } catch (error) { - reject(error); + if (completions === 2) { + done(); } }; const error = (error: any) => { - reject(error); + throw error; }; execute(link, { @@ -146,37 +122,34 @@ describe("BatchHttpLink", () => { }).subscribe(next(data2), error, complete); }); - itAsync( - "errors on an incorrect number of results for a batch", - (resolve, reject) => { - const link = new BatchHttpLink({ - uri: "/batch", - batchInterval: 0, - batchMax: 3, - }); + it("errors on an incorrect number of results for a batch", (done) => { + const link = new BatchHttpLink({ + uri: "/batch", + batchInterval: 0, + batchMax: 3, + }); - let errors = 0; - const next = (data: any) => { - reject("next should not have been called"); - }; + let errors = 0; + const next = (data: any) => { + throw new Error("next should not have been called"); + }; - const complete = () => { - reject("complete should not have been called"); - }; + const complete = () => { + throw new Error("complete should not have been called"); + }; - const error = (error: any) => { - errors++; + const error = (error: any) => { + errors++; - if (errors === 3) { - resolve(); - } - }; + if (errors === 3) { + done(); + } + }; - execute(link, { query: sampleQuery }).subscribe(next, error, complete); - execute(link, { query: sampleQuery }).subscribe(next, error, complete); - execute(link, { query: sampleQuery }).subscribe(next, error, complete); - } - ); + execute(link, { query: sampleQuery }).subscribe(next, error, complete); + execute(link, { query: sampleQuery }).subscribe(next, error, complete); + execute(link, { query: sampleQuery }).subscribe(next, error, complete); + }); describe("batchKey", () => { const query = gql` @@ -188,71 +161,64 @@ describe("BatchHttpLink", () => { } `; - itAsync( - "should batch queries with different options separately", - (resolve, reject) => { - let key = true; - const batchKey = () => { - key = !key; - return "" + !key; - }; + it("should batch queries with different options separately", (done) => { + let key = true; + const batchKey = () => { + key = !key; + return "" + !key; + }; - const link = ApolloLink.from([ - new BatchHttpLink({ - uri: (operation) => { - return operation.variables.endpoint; - }, - batchInterval: 1, - //if batchKey does not work, then the batch size would be 3 - batchMax: 2, - batchKey, - }), - ]); - - let count = 0; - const next = (expected: any) => (received: any) => { - try { - expect(received).toEqual(expected); - } catch (e) { - reject(e); - } - }; - const complete = () => { - count++; - if (count === 4) { - try { - const lawlCalls = fetchMock.calls("begin:/lawl"); - expect(lawlCalls.length).toBe(1); - const roflCalls = fetchMock.calls("begin:/rofl"); - expect(roflCalls.length).toBe(1); - resolve(); - } catch (e) { - reject(e); - } - } - }; + const link = ApolloLink.from([ + new BatchHttpLink({ + uri: (operation) => { + return operation.variables.endpoint; + }, + batchInterval: 1, + //if batchKey does not work, then the batch size would be 3 + batchMax: 2, + batchKey, + }), + ]); - [1, 2].forEach((x) => { - execute(link, { - query, - variables: { endpoint: "/rofl" }, - }).subscribe({ - next: next(roflData), - error: reject, - complete, - }); + let count = 0; + const next = (expected: any) => (received: any) => { + expect(received).toEqual(expected); + }; + const complete = () => { + count++; + if (count === 4) { + const lawlCalls = fetchMock.calls("begin:/lawl"); + expect(lawlCalls.length).toBe(1); + const roflCalls = fetchMock.calls("begin:/rofl"); + expect(roflCalls.length).toBe(1); + done(); + } + }; - execute(link, { - query, - variables: { endpoint: "/lawl" }, - }).subscribe({ - next: next(lawlData), - error: reject, - complete, - }); + [1, 2].forEach((x) => { + execute(link, { + query, + variables: { endpoint: "/rofl" }, + }).subscribe({ + next: next(roflData), + error: (error) => { + throw error; + }, + complete, }); - } - ); + + execute(link, { + query, + variables: { endpoint: "/lawl" }, + }).subscribe({ + next: next(lawlData), + error: (error) => { + throw error; + }, + complete, + }); + }); + }); }); }); @@ -333,127 +299,101 @@ describe("SharedHttpTest", () => { expect(() => createHttpLink()).not.toThrow(); }); - itAsync("calls next and then complete", (resolve, reject) => { - const next = jest.fn(); + it("calls next and then complete", async () => { const link = createHttpLink({ uri: "/data" }); const observable = execute(link, { query: sampleQuery, }); - observable.subscribe({ - next, - error: (error) => reject(error), - complete: makeCallback(resolve, reject, () => { - expect(next).toHaveBeenCalledTimes(1); - }), - }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + await expect(stream).toComplete(); }); - itAsync("calls error when fetch fails", (resolve, reject) => { + it("calls error when fetch fails", async () => { const link = createHttpLink({ uri: "/error" }); const observable = execute(link, { query: sampleQuery, }); - observable.subscribe( - (result) => reject("next should not have been called"), - makeCallback(resolve, reject, (error: any) => { - expect(error).toEqual(mockError.throws); - }), - () => reject("complete should not have been called") - ); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitError(mockError.throws); }); - itAsync("calls error when fetch fails", (resolve, reject) => { + it("calls error when fetch fails", async () => { const link = createHttpLink({ uri: "/error" }); const observable = execute(link, { query: sampleMutation, }); - observable.subscribe( - (result) => reject("next should not have been called"), - makeCallback(resolve, reject, (error: any) => { - expect(error).toEqual(mockError.throws); - }), - () => reject("complete should not have been called") - ); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitError(mockError.throws); }); - itAsync( - "strips unused variables, respecting nested fragments", - (resolve, reject) => { - const link = createHttpLink({ uri: "/data" }); - - const query = gql` - query PEOPLE($declaredAndUsed: String, $declaredButUnused: Int) { - people(surprise: $undeclared, noSurprise: $declaredAndUsed) { - ... on Doctor { - specialty(var: $usedByInlineFragment) - } - ...LawyerFragment + it("strips unused variables, respecting nested fragments", async () => { + const link = createHttpLink({ uri: "/data" }); + + const query = gql` + query PEOPLE($declaredAndUsed: String, $declaredButUnused: Int) { + people(surprise: $undeclared, noSurprise: $declaredAndUsed) { + ... on Doctor { + specialty(var: $usedByInlineFragment) } + ...LawyerFragment } - fragment LawyerFragment on Lawyer { - caseCount(var: $usedByNamedFragment) - } - `; - - const variables = { - unused: "strip", - declaredButUnused: "strip", - declaredAndUsed: "keep", - undeclared: "keep", - usedByInlineFragment: "keep", - usedByNamedFragment: "keep", - }; + } + fragment LawyerFragment on Lawyer { + caseCount(var: $usedByNamedFragment) + } + `; - execute(link, { - query, - variables, - }).subscribe({ - next: makeCallback(resolve, reject, () => { - const [uri, options] = fetchMock.lastCall()!; - const { method, body } = options!; - expect(JSON.parse(body as string)).toEqual([ - { - operationName: "PEOPLE", - query: print(query), - variables: { - declaredAndUsed: "keep", - undeclared: "keep", - usedByInlineFragment: "keep", - usedByNamedFragment: "keep", - }, - }, - ]); - expect(method).toBe("POST"); - expect(uri).toBe("/data"); - }), - error: (error) => reject(error), - }); - } - ); + const variables = { + unused: "strip", + declaredButUnused: "strip", + declaredAndUsed: "keep", + undeclared: "keep", + usedByInlineFragment: "keep", + usedByNamedFragment: "keep", + }; + + const stream = new ObservableStream(execute(link, { query, variables })); + + await expect(stream).toEmitNext(); + + const [uri, options] = fetchMock.lastCall()!; + const { method, body } = options!; + expect(JSON.parse(body as string)).toEqual([ + { + operationName: "PEOPLE", + query: print(query), + variables: { + declaredAndUsed: "keep", + undeclared: "keep", + usedByInlineFragment: "keep", + usedByNamedFragment: "keep", + }, + }, + ]); + expect(method).toBe("POST"); + expect(uri).toBe("/data"); + }); - itAsync("unsubscribes without calling subscriber", (resolve, reject) => { + it("unsubscribes without calling subscriber", async () => { const link = createHttpLink({ uri: "/data" }); const observable = execute(link, { query: sampleQuery, }); - const subscription = observable.subscribe( - (result) => reject("next should not have been called"), - (error) => reject(error), - () => reject("complete should not have been called") - ); - subscription.unsubscribe(); - expect(subscription.closed).toBe(true); - setTimeout(resolve, 50); + const stream = new ObservableStream(observable); + stream.unsubscribe(); + + await expect(stream).not.toEmitAnything(); }); - const verifyRequest = ( + const verifyRequest = async ( link: ApolloLink, - after: () => void, includeExtensions: boolean, - includeUnusedVariables: boolean, - reject: (e: Error) => void + includeUnusedVariables: boolean ) => { - const next = jest.fn(); const context = { info: "stub" }; const variables = { params: "stub" }; @@ -462,61 +402,37 @@ describe("SharedHttpTest", () => { context, variables, }); - observable.subscribe({ - next, - error: (error) => reject(error), - complete: () => { - try { - let body = convertBatchedBody(fetchMock.lastCall()![1]!.body); - expect(body.query).toBe(print(sampleMutation)); - expect(body.variables).toEqual( - includeUnusedVariables ? variables : {} - ); - expect(body.context).not.toBeDefined(); - if (includeExtensions) { - expect(body.extensions).toBeDefined(); - } else { - expect(body.extensions).not.toBeDefined(); - } - expect(next).toHaveBeenCalledTimes(1); - - after(); - } catch (e) { - reject(e as Error); - } - }, - }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + await expect(stream).toComplete(); + + let body = convertBatchedBody(fetchMock.lastCall()![1]!.body); + expect(body.query).toBe(print(sampleMutation)); + expect(body.variables).toEqual(includeUnusedVariables ? variables : {}); + expect(body.context).not.toBeDefined(); + if (includeExtensions) { + expect(body.extensions).toBeDefined(); + } else { + expect(body.extensions).not.toBeDefined(); + } }; - itAsync( - "passes all arguments to multiple fetch body including extensions", - (resolve, reject) => { - const link = createHttpLink({ uri: "/data", includeExtensions: true }); - verifyRequest( - link, - () => verifyRequest(link, resolve, true, false, reject), - true, - false, - reject - ); - } - ); + it("passes all arguments to multiple fetch body including extensions", async () => { + const link = createHttpLink({ uri: "/data", includeExtensions: true }); - itAsync( - "passes all arguments to multiple fetch body excluding extensions", - (resolve, reject) => { - const link = createHttpLink({ uri: "/data" }); - verifyRequest( - link, - () => verifyRequest(link, resolve, false, false, reject), - false, - false, - reject - ); - } - ); + await verifyRequest(link, true, false); + await verifyRequest(link, true, false); + }); + + it("passes all arguments to multiple fetch body excluding extensions", async () => { + const link = createHttpLink({ uri: "/data" }); + + await verifyRequest(link, false, false); + await verifyRequest(link, false, false); + }); - itAsync("calls multiple subscribers", (resolve, reject) => { + it("calls multiple subscribers", (done) => { const link = createHttpLink({ uri: "/data" }); const context = { info: "stub" }; const variables = { params: "stub" }; @@ -536,57 +452,52 @@ describe("SharedHttpTest", () => { // only one call because batchHttpLink can handle more than one subscriber // without starting a new request expect(fetchMock.calls().length).toBe(1); - resolve(); + done(); }, 50); }); - itAsync( - "calls remaining subscribers after unsubscribe", - (resolve, reject) => { - const link = createHttpLink({ uri: "/data" }); - const context = { info: "stub" }; - const variables = { params: "stub" }; + it("calls remaining subscribers after unsubscribe", (done) => { + const link = createHttpLink({ uri: "/data" }); + const context = { info: "stub" }; + const variables = { params: "stub" }; - const observable = execute(link, { - query: sampleMutation, - context, - variables, - }); + const observable = execute(link, { + query: sampleMutation, + context, + variables, + }); - observable.subscribe(subscriber); + observable.subscribe(subscriber); - setTimeout(() => { - const subscription = observable.subscribe(subscriber); - subscription.unsubscribe(); - }, 10); + setTimeout(() => { + const subscription = observable.subscribe(subscriber); + subscription.unsubscribe(); + }, 10); - setTimeout( - makeCallback(resolve, reject, () => { - expect(subscriber.next).toHaveBeenCalledTimes(1); - expect(subscriber.complete).toHaveBeenCalledTimes(1); - expect(subscriber.error).not.toHaveBeenCalled(); - resolve(); - }), - 50 - ); - } - ); + setTimeout(() => { + expect(subscriber.next).toHaveBeenCalledTimes(1); + expect(subscriber.complete).toHaveBeenCalledTimes(1); + expect(subscriber.error).not.toHaveBeenCalled(); + done(); + }, 50); + }); - itAsync("allows for dynamic endpoint setting", (resolve, reject) => { + it("allows for dynamic endpoint setting", async () => { const variables = { params: "stub" }; const link = createHttpLink({ uri: "/data" }); - execute(link, { - query: sampleQuery, - variables, - context: { uri: "/data2" }, - }).subscribe((result) => { - expect(result).toEqual(data2); - resolve(); - }); + const stream = new ObservableStream( + execute(link, { + query: sampleQuery, + variables, + context: { uri: "/data2" }, + }) + ); + + await expect(stream).toEmitValue(data2); }); - itAsync("adds headers to the request from the context", (resolve, reject) => { + it("adds headers to the request from the context", async () => { const variables = { params: "stub" }; const middleware = new ApolloLink((operation, forward) => { operation.setContext({ @@ -594,43 +505,42 @@ describe("SharedHttpTest", () => { }); return forward(operation).map((result) => { const { headers } = operation.getContext(); - try { - expect(headers).toBeDefined(); - } catch (e) { - reject(e); - } + expect(headers).toBeDefined(); return result; }); }); const link = middleware.concat(createHttpLink({ uri: "/data" })); - - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, (result: any) => { - const headers: Record = fetchMock.lastCall()![1]! - .headers as Record; - expect(headers.authorization).toBe("1234"); - expect(headers["content-type"]).toBe("application/json"); - expect(headers.accept).toBe("*/*"); - }) + const stream = new ObservableStream( + execute(link, { query: sampleQuery, variables }) ); + + await expect(stream).toEmitNext(); + + const headers: Record = fetchMock.lastCall()![1]! + .headers as Record; + expect(headers.authorization).toBe("1234"); + expect(headers["content-type"]).toBe("application/json"); + expect(headers.accept).toBe("*/*"); }); - itAsync("adds headers to the request from the setup", (resolve, reject) => { + it("adds headers to the request from the setup", async () => { const variables = { params: "stub" }; const link = createHttpLink({ uri: "/data", headers: { authorization: "1234" }, }); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, (result: any) => { - const headers: Record = fetchMock.lastCall()![1]! - .headers as Record; - expect(headers.authorization).toBe("1234"); - expect(headers["content-type"]).toBe("application/json"); - expect(headers.accept).toBe("*/*"); - }) + const stream = new ObservableStream( + execute(link, { query: sampleQuery, variables }) ); + + await expect(stream).toEmitNext(); + + const headers: Record = fetchMock.lastCall()![1]! + .headers as Record; + expect(headers.authorization).toBe("1234"); + expect(headers["content-type"]).toBe("application/json"); + expect(headers.accept).toBe("*/*"); }); it("uses the latest window.fetch function if options.fetch not configured", (done) => { @@ -688,138 +598,133 @@ describe("SharedHttpTest", () => { ); }); - itAsync( - "prioritizes context headers over setup headers", - (resolve, reject) => { - const variables = { params: "stub" }; - const middleware = new ApolloLink((operation, forward) => { - operation.setContext({ - headers: { authorization: "1234" }, - }); - return forward(operation); + it("prioritizes context headers over setup headers", async () => { + const variables = { params: "stub" }; + const middleware = new ApolloLink((operation, forward) => { + operation.setContext({ + headers: { authorization: "1234" }, }); - const link = middleware.concat( - createHttpLink({ uri: "/data", headers: { authorization: "no user" } }) - ); + return forward(operation); + }); + const link = middleware.concat( + createHttpLink({ uri: "/data", headers: { authorization: "no user" } }) + ); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, (result: any) => { - const headers: Record = fetchMock.lastCall()![1]! - .headers as Record; - expect(headers.authorization).toBe("1234"); - expect(headers["content-type"]).toBe("application/json"); - expect(headers.accept).toBe("*/*"); - }) - ); - } - ); + const stream = new ObservableStream( + execute(link, { query: sampleQuery, variables }) + ); - itAsync( - "adds headers to the request from the context on an operation", - (resolve, reject) => { - const variables = { params: "stub" }; - const link = createHttpLink({ uri: "/data" }); + await expect(stream).toEmitNext(); - const context = { - headers: { authorization: "1234" }, - }; + const headers: Record = fetchMock.lastCall()![1]! + .headers as Record; + expect(headers.authorization).toBe("1234"); + expect(headers["content-type"]).toBe("application/json"); + expect(headers.accept).toBe("*/*"); + }); + + it("adds headers to the request from the context on an operation", async () => { + const variables = { params: "stub" }; + const link = createHttpLink({ uri: "/data" }); + + const context = { + headers: { authorization: "1234" }, + }; + const stream = new ObservableStream( execute(link, { query: sampleQuery, variables, context, - }).subscribe( - makeCallback(resolve, reject, (result: any) => { - const headers: Record = fetchMock.lastCall()![1]! - .headers as Record; - expect(headers.authorization).toBe("1234"); - expect(headers["content-type"]).toBe("application/json"); - expect(headers.accept).toBe("*/*"); - }) - ); - } - ); + }) + ); - itAsync( - "adds headers w/ preserved case to the request from the setup", - (resolve, reject) => { - const variables = { params: "stub" }; - const link = createHttpLink({ - uri: "/data", - headers: { - authorization: "1234", - AUTHORIZATION: "1234", - "CONTENT-TYPE": "application/json", - }, - preserveHeaderCase: true, - }); + await expect(stream).toEmitNext(); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, (result: any) => { - const headers: any = fetchMock.lastCall()![1]!.headers; - expect(headers.AUTHORIZATION).toBe("1234"); - expect(headers["CONTENT-TYPE"]).toBe("application/json"); - expect(headers.accept).toBe("*/*"); - }) - ); - } - ); - - itAsync( - "prioritizes context headers w/ preserved case over setup headers", - (resolve, reject) => { - const variables = { params: "stub" }; - const middleware = new ApolloLink((operation, forward) => { - operation.setContext({ - headers: { AUTHORIZATION: "1234" }, - http: { preserveHeaderCase: true }, - }); - return forward(operation); - }); - const link = middleware.concat( - createHttpLink({ - uri: "/data", - headers: { authorization: "no user" }, - preserveHeaderCase: false, - }) - ); + const headers: Record = fetchMock.lastCall()![1]! + .headers as Record; + expect(headers.authorization).toBe("1234"); + expect(headers["content-type"]).toBe("application/json"); + expect(headers.accept).toBe("*/*"); + }); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, (result: any) => { - const headers: any = fetchMock.lastCall()![1]!.headers; - expect(headers.AUTHORIZATION).toBe("1234"); - expect(headers["content-type"]).toBe("application/json"); - expect(headers.accept).toBe("*/*"); - }) - ); - } - ); + it("adds headers w/ preserved case to the request from the setup", async () => { + const variables = { params: "stub" }; + const link = createHttpLink({ + uri: "/data", + headers: { + authorization: "1234", + AUTHORIZATION: "1234", + "CONTENT-TYPE": "application/json", + }, + preserveHeaderCase: true, + }); - itAsync( - "adds headers w/ preserved case to the request from the context on an operation", - (resolve, reject) => { - const variables = { params: "stub" }; - const link = createHttpLink({ uri: "/data" }); + const stream = new ObservableStream( + execute(link, { query: sampleQuery, variables }) + ); + + await expect(stream).toEmitNext(); - const context = { + const headers: any = fetchMock.lastCall()![1]!.headers; + expect(headers.AUTHORIZATION).toBe("1234"); + expect(headers["CONTENT-TYPE"]).toBe("application/json"); + expect(headers.accept).toBe("*/*"); + }); + + it("prioritizes context headers w/ preserved case over setup headers", async () => { + const variables = { params: "stub" }; + const middleware = new ApolloLink((operation, forward) => { + operation.setContext({ headers: { AUTHORIZATION: "1234" }, http: { preserveHeaderCase: true }, - }; + }); + return forward(operation); + }); + const link = middleware.concat( + createHttpLink({ + uri: "/data", + headers: { authorization: "no user" }, + preserveHeaderCase: false, + }) + ); + + const stream = new ObservableStream( + execute(link, { query: sampleQuery, variables }) + ); + + await expect(stream).toEmitNext(); + + const headers: any = fetchMock.lastCall()![1]!.headers; + expect(headers.AUTHORIZATION).toBe("1234"); + expect(headers["content-type"]).toBe("application/json"); + expect(headers.accept).toBe("*/*"); + }); + + it("adds headers w/ preserved case to the request from the context on an operation", async () => { + const variables = { params: "stub" }; + const link = createHttpLink({ uri: "/data" }); + + const context = { + headers: { AUTHORIZATION: "1234" }, + http: { preserveHeaderCase: true }, + }; + const stream = new ObservableStream( execute(link, { query: sampleQuery, variables, context, - }).subscribe( - makeCallback(resolve, reject, (result: any) => { - const headers: any = fetchMock.lastCall()![1]!.headers; - expect(headers.AUTHORIZATION).toBe("1234"); - expect(headers["content-type"]).toBe("application/json"); - expect(headers.accept).toBe("*/*"); - }) - ); - } - ); + }) + ); + + await expect(stream).toEmitNext(); - itAsync("adds creds to the request from the context", (resolve, reject) => { + const headers: any = fetchMock.lastCall()![1]!.headers; + expect(headers.AUTHORIZATION).toBe("1234"); + expect(headers["content-type"]).toBe("application/json"); + expect(headers.accept).toBe("*/*"); + }); + + it("adds creds to the request from the context", async () => { const variables = { params: "stub" }; const middleware = new ApolloLink((operation, forward) => { operation.setContext({ @@ -829,50 +734,53 @@ describe("SharedHttpTest", () => { }); const link = middleware.concat(createHttpLink({ uri: "/data" })); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, (result: any) => { - const creds = fetchMock.lastCall()![1]!.credentials; - expect(creds).toBe("same-team-yo"); - }) + const stream = new ObservableStream( + execute(link, { query: sampleQuery, variables }) ); + + await expect(stream).toEmitNext(); + + const creds = fetchMock.lastCall()![1]!.credentials; + expect(creds).toBe("same-team-yo"); }); - itAsync("adds creds to the request from the setup", (resolve, reject) => { + it("adds creds to the request from the setup", async () => { const variables = { params: "stub" }; const link = createHttpLink({ uri: "/data", credentials: "same-team-yo" }); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, (result: any) => { - const creds = fetchMock.lastCall()![1]!.credentials; - expect(creds).toBe("same-team-yo"); - }) + const stream = new ObservableStream( + execute(link, { query: sampleQuery, variables }) ); + + await expect(stream).toEmitNext(); + + const creds = fetchMock.lastCall()![1]!.credentials; + expect(creds).toBe("same-team-yo"); }); - itAsync( - "prioritizes creds from the context over the setup", - (resolve, reject) => { - const variables = { params: "stub" }; - const middleware = new ApolloLink((operation, forward) => { - operation.setContext({ - credentials: "same-team-yo", - }); - return forward(operation); + it("prioritizes creds from the context over the setup", async () => { + const variables = { params: "stub" }; + const middleware = new ApolloLink((operation, forward) => { + operation.setContext({ + credentials: "same-team-yo", }); - const link = middleware.concat( - createHttpLink({ uri: "/data", credentials: "error" }) - ); + return forward(operation); + }); + const link = middleware.concat( + createHttpLink({ uri: "/data", credentials: "error" }) + ); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, (result: any) => { - const creds = fetchMock.lastCall()![1]!.credentials; - expect(creds).toBe("same-team-yo"); - }) - ); - } - ); + const stream = new ObservableStream( + execute(link, { query: sampleQuery, variables }) + ); + + await expect(stream).toEmitNext(); + + const creds = fetchMock.lastCall()![1]!.credentials; + expect(creds).toBe("same-team-yo"); + }); - itAsync("adds uri to the request from the context", (resolve, reject) => { + it("adds uri to the request from the context", async () => { const variables = { params: "stub" }; const middleware = new ApolloLink((operation, forward) => { operation.setContext({ @@ -882,27 +790,31 @@ describe("SharedHttpTest", () => { }); const link = middleware.concat(createHttpLink()); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, (result: any) => { - const uri = fetchMock.lastUrl(); - expect(uri).toBe("/data"); - }) + const stream = new ObservableStream( + execute(link, { query: sampleQuery, variables }) ); + + await expect(stream).toEmitNext(); + + const uri = fetchMock.lastUrl(); + expect(uri).toBe("/data"); }); - itAsync("adds uri to the request from the setup", (resolve, reject) => { + it("adds uri to the request from the setup", async () => { const variables = { params: "stub" }; const link = createHttpLink({ uri: "/data" }); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, (result: any) => { - const uri = fetchMock.lastUrl(); - expect(uri).toBe("/data"); - }) + const stream = new ObservableStream( + execute(link, { query: sampleQuery, variables }) ); + + await expect(stream).toEmitNext(); + + const uri = fetchMock.lastUrl(); + expect(uri).toBe("/data"); }); - itAsync("prioritizes context uri over setup uri", (resolve, reject) => { + it("prioritizes context uri over setup uri", async () => { const variables = { params: "stub" }; const middleware = new ApolloLink((operation, forward) => { operation.setContext({ @@ -914,82 +826,77 @@ describe("SharedHttpTest", () => { createHttpLink({ uri: "/data", credentials: "error" }) ); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, (result: any) => { - const uri = fetchMock.lastUrl(); - - expect(uri).toBe("/apollo"); - }) + const stream = new ObservableStream( + execute(link, { query: sampleQuery, variables }) ); + + await expect(stream).toEmitNext(); + + const uri = fetchMock.lastUrl(); + expect(uri).toBe("/apollo"); }); - itAsync("allows uri to be a function", (resolve, reject) => { + it("allows uri to be a function", async () => { const variables = { params: "stub" }; const customFetch = (_uri: any, options: any) => { const { operationName } = convertBatchedBody(options.body); - try { - expect(operationName).toBe("SampleQuery"); - } catch (e) { - reject(e); - } + expect(operationName).toBe("SampleQuery"); return fetch("/dataFunc", options); }; const link = createHttpLink({ fetch: customFetch }); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, (result: any) => { - expect(fetchMock.lastUrl()).toBe("/dataFunc"); - }) + const stream = new ObservableStream( + execute(link, { query: sampleQuery, variables }) ); + + await expect(stream).toEmitNext(); + + expect(fetchMock.lastUrl()).toBe("/dataFunc"); }); - itAsync( - "adds fetchOptions to the request from the setup", - (resolve, reject) => { - const variables = { params: "stub" }; - const link = createHttpLink({ - uri: "/data", - fetchOptions: { someOption: "foo", mode: "no-cors" }, - }); + it("adds fetchOptions to the request from the setup", async () => { + const variables = { params: "stub" }; + const link = createHttpLink({ + uri: "/data", + fetchOptions: { someOption: "foo", mode: "no-cors" }, + }); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, (result: any) => { - const { someOption, mode, headers } = - fetchMock.lastCall()![1]! as any; - expect(someOption).toBe("foo"); - expect(mode).toBe("no-cors"); - expect(headers["content-type"]).toBe("application/json"); - }) - ); - } - ); - - itAsync( - "adds fetchOptions to the request from the context", - (resolve, reject) => { - const variables = { params: "stub" }; - const middleware = new ApolloLink((operation, forward) => { - operation.setContext({ - fetchOptions: { - someOption: "foo", - }, - }); - return forward(operation); + const stream = new ObservableStream( + execute(link, { query: sampleQuery, variables }) + ); + + await expect(stream).toEmitNext(); + + const { someOption, mode, headers } = fetchMock.lastCall()![1]! as any; + expect(someOption).toBe("foo"); + expect(mode).toBe("no-cors"); + expect(headers["content-type"]).toBe("application/json"); + }); + + it("adds fetchOptions to the request from the context", async () => { + const variables = { params: "stub" }; + const middleware = new ApolloLink((operation, forward) => { + operation.setContext({ + fetchOptions: { + someOption: "foo", + }, }); - const link = middleware.concat(createHttpLink({ uri: "/data" })); + return forward(operation); + }); + const link = middleware.concat(createHttpLink({ uri: "/data" })); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, (result: any) => { - const { someOption } = fetchMock.lastCall()![1]! as any; - expect(someOption).toBe("foo"); - resolve(); - }) - ); - } - ); + const stream = new ObservableStream( + execute(link, { query: sampleQuery, variables }) + ); - itAsync("uses the print option function when defined", (resolve, reject) => { + await expect(stream).toEmitNext(); + + const { someOption } = fetchMock.lastCall()![1]! as any; + expect(someOption).toBe("foo"); + }); + + it("uses the print option function when defined", async () => { const customPrinter = jest.fn( (ast: ASTNode, originalPrint: typeof print) => { return stripIgnoredCharacters(originalPrint(ast)); @@ -998,16 +905,16 @@ describe("SharedHttpTest", () => { const httpLink = createHttpLink({ uri: "data", print: customPrinter }); - execute(httpLink, { - query: sampleQuery, - }).subscribe( - makeCallback(resolve, reject, () => { - expect(customPrinter).toHaveBeenCalledTimes(1); - }) + const stream = new ObservableStream( + execute(httpLink, { query: sampleQuery }) ); + + await expect(stream).toEmitNext(); + + expect(customPrinter).toHaveBeenCalledTimes(1); }); - itAsync("prioritizes context over setup", (resolve, reject) => { + it("prioritizes context over setup", async () => { const variables = { params: "stub" }; const middleware = new ApolloLink((operation, forward) => { operation.setContext({ @@ -1021,53 +928,53 @@ describe("SharedHttpTest", () => { createHttpLink({ uri: "/data", fetchOptions: { someOption: "bar" } }) ); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, (result: any) => { - const { someOption } = fetchMock.lastCall()![1]! as any; - expect(someOption).toBe("foo"); - }) + const stream = new ObservableStream( + execute(link, { query: sampleQuery, variables }) ); + + await expect(stream).toEmitNext(); + + const { someOption } = fetchMock.lastCall()![1]! as any; + expect(someOption).toBe("foo"); }); - itAsync( - "allows for not sending the query with the request", - (resolve, reject) => { - const variables = { params: "stub" }; - const middleware = new ApolloLink((operation, forward) => { - operation.setContext({ - http: { - includeQuery: false, - includeExtensions: true, - }, - }); - operation.extensions.persistedQuery = { hash: "1234" }; - return forward(operation); + it("allows for not sending the query with the request", async () => { + const variables = { params: "stub" }; + const middleware = new ApolloLink((operation, forward) => { + operation.setContext({ + http: { + includeQuery: false, + includeExtensions: true, + }, }); - const link = middleware.concat(createHttpLink({ uri: "/data" })); + operation.extensions.persistedQuery = { hash: "1234" }; + return forward(operation); + }); + const link = middleware.concat(createHttpLink({ uri: "/data" })); + + const stream = new ObservableStream( + execute(link, { query: sampleQuery, variables }) + ); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, (result: any) => { - let body = convertBatchedBody(fetchMock.lastCall()![1]!.body); + await expect(stream).toEmitNext(); - expect(body.query).not.toBeDefined(); - expect(body.extensions).toEqual({ persistedQuery: { hash: "1234" } }); - resolve(); - }) - ); - } - ); + let body = convertBatchedBody(fetchMock.lastCall()![1]!.body); + + expect(body.query).not.toBeDefined(); + expect(body.extensions).toEqual({ persistedQuery: { hash: "1234" } }); + }); - itAsync("sets the raw response on context", (resolve, reject) => { + it("sets the raw response on context", async () => { const middleware = new ApolloLink((operation, forward) => { return new Observable((ob) => { const op = forward(operation); const sub = op.subscribe({ next: ob.next.bind(ob), error: ob.error.bind(ob), - complete: makeCallback(resolve, reject, () => { + complete: () => { expect(operation.getContext().response.headers.toBeDefined); ob.complete(); - }), + }, }); return () => { @@ -1078,12 +985,10 @@ describe("SharedHttpTest", () => { const link = middleware.concat(createHttpLink({ uri: "/data", fetch })); - execute(link, { query: sampleQuery }).subscribe( - (result) => { - resolve(); - }, - () => {} - ); + const stream = new ObservableStream(execute(link, { query: sampleQuery })); + + await expect(stream).toEmitNext(); + await expect(stream).toComplete(); }); it("removes @client fields from the query before sending it to the server", async () => { diff --git a/src/link/batch/__tests__/batchLink.ts b/src/link/batch/__tests__/batchLink.ts index e5930924c27..ff5aa72edd0 100644 --- a/src/link/batch/__tests__/batchLink.ts +++ b/src/link/batch/__tests__/batchLink.ts @@ -4,13 +4,14 @@ import { print } from "graphql"; import { ApolloLink, execute } from "../../core"; import { Operation, FetchResult, GraphQLRequest } from "../../core/types"; import { Observable } from "../../../utilities"; -import { itAsync } from "../../../testing"; +import { wait } from "../../../testing"; import { BatchLink, OperationBatcher, BatchHandler, BatchableRequest, } from "../batchLink"; +import { ObservableStream } from "../../../testing/internal"; interface MockedResponse { request: GraphQLRequest; @@ -57,22 +58,6 @@ function createOperation(starting: any, operation: GraphQLRequest): Operation { return operation as Operation; } -function terminatingCheck( - resolve: () => any, - reject: (e: any) => any, - callback: (...args: TArgs) => any -) { - return function () { - try { - // @ts-expect-error - callback.apply(this, arguments); - resolve(); - } catch (error) { - reject(error); - } - } as typeof callback; -} - function requestToKey(request: GraphQLRequest): string { const queryString = typeof request.query === "string" ? request.query : print(request.query); @@ -221,195 +206,129 @@ describe("OperationBatcher", () => { } ); - itAsync( - "should be able to consume from a queue containing a single query", - (resolve, reject) => { - const myBatcher = new OperationBatcher({ - batchInterval: 10, - batchHandler, - }); + it("should be able to consume from a queue containing a single query", async () => { + const myBatcher = new OperationBatcher({ + batchInterval: 10, + batchHandler, + }); + + const observable = myBatcher.enqueueRequest({ operation }); + const stream = new ObservableStream(observable); + + const observables: (Observable | undefined)[] = + myBatcher.consumeQueue()!; - myBatcher.enqueueRequest({ operation }).subscribe( - terminatingCheck(resolve, reject, (resultObj: any) => { - expect(myBatcher["batchesByKey"].get("")).toBeUndefined(); - expect(resultObj).toEqual({ data }); - }) - ); - const observables: (Observable | undefined)[] = - myBatcher.consumeQueue()!; - - try { - expect(observables.length).toBe(1); - } catch (e) { - reject(e); + expect(observables.length).toBe(1); + expect(myBatcher["batchesByKey"].get("")).toBeUndefined(); + + await expect(stream).toEmitValue({ data }); + }); + + it("should be able to consume from a queue containing multiple queries", async () => { + const request2: Operation = createOperation( + {}, + { + query, } - } - ); + ); - itAsync( - "should be able to consume from a queue containing multiple queries", - (resolve, reject) => { - const request2: Operation = createOperation( - {}, - { - query, - } - ); + const BH = createMockBatchHandler( + { + request: { query }, + result: { data }, + }, + { + request: { query }, + result: { data }, + } + ); - const BH = createMockBatchHandler( - { - request: { query }, - result: { data }, - }, - { - request: { query }, - result: { data }, - } - ); + const myBatcher = new OperationBatcher({ + batchInterval: 10, + batchMax: 10, + batchHandler: BH, + }); + const observable1 = myBatcher.enqueueRequest({ operation }); + const observable2 = myBatcher.enqueueRequest({ operation: request2 }); - const myBatcher = new OperationBatcher({ - batchInterval: 10, - batchMax: 10, - batchHandler: BH, - }); - const observable1 = myBatcher.enqueueRequest({ operation }); - const observable2 = myBatcher.enqueueRequest({ operation: request2 }); - let notify = false; - observable1.subscribe((resultObj1) => { - try { - expect(resultObj1).toEqual({ data }); - } catch (e) { - reject(e); - } + const stream1 = new ObservableStream(observable1); + const stream2 = new ObservableStream(observable2); - if (notify) { - resolve(); - } else { - notify = true; - } - }); + expect(myBatcher["batchesByKey"].get("")!.size).toBe(2); + const observables: (Observable | undefined)[] = + myBatcher.consumeQueue()!; + expect(myBatcher["batchesByKey"].get("")).toBeUndefined(); + expect(observables.length).toBe(2); - observable2.subscribe((resultObj2) => { - try { - expect(resultObj2).toEqual({ data }); - } catch (e) { - reject(e); - } + await expect(stream1).toEmitValue({ data }); + await expect(stream2).toEmitValue({ data }); + }); - if (notify) { - resolve(); - } else { - notify = true; - } - }); + it("should be able to consume from a queue containing multiple queries with different batch keys", async () => { + // NOTE: this test was added to ensure that queries don't "hang" when consumed by BatchLink. + // "Hanging" in this case results in this test never resolving. So + // if this test times out it's probably a real issue and not a flake + const request2: Operation = createOperation( + {}, + { + query, + } + ); - try { - expect(myBatcher["batchesByKey"].get("")!.size).toBe(2); - const observables: (Observable | undefined)[] = - myBatcher.consumeQueue()!; - expect(myBatcher["batchesByKey"].get("")).toBeUndefined(); - expect(observables.length).toBe(2); - } catch (e) { - reject(e); + const BH = createMockBatchHandler( + { + request: { query }, + result: { data }, + }, + { + request: { query }, + result: { data }, } - } - ); + ); - itAsync( - "should be able to consume from a queue containing multiple queries with different batch keys", - (resolve, reject) => { - // NOTE: this test was added to ensure that queries don't "hang" when consumed by BatchLink. - // "Hanging" in this case results in this test never resolving. So - // if this test times out it's probably a real issue and not a flake - const request2: Operation = createOperation( - {}, - { - query, - } - ); + let key = true; + const batchKey = () => { + key = !key; + return "" + !key; + }; - const BH = createMockBatchHandler( - { - request: { query }, - result: { data }, - }, - { - request: { query }, - result: { data }, - } - ); - - let key = true; - const batchKey = () => { - key = !key; - return "" + !key; - }; - - const myBatcher = new OperationBatcher({ - batchInterval: 10, - batchMax: 10, - batchHandler: BH, - batchKey, - }); + const myBatcher = new OperationBatcher({ + batchInterval: 10, + batchMax: 10, + batchHandler: BH, + batchKey, + }); - const observable1 = myBatcher.enqueueRequest({ operation }); - const observable2 = myBatcher.enqueueRequest({ operation: request2 }); + const observable1 = myBatcher.enqueueRequest({ operation }); + const observable2 = myBatcher.enqueueRequest({ operation: request2 }); - let notify = false; - observable1.subscribe((resultObj1) => { - try { - expect(resultObj1).toEqual({ data }); - } catch (e) { - reject(e); - } + const stream1 = new ObservableStream(observable1); + const stream2 = new ObservableStream(observable2); - if (notify) { - resolve(); - } else { - notify = true; - } - }); + jest.runAllTimers(); - observable2.subscribe((resultObj2) => { - try { - expect(resultObj2).toEqual({ data }); - } catch (e) { - reject(e); - } + await expect(stream1).toEmitValue({ data }); + await expect(stream2).toEmitValue({ data }); + }); - if (notify) { - resolve(); - } else { - notify = true; - } - }); + it("should return a promise when we enqueue a request and resolve it with a result", async () => { + const BH = createMockBatchHandler({ + request: { query }, + result: { data }, + }); + const myBatcher = new OperationBatcher({ + batchInterval: 10, + batchHandler: BH, + }); + const observable = myBatcher.enqueueRequest({ operation }); + const stream = new ObservableStream(observable); - jest.runAllTimers(); - } - ); + myBatcher.consumeQueue(); - itAsync( - "should return a promise when we enqueue a request and resolve it with a result", - (resolve, reject) => { - const BH = createMockBatchHandler({ - request: { query }, - result: { data }, - }); - const myBatcher = new OperationBatcher({ - batchInterval: 10, - batchHandler: BH, - }); - const observable = myBatcher.enqueueRequest({ operation }); - observable.subscribe( - terminatingCheck(resolve, reject, (result: any) => { - expect(result).toEqual({ data }); - }) - ); - myBatcher.consumeQueue(); - } - ); + await expect(stream).toEmitValue({ data }); + }); - itAsync("should be able to debounce requests", (resolve, reject) => { + it("should be able to debounce requests", () => { const batchInterval = 10; const myBatcher = new OperationBatcher({ batchDebounce: true, @@ -442,11 +361,10 @@ describe("OperationBatcher", () => { // and expect the queue to be empty. jest.advanceTimersByTime(batchInterval / 2); expect(myBatcher["batchesByKey"].size).toEqual(0); - resolve(); }); }); - itAsync("should work when single query", (resolve, reject) => { + it("should work when single query", async () => { const data = { lastName: "Ever", firstName: "Greatest", @@ -470,152 +388,138 @@ describe("OperationBatcher", () => { const operation: Operation = createOperation({}, { query }); batcher.enqueueRequest({ operation }).subscribe({}); - try { - expect(batcher["batchesByKey"].get("")!.size).toBe(1); - } catch (e) { - reject(e); - } - - setTimeout( - terminatingCheck(resolve, reject, () => { - expect(batcher["batchesByKey"].get("")).toBeUndefined(); - }), - 20 - ); + expect(batcher["batchesByKey"].get("")!.size).toBe(1); + const promise = wait(20); jest.runAllTimers(); + await promise; + + expect(batcher["batchesByKey"].get("")).toBeUndefined(); }); - itAsync( - "should cancel single query in queue when unsubscribing", - (resolve, reject) => { - const data = { - lastName: "Ever", - firstName: "Greatest", - }; + it("should cancel single query in queue when unsubscribing", async () => { + const data = { + lastName: "Ever", + firstName: "Greatest", + }; - const batcher = new OperationBatcher({ - batchInterval: 10, - batchHandler: () => - new Observable((observer) => { - observer.next([{ data }]); - setTimeout(observer.complete.bind(observer)); - }), - }); + const batcher = new OperationBatcher({ + batchInterval: 10, + batchHandler: () => + new Observable((observer) => { + observer.next([{ data }]); + setTimeout(observer.complete.bind(observer)); + }), + }); - const query = gql` - query { - author { - firstName - lastName - } + const query = gql` + query { + author { + firstName + lastName } - `; + } + `; - batcher - .enqueueRequest({ - operation: createOperation({}, { query }), - }) - .subscribe(() => reject("next should never be called")) - .unsubscribe(); + batcher + .enqueueRequest({ + operation: createOperation({}, { query }), + }) + .subscribe(() => { + throw new Error("next should never be called"); + }) + .unsubscribe(); - expect(batcher["batchesByKey"].get("")).toBeUndefined(); - resolve(); - } - ); - - itAsync( - "should cancel single query in queue with multiple subscriptions", - (resolve, reject) => { - const data = { - lastName: "Ever", - firstName: "Greatest", - }; - const batcher = new OperationBatcher({ - batchInterval: 10, - batchHandler: () => - new Observable((observer) => { - observer.next([{ data }]); - setTimeout(observer.complete.bind(observer)); - }), - }); - const query = gql` - query { - author { - firstName - lastName - } + expect(batcher["batchesByKey"].get("")).toBeUndefined(); + }); + + it("should cancel single query in queue with multiple subscriptions", () => { + const data = { + lastName: "Ever", + firstName: "Greatest", + }; + const batcher = new OperationBatcher({ + batchInterval: 10, + batchHandler: () => + new Observable((observer) => { + observer.next([{ data }]); + setTimeout(observer.complete.bind(observer)); + }), + }); + const query = gql` + query { + author { + firstName + lastName } - `; - const operation: Operation = createOperation({}, { query }); + } + `; + const operation: Operation = createOperation({}, { query }); - const observable = batcher.enqueueRequest({ operation }); + const observable = batcher.enqueueRequest({ operation }); - const checkQueuedRequests = (expectedSubscriberCount: number) => { - const batch = batcher["batchesByKey"].get(""); - expect(batch).not.toBeUndefined(); - expect(batch!.size).toBe(1); - batch!.forEach((request) => { - expect(request.subscribers.size).toBe(expectedSubscriberCount); - }); - }; + const checkQueuedRequests = (expectedSubscriberCount: number) => { + const batch = batcher["batchesByKey"].get(""); + expect(batch).not.toBeUndefined(); + expect(batch!.size).toBe(1); + batch!.forEach((request) => { + expect(request.subscribers.size).toBe(expectedSubscriberCount); + }); + }; - const sub1 = observable.subscribe(() => - reject("next should never be called") - ); - checkQueuedRequests(1); + const sub1 = observable.subscribe(() => { + throw new Error("next should never be called"); + }); + checkQueuedRequests(1); - const sub2 = observable.subscribe(() => - reject("next should never be called") - ); - checkQueuedRequests(2); + const sub2 = observable.subscribe(() => { + throw new Error("next should never be called"); + }); + checkQueuedRequests(2); - sub1.unsubscribe(); - checkQueuedRequests(1); + sub1.unsubscribe(); + checkQueuedRequests(1); - sub2.unsubscribe(); - expect(batcher["batchesByKey"].get("")).toBeUndefined(); - resolve(); - } - ); + sub2.unsubscribe(); + expect(batcher["batchesByKey"].get("")).toBeUndefined(); + }); - itAsync( - "should cancel single query in flight when unsubscribing", - (resolve, reject) => { - const batcher = new OperationBatcher({ - batchInterval: 10, - batchHandler: () => - new Observable(() => { - // Instead of typically starting an XHR, we trigger the unsubscription from outside - setTimeout(() => subscription?.unsubscribe(), 5); - - return () => { - expect(batcher["batchesByKey"].get("")).toBeUndefined(); - resolve(); - }; - }), - }); + it("should cancel single query in flight when unsubscribing", (done) => { + const batcher = new OperationBatcher({ + batchInterval: 10, + batchHandler: () => + new Observable(() => { + // Instead of typically starting an XHR, we trigger the unsubscription from outside + setTimeout(() => subscription?.unsubscribe(), 5); + + return () => { + expect(batcher["batchesByKey"].get("")).toBeUndefined(); + done(); + }; + }), + }); - const query = gql` - query { - author { - firstName - lastName - } + const query = gql` + query { + author { + firstName + lastName } - `; + } + `; - const subscription = batcher - .enqueueRequest({ - operation: createOperation({}, { query }), - }) - .subscribe(() => reject("next should never be called")); + const subscription = batcher + .enqueueRequest({ + operation: createOperation({}, { query }), + }) + .subscribe(() => { + throw new Error("next should never be called"); + }); - jest.runAllTimers(); - } - ); + jest.runAllTimers(); + }); - itAsync("should correctly batch multiple queries", (resolve, reject) => { + it("should correctly batch multiple queries", async () => { const data = { lastName: "Ever", firstName: "Greatest", @@ -646,126 +550,109 @@ describe("OperationBatcher", () => { batcher.enqueueRequest({ operation }).subscribe({}); batcher.enqueueRequest({ operation: operation2 }).subscribe({}); - try { - expect(batcher["batchesByKey"].get("")!.size).toBe(2); - } catch (e) { - reject(e); - } + expect(batcher["batchesByKey"].get("")!.size).toBe(2); setTimeout(() => { // The batch shouldn't be fired yet, so we can add one more request. batcher.enqueueRequest({ operation: operation3 }).subscribe({}); - try { - expect(batcher["batchesByKey"].get("")!.size).toBe(3); - } catch (e) { - reject(e); - } + expect(batcher["batchesByKey"].get("")!.size).toBe(3); }, 5); - setTimeout( - terminatingCheck(resolve, reject, () => { - // The batch should've been fired by now. - expect(batcher["batchesByKey"].get("")).toBeUndefined(); - }), - 20 - ); - + const promise = wait(20); jest.runAllTimers(); + await promise; + + // The batch should've been fired by now. + expect(batcher["batchesByKey"].get("")).toBeUndefined(); }); - itAsync( - "should cancel multiple queries in queue when unsubscribing and let pass still subscribed one", - (resolve, reject) => { - const data2 = { - lastName: "Hauser", - firstName: "Evans", - }; + it("should cancel multiple queries in queue when unsubscribing and let pass still subscribed one", (done) => { + const data2 = { + lastName: "Hauser", + firstName: "Evans", + }; - const batcher = new OperationBatcher({ - batchInterval: 10, - batchHandler: () => - new Observable((observer) => { - observer.next([{ data: data2 }]); - setTimeout(observer.complete.bind(observer)); - }), - }); + const batcher = new OperationBatcher({ + batchInterval: 10, + batchHandler: () => + new Observable((observer) => { + observer.next([{ data: data2 }]); + setTimeout(observer.complete.bind(observer)); + }), + }); - const query = gql` - query { - author { - firstName - lastName - } + const query = gql` + query { + author { + firstName + lastName } - `; + } + `; + + const operation: Operation = createOperation({}, { query }); + const operation2: Operation = createOperation({}, { query }); + const operation3: Operation = createOperation({}, { query }); - const operation: Operation = createOperation({}, { query }); - const operation2: Operation = createOperation({}, { query }); - const operation3: Operation = createOperation({}, { query }); + const sub1 = batcher.enqueueRequest({ operation }).subscribe(() => { + throw new Error("next should never be called"); + }); + batcher.enqueueRequest({ operation: operation2 }).subscribe((result) => { + expect(result.data).toBe(data2); - const sub1 = batcher - .enqueueRequest({ operation }) - .subscribe(() => reject("next should never be called")); - batcher.enqueueRequest({ operation: operation2 }).subscribe((result) => { - expect(result.data).toBe(data2); + // The batch should've been fired by now. + expect(batcher["batchesByKey"].get("")).toBeUndefined(); - // The batch should've been fired by now. - expect(batcher["batchesByKey"].get("")).toBeUndefined(); + done(); + }); - resolve(); - }); + expect(batcher["batchesByKey"].get("")!.size).toBe(2); + sub1.unsubscribe(); + expect(batcher["batchesByKey"].get("")!.size).toBe(1); + + setTimeout(() => { + // The batch shouldn't be fired yet, so we can add one more request. + const sub3 = batcher + .enqueueRequest({ operation: operation3 }) + .subscribe(() => { + throw new Error("next should never be called"); + }); expect(batcher["batchesByKey"].get("")!.size).toBe(2); - sub1.unsubscribe(); + sub3.unsubscribe(); expect(batcher["batchesByKey"].get("")!.size).toBe(1); + }, 5); - setTimeout(() => { - // The batch shouldn't be fired yet, so we can add one more request. - const sub3 = batcher - .enqueueRequest({ operation: operation3 }) - .subscribe(() => reject("next should never be called")); - expect(batcher["batchesByKey"].get("")!.size).toBe(2); - - sub3.unsubscribe(); - expect(batcher["batchesByKey"].get("")!.size).toBe(1); - }, 5); + jest.runAllTimers(); + }); - jest.runAllTimers(); - } - ); - - itAsync( - "should reject the promise if there is a network error", - (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } + it("should reject the promise if there is a network error", async () => { + const query = gql` + query { + author { + firstName + lastName } - `; - const operation: Operation = createOperation({}, { query }); - const error = new Error("Network error"); - const BH = createMockBatchHandler({ - request: { query }, - error, - }); - const batcher = new OperationBatcher({ - batchInterval: 10, - batchHandler: BH, - }); + } + `; + const operation: Operation = createOperation({}, { query }); + const error = new Error("Network error"); + const BH = createMockBatchHandler({ + request: { query }, + error, + }); + const batcher = new OperationBatcher({ + batchInterval: 10, + batchHandler: BH, + }); - const observable = batcher.enqueueRequest({ operation }); - observable.subscribe({ - error: terminatingCheck(resolve, reject, (resError: Error) => { - expect(resError.message).toBe("Network error"); - }), - }); - batcher.consumeQueue(); - } - ); + const observable = batcher.enqueueRequest({ operation }); + const stream = new ObservableStream(observable); + batcher.consumeQueue(); + + await expect(stream).toEmitError(error); + }); }); describe("BatchLink", () => { @@ -781,25 +668,21 @@ describe("BatchLink", () => { ).not.toThrow(); }); - itAsync("passes forward on", (resolve, reject) => { + it("passes forward on", async () => { + expect.assertions(3); const link = ApolloLink.from([ new BatchLink({ batchInterval: 0, batchMax: 1, batchHandler: (operation, forward) => { - try { - expect(forward!.length).toBe(1); - expect(operation.length).toBe(1); - } catch (e) { - reject(e); - } + expect(forward!.length).toBe(1); + expect(operation.length).toBe(1); + return forward![0]!(operation[0]).map((result) => [result]); }, }), new ApolloLink((operation) => { - terminatingCheck(resolve, reject, () => { - expect(operation.query).toEqual(query); - })(); + expect(operation.query).toEqual(query); return null; }), ]); @@ -812,7 +695,7 @@ describe("BatchLink", () => { query, } ) - ).subscribe((result) => reject()); + ).subscribe(() => {}); }); it("raises warning if terminating", () => { @@ -849,28 +732,17 @@ describe("BatchLink", () => { expect(calls).toBe(2); }); - itAsync("correctly uses batch size", (resolve, reject) => { + it("correctly uses batch size", async () => { const sizes = [1, 2, 3]; const terminating = new ApolloLink((operation) => { - try { - expect(operation.query).toEqual(query); - } catch (e) { - reject(e); - } + expect(operation.query).toEqual(query); return Observable.of(operation.variables.count); }); - let runBatchSize = () => { - const size = sizes.pop(); - if (!size) resolve(); - + let runBatchSize = async (size: number) => { const batchHandler = jest.fn((operation, forward) => { - try { - expect(operation.length).toBe(size); - expect(forward.length).toBe(size); - } catch (e) { - reject(e); - } + expect(operation.length).toBe(size); + expect(forward.length).toBe(size); const observables = forward.map((f: any, i: any) => f(operation[i])); return new Observable((observer) => { const data: any[] = []; @@ -895,45 +767,43 @@ describe("BatchLink", () => { terminating, ]); - Array.from(new Array(size)).forEach((_, i) => { - execute(link, { - query, - variables: { count: i }, - }).subscribe({ - next: (data) => { - expect(data).toBe(i); - }, - complete: () => { - try { - expect(batchHandler.mock.calls.length).toBe(1); - } catch (e) { - reject(e); - } - runBatchSize(); - }, - }); - }); + return Promise.all( + Array.from(new Array(size)).map((_, i) => { + return new Promise((resolve) => { + execute(link, { + query, + variables: { count: i }, + }).subscribe({ + next: (data) => { + expect(data).toBe(i); + }, + complete: () => { + expect(batchHandler.mock.calls.length).toBe(1); + resolve(); + }, + }); + }); + }) + ); }; - runBatchSize(); + for (const size of sizes) { + await runBatchSize(size); + } }); - itAsync("correctly follows batch interval", (resolve, reject) => { + it("correctly follows batch interval", (done) => { const intervals = [10, 20, 30]; const runBatchInterval = () => { const mock = jest.fn(); const batchInterval = intervals.pop(); - if (!batchInterval) return resolve(); + if (!batchInterval) return done(); const batchHandler = jest.fn((operation, forward) => { - try { - expect(operation.length).toBe(1); - expect(forward.length).toBe(1); - } catch (e) { - reject(e); - } + expect(operation.length).toBe(1); + expect(forward.length).toBe(1); return forward[0](operation[0]).map((d: any) => [d]); }); @@ -957,11 +827,7 @@ describe("BatchLink", () => { ) ).subscribe({ next: (data) => { - try { - expect(data).toBe(42); - } catch (e) { - reject(e); - } + expect(data).toBe(42); }, complete: () => { mock(batchHandler.mock.calls.length); @@ -972,19 +838,15 @@ describe("BatchLink", () => { await delay(batchInterval); const checkCalls = mock.mock.calls.slice(0, -1); - try { - expect(checkCalls.length).toBe(2); - checkCalls.forEach((args) => expect(args[0]).toBe(0)); - expect(mock).lastCalledWith(1); - expect(batchHandler.mock.calls.length).toBe(1); - } catch (e) { - reject(e); - } + expect(checkCalls.length).toBe(2); + checkCalls.forEach((args) => expect(args[0]).toBe(0)); + expect(mock).lastCalledWith(1); + expect(batchHandler.mock.calls.length).toBe(1); runBatchInterval(); }; - delayedBatchInterval(); + void delayedBatchInterval(); mock(batchHandler.mock.calls.length); mock(batchHandler.mock.calls.length); @@ -994,97 +856,82 @@ describe("BatchLink", () => { runBatchInterval(); }); - itAsync( - "throws an error when more requests than results", - (resolve, reject) => { - const result = [{ data: {} }]; - const batchHandler = jest.fn((op) => Observable.of(result)); + it("throws an error when more requests than results", () => { + expect.assertions(4); + const result = [{ data: {} }]; + const batchHandler = jest.fn((op) => Observable.of(result)); + + const link = ApolloLink.from([ + new BatchLink({ + batchInterval: 10, + batchMax: 2, + batchHandler, + }), + ]); + + [1, 2].forEach((x) => { + execute(link, { + query, + }).subscribe({ + next: (data) => { + throw new Error("next should not be called"); + }, + error: (error: any) => { + expect(error).toBeDefined(); + expect(error.result).toEqual(result); + }, + complete: () => { + throw new Error("complete should not be called"); + }, + }); + }); + }); + + describe("batchKey", () => { + it("should allow different batches to be created separately", (done) => { + const data = { data: {} }; + const result = [data, data]; + + const batchHandler = jest.fn((op) => { + expect(op.length).toBe(2); + return Observable.of(result); + }); + let key = true; + const batchKey = () => { + key = !key; + return "" + !key; + }; const link = ApolloLink.from([ new BatchLink({ - batchInterval: 10, + batchInterval: 1, + //if batchKey does not work, then the batch size would be 3 batchMax: 2, batchHandler, + batchKey, }), ]); - [1, 2].forEach((x) => { + let count = 0; + [1, 2, 3, 4].forEach(() => { execute(link, { query, }).subscribe({ - next: (data) => { - reject("next should not be called"); + next: (d) => { + expect(d).toEqual(data); + }, + error: (e) => { + throw e; }, - error: terminatingCheck(resolve, reject, (error: any) => { - expect(error).toBeDefined(); - expect(error.result).toEqual(result); - }), complete: () => { - reject("complete should not be called"); + count++; + if (count === 4) { + expect(batchHandler.mock.calls.length).toBe(2); + done(); + } }, }); }); - } - ); - - describe("batchKey", () => { - itAsync( - "should allow different batches to be created separately", - (resolve, reject) => { - const data = { data: {} }; - const result = [data, data]; - - const batchHandler = jest.fn((op) => { - try { - expect(op.length).toBe(2); - } catch (e) { - reject(e); - } - return Observable.of(result); - }); - let key = true; - const batchKey = () => { - key = !key; - return "" + !key; - }; - - const link = ApolloLink.from([ - new BatchLink({ - batchInterval: 1, - //if batchKey does not work, then the batch size would be 3 - batchMax: 2, - batchHandler, - batchKey, - }), - ]); - - let count = 0; - [1, 2, 3, 4].forEach(() => { - execute(link, { - query, - }).subscribe({ - next: (d) => { - try { - expect(d).toEqual(data); - } catch (e) { - reject(e); - } - }, - error: reject, - complete: () => { - count++; - if (count === 4) { - try { - expect(batchHandler.mock.calls.length).toBe(2); - resolve(); - } catch (e) { - reject(e); - } - } - }, - }); - }); - } - ); + }); }); }); diff --git a/src/link/context/__tests__/index.ts b/src/link/context/__tests__/index.ts index 8aa7a6be03e..c80be213a95 100644 --- a/src/link/context/__tests__/index.ts +++ b/src/link/context/__tests__/index.ts @@ -4,7 +4,8 @@ import { ApolloLink } from "../../core"; import { Observable } from "../../../utilities/observables/Observable"; import { execute } from "../../core/execute"; import { setContext } from "../index"; -import { itAsync } from "../../../testing"; +import { wait } from "../../../testing"; +import { ObservableStream } from "../../../testing/internal"; const sleep = (ms: number) => new Promise((s) => setTimeout(s, ms)); const query = gql` @@ -18,68 +19,53 @@ const data = { foo: { bar: true }, }; -itAsync( - "can be used to set the context with a simple function", - (resolve, reject) => { - const withContext = setContext(() => ({ dynamicallySet: true })); +it("can be used to set the context with a simple function", async () => { + const withContext = setContext(() => ({ dynamicallySet: true })); - const mockLink = new ApolloLink((operation) => { - expect(operation.getContext().dynamicallySet).toBe(true); - return Observable.of({ data }); - }); + const mockLink = new ApolloLink((operation) => { + expect(operation.getContext().dynamicallySet).toBe(true); + return Observable.of({ data }); + }); - const link = withContext.concat(mockLink); + const link = withContext.concat(mockLink); + const stream = new ObservableStream(execute(link, { query })); - execute(link, { query }).subscribe((result) => { - expect(result.data).toEqual(data); - resolve(); - }); - } -); - -itAsync( - "can be used to set the context with a function returning a promise", - (resolve, reject) => { - const withContext = setContext(() => - Promise.resolve({ dynamicallySet: true }) - ); - - const mockLink = new ApolloLink((operation) => { - expect(operation.getContext().dynamicallySet).toBe(true); - return Observable.of({ data }); - }); + await expect(stream).toEmitValue({ data }); +}); - const link = withContext.concat(mockLink); +it("can be used to set the context with a function returning a promise", async () => { + const withContext = setContext(() => + Promise.resolve({ dynamicallySet: true }) + ); - execute(link, { query }).subscribe((result) => { - expect(result.data).toEqual(data); - resolve(); - }); - } -); - -itAsync( - "can be used to set the context with a function returning a promise that is delayed", - (resolve, reject) => { - const withContext = setContext(() => - sleep(25).then(() => ({ dynamicallySet: true })) - ); - - const mockLink = new ApolloLink((operation) => { - expect(operation.getContext().dynamicallySet).toBe(true); - return Observable.of({ data }); - }); + const mockLink = new ApolloLink((operation) => { + expect(operation.getContext().dynamicallySet).toBe(true); + return Observable.of({ data }); + }); - const link = withContext.concat(mockLink); + const link = withContext.concat(mockLink); + const stream = new ObservableStream(execute(link, { query })); - execute(link, { query }).subscribe((result) => { - expect(result.data).toEqual(data); - resolve(); - }); - } -); + await expect(stream).toEmitValue({ data }); +}); + +it("can be used to set the context with a function returning a promise that is delayed", async () => { + const withContext = setContext(() => + sleep(25).then(() => ({ dynamicallySet: true })) + ); + + const mockLink = new ApolloLink((operation) => { + expect(operation.getContext().dynamicallySet).toBe(true); + return Observable.of({ data }); + }); -itAsync("handles errors in the lookup correclty", (resolve, reject) => { + const link = withContext.concat(mockLink); + const stream = new ObservableStream(execute(link, { query })); + + await expect(stream).toEmitValue({ data }); +}); + +it("handles errors in the lookup correclty", async () => { const withContext = setContext(() => sleep(5).then(() => { throw new Error("dang"); @@ -92,32 +78,27 @@ itAsync("handles errors in the lookup correclty", (resolve, reject) => { const link = withContext.concat(mockLink); - execute(link, { query }).subscribe(reject, (e) => { - expect(e.message).toBe("dang"); - resolve(); - }); + const stream = new ObservableStream(execute(link, { query })); + + await expect(stream).toEmitError("dang"); }); -itAsync( - "handles errors in the lookup correclty with a normal function", - (resolve, reject) => { - const withContext = setContext(() => { - throw new Error("dang"); - }); - const mockLink = new ApolloLink((operation) => { - return Observable.of({ data }); - }); +it("handles errors in the lookup correctly with a normal function", async () => { + const withContext = setContext(() => { + throw new Error("dang"); + }); - const link = withContext.concat(mockLink); + const mockLink = new ApolloLink((operation) => { + return Observable.of({ data }); + }); - execute(link, { query }).subscribe(reject, (e) => { - expect(e.message).toBe("dang"); - resolve(); - }); - } -); + const link = withContext.concat(mockLink); + const stream = new ObservableStream(execute(link, { query })); -itAsync("has access to the request information", (resolve, reject) => { + await expect(stream).toEmitError("dang"); +}); + +it("has access to the request information", async () => { const withContext = setContext(({ operationName, query, variables }) => sleep(1).then(() => Promise.resolve({ @@ -137,13 +118,14 @@ itAsync("has access to the request information", (resolve, reject) => { }); const link = withContext.concat(mockLink); + const stream = new ObservableStream( + execute(link, { query, variables: { id: 1 } }) + ); - execute(link, { query, variables: { id: 1 } }).subscribe((result) => { - expect(result.data).toEqual(data); - resolve(); - }); + await expect(stream).toEmitValue({ data }); }); -itAsync("has access to the context at execution time", (resolve, reject) => { + +it("has access to the context at execution time", async () => { const withContext = setContext((_, { count }) => sleep(1).then(() => ({ count: count + 1 })) ); @@ -155,14 +137,14 @@ itAsync("has access to the context at execution time", (resolve, reject) => { }); const link = withContext.concat(mockLink); + const stream = new ObservableStream( + execute(link, { query, context: { count: 1 } }) + ); - execute(link, { query, context: { count: 1 } }).subscribe((result) => { - expect(result.data).toEqual(data); - resolve(); - }); + await expect(stream).toEmitValue({ data }); }); -itAsync("unsubscribes correctly", (resolve, reject) => { +it("unsubscribes correctly", async () => { const withContext = setContext((_, { count }) => sleep(1).then(() => ({ count: count + 1 })) ); @@ -175,18 +157,19 @@ itAsync("unsubscribes correctly", (resolve, reject) => { const link = withContext.concat(mockLink); - let handle = execute(link, { - query, - context: { count: 1 }, - }).subscribe((result) => { - expect(result.data).toEqual(data); - handle.unsubscribe(); - resolve(); - }); + const stream = new ObservableStream( + execute(link, { + query, + context: { count: 1 }, + }) + ); + + await expect(stream).toEmitValue({ data }); + stream.unsubscribe(); }); -itAsync("unsubscribes without throwing before data", (resolve, reject) => { - let called: boolean; +it("unsubscribes without throwing before data", async () => { + let called!: boolean; const withContext = setContext((_, { count }) => { called = true; return sleep(1).then(() => ({ count: count + 1 })); @@ -209,51 +192,43 @@ itAsync("unsubscribes without throwing before data", (resolve, reject) => { query, context: { count: 1 }, }).subscribe((result) => { - reject("should have unsubscribed"); + throw new Error("should have unsubscribed"); }); - setTimeout(() => { - handle.unsubscribe(); - expect(called).toBe(true); - resolve(); - }, 10); + await wait(10); + + handle.unsubscribe(); + expect(called).toBe(true); }); -itAsync( - "does not start the next link subscription if the upstream subscription is already closed", - (resolve, reject) => { - let promiseResolved = false; - const withContext = setContext(() => - sleep(5).then(() => { - promiseResolved = true; - return { dynamicallySet: true }; - }) - ); - - let mockLinkCalled = false; - const mockLink = new ApolloLink(() => { - mockLinkCalled = true; - reject("link should not be called"); - return new Observable((observer) => { - observer.error("link should not have been observed"); - }); - }); +it("does not start the next link subscription if the upstream subscription is already closed", async () => { + let promiseResolved = false; + const withContext = setContext(() => + sleep(5).then(() => { + promiseResolved = true; + return { dynamicallySet: true }; + }) + ); - const link = withContext.concat(mockLink); + let mockLinkCalled = false; + const mockLink = new ApolloLink(() => { + mockLinkCalled = true; + throw new Error("link should not be called"); + }); - let subscriptionReturnedData = false; - let handle = execute(link, { query }).subscribe((result) => { - subscriptionReturnedData = true; - reject("subscription should not return data"); - }); + const link = withContext.concat(mockLink); - handle.unsubscribe(); + let subscriptionReturnedData = false; + let handle = execute(link, { query }).subscribe((result) => { + subscriptionReturnedData = true; + throw new Error("subscription should not return data"); + }); - setTimeout(() => { - expect(promiseResolved).toBe(true); - expect(mockLinkCalled).toBe(false); - expect(subscriptionReturnedData).toBe(false); - resolve(); - }, 10); - } -); + handle.unsubscribe(); + + await wait(10); + + expect(promiseResolved).toBe(true); + expect(mockLinkCalled).toBe(false); + expect(subscriptionReturnedData).toBe(false); +}); diff --git a/src/link/error/__tests__/index.ts b/src/link/error/__tests__/index.ts index 5e886ff58d3..0a3bf2bbfb8 100644 --- a/src/link/error/__tests__/index.ts +++ b/src/link/error/__tests__/index.ts @@ -5,10 +5,10 @@ import { execute } from "../../core/execute"; import { ServerError, throwServerError } from "../../utils/throwServerError"; import { Observable } from "../../../utilities/observables/Observable"; import { onError, ErrorLink } from "../"; -import { itAsync } from "../../../testing"; +import { ObservableStream } from "../../../testing/internal"; describe("error handling", () => { - itAsync("has an easy way to handle GraphQL errors", (resolve, reject) => { + it("has an easy way to handle GraphQL errors", async () => { const query = gql` { foo { @@ -17,7 +17,7 @@ describe("error handling", () => { } `; - let called: boolean; + let called = false; const errorLink = onError(({ graphQLErrors, networkError }) => { expect(graphQLErrors![0].message).toBe("resolver blew up"); called = true; @@ -34,47 +34,44 @@ describe("error handling", () => { ); const link = errorLink.concat(mockLink); + const stream = new ObservableStream(execute(link, { query })); - execute(link, { query }).subscribe((result) => { - expect(result.errors![0].message).toBe("resolver blew up"); - expect(called).toBe(true); - resolve(); - }); + const result = await stream.takeNext(); + + expect(result.errors![0].message).toBe("resolver blew up"); + expect(called).toBe(true); }); - itAsync( - "has an easy way to log client side (network) errors", - (resolve, reject) => { - const query = gql` - query Foo { - foo { - bar - } + + it("has an easy way to log client side (network) errors", async () => { + const query = gql` + query Foo { + foo { + bar } - `; + } + `; - let called: boolean; - const errorLink = onError(({ operation, networkError }) => { - expect(networkError!.message).toBe("app is crashing"); - expect(operation.operationName).toBe("Foo"); - called = true; - }); + let called = false; + const errorLink = onError(({ operation, networkError }) => { + expect(networkError!.message).toBe("app is crashing"); + expect(operation.operationName).toBe("Foo"); + called = true; + }); - const mockLink = new ApolloLink((operation) => { - throw new Error("app is crashing"); - }); + const mockLink = new ApolloLink((operation) => { + throw new Error("app is crashing"); + }); - const link = errorLink.concat(mockLink); + const link = errorLink.concat(mockLink); + const stream = new ObservableStream(execute(link, { query })); - execute(link, { query }).subscribe({ - error: (e) => { - expect(e.message).toBe("app is crashing"); - expect(called).toBe(true); - resolve(); - }, - }); - } - ); - itAsync("captures errors within links", (resolve, reject) => { + const error = await stream.takeError(); + + expect(error.message).toBe("app is crashing"); + expect(called).toBe(true); + }); + + it("captures errors within links", async () => { const query = gql` query Foo { foo { @@ -83,7 +80,7 @@ describe("error handling", () => { } `; - let called: boolean; + let called = false; const errorLink = onError(({ operation, networkError }) => { expect(networkError!.message).toBe("app is crashing"); expect(operation.operationName).toBe("Foo"); @@ -97,89 +94,79 @@ describe("error handling", () => { }); const link = errorLink.concat(mockLink); + const stream = new ObservableStream(execute(link, { query })); - execute(link, { query }).subscribe({ - error: (e) => { - expect(e.message).toBe("app is crashing"); - expect(called).toBe(true); - resolve(); - }, - }); + const error = await stream.takeError(); + + expect(error.message).toBe("app is crashing"); + expect(called).toBe(true); }); - itAsync( - "captures networkError.statusCode within links", - (resolve, reject) => { - const query = gql` - query Foo { - foo { - bar - } + + it("captures networkError.statusCode within links", async () => { + const query = gql` + query Foo { + foo { + bar } - `; - - let called: boolean; - const errorLink = onError(({ operation, networkError }) => { - expect(networkError!.message).toBe("app is crashing"); - expect(networkError!.name).toBe("ServerError"); - expect((networkError as ServerError).statusCode).toBe(500); - expect((networkError as ServerError).response.ok).toBe(false); - expect(operation.operationName).toBe("Foo"); - called = true; - }); + } + `; - const mockLink = new ApolloLink((operation) => { - return new Observable((obs) => { - const response = { status: 500, ok: false } as Response; - throwServerError(response, "ServerError", "app is crashing"); - }); + let called = false; + const errorLink = onError(({ operation, networkError }) => { + expect(networkError!.message).toBe("app is crashing"); + expect(networkError!.name).toBe("ServerError"); + expect((networkError as ServerError).statusCode).toBe(500); + expect((networkError as ServerError).response.ok).toBe(false); + expect(operation.operationName).toBe("Foo"); + called = true; + }); + + const mockLink = new ApolloLink((operation) => { + return new Observable((obs) => { + const response = { status: 500, ok: false } as Response; + throwServerError(response, "ServerError", "app is crashing"); }); + }); - const link = errorLink.concat(mockLink); + const link = errorLink.concat(mockLink); + const stream = new ObservableStream(execute(link, { query })); - execute(link, { query }).subscribe({ - error: (e) => { - expect(e.message).toBe("app is crashing"); - expect(called).toBe(true); - resolve(); - }, - }); - } - ); - itAsync( - "sets graphQLErrors to undefined if networkError.result is an empty string", - (resolve, reject) => { - const query = gql` - query Foo { - foo { - bar - } + const error = await stream.takeError(); + + expect(error.message).toBe("app is crashing"); + expect(called).toBe(true); + }); + + it("sets graphQLErrors to undefined if networkError.result is an empty string", async () => { + const query = gql` + query Foo { + foo { + bar } - `; + } + `; - let called: boolean; - const errorLink = onError(({ graphQLErrors }) => { - expect(graphQLErrors).toBeUndefined(); - called = true; - }); + let called = false; + const errorLink = onError(({ graphQLErrors }) => { + expect(graphQLErrors).toBeUndefined(); + called = true; + }); - const mockLink = new ApolloLink((operation) => { - return new Observable((obs) => { - const response = { status: 500, ok: false } as Response; - throwServerError(response, "", "app is crashing"); - }); + const mockLink = new ApolloLink((operation) => { + return new Observable((obs) => { + const response = { status: 500, ok: false } as Response; + throwServerError(response, "", "app is crashing"); }); + }); - const link = errorLink.concat(mockLink); + const link = errorLink.concat(mockLink); + const stream = new ObservableStream(execute(link, { query })); - execute(link, { query }).subscribe({ - error: (e) => { - expect(called).toBe(true); - resolve(); - }, - }); - } - ); - itAsync("completes if no errors", (resolve, reject) => { + await expect(stream).toEmitError(); + expect(called).toBe(true); + }); + + it("completes if no errors", async () => { const query = gql` { foo { @@ -197,12 +184,13 @@ describe("error handling", () => { }); const link = errorLink.concat(mockLink); + const stream = new ObservableStream(execute(link, { query })); - execute(link, { query }).subscribe({ - complete: resolve, - }); + await expect(stream).toEmitNext(); + await expect(stream).toComplete(); }); - itAsync("allows an error to be ignored", (resolve, reject) => { + + it("allows an error to be ignored", async () => { const query = gql` { foo { @@ -225,17 +213,16 @@ describe("error handling", () => { }); const link = errorLink.concat(mockLink); + const stream = new ObservableStream(execute(link, { query })); - execute(link, { query }).subscribe({ - next: ({ errors, data }) => { - expect(errors).toBe(null); - expect(data).toEqual({ foo: { id: 1 } }); - }, - complete: resolve, + await expect(stream).toEmitValue({ + errors: null, + data: { foo: { id: 1 } }, }); + await expect(stream).toComplete(); }); - itAsync("can be unsubcribed", (resolve, reject) => { + it("can be unsubcribed", async () => { const query = gql` { foo { @@ -258,62 +245,56 @@ describe("error handling", () => { }); const link = errorLink.concat(mockLink); + const stream = new ObservableStream(execute(link, { query })); - const sub = execute(link, { query }).subscribe({ - complete: () => { - reject("completed"); - }, - }); - - sub.unsubscribe(); + stream.unsubscribe(); - setTimeout(resolve, 10); + await expect(stream).not.toEmitAnything(); }); - itAsync( - "includes the operation and any data along with a graphql error", - (resolve, reject) => { - const query = gql` - query Foo { - foo { - bar - } + it("includes the operation and any data along with a graphql error", async () => { + const query = gql` + query Foo { + foo { + bar } - `; - - let called: boolean; - const errorLink = onError(({ graphQLErrors, response, operation }) => { - expect(graphQLErrors![0].message).toBe("resolver blew up"); - expect(response!.data!.foo).toBe(true); - expect(operation.operationName).toBe("Foo"); - expect(operation.getContext().bar).toBe(true); - called = true; - }); + } + `; - const mockLink = new ApolloLink((operation) => - Observable.of({ - data: { foo: true }, - errors: [ - { - message: "resolver blew up", - }, - ], - } as any) - ); - - const link = errorLink.concat(mockLink); - - execute(link, { query, context: { bar: true } }).subscribe((result) => { - expect(result.errors![0].message).toBe("resolver blew up"); - expect(called).toBe(true); - resolve(); - }); - } - ); + let called = false; + const errorLink = onError(({ graphQLErrors, response, operation }) => { + expect(graphQLErrors![0].message).toBe("resolver blew up"); + expect(response!.data!.foo).toBe(true); + expect(operation.operationName).toBe("Foo"); + expect(operation.getContext().bar).toBe(true); + called = true; + }); + + const mockLink = new ApolloLink((operation) => + Observable.of({ + data: { foo: true }, + errors: [ + { + message: "resolver blew up", + }, + ], + } as any) + ); + + const link = errorLink.concat(mockLink); + const stream = new ObservableStream( + execute(link, { query, context: { bar: true } }) + ); + + const result = await stream.takeNext(); + + expect(result.errors![0].message).toBe("resolver blew up"); + expect(called).toBe(true); + }); }); describe("error handling with class", () => { - itAsync("has an easy way to handle GraphQL errors", (resolve, reject) => { + it("has an easy way to handle GraphQL errors", async () => { const query = gql` { foo { @@ -322,7 +303,7 @@ describe("error handling with class", () => { } `; - let called: boolean; + let called = false; const errorLink = new ErrorLink(({ graphQLErrors, networkError }) => { expect(graphQLErrors![0].message).toBe("resolver blew up"); called = true; @@ -339,46 +320,43 @@ describe("error handling with class", () => { ); const link = errorLink.concat(mockLink); + const stream = new ObservableStream(execute(link, { query })); - execute(link, { query }).subscribe((result) => { - expect(result!.errors![0].message).toBe("resolver blew up"); - expect(called).toBe(true); - resolve(); - }); + const result = await stream.takeNext(); + + expect(result!.errors![0].message).toBe("resolver blew up"); + expect(called).toBe(true); }); - itAsync( - "has an easy way to log client side (network) errors", - (resolve, reject) => { - const query = gql` - { - foo { - bar - } + + it("has an easy way to log client side (network) errors", async () => { + const query = gql` + { + foo { + bar } - `; + } + `; - let called: boolean; - const errorLink = new ErrorLink(({ networkError }) => { - expect(networkError!.message).toBe("app is crashing"); - called = true; - }); + let called = false; + const errorLink = new ErrorLink(({ networkError }) => { + expect(networkError!.message).toBe("app is crashing"); + called = true; + }); - const mockLink = new ApolloLink((operation) => { - throw new Error("app is crashing"); - }); + const mockLink = new ApolloLink((operation) => { + throw new Error("app is crashing"); + }); - const link = errorLink.concat(mockLink); + const link = errorLink.concat(mockLink); + const stream = new ObservableStream(execute(link, { query })); - execute(link, { query }).subscribe({ - error: (e) => { - expect(e.message).toBe("app is crashing"); - expect(called).toBe(true); - resolve(); - }, - }); - } - ); - itAsync("captures errors within links", (resolve, reject) => { + const error = await stream.takeError(); + + expect(error.message).toBe("app is crashing"); + expect(called).toBe(true); + }); + + it("captures errors within links", async () => { const query = gql` { foo { @@ -387,7 +365,7 @@ describe("error handling with class", () => { } `; - let called: boolean; + let called = false; const errorLink = new ErrorLink(({ networkError }) => { expect(networkError!.message).toBe("app is crashing"); called = true; @@ -400,16 +378,15 @@ describe("error handling with class", () => { }); const link = errorLink.concat(mockLink); + const stream = new ObservableStream(execute(link, { query })); - execute(link, { query }).subscribe({ - error: (e) => { - expect(e.message).toBe("app is crashing"); - expect(called).toBe(true); - resolve(); - }, - }); + const error = await stream.takeError(); + + expect(error.message).toBe("app is crashing"); + expect(called).toBe(true); }); - itAsync("completes if no errors", (resolve, reject) => { + + it("completes if no errors", async () => { const query = gql` { foo { @@ -428,11 +405,13 @@ describe("error handling with class", () => { const link = errorLink.concat(mockLink); - execute(link, { query }).subscribe({ - complete: resolve, - }); + const stream = new ObservableStream(execute(link, { query })); + + await expect(stream).toEmitNext(); + await expect(stream).toComplete(); }); - itAsync("can be unsubcribed", (resolve, reject) => { + + it("can be unsubcribed", async () => { const query = gql` { foo { @@ -455,16 +434,11 @@ describe("error handling with class", () => { }); const link = errorLink.concat(mockLink); + const stream = new ObservableStream(execute(link, { query })); - const sub = execute(link, { query }).subscribe({ - complete: () => { - reject("completed"); - }, - }); - - sub.unsubscribe(); + stream.unsubscribe(); - setTimeout(resolve, 10); + await expect(stream).not.toEmitAnything(); }); }); @@ -491,118 +465,92 @@ describe("support for request retrying", () => { message: "some other error", }; - itAsync( - "returns the retried request when forward(operation) is called", - (resolve, reject) => { - let errorHandlerCalled = false; - - let timesCalled = 0; - const mockHttpLink = new ApolloLink((operation) => { - if (timesCalled === 0) { - timesCalled++; - // simulate the first request being an error - return new Observable((observer) => { - observer.next(ERROR_RESPONSE as any); - observer.complete(); - }); - } else { - return new Observable((observer) => { - observer.next(GOOD_RESPONSE); - observer.complete(); - }); - } - }); + it("returns the retried request when forward(operation) is called", async () => { + let errorHandlerCalled = false; - const errorLink = new ErrorLink( - ({ graphQLErrors, response, operation, forward }) => { - try { - if (graphQLErrors) { - errorHandlerCalled = true; - expect(graphQLErrors).toEqual(ERROR_RESPONSE.errors); - expect(response!.data).not.toBeDefined(); - expect(operation.operationName).toBe("Foo"); - expect(operation.getContext().bar).toBe(true); - // retry operation if it resulted in an error - return forward(operation); - } - } catch (error) { - reject(error); - } - } - ); - - const link = errorLink.concat(mockHttpLink); - - execute(link, { query: QUERY, context: { bar: true } }).subscribe({ - next(result) { - try { - expect(errorHandlerCalled).toBe(true); - expect(result).toEqual(GOOD_RESPONSE); - } catch (error) { - return reject(error); - } - }, - complete() { - resolve(); - }, - }); - } - ); - - itAsync( - "supports retrying when the initial request had networkError", - (resolve, reject) => { - let errorHandlerCalled = false; - - let timesCalled = 0; - const mockHttpLink = new ApolloLink((operation) => { - if (timesCalled === 0) { - timesCalled++; - // simulate the first request being an error - return new Observable((observer) => { - observer.error(NETWORK_ERROR); - }); - } else { - return new Observable((observer) => { - observer.next(GOOD_RESPONSE); - observer.complete(); - }); + let timesCalled = 0; + const mockHttpLink = new ApolloLink((operation) => { + if (timesCalled === 0) { + timesCalled++; + // simulate the first request being an error + return new Observable((observer) => { + observer.next(ERROR_RESPONSE as any); + observer.complete(); + }); + } else { + return new Observable((observer) => { + observer.next(GOOD_RESPONSE); + observer.complete(); + }); + } + }); + + const errorLink = new ErrorLink( + ({ graphQLErrors, response, operation, forward }) => { + if (graphQLErrors) { + errorHandlerCalled = true; + expect(graphQLErrors).toEqual(ERROR_RESPONSE.errors); + expect(response!.data).not.toBeDefined(); + expect(operation.operationName).toBe("Foo"); + expect(operation.getContext().bar).toBe(true); + // retry operation if it resulted in an error + return forward(operation); } - }); + } + ); + + const link = errorLink.concat(mockHttpLink); + + const stream = new ObservableStream( + execute(link, { query: QUERY, context: { bar: true } }) + ); + + await expect(stream).toEmitValue(GOOD_RESPONSE); + expect(errorHandlerCalled).toBe(true); + await expect(stream).toComplete(); + }); + + it("supports retrying when the initial request had networkError", async () => { + let errorHandlerCalled = false; - const errorLink = new ErrorLink( - ({ networkError, response, operation, forward }) => { - try { - if (networkError) { - errorHandlerCalled = true; - expect(networkError).toEqual(NETWORK_ERROR); - return forward(operation); - } - } catch (error) { - reject(error); - } + let timesCalled = 0; + const mockHttpLink = new ApolloLink((operation) => { + if (timesCalled === 0) { + timesCalled++; + // simulate the first request being an error + return new Observable((observer) => { + observer.error(NETWORK_ERROR); + }); + } else { + return new Observable((observer) => { + observer.next(GOOD_RESPONSE); + observer.complete(); + }); + } + }); + + const errorLink = new ErrorLink( + ({ networkError, response, operation, forward }) => { + if (networkError) { + errorHandlerCalled = true; + expect(networkError).toEqual(NETWORK_ERROR); + return forward(operation); } - ); - - const link = errorLink.concat(mockHttpLink); - - execute(link, { query: QUERY, context: { bar: true } }).subscribe({ - next(result) { - try { - expect(errorHandlerCalled).toBe(true); - expect(result).toEqual(GOOD_RESPONSE); - } catch (error) { - return reject(error); - } - }, - complete() { - resolve(); - }, - }); - } - ); + } + ); + + const link = errorLink.concat(mockHttpLink); + + const stream = new ObservableStream( + execute(link, { query: QUERY, context: { bar: true } }) + ); + + await expect(stream).toEmitValue(GOOD_RESPONSE); + expect(errorHandlerCalled).toBe(true); + await expect(stream).toComplete(); + }); - itAsync("returns errors from retried requests", (resolve, reject) => { + it("returns errors from retried requests", async () => { let errorHandlerCalled = false; let timesCalled = 0; @@ -623,38 +571,25 @@ describe("support for request retrying", () => { const errorLink = new ErrorLink( ({ graphQLErrors, networkError, response, operation, forward }) => { - try { - if (graphQLErrors) { - errorHandlerCalled = true; - expect(graphQLErrors).toEqual(ERROR_RESPONSE.errors); - expect(response!.data).not.toBeDefined(); - expect(operation.operationName).toBe("Foo"); - expect(operation.getContext().bar).toBe(true); - // retry operation if it resulted in an error - return forward(operation); - } - } catch (error) { - reject(error); + if (graphQLErrors) { + errorHandlerCalled = true; + expect(graphQLErrors).toEqual(ERROR_RESPONSE.errors); + expect(response!.data).not.toBeDefined(); + expect(operation.operationName).toBe("Foo"); + expect(operation.getContext().bar).toBe(true); + // retry operation if it resulted in an error + return forward(operation); } } ); const link = errorLink.concat(mockHttpLink); - let observerNextCalled = false; - execute(link, { query: QUERY, context: { bar: true } }).subscribe({ - next(result) { - // should not be called - observerNextCalled = true; - }, - error(error) { - // note that complete will not be after an error - // therefore we should end the test here with resolve() - expect(errorHandlerCalled).toBe(true); - expect(observerNextCalled).toBe(false); - expect(error).toEqual(NETWORK_ERROR); - resolve(); - }, - }); + const stream = new ObservableStream( + execute(link, { query: QUERY, context: { bar: true } }) + ); + + await expect(stream).toEmitError(NETWORK_ERROR); + expect(errorHandlerCalled).toBe(true); }); }); diff --git a/src/link/http/__tests__/HttpLink.ts b/src/link/http/__tests__/HttpLink.ts index ad58e4c40c9..5d8e9a155dd 100644 --- a/src/link/http/__tests__/HttpLink.ts +++ b/src/link/http/__tests__/HttpLink.ts @@ -19,7 +19,8 @@ import { ClientParseError } from "../serializeFetchParameter"; import { ServerParseError } from "../parseAndCheckHttpResponse"; import { FetchResult, ServerError } from "../../.."; import { voidFetchDuringEachTest } from "./helpers"; -import { itAsync } from "../../../testing"; +import { wait } from "../../../testing"; +import { ObservableStream } from "../../../testing/internal"; const sampleQuery = gql` query SampleQuery { @@ -85,22 +86,6 @@ const sampleSubscriptionWithDefer = gql` } `; -function makeCallback( - resolve: () => void, - reject: (error: Error) => void, - callback: (...args: TArgs) => any -) { - return function () { - try { - // @ts-expect-error - callback.apply(this, arguments); - resolve(); - } catch (error) { - reject(error as Error); - } - } as typeof callback; -} - function convertBatchedBody(body: BodyInit | null | undefined) { return JSON.parse(body as string); } @@ -153,26 +138,18 @@ describe("HttpLink", () => { expect(() => new HttpLink()).not.toThrow(); }); - itAsync( - "constructor creates link that can call next and then complete", - (resolve, reject) => { - const next = jest.fn(); - const link = new HttpLink({ uri: "/data" }); - const observable = execute(link, { - query: sampleQuery, - }); - observable.subscribe({ - next, - error: (error) => expect(false), - complete: () => { - expect(next).toHaveBeenCalledTimes(1); - resolve(); - }, - }); - } - ); + it("constructor creates link that can call next and then complete", async () => { + const link = new HttpLink({ uri: "/data" }); + const observable = execute(link, { + query: sampleQuery, + }); + const stream = new ObservableStream(observable); - itAsync("supports using a GET request", (resolve, reject) => { + await expect(stream).toEmitNext(); + await expect(stream).toComplete(); + }); + + it("supports using a GET request", async () => { const variables = { params: "stub" }; const extensions = { myExtension: "foo" }; @@ -183,298 +160,290 @@ describe("HttpLink", () => { includeUnusedVariables: true, }); - execute(link, { query: sampleQuery, variables, extensions }).subscribe({ - next: makeCallback(resolve, reject, () => { - const [uri, options] = fetchMock.lastCall()!; - const { method, body } = options!; - expect(body).toBeUndefined(); - expect(method).toBe("GET"); - expect(uri).toBe( - "/data?query=query%20SampleQuery%20%7B%0A%20%20stub%20%7B%0A%20%20%20%20id%0A%20%20%7D%0A%7D&operationName=SampleQuery&variables=%7B%22params%22%3A%22stub%22%7D&extensions=%7B%22myExtension%22%3A%22foo%22%7D" - ); - }), - error: (error) => reject(error), + const observable = execute(link, { + query: sampleQuery, + variables, + extensions, }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + await expect(stream).toComplete(); + + const [uri, options] = fetchMock.lastCall()!; + const { method, body } = options!; + + expect(body).toBeUndefined(); + expect(method).toBe("GET"); + expect(uri).toBe( + "/data?query=query%20SampleQuery%20%7B%0A%20%20stub%20%7B%0A%20%20%20%20id%0A%20%20%7D%0A%7D&operationName=SampleQuery&variables=%7B%22params%22%3A%22stub%22%7D&extensions=%7B%22myExtension%22%3A%22foo%22%7D" + ); }); - itAsync("supports using a GET request with search", (resolve, reject) => { + it("supports using a GET request with search", async () => { const variables = { params: "stub" }; const link = createHttpLink({ uri: "/data?foo=bar", fetchOptions: { method: "GET" }, }); + const observable = execute(link, { query: sampleQuery, variables }); + const stream = new ObservableStream(observable); - execute(link, { query: sampleQuery, variables }).subscribe({ - next: makeCallback(resolve, reject, () => { - const [uri, options] = fetchMock.lastCall()!; - const { method, body } = options!; - expect(body).toBeUndefined(); - expect(method).toBe("GET"); - expect(uri).toBe( - "/data?foo=bar&query=query%20SampleQuery%20%7B%0A%20%20stub%20%7B%0A%20%20%20%20id%0A%20%20%7D%0A%7D&operationName=SampleQuery&variables=%7B%7D" - ); - }), - error: (error) => reject(error), - }); + await expect(stream).toEmitNext(); + await expect(stream).toComplete(); + + const [uri, options] = fetchMock.lastCall()!; + const { method, body } = options!; + + expect(body).toBeUndefined(); + expect(method).toBe("GET"); + expect(uri).toBe( + "/data?foo=bar&query=query%20SampleQuery%20%7B%0A%20%20stub%20%7B%0A%20%20%20%20id%0A%20%20%7D%0A%7D&operationName=SampleQuery&variables=%7B%7D" + ); }); - itAsync( - "supports using a GET request on the context", - (resolve, reject) => { - const variables = { params: "stub" }; - const link = createHttpLink({ - uri: "/data", - }); + it("supports using a GET request on the context", async () => { + const variables = { params: "stub" }; + const link = createHttpLink({ + uri: "/data", + }); - execute(link, { - query: sampleQuery, - variables, - context: { - fetchOptions: { method: "GET" }, - }, - }).subscribe( - makeCallback(resolve, reject, () => { - const [uri, options] = fetchMock.lastCall()!; - const { method, body } = options!; - expect(body).toBeUndefined(); - expect(method).toBe("GET"); - expect(uri).toBe( - "/data?query=query%20SampleQuery%20%7B%0A%20%20stub%20%7B%0A%20%20%20%20id%0A%20%20%7D%0A%7D&operationName=SampleQuery&variables=%7B%7D" - ); - }) - ); - } - ); + const observable = execute(link, { + query: sampleQuery, + variables, + context: { + fetchOptions: { method: "GET" }, + }, + }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + await expect(stream).toComplete(); + + const [uri, options] = fetchMock.lastCall()!; + const { method, body } = options!; + + expect(body).toBeUndefined(); + expect(method).toBe("GET"); + expect(uri).toBe( + "/data?query=query%20SampleQuery%20%7B%0A%20%20stub%20%7B%0A%20%20%20%20id%0A%20%20%7D%0A%7D&operationName=SampleQuery&variables=%7B%7D" + ); + }); - itAsync("uses GET with useGETForQueries", (resolve, reject) => { + it("uses GET with useGETForQueries", async () => { const variables = { params: "stub" }; const link = createHttpLink({ uri: "/data", useGETForQueries: true, }); - execute(link, { + const observable = execute(link, { query: sampleQuery, variables, - }).subscribe( - makeCallback(resolve, reject, () => { - const [uri, options] = fetchMock.lastCall()!; - const { method, body } = options!; - expect(body).toBeUndefined(); - expect(method).toBe("GET"); - expect(uri).toBe( - "/data?query=query%20SampleQuery%20%7B%0A%20%20stub%20%7B%0A%20%20%20%20id%0A%20%20%7D%0A%7D&operationName=SampleQuery&variables=%7B%7D" - ); - }) + }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + await expect(stream).toComplete(); + + const [uri, options] = fetchMock.lastCall()!; + const { method, body } = options!; + expect(body).toBeUndefined(); + expect(method).toBe("GET"); + expect(uri).toBe( + "/data?query=query%20SampleQuery%20%7B%0A%20%20stub%20%7B%0A%20%20%20%20id%0A%20%20%7D%0A%7D&operationName=SampleQuery&variables=%7B%7D" ); }); - itAsync( - "uses POST for mutations with useGETForQueries", - (resolve, reject) => { - const variables = { params: "stub" }; - const link = createHttpLink({ - uri: "/data", - useGETForQueries: true, - }); + it("uses POST for mutations with useGETForQueries", async () => { + const variables = { params: "stub" }; + const link = createHttpLink({ + uri: "/data", + useGETForQueries: true, + }); - execute(link, { - query: sampleMutation, - variables, - }).subscribe( - makeCallback(resolve, reject, () => { - const [uri, options] = fetchMock.lastCall()!; - const { method, body } = options!; - expect(body).toBeDefined(); - expect(method).toBe("POST"); - expect(uri).toBe("/data"); - }) - ); - } - ); - - itAsync( - "strips unused variables, respecting nested fragments", - (resolve, reject) => { - const link = createHttpLink({ uri: "/data" }); - - const query = gql` - query PEOPLE($declaredAndUsed: String, $declaredButUnused: Int) { - people(surprise: $undeclared, noSurprise: $declaredAndUsed) { - ... on Doctor { - specialty(var: $usedByInlineFragment) - } - ...LawyerFragment + const observable = execute(link, { + query: sampleMutation, + variables, + }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + await expect(stream).toComplete(); + + const [uri, options] = fetchMock.lastCall()!; + const { method, body } = options!; + + expect(body).toBeDefined(); + expect(method).toBe("POST"); + expect(uri).toBe("/data"); + }); + + it("strips unused variables, respecting nested fragments", async () => { + const link = createHttpLink({ uri: "/data" }); + + const query = gql` + query PEOPLE($declaredAndUsed: String, $declaredButUnused: Int) { + people(surprise: $undeclared, noSurprise: $declaredAndUsed) { + ... on Doctor { + specialty(var: $usedByInlineFragment) } + ...LawyerFragment } - fragment LawyerFragment on Lawyer { - caseCount(var: $usedByNamedFragment) - } - `; + } + fragment LawyerFragment on Lawyer { + caseCount(var: $usedByNamedFragment) + } + `; + + const variables = { + unused: "strip", + declaredButUnused: "strip", + declaredAndUsed: "keep", + undeclared: "keep", + usedByInlineFragment: "keep", + usedByNamedFragment: "keep", + }; + + const observable = execute(link, { + query, + variables, + }); + const stream = new ObservableStream(observable); - const variables = { - unused: "strip", - declaredButUnused: "strip", + await expect(stream).toEmitNext(); + await expect(stream).toComplete(); + + const [uri, options] = fetchMock.lastCall()!; + const { method, body } = options!; + + expect(JSON.parse(body as string)).toEqual({ + operationName: "PEOPLE", + query: print(query), + variables: { declaredAndUsed: "keep", undeclared: "keep", usedByInlineFragment: "keep", usedByNamedFragment: "keep", - }; - - execute(link, { - query, - variables, - }).subscribe({ - next: makeCallback(resolve, reject, () => { - const [uri, options] = fetchMock.lastCall()!; - const { method, body } = options!; - expect(JSON.parse(body as string)).toEqual({ - operationName: "PEOPLE", - query: print(query), - variables: { - declaredAndUsed: "keep", - undeclared: "keep", - usedByInlineFragment: "keep", - usedByNamedFragment: "keep", - }, - }); - expect(method).toBe("POST"); - expect(uri).toBe("/data"); - }), - error: (error) => reject(error), - }); - } - ); + }, + }); + expect(method).toBe("POST"); + expect(uri).toBe("/data"); + }); - itAsync( - "should add client awareness settings to request headers", - (resolve, reject) => { - const variables = { params: "stub" }; - const link = createHttpLink({ - uri: "/data", - }); + it("should add client awareness settings to request headers", async () => { + const variables = { params: "stub" }; + const link = createHttpLink({ + uri: "/data", + }); - const clientAwareness = { - name: "Some Client Name", - version: "1.0.1", - }; + const clientAwareness = { + name: "Some Client Name", + version: "1.0.1", + }; - execute(link, { - query: sampleQuery, - variables, - context: { - clientAwareness, - }, - }).subscribe( - makeCallback(resolve, reject, () => { - const [, options] = fetchMock.lastCall()!; - const { headers } = options as any; - expect(headers["apollographql-client-name"]).toBeDefined(); - expect(headers["apollographql-client-name"]).toEqual( - clientAwareness.name - ); - expect(headers["apollographql-client-version"]).toBeDefined(); - expect(headers["apollographql-client-version"]).toEqual( - clientAwareness.version - ); - }) - ); - } - ); + const observable = execute(link, { + query: sampleQuery, + variables, + context: { + clientAwareness, + }, + }); + const stream = new ObservableStream(observable); - itAsync( - "should not add empty client awareness settings to request headers", - (resolve, reject) => { - const variables = { params: "stub" }; - const link = createHttpLink({ - uri: "/data", - }); + await expect(stream).toEmitNext(); + await expect(stream).toComplete(); - const hasOwn = Object.prototype.hasOwnProperty; - const clientAwareness = {}; - execute(link, { - query: sampleQuery, - variables, - context: { - clientAwareness, - }, - }).subscribe( - makeCallback(resolve, reject, () => { - const [, options] = fetchMock.lastCall()!; - const { headers } = options as any; - expect(hasOwn.call(headers, "apollographql-client-name")).toBe( - false - ); - expect(hasOwn.call(headers, "apollographql-client-version")).toBe( - false - ); - }) - ); - } - ); + const [, options] = fetchMock.lastCall()!; + const { headers } = options as any; + expect(headers["apollographql-client-name"]).toBeDefined(); + expect(headers["apollographql-client-name"]).toEqual( + clientAwareness.name + ); + expect(headers["apollographql-client-version"]).toBeDefined(); + expect(headers["apollographql-client-version"]).toEqual( + clientAwareness.version + ); + }); - itAsync( - "throws for GET if the variables can't be stringified", - (resolve, reject) => { - const link = createHttpLink({ - uri: "/data", - useGETForQueries: true, - includeUnusedVariables: true, - }); + it("should not add empty client awareness settings to request headers", async () => { + const variables = { params: "stub" }; + const link = createHttpLink({ + uri: "/data", + }); - let b; - const a: any = { b }; - b = { a }; - a.b = b; - const variables = { - a, - b, - }; - execute(link, { query: sampleQuery, variables }).subscribe( - (result) => { - reject("next should have been thrown from the link"); - }, - makeCallback(resolve, reject, (e: ClientParseError) => { - expect(e.message).toMatch(/Variables map is not serializable/); - expect(e.parseError.message).toMatch( - /Converting circular structure to JSON/ - ); - }) - ); - } - ); + const hasOwn = Object.prototype.hasOwnProperty; + const clientAwareness = {}; + const observable = execute(link, { + query: sampleQuery, + variables, + context: { + clientAwareness, + }, + }); + const stream = new ObservableStream(observable); - itAsync( - "throws for GET if the extensions can't be stringified", - (resolve, reject) => { - const link = createHttpLink({ - uri: "/data", - useGETForQueries: true, - includeExtensions: true, - }); + await expect(stream).toEmitNext(); + await expect(stream).toComplete(); - let b; - const a: any = { b }; - b = { a }; - a.b = b; - const extensions = { - a, - b, - }; - execute(link, { query: sampleQuery, extensions }).subscribe( - (result) => { - reject("next should have been thrown from the link"); - }, - makeCallback(resolve, reject, (e: ClientParseError) => { - expect(e.message).toMatch(/Extensions map is not serializable/); - expect(e.parseError.message).toMatch( - /Converting circular structure to JSON/ - ); - }) - ); - } - ); + const [, options] = fetchMock.lastCall()!; + const { headers } = options as any; + expect(hasOwn.call(headers, "apollographql-client-name")).toBe(false); + expect(hasOwn.call(headers, "apollographql-client-version")).toBe(false); + }); + + it("throws for GET if the variables can't be stringified", async () => { + const link = createHttpLink({ + uri: "/data", + useGETForQueries: true, + includeUnusedVariables: true, + }); + + let b; + const a: any = { b }; + b = { a }; + a.b = b; + const variables = { + a, + b, + }; + const observable = execute(link, { query: sampleQuery, variables }); + const stream = new ObservableStream(observable); + + const error = await stream.takeError(); + + expect(error.message).toMatch(/Variables map is not serializable/); + expect(error.parseError.message).toMatch( + /Converting circular structure to JSON/ + ); + }); + + it("throws for GET if the extensions can't be stringified", async () => { + const link = createHttpLink({ + uri: "/data", + useGETForQueries: true, + includeExtensions: true, + }); + + let b; + const a: any = { b }; + b = { a }; + a.b = b; + const extensions = { + a, + b, + }; + const observable = execute(link, { query: sampleQuery, extensions }); + const stream = new ObservableStream(observable); + + const error = await stream.takeError(); + + expect(error.message).toMatch(/Extensions map is not serializable/); + expect(error.parseError.message).toMatch( + /Converting circular structure to JSON/ + ); + }); it("raises warning if called with concat", () => { const link = createHttpLink(); @@ -494,71 +463,65 @@ describe("HttpLink", () => { expect(() => createHttpLink()).not.toThrow(); }); - itAsync("calls next and then complete", (resolve, reject) => { - const next = jest.fn(); + it("calls next and then complete", async () => { const link = createHttpLink({ uri: "data" }); const observable = execute(link, { query: sampleQuery, }); - observable.subscribe({ - next, - error: (error) => reject(error), - complete: makeCallback(resolve, reject, () => { - expect(next).toHaveBeenCalledTimes(1); - }), - }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + await expect(stream).toComplete(); }); - itAsync("calls error when fetch fails", (resolve, reject) => { + it("calls error when fetch fails", async () => { const link = createHttpLink({ uri: "error" }); const observable = execute(link, { query: sampleQuery, }); - observable.subscribe( - (result) => reject("next should not have been called"), - makeCallback(resolve, reject, (error: TypeError) => { - expect(error).toEqual(mockError.throws); - }), - () => reject("complete should not have been called") - ); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitError(mockError.throws); }); - itAsync("calls error when fetch fails", (resolve, reject) => { + it("calls error when fetch fails", async () => { const link = createHttpLink({ uri: "error" }); const observable = execute(link, { query: sampleMutation, }); - observable.subscribe( - (result) => reject("next should not have been called"), - makeCallback(resolve, reject, (error: TypeError) => { - expect(error).toEqual(mockError.throws); - }), - () => reject("complete should not have been called") - ); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitError(mockError.throws); }); - itAsync("unsubscribes without calling subscriber", (resolve, reject) => { + it("unsubscribes without calling subscriber", async () => { const link = createHttpLink({ uri: "data" }); const observable = execute(link, { query: sampleQuery, }); const subscription = observable.subscribe( - (result) => reject("next should not have been called"), - (error) => reject(error), - () => reject("complete should not have been called") + () => { + throw new Error("next should not have been called"); + }, + (error) => { + throw error; + }, + () => { + throw "complete should not have been called"; + } ); subscription.unsubscribe(); + expect(subscription.closed).toBe(true); - setTimeout(resolve, 50); + + // Ensure none of the callbacks throw after our assertion + await wait(10); }); - const verifyRequest = ( + const verifyRequest = async ( link: ApolloLink, - resolve: () => void, - includeExtensions: boolean, - reject: (error: any) => any + includeExtensions: boolean ) => { - const next = jest.fn(); const context = { info: "stub" }; const variables = { params: "stub" }; @@ -567,57 +530,37 @@ describe("HttpLink", () => { context, variables, }); - observable.subscribe({ - next, - error: (error) => reject(error), - complete: () => { - try { - let body = convertBatchedBody(fetchMock.lastCall()![1]!.body); - expect(body.query).toBe(print(sampleMutation)); - expect(body.variables).toEqual({}); - expect(body.context).not.toBeDefined(); - if (includeExtensions) { - expect(body.extensions).toBeDefined(); - } else { - expect(body.extensions).not.toBeDefined(); - } - expect(next).toHaveBeenCalledTimes(1); - - resolve(); - } catch (e) { - reject(e); - } - }, - }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + await expect(stream).toComplete(); + + let body = convertBatchedBody(fetchMock.lastCall()![1]!.body); + expect(body.query).toBe(print(sampleMutation)); + expect(body.variables).toEqual({}); + expect(body.context).not.toBeDefined(); + if (includeExtensions) { + expect(body.extensions).toBeDefined(); + } else { + expect(body.extensions).not.toBeDefined(); + } }; - itAsync( - "passes all arguments to multiple fetch body including extensions", - (resolve, reject) => { - const link = createHttpLink({ uri: "data", includeExtensions: true }); - verifyRequest( - link, - () => verifyRequest(link, resolve, true, reject), - true, - reject - ); - } - ); + it("passes all arguments to multiple fetch body including extensions", async () => { + const link = createHttpLink({ uri: "data", includeExtensions: true }); - itAsync( - "passes all arguments to multiple fetch body excluding extensions", - (resolve, reject) => { - const link = createHttpLink({ uri: "data" }); - verifyRequest( - link, - () => verifyRequest(link, resolve, false, reject), - false, - reject - ); - } - ); + await verifyRequest(link, true); + await verifyRequest(link, true); + }); + + it("passes all arguments to multiple fetch body excluding extensions", async () => { + const link = createHttpLink({ uri: "data" }); + + await verifyRequest(link, false); + await verifyRequest(link, false); + }); - itAsync("calls multiple subscribers", (resolve, reject) => { + it("calls multiple subscribers", async () => { const link = createHttpLink({ uri: "data" }); const context = { info: "stub" }; const variables = { params: "stub" }; @@ -630,159 +573,144 @@ describe("HttpLink", () => { observable.subscribe(subscriber); observable.subscribe(subscriber); - setTimeout(() => { - expect(subscriber.next).toHaveBeenCalledTimes(2); - expect(subscriber.complete).toHaveBeenCalledTimes(2); - expect(subscriber.error).not.toHaveBeenCalled(); - expect(fetchMock.calls().length).toBe(2); - resolve(); - }, 50); - }); - - itAsync( - "calls remaining subscribers after unsubscribe", - (resolve, reject) => { - const link = createHttpLink({ uri: "data" }); - const context = { info: "stub" }; - const variables = { params: "stub" }; - - const observable = execute(link, { - query: sampleMutation, - context, - variables, - }); + await wait(50); - observable.subscribe(subscriber); + expect(subscriber.next).toHaveBeenCalledTimes(2); + expect(subscriber.complete).toHaveBeenCalledTimes(2); + expect(subscriber.error).not.toHaveBeenCalled(); + expect(fetchMock.calls().length).toBe(2); + }); - setTimeout(() => { - const subscription = observable.subscribe(subscriber); - subscription.unsubscribe(); - }, 10); + it("calls remaining subscribers after unsubscribe", async () => { + const link = createHttpLink({ uri: "data" }); + const context = { info: "stub" }; + const variables = { params: "stub" }; - setTimeout( - makeCallback(resolve, reject, () => { - expect(subscriber.next).toHaveBeenCalledTimes(1); - expect(subscriber.complete).toHaveBeenCalledTimes(1); - expect(subscriber.error).not.toHaveBeenCalled(); - resolve(); - }), - 50 - ); - } - ); + const observable = execute(link, { + query: sampleMutation, + context, + variables, + }); + + observable.subscribe(subscriber); + + await wait(10); + + const subscription = observable.subscribe(subscriber); + subscription.unsubscribe(); + + await wait(50); - itAsync("allows for dynamic endpoint setting", (resolve, reject) => { + expect(subscriber.next).toHaveBeenCalledTimes(1); + expect(subscriber.complete).toHaveBeenCalledTimes(1); + expect(subscriber.error).not.toHaveBeenCalled(); + }); + + it("allows for dynamic endpoint setting", async () => { const variables = { params: "stub" }; const link = createHttpLink({ uri: "data" }); - execute(link, { + const observable = execute(link, { query: sampleQuery, variables, context: { uri: "data2" }, - }).subscribe((result) => { - expect(result).toEqual(data2); - resolve(); }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue(data2); }); - itAsync( - "adds headers to the request from the context", - (resolve, reject) => { - const variables = { params: "stub" }; - const middleware = new ApolloLink((operation, forward) => { - operation.setContext({ - headers: { authorization: "1234" }, - }); - return forward(operation).map((result) => { - const { headers } = operation.getContext(); - try { - expect(headers).toBeDefined(); - } catch (e) { - reject(e); - } - return result; - }); + it("adds headers to the request from the context", async () => { + const variables = { params: "stub" }; + const middleware = new ApolloLink((operation, forward) => { + operation.setContext({ + headers: { authorization: "1234" }, }); - const link = middleware.concat(createHttpLink({ uri: "data" })); - - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, () => { - const headers = fetchMock.lastCall()![1]!.headers as any; - expect(headers.authorization).toBe("1234"); - expect(headers["content-type"]).toBe("application/json"); - expect(headers.accept).toBe("*/*"); - }) - ); - } - ); + return forward(operation).map((result) => { + const { headers } = operation.getContext(); + expect(headers).toBeDefined(); - itAsync("adds headers to the request from the setup", (resolve, reject) => { + return result; + }); + }); + const link = middleware.concat(createHttpLink({ uri: "data" })); + + const observable = execute(link, { query: sampleQuery, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + + const headers = fetchMock.lastCall()![1]!.headers as any; + + expect(headers.authorization).toBe("1234"); + expect(headers["content-type"]).toBe("application/json"); + expect(headers.accept).toBe("*/*"); + }); + + it("adds headers to the request from the setup", async () => { const variables = { params: "stub" }; const link = createHttpLink({ uri: "data", headers: { authorization: "1234" }, }); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, () => { - const headers = fetchMock.lastCall()![1]!.headers as any; - expect(headers.authorization).toBe("1234"); - expect(headers["content-type"]).toBe("application/json"); - expect(headers.accept).toBe("*/*"); - }) - ); + const observable = execute(link, { query: sampleQuery, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + + const headers = fetchMock.lastCall()![1]!.headers as any; + expect(headers.authorization).toBe("1234"); + expect(headers["content-type"]).toBe("application/json"); + expect(headers.accept).toBe("*/*"); }); - itAsync( - "prioritizes context headers over setup headers", - (resolve, reject) => { - const variables = { params: "stub" }; - const middleware = new ApolloLink((operation, forward) => { - operation.setContext({ - headers: { authorization: "1234" }, - }); - return forward(operation); + it("prioritizes context headers over setup headers", async () => { + const variables = { params: "stub" }; + const middleware = new ApolloLink((operation, forward) => { + operation.setContext({ + headers: { authorization: "1234" }, }); - const link = middleware.concat( - createHttpLink({ uri: "data", headers: { authorization: "no user" } }) - ); + return forward(operation); + }); + const link = middleware.concat( + createHttpLink({ uri: "data", headers: { authorization: "no user" } }) + ); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, () => { - const headers = fetchMock.lastCall()![1]!.headers as any; - expect(headers.authorization).toBe("1234"); - expect(headers["content-type"]).toBe("application/json"); - expect(headers.accept).toBe("*/*"); - }) - ); - } - ); + const observable = execute(link, { query: sampleQuery, variables }); + const stream = new ObservableStream(observable); - itAsync( - "adds headers to the request from the context on an operation", - (resolve, reject) => { - const variables = { params: "stub" }; - const link = createHttpLink({ uri: "data" }); + await expect(stream).toEmitNext(); - const context = { - headers: { authorization: "1234" }, - }; - execute(link, { - query: sampleQuery, - variables, - context, - }).subscribe( - makeCallback(resolve, reject, () => { - const headers = fetchMock.lastCall()![1]!.headers as any; - expect(headers.authorization).toBe("1234"); - expect(headers["content-type"]).toBe("application/json"); - expect(headers.accept).toBe("*/*"); - }) - ); - } - ); + const headers = fetchMock.lastCall()![1]!.headers as any; + expect(headers.authorization).toBe("1234"); + expect(headers["content-type"]).toBe("application/json"); + expect(headers.accept).toBe("*/*"); + }); - itAsync("adds creds to the request from the context", (resolve, reject) => { + it("adds headers to the request from the context on an operation", async () => { + const variables = { params: "stub" }; + const link = createHttpLink({ uri: "data" }); + + const context = { + headers: { authorization: "1234" }, + }; + const observable = execute(link, { + query: sampleQuery, + variables, + context, + }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + + const headers = fetchMock.lastCall()![1]!.headers as any; + expect(headers.authorization).toBe("1234"); + expect(headers["content-type"]).toBe("application/json"); + expect(headers.accept).toBe("*/*"); + }); + + it("adds creds to the request from the context", async () => { const variables = { params: "stub" }; const middleware = new ApolloLink((operation, forward) => { operation.setContext({ @@ -792,50 +720,50 @@ describe("HttpLink", () => { }); const link = middleware.concat(createHttpLink({ uri: "data" })); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, () => { - const creds = fetchMock.lastCall()![1]!.credentials; - expect(creds).toBe("same-team-yo"); - }) - ); + const observable = execute(link, { query: sampleQuery, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + + const creds = fetchMock.lastCall()![1]!.credentials; + expect(creds).toBe("same-team-yo"); }); - itAsync("adds creds to the request from the setup", (resolve, reject) => { + it("adds creds to the request from the setup", async () => { const variables = { params: "stub" }; const link = createHttpLink({ uri: "data", credentials: "same-team-yo" }); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, () => { - const creds = fetchMock.lastCall()![1]!.credentials; - expect(creds).toBe("same-team-yo"); - }) - ); + const observable = execute(link, { query: sampleQuery, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + + const creds = fetchMock.lastCall()![1]!.credentials; + expect(creds).toBe("same-team-yo"); }); - itAsync( - "prioritizes creds from the context over the setup", - (resolve, reject) => { - const variables = { params: "stub" }; - const middleware = new ApolloLink((operation, forward) => { - operation.setContext({ - credentials: "same-team-yo", - }); - return forward(operation); + it("prioritizes creds from the context over the setup", async () => { + const variables = { params: "stub" }; + const middleware = new ApolloLink((operation, forward) => { + operation.setContext({ + credentials: "same-team-yo", }); - const link = middleware.concat( - createHttpLink({ uri: "data", credentials: "error" }) - ); + return forward(operation); + }); + const link = middleware.concat( + createHttpLink({ uri: "data", credentials: "error" }) + ); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, () => { - const creds = fetchMock.lastCall()![1]!.credentials; - expect(creds).toBe("same-team-yo"); - }) - ); - } - ); + const observable = execute(link, { query: sampleQuery, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); - itAsync("adds uri to the request from the context", (resolve, reject) => { + const creds = fetchMock.lastCall()![1]!.credentials; + expect(creds).toBe("same-team-yo"); + }); + + it("adds uri to the request from the context", async () => { const variables = { params: "stub" }; const middleware = new ApolloLink((operation, forward) => { operation.setContext({ @@ -845,27 +773,29 @@ describe("HttpLink", () => { }); const link = middleware.concat(createHttpLink()); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, () => { - const uri = fetchMock.lastUrl(); - expect(uri).toBe("/data"); - }) - ); + const observable = execute(link, { query: sampleQuery, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + + const uri = fetchMock.lastUrl(); + expect(uri).toBe("/data"); }); - itAsync("adds uri to the request from the setup", (resolve, reject) => { + it("adds uri to the request from the setup", async () => { const variables = { params: "stub" }; const link = createHttpLink({ uri: "data" }); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, () => { - const uri = fetchMock.lastUrl(); - expect(uri).toBe("/data"); - }) - ); + const observable = execute(link, { query: sampleQuery, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + + const uri = fetchMock.lastUrl(); + expect(uri).toBe("/data"); }); - itAsync("prioritizes context uri over setup uri", (resolve, reject) => { + it("prioritizes context uri over setup uri", async () => { const variables = { params: "stub" }; const middleware = new ApolloLink((operation, forward) => { operation.setContext({ @@ -877,168 +807,139 @@ describe("HttpLink", () => { createHttpLink({ uri: "data", credentials: "error" }) ); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, () => { - const uri = fetchMock.lastUrl(); + const observable = execute(link, { query: sampleQuery, variables }); + const stream = new ObservableStream(observable); - expect(uri).toBe("/apollo"); - }) - ); + await expect(stream).toEmitNext(); + + const uri = fetchMock.lastUrl(); + expect(uri).toBe("/apollo"); }); - itAsync("allows uri to be a function", (resolve, reject) => { + it("allows uri to be a function", async () => { const variables = { params: "stub" }; const customFetch: typeof fetch = (uri, options) => { const { operationName } = convertBatchedBody(options!.body); - try { - expect(operationName).toBe("SampleQuery"); - } catch (e) { - reject(e); - } + expect(operationName).toBe("SampleQuery"); + return fetch("dataFunc", options); }; const link = createHttpLink({ fetch: customFetch }); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, () => { - expect(fetchMock.lastUrl()).toBe("/dataFunc"); - }) - ); + const observable = execute(link, { query: sampleQuery, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + + expect(fetchMock.lastUrl()).toBe("/dataFunc"); }); - itAsync( - "adds fetchOptions to the request from the setup", - (resolve, reject) => { - const variables = { params: "stub" }; - const link = createHttpLink({ - uri: "data", - fetchOptions: { someOption: "foo", mode: "no-cors" }, - }); + it("adds fetchOptions to the request from the setup", async () => { + const variables = { params: "stub" }; + const link = createHttpLink({ + uri: "data", + fetchOptions: { someOption: "foo", mode: "no-cors" }, + }); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, () => { - const { someOption, mode, headers } = - fetchMock.lastCall()![1] as any; - expect(someOption).toBe("foo"); - expect(mode).toBe("no-cors"); - expect(headers["content-type"]).toBe("application/json"); - }) - ); - } - ); - - itAsync( - "adds fetchOptions to the request from the context", - (resolve, reject) => { - const variables = { params: "stub" }; - const middleware = new ApolloLink((operation, forward) => { - operation.setContext({ - fetchOptions: { - someOption: "foo", - }, - }); - return forward(operation); + const observable = execute(link, { query: sampleQuery, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + + const { someOption, mode, headers } = fetchMock.lastCall()![1] as any; + expect(someOption).toBe("foo"); + expect(mode).toBe("no-cors"); + expect(headers["content-type"]).toBe("application/json"); + }); + + it("adds fetchOptions to the request from the context", async () => { + const variables = { params: "stub" }; + const middleware = new ApolloLink((operation, forward) => { + operation.setContext({ + fetchOptions: { + someOption: "foo", + }, }); - const link = middleware.concat(createHttpLink({ uri: "data" })); + return forward(operation); + }); + const link = middleware.concat(createHttpLink({ uri: "data" })); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, () => { - const { someOption } = fetchMock.lastCall()![1] as any; - expect(someOption).toBe("foo"); - resolve(); - }) - ); - } - ); - - itAsync( - "uses the latest window.fetch function if options.fetch not configured", - (resolve, reject) => { - const httpLink = createHttpLink({ uri: "data" }); - - const fetch = window.fetch; - expect(typeof fetch).toBe("function"); - - const fetchSpy = jest.spyOn(window, "fetch"); - fetchSpy.mockImplementation(() => - Promise.resolve({ - text() { - return Promise.resolve( - JSON.stringify({ - data: { hello: "from spy" }, - }) - ); - }, - } as Response) - ); + const observable = execute(link, { query: sampleQuery, variables }); + const stream = new ObservableStream(observable); - const spyFn = window.fetch; - expect(spyFn).not.toBe(fetch); + await expect(stream).toEmitNext(); - subscriptions.add( - execute(httpLink, { - query: sampleQuery, - }).subscribe({ - error: reject, + const { someOption } = fetchMock.lastCall()![1] as any; + expect(someOption).toBe("foo"); + }); - next(result) { - expect(fetchSpy).toHaveBeenCalledTimes(1); - expect(result).toEqual({ - data: { hello: "from spy" }, - }); - - fetchSpy.mockRestore(); - expect(window.fetch).toBe(fetch); - - subscriptions.add( - execute(httpLink, { - query: sampleQuery, - }).subscribe({ - error: reject, - next(result) { - expect(result).toEqual({ - data: { hello: "world" }, - }); - resolve(); - }, - }) - ); - }, - }) - ); - } - ); - - itAsync( - "uses the print option function when defined", - (resolve, reject) => { - const customPrinter = jest.fn( - (ast: ASTNode, originalPrint: typeof print) => { - return stripIgnoredCharacters(originalPrint(ast)); - } - ); + it("uses the latest window.fetch function if options.fetch not configured", async () => { + const httpLink = createHttpLink({ uri: "data" }); - const httpLink = createHttpLink({ uri: "data", print: customPrinter }); + const fetch = window.fetch; + expect(typeof fetch).toBe("function"); - execute(httpLink, { - query: sampleQuery, - context: { - fetchOptions: { method: "GET" }, - }, - }).subscribe( - makeCallback(resolve, reject, () => { - expect(customPrinter).toHaveBeenCalledTimes(1); - const [uri] = fetchMock.lastCall()!; - expect(uri).toBe( - "/data?query=query%20SampleQuery%7Bstub%7Bid%7D%7D&operationName=SampleQuery&variables=%7B%7D" + const fetchSpy = jest.spyOn(window, "fetch"); + fetchSpy.mockImplementation(() => + Promise.resolve({ + text() { + return Promise.resolve( + JSON.stringify({ + data: { hello: "from spy" }, + }) ); - }) - ); - } - ); + }, + } as Response) + ); + + const spyFn = window.fetch; + expect(spyFn).not.toBe(fetch); + + const stream = new ObservableStream( + execute(httpLink, { query: sampleQuery }) + ); + + await expect(stream).toEmitValue({ data: { hello: "from spy" } }); + expect(fetchSpy).toHaveBeenCalledTimes(1); + + fetchSpy.mockRestore(); + expect(window.fetch).toBe(fetch); + + const stream2 = new ObservableStream( + execute(httpLink, { query: sampleQuery }) + ); + + await expect(stream2).toEmitValue({ data: { hello: "world" } }); + }); + + it("uses the print option function when defined", async () => { + const customPrinter = jest.fn( + (ast: ASTNode, originalPrint: typeof print) => { + return stripIgnoredCharacters(originalPrint(ast)); + } + ); + + const httpLink = createHttpLink({ uri: "data", print: customPrinter }); + + const observable = execute(httpLink, { + query: sampleQuery, + context: { + fetchOptions: { method: "GET" }, + }, + }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); - itAsync("prioritizes context over setup", (resolve, reject) => { + expect(customPrinter).toHaveBeenCalledTimes(1); + const [uri] = fetchMock.lastCall()!; + expect(uri).toBe( + "/data?query=query%20SampleQuery%7Bstub%7Bid%7D%7D&operationName=SampleQuery&variables=%7B%7D" + ); + }); + + it("prioritizes context over setup", async () => { const variables = { params: "stub" }; const middleware = new ApolloLink((operation, forward) => { operation.setContext({ @@ -1052,55 +953,53 @@ describe("HttpLink", () => { createHttpLink({ uri: "data", fetchOptions: { someOption: "bar" } }) ); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, () => { - const { someOption } = fetchMock.lastCall()![1] as any; - expect(someOption).toBe("foo"); - }) - ); + const observable = execute(link, { query: sampleQuery, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + + const { someOption } = fetchMock.lastCall()![1] as any; + expect(someOption).toBe("foo"); }); - itAsync( - "allows for not sending the query with the request", - (resolve, reject) => { - const variables = { params: "stub" }; - const middleware = new ApolloLink((operation, forward) => { - operation.setContext({ - http: { - includeQuery: false, - includeExtensions: true, - }, - }); - operation.extensions.persistedQuery = { hash: "1234" }; - return forward(operation); + it("allows for not sending the query with the request", async () => { + const variables = { params: "stub" }; + const middleware = new ApolloLink((operation, forward) => { + operation.setContext({ + http: { + includeQuery: false, + includeExtensions: true, + }, }); - const link = middleware.concat(createHttpLink({ uri: "data" })); + operation.extensions.persistedQuery = { hash: "1234" }; + return forward(operation); + }); + const link = middleware.concat(createHttpLink({ uri: "data" })); - execute(link, { query: sampleQuery, variables }).subscribe( - makeCallback(resolve, reject, () => { - let body = convertBatchedBody(fetchMock.lastCall()![1]!.body); + const observable = execute(link, { query: sampleQuery, variables }); + const stream = new ObservableStream(observable); - expect(body.query).not.toBeDefined(); - expect(body.extensions).toEqual({ - persistedQuery: { hash: "1234" }, - }); - resolve(); - }) - ); - } - ); + await expect(stream).toEmitNext(); + + let body = convertBatchedBody(fetchMock.lastCall()![1]!.body); - itAsync("sets the raw response on context", (resolve, reject) => { + expect(body.query).not.toBeDefined(); + expect(body.extensions).toEqual({ + persistedQuery: { hash: "1234" }, + }); + }); + + it("sets the raw response on context", async () => { const middleware = new ApolloLink((operation, forward) => { return new Observable((ob) => { const op = forward(operation); const sub = op.subscribe({ next: ob.next.bind(ob), error: ob.error.bind(ob), - complete: makeCallback(resolve, reject, () => { + complete: () => { expect(operation.getContext().response.headers.toBeDefined); ob.complete(); - }), + }, }); return () => { @@ -1111,12 +1010,11 @@ describe("HttpLink", () => { const link = middleware.concat(createHttpLink({ uri: "data", fetch })); - execute(link, { query: sampleQuery }).subscribe( - (result) => { - resolve(); - }, - () => {} - ); + const observable = execute(link, { query: sampleQuery }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + await expect(stream).toComplete(); }); it("removes @client fields from the query before sending it to the server", async () => { @@ -1195,26 +1093,22 @@ describe("HttpLink", () => { describe("Dev warnings", () => { voidFetchDuringEachTest(); - itAsync("warns if fetch is undeclared", (resolve, reject) => { + it("warns if fetch is undeclared", async () => { try { createHttpLink({ uri: "data" }); - reject("warning wasn't called"); + throw new Error("warning wasn't called"); } catch (e) { - makeCallback(resolve, reject, () => - expect((e as Error).message).toMatch(/has not been found globally/) - )(); + expect((e as Error).message).toMatch(/has not been found globally/); } }); - itAsync("warns if fetch is undefined", (resolve, reject) => { + it("warns if fetch is undefined", async () => { window.fetch = undefined as any; try { createHttpLink({ uri: "data" }); - reject("warning wasn't called"); + throw new Error("warning wasn't called"); } catch (e) { - makeCallback(resolve, reject, () => - expect((e as Error).message).toMatch(/has not been found globally/) - )(); + expect((e as Error).message).toMatch(/has not been found globally/); } }); @@ -1259,18 +1153,18 @@ describe("HttpLink", () => { beforeEach(() => { fetch.mockReset(); }); - itAsync("makes it easy to do stuff on a 401", (resolve, reject) => { + it("makes it easy to do stuff on a 401", async () => { const middleware = new ApolloLink((operation, forward) => { return new Observable((ob) => { fetch.mockReturnValueOnce(Promise.resolve({ status: 401, text })); const op = forward(operation); const sub = op.subscribe({ next: ob.next.bind(ob), - error: makeCallback(resolve, reject, (e: ServerError) => { + error: (e: ServerError) => { expect(e.message).toMatch(/Received status code 401/); expect(e.statusCode).toEqual(401); ob.error(e); - }), + }, complete: ob.complete.bind(ob), }); @@ -1284,115 +1178,94 @@ describe("HttpLink", () => { createHttpLink({ uri: "data", fetch: fetch as any }) ); - execute(link, { query: sampleQuery }).subscribe( - (result) => { - reject("next should have been thrown from the network"); - }, - () => {} - ); + const observable = execute(link, { query: sampleQuery }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitError(); }); - itAsync("throws an error if response code is > 300", (resolve, reject) => { + it("throws an error if response code is > 300", async () => { fetch.mockReturnValueOnce(Promise.resolve({ status: 400, text })); const link = createHttpLink({ uri: "data", fetch: fetch as any }); - execute(link, { query: sampleQuery }).subscribe( - (result) => { - reject("next should have been thrown from the network"); - }, - makeCallback(resolve, reject, (e: ServerError) => { - expect(e.message).toMatch(/Received status code 400/); - expect(e.statusCode).toBe(400); - expect(e.result).toEqual(responseBody); - }) + const observable = execute(link, { query: sampleQuery }); + const stream = new ObservableStream(observable); + + const error: ServerError = await stream.takeError(); + + expect(error.message).toMatch(/Received status code 400/); + expect(error.statusCode).toBe(400); + expect(error.result).toEqual(responseBody); + }); + + it("throws an error if response code is > 300 and handles string response body", async () => { + fetch.mockReturnValueOnce( + Promise.resolve({ status: 302, text: textWithStringError }) ); + const link = createHttpLink({ uri: "data", fetch: fetch as any }); + const observable = execute(link, { query: sampleQuery }); + const stream = new ObservableStream(observable); + + const error: ServerError = await stream.takeError(); + + expect(error.message).toMatch(/Received status code 302/); + expect(error.statusCode).toBe(302); + expect(error.result).toEqual(responseBody); }); - itAsync( - "throws an error if response code is > 300 and handles string response body", - (resolve, reject) => { - fetch.mockReturnValueOnce( - Promise.resolve({ status: 302, text: textWithStringError }) - ); - const link = createHttpLink({ uri: "data", fetch: fetch as any }); - execute(link, { query: sampleQuery }).subscribe( - (result) => { - reject("next should have been thrown from the network"); - }, - makeCallback(resolve, reject, (e: ServerError) => { - expect(e.message).toMatch(/Received status code 302/); - expect(e.statusCode).toBe(302); - expect(e.result).toEqual(responseBody); - }) - ); - } - ); - itAsync( - "throws an error if response code is > 300 and returns data", - (resolve, reject) => { - fetch.mockReturnValueOnce( - Promise.resolve({ status: 400, text: textWithData }) - ); - const link = createHttpLink({ uri: "data", fetch: fetch as any }); + it("throws an error if response code is > 300 and returns data", async () => { + fetch.mockReturnValueOnce( + Promise.resolve({ status: 400, text: textWithData }) + ); - let called = false; + const link = createHttpLink({ uri: "data", fetch: fetch as any }); - execute(link, { query: sampleQuery }).subscribe( - (result) => { - called = true; - expect(result).toEqual(responseBody); - }, - (e) => { - expect(called).toBe(true); - expect(e.message).toMatch(/Received status code 400/); - expect(e.statusCode).toBe(400); - expect(e.result).toEqual(responseBody); - resolve(); - } - ); - } - ); - itAsync( - "throws an error if only errors are returned", - (resolve, reject) => { - fetch.mockReturnValueOnce( - Promise.resolve({ status: 400, text: textWithErrors }) - ); + const observable = execute(link, { query: sampleQuery }); + const stream = new ObservableStream(observable); - const link = createHttpLink({ uri: "data", fetch: fetch as any }); - execute(link, { query: sampleQuery }).subscribe( - (result) => { - reject("should not have called result because we have no data"); - }, - (e) => { - expect(e.message).toMatch(/Received status code 400/); - expect(e.statusCode).toBe(400); - expect(e.result).toEqual(responseBody); - resolve(); - } - ); - } - ); - itAsync( - "throws an error if empty response from the server ", - (resolve, reject) => { - fetch.mockReturnValueOnce(Promise.resolve({ text })); - text.mockReturnValueOnce(Promise.resolve('{ "body": "boo" }')); - const link = createHttpLink({ uri: "data", fetch: fetch as any }); + const result = await stream.takeNext(); - execute(link, { query: sampleQuery }).subscribe( - (result) => { - reject("next should have been thrown from the network"); - }, - makeCallback(resolve, reject, (e: Error) => { - expect(e.message).toMatch( - /Server response was missing for query 'SampleQuery'/ - ); - }) - ); - } - ); - itAsync("throws if the body can't be stringified", (resolve, reject) => { + expect(result).toEqual(responseBody); + + const error = await stream.takeError(); + + expect(error.message).toMatch(/Received status code 400/); + expect(error.statusCode).toBe(400); + expect(error.result).toEqual(responseBody); + }); + + it("throws an error if only errors are returned", async () => { + fetch.mockReturnValueOnce( + Promise.resolve({ status: 400, text: textWithErrors }) + ); + + const link = createHttpLink({ uri: "data", fetch: fetch as any }); + const observable = execute(link, { query: sampleQuery }); + const stream = new ObservableStream(observable); + + const error = await stream.takeError(); + + expect(error.message).toMatch(/Received status code 400/); + expect(error.statusCode).toBe(400); + expect(error.result).toEqual(responseBody); + }); + + it("throws an error if empty response from the server ", async () => { + fetch.mockReturnValueOnce(Promise.resolve({ text })); + text.mockReturnValueOnce(Promise.resolve('{ "body": "boo" }')); + const link = createHttpLink({ uri: "data", fetch: fetch as any }); + + const observable = execute(link, { query: sampleQuery }); + const stream = new ObservableStream(observable); + + const error = await stream.takeError(); + + expect(error.message).toMatch( + /Server response was missing for query 'SampleQuery'/ + ); + }); + + it("throws if the body can't be stringified", async () => { fetch.mockReturnValueOnce(Promise.resolve({ data: {}, text })); const link = createHttpLink({ uri: "data", @@ -1408,16 +1281,14 @@ describe("HttpLink", () => { a, b, }; - execute(link, { query: sampleQuery, variables }).subscribe( - (result) => { - reject("next should have been thrown from the link"); - }, - makeCallback(resolve, reject, (e: ClientParseError) => { - expect(e.message).toMatch(/Payload is not serializable/); - expect(e.parseError.message).toMatch( - /Converting circular structure to JSON/ - ); - }) + const observable = execute(link, { query: sampleQuery, variables }); + const stream = new ObservableStream(observable); + + const error: ClientParseError = await stream.takeError(); + + expect(error.message).toMatch(/Payload is not serializable/); + expect(error.parseError.message).toMatch( + /Converting circular structure to JSON/ ); }); @@ -1563,51 +1434,41 @@ describe("HttpLink", () => { const body = "{"; const unparsableJson = jest.fn(() => Promise.resolve(body)); - itAsync( - "throws a Server error if response is > 300 with unparsable json", - (resolve, reject) => { - fetch.mockReturnValueOnce( - Promise.resolve({ status: 400, text: unparsableJson }) - ); - const link = createHttpLink({ uri: "data", fetch: fetch as any }); + it("throws a Server error if response is > 300 with unparsable json", async () => { + fetch.mockReturnValueOnce( + Promise.resolve({ status: 400, text: unparsableJson }) + ); + const link = createHttpLink({ uri: "data", fetch: fetch as any }); - execute(link, { query: sampleQuery }).subscribe( - (result) => { - reject("next should have been thrown from the network"); - }, - makeCallback(resolve, reject, (e: ServerParseError) => { - expect(e.message).toMatch( - "Response not successful: Received status code 400" - ); - expect(e.statusCode).toBe(400); - expect(e.response).toBeDefined(); - expect(e.bodyText).toBe(undefined); - }) - ); - } - ); + const observable = execute(link, { query: sampleQuery }); + const stream = new ObservableStream(observable); - itAsync( - "throws a ServerParse error if response is 200 with unparsable json", - (resolve, reject) => { - fetch.mockReturnValueOnce( - Promise.resolve({ status: 200, text: unparsableJson }) - ); - const link = createHttpLink({ uri: "data", fetch: fetch as any }); + const error: ServerParseError = await stream.takeError(); - execute(link, { query: sampleQuery }).subscribe( - (result) => { - reject("next should have been thrown from the network"); - }, - makeCallback(resolve, reject, (e: ServerParseError) => { - expect(e.message).toMatch(/JSON/); - expect(e.statusCode).toBe(200); - expect(e.response).toBeDefined(); - expect(e.bodyText).toBe(body); - }) - ); - } - ); + expect(error.message).toMatch( + "Response not successful: Received status code 400" + ); + expect(error.statusCode).toBe(400); + expect(error.response).toBeDefined(); + expect(error.bodyText).toBe(undefined); + }); + + it("throws a ServerParse error if response is 200 with unparsable json", async () => { + fetch.mockReturnValueOnce( + Promise.resolve({ status: 200, text: unparsableJson }) + ); + const link = createHttpLink({ uri: "data", fetch: fetch as any }); + + const observable = execute(link, { query: sampleQuery }); + const stream = new ObservableStream(observable); + + const error: ServerParseError = await stream.takeError(); + + expect(error.message).toMatch(/JSON/); + expect(error.statusCode).toBe(200); + expect(error.response).toBeDefined(); + expect(error.bodyText).toBe(body); + }); }); describe("Multipart responses", () => { @@ -1854,72 +1715,63 @@ describe("HttpLink", () => { ); }); - itAsync( - "sets correct accept header on request with deferred query", - (resolve, reject) => { - const stream = Readable.from( - body.split("\r\n").map((line) => line + "\r\n") - ); - const fetch = jest.fn(async () => ({ - status: 200, - body: stream, - headers: new Headers({ "Content-Type": "multipart/mixed" }), - })); - const link = new HttpLink({ - fetch: fetch as any, - }); - execute(link, { - query: sampleDeferredQuery, - }).subscribe( - makeCallback(resolve, reject, () => { - expect(fetch).toHaveBeenCalledWith( - "/graphql", - expect.objectContaining({ - headers: { - "content-type": "application/json", - accept: - "multipart/mixed;deferSpec=20220824,application/json", - }, - }) - ); - }) - ); - } - ); + it("sets correct accept header on request with deferred query", async () => { + const stream = Readable.from( + body.split("\r\n").map((line) => line + "\r\n") + ); + const fetch = jest.fn(async () => ({ + status: 200, + body: stream, + headers: new Headers({ "Content-Type": "multipart/mixed" }), + })); + const link = new HttpLink({ + fetch: fetch as any, + }); + const observable = execute(link, { query: sampleDeferredQuery }); + const observableStream = new ObservableStream(observable); + + await expect(observableStream).toEmitNext(); + + expect(fetch).toHaveBeenCalledWith( + "/graphql", + expect.objectContaining({ + headers: { + "content-type": "application/json", + accept: "multipart/mixed;deferSpec=20220824,application/json", + }, + }) + ); + }); // ensure that custom directives beginning with '@defer..' do not trigger // custom accept header for multipart responses - itAsync( - "sets does not set accept header on query with custom directive begging with @defer", - (resolve, reject) => { - const stream = Readable.from( - body.split("\r\n").map((line) => line + "\r\n") - ); - const fetch = jest.fn(async () => ({ - status: 200, - body: stream, - headers: new Headers({ "Content-Type": "multipart/mixed" }), - })); - const link = new HttpLink({ - fetch: fetch as any, - }); - execute(link, { - query: sampleQueryCustomDirective, - }).subscribe( - makeCallback(resolve, reject, () => { - expect(fetch).toHaveBeenCalledWith( - "/graphql", - expect.objectContaining({ - headers: { - accept: "*/*", - "content-type": "application/json", - }, - }) - ); - }) - ); - } - ); + it("sets does not set accept header on query with custom directive begging with @defer", async () => { + const stream = Readable.from( + body.split("\r\n").map((line) => line + "\r\n") + ); + const fetch = jest.fn(async () => ({ + status: 200, + body: stream, + headers: new Headers({ "Content-Type": "multipart/mixed" }), + })); + const link = new HttpLink({ + fetch: fetch as any, + }); + const observable = execute(link, { query: sampleQueryCustomDirective }); + const observableStream = new ObservableStream(observable); + + await expect(observableStream).toEmitNext(); + + expect(fetch).toHaveBeenCalledWith( + "/graphql", + expect.objectContaining({ + headers: { + accept: "*/*", + "content-type": "application/json", + }, + }) + ); + }); }); describe("subscriptions", () => { @@ -2194,38 +2046,34 @@ describe("HttpLink", () => { ); }); - itAsync( - "sets correct accept header on request with subscription", - (resolve, reject) => { - const stream = Readable.from( - subscriptionsBody.split("\r\n").map((line) => line + "\r\n") - ); - const fetch = jest.fn(async () => ({ - status: 200, - body: stream, - headers: new Headers({ "Content-Type": "multipart/mixed" }), - })); - const link = new HttpLink({ - fetch: fetch as any, - }); - execute(link, { - query: sampleSubscription, - }).subscribe( - makeCallback(resolve, reject, () => { - expect(fetch).toHaveBeenCalledWith( - "/graphql", - expect.objectContaining({ - headers: { - "content-type": "application/json", - accept: - "multipart/mixed;boundary=graphql;subscriptionSpec=1.0,application/json", - }, - }) - ); - }) - ); - } - ); + it("sets correct accept header on request with subscription", async () => { + const stream = Readable.from( + subscriptionsBody.split("\r\n").map((line) => line + "\r\n") + ); + const fetch = jest.fn(async () => ({ + status: 200, + body: stream, + headers: new Headers({ "Content-Type": "multipart/mixed" }), + })); + const link = new HttpLink({ + fetch: fetch as any, + }); + const observable = execute(link, { query: sampleSubscription }); + const observableStream = new ObservableStream(observable); + + await expect(observableStream).toEmitNext(); + + expect(fetch).toHaveBeenCalledWith( + "/graphql", + expect.objectContaining({ + headers: { + "content-type": "application/json", + accept: + "multipart/mixed;boundary=graphql;subscriptionSpec=1.0,application/json", + }, + }) + ); + }); }); }); }); diff --git a/src/link/persisted-queries/__tests__/persisted-queries.test.ts b/src/link/persisted-queries/__tests__/persisted-queries.test.ts index 7b4fecaf99f..84ce1840b81 100644 --- a/src/link/persisted-queries/__tests__/persisted-queries.test.ts +++ b/src/link/persisted-queries/__tests__/persisted-queries.test.ts @@ -9,8 +9,9 @@ import { Observable } from "../../../utilities"; import { createHttpLink } from "../../http/createHttpLink"; import { createPersistedQueryLink as createPersistedQuery, VERSION } from ".."; -import { itAsync } from "../../../testing"; +import { wait } from "../../../testing"; import { toPromise } from "../../utils"; +import { ObservableStream } from "../../../testing/internal"; // Necessary configuration in order to mock multiple requests // to a single (/graphql) endpoint @@ -79,52 +80,53 @@ describe("happy path", () => { fetchMock.restore(); }); - itAsync( - "sends a sha256 hash of the query under extensions", - (resolve, reject) => { - fetchMock.post( - "/graphql", - () => new Promise((resolve) => resolve({ body: response })) - ); - const link = createPersistedQuery({ sha256 }).concat(createHttpLink()); - execute(link, { query, variables }).subscribe((result) => { - expect(result.data).toEqual(data); - const [uri, request] = fetchMock.lastCall()!; - expect(uri).toEqual("/graphql"); - expect(request!.body!).toBe( - JSON.stringify({ - operationName: "Test", - variables, - extensions: { - persistedQuery: { - version: VERSION, - sha256Hash: hash, - }, - }, - }) - ); - resolve(); - }, reject); - } - ); + it("sends a sha256 hash of the query under extensions", async () => { + fetchMock.post( + "/graphql", + () => new Promise((resolve) => resolve({ body: response })) + ); + const link = createPersistedQuery({ sha256 }).concat(createHttpLink()); + const observable = execute(link, { query, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue({ data }); + + const [uri, request] = fetchMock.lastCall()!; - itAsync("sends a version along with the request", (resolve, reject) => { + expect(uri).toEqual("/graphql"); + expect(request!.body!).toBe( + JSON.stringify({ + operationName: "Test", + variables, + extensions: { + persistedQuery: { + version: VERSION, + sha256Hash: hash, + }, + }, + }) + ); + }); + + it("sends a version along with the request", async () => { fetchMock.post( "/graphql", () => new Promise((resolve) => resolve({ body: response })) ); const link = createPersistedQuery({ sha256 }).concat(createHttpLink()); - execute(link, { query, variables }).subscribe((result) => { - expect(result.data).toEqual(data); - const [uri, request] = fetchMock.lastCall()!; - expect(uri).toEqual("/graphql"); - const parsed = JSON.parse(request!.body!.toString()); - expect(parsed.extensions.persistedQuery.version).toBe(VERSION); - resolve(); - }, reject); + const observable = execute(link, { query, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue({ data }); + + const [uri, request] = fetchMock.lastCall()!; + expect(uri).toEqual("/graphql"); + + const parsed = JSON.parse(request!.body!.toString()); + expect(parsed.extensions.persistedQuery.version).toBe(VERSION); }); - itAsync("memoizes between requests", (resolve, reject) => { + it("memoizes between requests", async () => { fetchMock.post( "/graphql", () => new Promise((resolve) => resolve({ body: response })), @@ -140,15 +142,23 @@ describe("happy path", () => { createHttpLink() ); - execute(link, { query, variables }).subscribe((result) => { + { + const observable = execute(link, { query, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue({ data }); + await expect(stream).toComplete(); + expect(hashSpy).toHaveBeenCalledTimes(1); + } + + { + const observable = execute(link, { query, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue({ data }); + await expect(stream).toComplete(); expect(hashSpy).toHaveBeenCalledTimes(1); - expect(result.data).toEqual(data); - execute(link, { query, variables }).subscribe((result2) => { - expect(hashSpy).toHaveBeenCalledTimes(1); - expect(result2.data).toEqual(data); - resolve(); - }, reject); - }, reject); + } }); it("clears the cache when calling `resetHashCache`", async () => { @@ -177,7 +187,7 @@ describe("happy path", () => { await expect(hashRefs[0]).toBeGarbageCollected(); }); - itAsync("supports loading the hash from other method", (resolve, reject) => { + it("supports loading the hash from other method", async () => { fetchMock.post( "/graphql", () => new Promise((resolve) => resolve({ body: response })) @@ -187,33 +197,34 @@ describe("happy path", () => { createHttpLink() ); - execute(link, { query, variables }).subscribe((result) => { - expect(result.data).toEqual(data); - const [uri, request] = fetchMock.lastCall()!; - expect(uri).toEqual("/graphql"); - const parsed = JSON.parse(request!.body!.toString()); - expect(parsed.extensions.persistedQuery.sha256Hash).toBe("foo"); - resolve(); - }, reject); + const observable = execute(link, { query, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue({ data }); + + const [uri, request] = fetchMock.lastCall()!; + expect(uri).toEqual("/graphql"); + + const parsed = JSON.parse(request!.body!.toString()); + expect(parsed.extensions.persistedQuery.sha256Hash).toBe("foo"); }); - itAsync("errors if unable to convert to sha256", (resolve, reject) => { + it("errors if unable to convert to sha256", async () => { fetchMock.post( "/graphql", () => new Promise((resolve) => resolve({ body: response })) ); const link = createPersistedQuery({ sha256 }).concat(createHttpLink()); - execute(link, { query: "1234", variables } as any).subscribe( - reject as any, - (error) => { - expect(error.message).toMatch(/Invalid AST Node/); - resolve(); - } - ); + const observable = execute(link, { query: "1234", variables } as any); + const stream = new ObservableStream(observable); + + const error = await stream.takeError(); + + expect(error.message).toMatch(/Invalid AST Node/); }); - itAsync("unsubscribes correctly", (resolve, reject) => { + it("unsubscribes correctly", async () => { const delay = new ApolloLink(() => { return new Observable((ob) => { setTimeout(() => { @@ -224,92 +235,70 @@ describe("happy path", () => { }); const link = createPersistedQuery({ sha256 }).concat(delay); - const sub = execute(link, { query, variables }).subscribe( - reject, - reject, - reject - ); + const observable = execute(link, { query, variables }); + const stream = new ObservableStream(observable); - setTimeout(() => { - sub.unsubscribe(); - resolve(); - }, 10); + await wait(10); + + stream.unsubscribe(); + + await expect(stream).not.toEmitAnything({ timeout: 150 }); }); - itAsync( - "should error if `sha256` and `generateHash` options are both missing", - (resolve, reject) => { - const createPersistedQueryFn = createPersistedQuery as any; - try { - createPersistedQueryFn(); - reject("should have thrown an error"); - } catch (error) { - expect( - (error as Error).message.indexOf( - 'Missing/invalid "sha256" or "generateHash" function' - ) - ).toBe(0); - resolve(); - } - } - ); + it("should error if `sha256` and `generateHash` options are both missing", async () => { + const createPersistedQueryFn = createPersistedQuery as any; + + expect(() => createPersistedQueryFn()).toThrow( + 'Missing/invalid "sha256" or "generateHash" function' + ); + }); - itAsync( - "should error if `sha256` or `generateHash` options are not functions", - (resolve, reject) => { + it.each(["sha256", "generateHash"])( + "should error if `%s` option is not a function", + async (option) => { const createPersistedQueryFn = createPersistedQuery as any; - [{ sha256: "ooops" }, { generateHash: "ooops" }].forEach((options) => { - try { - createPersistedQueryFn(options); - reject("should have thrown an error"); - } catch (error) { - expect( - (error as Error).message.indexOf( - 'Missing/invalid "sha256" or "generateHash" function' - ) - ).toBe(0); - resolve(); - } - }); + + expect(() => createPersistedQueryFn({ [option]: "ooops" })).toThrow( + 'Missing/invalid "sha256" or "generateHash" function' + ); } ); - itAsync( - "should work with a synchronous SHA-256 function", - (resolve, reject) => { - const crypto = require("crypto"); - const sha256Hash = crypto.createHmac("sha256", queryString).digest("hex"); + it("should work with a synchronous SHA-256 function", async () => { + const crypto = require("crypto"); + const sha256Hash = crypto.createHmac("sha256", queryString).digest("hex"); - fetchMock.post( - "/graphql", - () => new Promise((resolve) => resolve({ body: response })) - ); - const link = createPersistedQuery({ - sha256(data) { - return crypto.createHmac("sha256", data).digest("hex"); + fetchMock.post( + "/graphql", + () => new Promise((resolve) => resolve({ body: response })) + ); + const link = createPersistedQuery({ + sha256(data) { + return crypto.createHmac("sha256", data).digest("hex"); + }, + }).concat(createHttpLink()); + + const observable = execute(link, { query, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue({ data }); + + const [uri, request] = fetchMock.lastCall()!; + + expect(uri).toEqual("/graphql"); + expect(request!.body!).toBe( + JSON.stringify({ + operationName: "Test", + variables, + extensions: { + persistedQuery: { + version: VERSION, + sha256Hash: sha256Hash, + }, }, - }).concat(createHttpLink()); - - execute(link, { query, variables }).subscribe((result) => { - expect(result.data).toEqual(data); - const [uri, request] = fetchMock.lastCall()!; - expect(uri).toEqual("/graphql"); - expect(request!.body!).toBe( - JSON.stringify({ - operationName: "Test", - variables, - extensions: { - persistedQuery: { - version: VERSION, - sha256Hash: sha256Hash, - }, - }, - }) - ); - resolve(); - }, reject); - } - ); + }) + ); + }); }); describe("failure path", () => { @@ -356,98 +345,99 @@ describe("failure path", () => { }) ); - itAsync( - "sends GET for the first response only with useGETForHashedQueries", - (resolve, reject) => { - const params = new URLSearchParams({ - operationName: "Test", - variables: JSON.stringify({ - id: 1, - }), - extensions: JSON.stringify({ - persistedQuery: { - version: 1, - sha256Hash: hash, - }, - }), - }).toString(); - fetchMock.get( - `/graphql?${params}`, - () => new Promise((resolve) => resolve({ body: errorResponse })) - ); - fetchMock.post( - "/graphql", - () => new Promise((resolve) => resolve({ body: response })) - ); - const link = createPersistedQuery({ - sha256, - useGETForHashedQueries: true, - }).concat(createHttpLink()); - execute(link, { query, variables }).subscribe((result) => { - expect(result.data).toEqual(data); - const [[, failure]] = fetchMock.calls(); - expect(failure!.method).toBe("GET"); - expect(failure!.body).not.toBeDefined(); - const [, [, success]] = fetchMock.calls(); - expect(success!.method).toBe("POST"); - expect(JSON.parse(success!.body!.toString()).query).toBe(queryString); - expect( - JSON.parse(success!.body!.toString()).extensions.persistedQuery - .sha256Hash - ).toBe(hash); - resolve(); - }, reject); - } - ); + it("sends GET for the first response only with useGETForHashedQueries", async () => { + const params = new URLSearchParams({ + operationName: "Test", + variables: JSON.stringify({ + id: 1, + }), + extensions: JSON.stringify({ + persistedQuery: { + version: 1, + sha256Hash: hash, + }, + }), + }).toString(); + fetchMock.get( + `/graphql?${params}`, + () => new Promise((resolve) => resolve({ body: errorResponse })) + ); + fetchMock.post( + "/graphql", + () => new Promise((resolve) => resolve({ body: response })) + ); + const link = createPersistedQuery({ + sha256, + useGETForHashedQueries: true, + }).concat(createHttpLink()); + const observable = execute(link, { query, variables }); + const stream = new ObservableStream(observable); - itAsync( - "sends POST for both requests without useGETForHashedQueries", - (resolve, reject) => { - fetchMock.post( - "/graphql", - () => new Promise((resolve) => resolve({ body: errorResponse })), - { repeat: 1 } - ); - fetchMock.post( - "/graphql", - () => new Promise((resolve) => resolve({ body: response })), - { repeat: 1 } - ); - const link = createPersistedQuery({ sha256 }).concat(createHttpLink()); - execute(link, { query, variables }).subscribe((result) => { - expect(result.data).toEqual(data); - const [[, failure]] = fetchMock.calls(); - expect(failure!.method).toBe("POST"); - expect(JSON.parse(failure!.body!.toString())).toEqual({ - operationName: "Test", - variables, - extensions: { - persistedQuery: { - version: VERSION, - sha256Hash: hash, - }, - }, - }); - const [, [, success]] = fetchMock.calls(); - expect(success!.method).toBe("POST"); - expect(JSON.parse(success!.body!.toString())).toEqual({ - operationName: "Test", - query: queryString, - variables, - extensions: { - persistedQuery: { - version: VERSION, - sha256Hash: hash, - }, - }, - }); - resolve(); - }, reject); - } - ); + await expect(stream).toEmitValue({ data }); + + const [[, failure]] = fetchMock.calls(); + + expect(failure!.method).toBe("GET"); + expect(failure!.body).not.toBeDefined(); + + const [, [, success]] = fetchMock.calls(); + + expect(success!.method).toBe("POST"); + expect(JSON.parse(success!.body!.toString()).query).toBe(queryString); + expect( + JSON.parse(success!.body!.toString()).extensions.persistedQuery.sha256Hash + ).toBe(hash); + }); + + it("sends POST for both requests without useGETForHashedQueries", async () => { + fetchMock.post( + "/graphql", + () => new Promise((resolve) => resolve({ body: errorResponse })), + { repeat: 1 } + ); + fetchMock.post( + "/graphql", + () => new Promise((resolve) => resolve({ body: response })), + { repeat: 1 } + ); + const link = createPersistedQuery({ sha256 }).concat(createHttpLink()); + const observable = execute(link, { query, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue({ data }); + + const [[, failure]] = fetchMock.calls(); + + expect(failure!.method).toBe("POST"); + expect(JSON.parse(failure!.body!.toString())).toEqual({ + operationName: "Test", + variables, + extensions: { + persistedQuery: { + version: VERSION, + sha256Hash: hash, + }, + }, + }); + + const [, [, success]] = fetchMock.calls(); + + expect(success!.method).toBe("POST"); + expect(JSON.parse(success!.body!.toString())).toEqual({ + operationName: "Test", + query: queryString, + variables, + extensions: { + persistedQuery: { + version: VERSION, + sha256Hash: hash, + }, + }, + }); + }); // https://github.com/apollographql/apollo-client/pull/7456 - itAsync("forces POST request when sending full query", (resolve, reject) => { + it("forces POST request when sending full query", async () => { fetchMock.post( "/graphql", () => new Promise((resolve) => resolve({ body: giveUpResponse })), @@ -469,29 +459,33 @@ describe("failure path", () => { return true; }, }).concat(createHttpLink()); - execute(link, { query, variables }).subscribe((result) => { - expect(result.data).toEqual(data); - const [[, failure]] = fetchMock.calls(); - expect(failure!.method).toBe("POST"); - expect(JSON.parse(failure!.body!.toString())).toEqual({ - operationName: "Test", - variables, - extensions: { - persistedQuery: { - version: VERSION, - sha256Hash: hash, - }, + const observable = execute(link, { query, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue({ data }); + + const [[, failure]] = fetchMock.calls(); + + expect(failure!.method).toBe("POST"); + expect(JSON.parse(failure!.body!.toString())).toEqual({ + operationName: "Test", + variables, + extensions: { + persistedQuery: { + version: VERSION, + sha256Hash: hash, }, - }); - const [, [, success]] = fetchMock.calls(); - expect(success!.method).toBe("POST"); - expect(JSON.parse(success!.body!.toString())).toEqual({ - operationName: "Test", - query: queryString, - variables, - }); - resolve(); - }, reject); + }, + }); + + const [, [, success]] = fetchMock.calls(); + + expect(success!.method).toBe("POST"); + expect(JSON.parse(success!.body!.toString())).toEqual({ + operationName: "Test", + query: queryString, + variables, + }); }); it.each([ @@ -583,7 +577,7 @@ describe("failure path", () => { } ); - itAsync("works with multiple errors", (resolve, reject) => { + it("works with multiple errors", async () => { fetchMock.post( "/graphql", () => new Promise((resolve) => resolve({ body: multiResponse })), @@ -595,76 +589,84 @@ describe("failure path", () => { { repeat: 1 } ); const link = createPersistedQuery({ sha256 }).concat(createHttpLink()); - execute(link, { query, variables }).subscribe((result) => { - expect(result.data).toEqual(data); - const [[, failure]] = fetchMock.calls(); - expect(JSON.parse(failure!.body!.toString()).query).not.toBeDefined(); - const [, [, success]] = fetchMock.calls(); - expect(JSON.parse(success!.body!.toString()).query).toBe(queryString); - expect( - JSON.parse(success!.body!.toString()).extensions.persistedQuery - .sha256Hash - ).toBe(hash); - resolve(); - }, reject); + const observable = execute(link, { query, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue({ data }); + + const [[, failure]] = fetchMock.calls(); + + expect(JSON.parse(failure!.body!.toString()).query).not.toBeDefined(); + + const [, [, success]] = fetchMock.calls(); + + expect(JSON.parse(success!.body!.toString()).query).toBe(queryString); + expect( + JSON.parse(success!.body!.toString()).extensions.persistedQuery.sha256Hash + ).toBe(hash); }); describe.each([[400], [500]])("status %s", (status) => { - itAsync( - `handles a ${status} network with a "PERSISTED_QUERY_NOT_FOUND" error and still retries`, - (resolve, reject) => { - let requestCount = 0; - fetchMock.post( - "/graphql", - () => new Promise((resolve) => resolve({ body: response })), - { repeat: 1 } - ); + it(`handles a ${status} network with a "PERSISTED_QUERY_NOT_FOUND" error and still retries`, async () => { + let requestCount = 0; + fetchMock.post( + "/graphql", + () => new Promise((resolve) => resolve({ body: response })), + { repeat: 1 } + ); - // mock it again so we can verify it doesn't try anymore - fetchMock.post( - "/graphql", - () => new Promise((resolve) => resolve({ body: response })), - { repeat: 5 } - ); + // mock it again so we can verify it doesn't try anymore + fetchMock.post( + "/graphql", + () => new Promise((resolve) => resolve({ body: response })), + { repeat: 5 } + ); - const fetcher = (...args: any[]) => { - if (++requestCount % 2) { - return Promise.resolve({ - json: () => Promise.resolve(errorResponseWithCode), - text: () => Promise.resolve(errorResponseWithCode), - status, - }); - } - // @ts-expect-error - return global.fetch.apply(null, args); - }; - const link = createPersistedQuery({ sha256 }).concat( - createHttpLink({ fetch: fetcher } as any) - ); + const fetcher = (...args: any[]) => { + if (++requestCount % 2) { + return Promise.resolve({ + json: () => Promise.resolve(errorResponseWithCode), + text: () => Promise.resolve(errorResponseWithCode), + status, + }); + } + // @ts-expect-error + return global.fetch.apply(null, args); + }; + const link = createPersistedQuery({ sha256 }).concat( + createHttpLink({ fetch: fetcher } as any) + ); - execute(link, { query, variables }).subscribe((result) => { - expect(result.data).toEqual(data); - const [[, success]] = fetchMock.calls(); - expect(JSON.parse(success!.body!.toString()).query).toBe(queryString); - expect( - JSON.parse(success!.body!.toString()).extensions.persistedQuery - .sha256Hash - ).toBe(hash); - execute(link, { query, variables }).subscribe((secondResult) => { - expect(secondResult.data).toEqual(data); - const [, [, success]] = fetchMock.calls(); - expect(JSON.parse(success!.body!.toString()).query).toBe( - queryString - ); - expect( - JSON.parse(success!.body!.toString()).extensions.persistedQuery - .sha256Hash - ).toBe(hash); - resolve(); - }, reject); - }, reject); + { + const observable = execute(link, { query, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue({ data }); + + const [[, success]] = fetchMock.calls(); + + expect(JSON.parse(success!.body!.toString()).query).toBe(queryString); + expect( + JSON.parse(success!.body!.toString()).extensions.persistedQuery + .sha256Hash + ).toBe(hash); } - ); + + { + const observable = execute(link, { query, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue({ data }); + + const [, [, success]] = fetchMock.calls(); + + expect(JSON.parse(success!.body!.toString()).query).toBe(queryString); + expect( + JSON.parse(success!.body!.toString()).extensions.persistedQuery + .sha256Hash + ).toBe(hash); + } + }); it(`will fail on an unrelated ${status} network error, but still send a hash the next request`, async () => { let failed = false; @@ -711,43 +713,42 @@ describe("failure path", () => { ).toBe(hash); }); - itAsync( - `handles ${status} response network error and graphql error without disabling persistedQuery support`, - (resolve, reject) => { - let failed = false; - fetchMock.post( - "/graphql", - () => new Promise((resolve) => resolve({ body: response })), - { repeat: 1 } - ); + it(`handles ${status} response network error and graphql error without disabling persistedQuery support`, async () => { + let failed = false; + fetchMock.post( + "/graphql", + () => new Promise((resolve) => resolve({ body: response })), + { repeat: 1 } + ); - const fetcher = (...args: any[]) => { - if (!failed) { - failed = true; - return Promise.resolve({ - json: () => Promise.resolve(errorResponse), - text: () => Promise.resolve(errorResponse), - status, - }); - } - // @ts-expect-error - return global.fetch.apply(null, args); - }; - - const link = createPersistedQuery({ sha256 }).concat( - createHttpLink({ fetch: fetcher } as any) - ); + const fetcher = (...args: any[]) => { + if (!failed) { + failed = true; + return Promise.resolve({ + json: () => Promise.resolve(errorResponse), + text: () => Promise.resolve(errorResponse), + status, + }); + } + // @ts-expect-error + return global.fetch.apply(null, args); + }; - execute(link, { query, variables }).subscribe((result) => { - expect(result.data).toEqual(data); - const [[, success]] = fetchMock.calls(); - expect(JSON.parse(success!.body!.toString()).query).toBe(queryString); - expect( - JSON.parse(success!.body!.toString()).extensions - ).not.toBeUndefined(); - resolve(); - }, reject); - } - ); + const link = createPersistedQuery({ sha256 }).concat( + createHttpLink({ fetch: fetcher } as any) + ); + + const observable = execute(link, { query, variables }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue({ data }); + + const [[, success]] = fetchMock.calls(); + + expect(JSON.parse(success!.body!.toString()).query).toBe(queryString); + expect( + JSON.parse(success!.body!.toString()).extensions + ).not.toBeUndefined(); + }); }); }); diff --git a/src/link/ws/__tests__/webSocketLink.ts b/src/link/ws/__tests__/webSocketLink.ts index d24b118c44d..5859d6ed785 100644 --- a/src/link/ws/__tests__/webSocketLink.ts +++ b/src/link/ws/__tests__/webSocketLink.ts @@ -5,7 +5,7 @@ import gql from "graphql-tag"; import { Observable } from "../../../utilities"; import { execute } from "../../core"; import { WebSocketLink } from ".."; -import { itAsync } from "../../../testing"; +import { ObservableStream } from "../../../testing/internal"; const query = gql` query SampleQuery { @@ -43,95 +43,84 @@ describe("WebSocketLink", () => { // it('should pass the correct initialization parameters to the Subscription Client', () => { // }); - itAsync( - "should call request on the client for a query", - (resolve, reject) => { - const result = { data: { data: "result" } }; - const client: any = {}; - const observable = Observable.of(result); - client.__proto__ = SubscriptionClient.prototype; - client.request = jest.fn(); - client.request.mockReturnValueOnce(observable); - const link = new WebSocketLink(client); - - const obs = execute(link, { query }); - expect(obs).toEqual(observable); - obs.subscribe((data) => { - expect(data).toEqual(result); - expect(client.request).toHaveBeenCalledTimes(1); - resolve(); - }); - } - ); - - itAsync( - "should call query on the client for a mutation", - (resolve, reject) => { - const result = { data: { data: "result" } }; - const client: any = {}; - const observable = Observable.of(result); - client.__proto__ = SubscriptionClient.prototype; - client.request = jest.fn(); - client.request.mockReturnValueOnce(observable); - const link = new WebSocketLink(client); - - const obs = execute(link, { query: mutation }); - expect(obs).toEqual(observable); - obs.subscribe((data) => { - expect(data).toEqual(result); - expect(client.request).toHaveBeenCalledTimes(1); - resolve(); - }); - } - ); - - itAsync( - "should call request on the subscriptions client for subscription", - (resolve, reject) => { - const result = { data: { data: "result" } }; - const client: any = {}; - const observable = Observable.of(result); - client.__proto__ = SubscriptionClient.prototype; - client.request = jest.fn(); - client.request.mockReturnValueOnce(observable); - const link = new WebSocketLink(client); - - const obs = execute(link, { query: subscription }); - expect(obs).toEqual(observable); - obs.subscribe((data) => { - expect(data).toEqual(result); - expect(client.request).toHaveBeenCalledTimes(1); - resolve(); - }); - } - ); - - itAsync( - "should call next with multiple results for subscription", - (resolve, reject) => { - const results = [ - { data: { data: "result1" } }, - { data: { data: "result2" } }, - ]; - const client: any = {}; - client.__proto__ = SubscriptionClient.prototype; - client.request = jest.fn(() => { - const copy = [...results]; - return new Observable((observer) => { - observer.next(copy[0]); - observer.next(copy[1]); - }); - }); + it("should call request on the client for a query", async () => { + const result = { data: { data: "result" } }; + const client: any = {}; + const observable = Observable.of(result); + client.__proto__ = SubscriptionClient.prototype; + client.request = jest.fn(); + client.request.mockReturnValueOnce(observable); + const link = new WebSocketLink(client); + + const obs = execute(link, { query }); + expect(obs).toEqual(observable); + + const stream = new ObservableStream(obs); + + await expect(stream).toEmitValue(result); + expect(client.request).toHaveBeenCalledTimes(1); + }); + + it("should call query on the client for a mutation", async () => { + const result = { data: { data: "result" } }; + const client: any = {}; + const observable = Observable.of(result); + client.__proto__ = SubscriptionClient.prototype; + client.request = jest.fn(); + client.request.mockReturnValueOnce(observable); + const link = new WebSocketLink(client); + + const obs = execute(link, { query: mutation }); + expect(obs).toEqual(observable); - const link = new WebSocketLink(client); + const stream = new ObservableStream(obs); - execute(link, { query: subscription }).subscribe((data) => { - expect(client.request).toHaveBeenCalledTimes(1); - expect(data).toEqual(results.shift()); - if (results.length === 0) { - resolve(); - } + await expect(stream).toEmitValue(result); + expect(client.request).toHaveBeenCalledTimes(1); + }); + + it("should call request on the subscriptions client for subscription", async () => { + const result = { data: { data: "result" } }; + const client: any = {}; + const observable = Observable.of(result); + client.__proto__ = SubscriptionClient.prototype; + client.request = jest.fn(); + client.request.mockReturnValueOnce(observable); + const link = new WebSocketLink(client); + + const obs = execute(link, { query: subscription }); + expect(obs).toEqual(observable); + + const stream = new ObservableStream(obs); + + await expect(stream).toEmitValue(result); + expect(client.request).toHaveBeenCalledTimes(1); + }); + + it("should call next with multiple results for subscription", async () => { + const results = [ + { data: { data: "result1" } }, + { data: { data: "result2" } }, + ]; + const client: any = {}; + client.__proto__ = SubscriptionClient.prototype; + client.request = jest.fn(() => { + const copy = [...results]; + return new Observable((observer) => { + observer.next(copy[0]); + observer.next(copy[1]); }); - } - ); + }); + + const link = new WebSocketLink(client); + + const observable = execute(link, { query: subscription }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue(results.shift()); + await expect(stream).toEmitValue(results.shift()); + + expect(client.request).toHaveBeenCalledTimes(1); + expect(results).toHaveLength(0); + }); }); diff --git a/src/masking/__benches__/types.bench.ts b/src/masking/__benches__/types.bench.ts index 621fb29dfaf..0ae97b7edc4 100644 --- a/src/masking/__benches__/types.bench.ts +++ b/src/masking/__benches__/types.bench.ts @@ -299,7 +299,7 @@ test("MaybeMasked handles odd types", (prefix) => { bench(prefix + "unknown instantiations", () => { attest>(); - }).types([52, "instantiations"]); + }).types([54, "instantiations"]); bench(prefix + "unknown functionality", () => { expectTypeOf>().toBeUnknown(); }); @@ -464,3 +464,131 @@ test("base type, multiple fragments on sub-types", (prefix) => { }>(); }); }); + +test("does not detect `$fragmentRefs` if type contains `any`", (prefix) => { + interface Source { + foo: { bar: any[] }; + " $fragmentName": "foo"; + } + + bench(prefix + "instantiations", () => { + return {} as MaybeMasked; + }).types([6, "instantiations"]); + + bench(prefix + "functionality", () => { + const x = {} as MaybeMasked; + + expectTypeOf(x).branded.toEqualTypeOf(); + }); +}); + +test("leaves tuples alone", (prefix) => { + interface Source { + coords: [long: number, lat: number]; + } + + bench(prefix + "instantiations", () => { + return {} as Unmasked; + }).types([5, "instantiations"]); + + bench(prefix + "functionality", () => { + const x = {} as Unmasked; + + expectTypeOf(x).branded.toEqualTypeOf<{ + coords: [long: number, lat: number]; + }>(); + }); +}); + +test("does not detect `$fragmentRefs` if type is a record type", (prefix) => { + interface MetadataItem { + foo: string; + } + + interface Source { + metadata: Record; + " $fragmentName": "Source"; + } + + bench(prefix + "instantiations", () => { + return {} as MaybeMasked; + }).types([6, "instantiations"]); + + bench(prefix + "functionality", () => { + const x = {} as MaybeMasked; + + expectTypeOf(x).branded.toEqualTypeOf(); + }); +}); + +test("does not detect `$fragmentRefs` on types with index signatures", (prefix) => { + interface Source { + foo: string; + " $fragmentName": "Source"; + [key: string]: string; + } + + bench(prefix + "instantiations", () => { + return {} as MaybeMasked; + }).types([6, "instantiations"]); + + bench(prefix + "functionality", () => { + const x = {} as MaybeMasked; + + expectTypeOf(x).branded.toEqualTypeOf(); + }); +}); + +test("detects `$fragmentRefs` on types with index signatures", (prefix) => { + type Source = { + __typename: "Foo"; + id: number; + metadata: Record; + structuredMetadata: StructuredMetadata; + } & { " $fragmentName"?: "UserFieldsFragment" } & { + " $fragmentRefs"?: { + FooFragment: FooFragment; + }; + }; + + interface StructuredMetadata { + bar: number; + [index: string]: number; + } + + type FooFragment = { + __typename: "Foo"; + foo: string; + } & { " $fragmentName"?: "FooFragment" }; + + bench(prefix + "instantiations", () => { + return {} as MaybeMasked; + }).types([6, "instantiations"]); + + bench(prefix + "functionality", () => { + const x = {} as MaybeMasked; + + expectTypeOf(x).branded.toEqualTypeOf<{ + __typename: "Foo"; + id: number; + metadata: Record; + foo: string; + structuredMetadata: StructuredMetadata; + }>(); + }); +}); + +test("recursive types: no error 'Type instantiation is excessively deep and possibly infinite.'", (prefix) => { + // this type is self-recursive + type Source = import("graphql").IntrospectionQuery; + + bench(prefix + "instantiations", () => { + return {} as MaybeMasked; + }).types([6, "instantiations"]); + + bench(prefix + "functionality", () => { + const x = {} as MaybeMasked; + + expectTypeOf(x).branded.toEqualTypeOf(); + }); +}); diff --git a/src/masking/internal/types.ts b/src/masking/internal/types.ts index ead6b64abdf..828734a8934 100644 --- a/src/masking/internal/types.ts +++ b/src/masking/internal/types.ts @@ -1,4 +1,4 @@ -import type { Prettify } from "../../utilities/index.js"; +import type { Prettify, RemoveIndexSignature } from "../../utilities/index.js"; export type IsAny = 0 extends 1 & T ? true : false; @@ -20,7 +20,6 @@ export type UnwrapFragmentRefs = > > > - : TData extends Array ? Array> : TData extends object ? { [K in keyof TData]: UnwrapFragmentRefs; @@ -183,9 +182,13 @@ export type RemoveMaskedMarker = Omit; export type RemoveFragmentName = T extends any ? Omit : T; -export type ContainsFragmentsRefs = - TData extends object ? - " $fragmentRefs" extends keyof TData ? - true - : ContainsFragmentsRefs - : false; +type Exact = (x: T) => T; +export type ContainsFragmentsRefs = true extends ( + IsAny +) ? + false +: TData extends object ? + Exact extends Seen ? false + : " $fragmentRefs" extends keyof RemoveIndexSignature ? true + : ContainsFragmentsRefs> +: false; diff --git a/src/react/context/__tests__/ApolloConsumer.test.tsx b/src/react/context/__tests__/ApolloConsumer.test.tsx index aed5384c415..a27a02d782f 100644 --- a/src/react/context/__tests__/ApolloConsumer.test.tsx +++ b/src/react/context/__tests__/ApolloConsumer.test.tsx @@ -7,7 +7,6 @@ import { InMemoryCache as Cache } from "../../../cache"; import { ApolloProvider } from "../ApolloProvider"; import { ApolloConsumer } from "../ApolloConsumer"; import { getApolloContext } from "../ApolloContext"; -import { itAsync } from "../../../testing"; const client = new ApolloClient({ cache: new Cache(), @@ -15,17 +14,13 @@ const client = new ApolloClient({ }); describe(" component", () => { - itAsync("has a render prop", (resolve, reject) => { + it("has a render prop", (done) => { render( {(clientRender) => { - try { - expect(clientRender).toBe(client); - resolve(); - } catch (e) { - reject(e); - } + expect(clientRender).toBe(client); + done(); return null; }} diff --git a/src/react/hoc/__tests__/ssr/getDataFromTree.test.tsx b/src/react/hoc/__tests__/ssr/getDataFromTree.test.tsx index 70daf897951..dc250d8f56d 100644 --- a/src/react/hoc/__tests__/ssr/getDataFromTree.test.tsx +++ b/src/react/hoc/__tests__/ssr/getDataFromTree.test.tsx @@ -8,7 +8,7 @@ import { DocumentNode } from "graphql"; import { ApolloClient, TypedDocumentNode } from "../../../../core"; import { ApolloProvider } from "../../../context"; import { InMemoryCache as Cache } from "../../../../cache"; -import { itAsync, mockSingleLink } from "../../../../testing"; +import { mockSingleLink } from "../../../../testing"; import { Query } from "../../../components"; import { getDataFromTree, getMarkupFromTree } from "../../../ssr"; import { graphql } from "../../graphql"; @@ -543,86 +543,78 @@ describe("SSR", () => { }); }); - itAsync( - "should allow for setting state in a component", - (resolve, reject) => { - const query = gql` - query user($id: ID) { - currentUser(id: $id) { - firstName - } + it("should allow for setting state in a component", async () => { + const query = gql` + query user($id: ID) { + currentUser(id: $id) { + firstName } - `; - const resultData = { currentUser: { firstName: "James" } }; - const variables = { id: "1" }; - const link = mockSingleLink({ - request: { query, variables }, - result: { data: resultData }, - }); - - const cache = new Cache({ addTypename: false }); - const apolloClient = new ApolloClient({ - link, - cache, - }); - - interface Props { - id: string; } - interface Data { - currentUser: { - firstName: string; + `; + const resultData = { currentUser: { firstName: "James" } }; + const variables = { id: "1" }; + const link = mockSingleLink({ + request: { query, variables }, + result: { data: resultData }, + }); + + const cache = new Cache({ addTypename: false }); + const apolloClient = new ApolloClient({ + link, + cache, + }); + + interface Props { + id: string; + } + interface Data { + currentUser: { + firstName: string; + }; + } + interface Variables { + id: string; + } + + class Element extends React.Component< + ChildProps, + { thing: number } + > { + state = { thing: 1 }; + + static getDerivedStateFromProps() { + return { + thing: 2, }; } - interface Variables { - id: string; - } - class Element extends React.Component< - ChildProps, - { thing: number } - > { - state = { thing: 1 }; + render() { + const { data } = this.props; + expect(this.state.thing).toBe(2); + return ( +
+ {!data || data.loading || !data.currentUser ? + "loading" + : data.currentUser.firstName} +
+ ); + } + } - static getDerivedStateFromProps() { - return { - thing: 2, - }; - } + const ElementWithData = graphql(query)(Element); - render() { - const { data } = this.props; - expect(this.state.thing).toBe(2); - return ( -
- {!data || data.loading || !data.currentUser ? - "loading" - : data.currentUser.firstName} -
- ); - } - } + const app = ( + + + + ); - const ElementWithData = graphql(query)(Element); + await getDataFromTree(app); - const app = ( - - - - ); - - getDataFromTree(app) - .then(() => { - const initialState = cache.extract(); - expect(initialState).toBeTruthy(); - expect( - initialState.ROOT_QUERY!['currentUser({"id":"1"})'] - ).toBeTruthy(); - resolve(); - }) - .catch(console.error); - } - ); + const initialState = cache.extract(); + expect(initialState).toBeTruthy(); + expect(initialState.ROOT_QUERY!['currentUser({"id":"1"})']).toBeTruthy(); + }); it("should correctly initialize an empty state to null", () => { class Element extends React.Component { @@ -651,7 +643,7 @@ describe("SSR", () => { return getDataFromTree(); }); - itAsync("should allow prepping state from props", (resolve, reject) => { + it("should allow prepping state from props", async () => { const query = gql` query user($id: ID) { currentUser(id: $id) { @@ -730,16 +722,11 @@ describe("SSR", () => {
); - getDataFromTree(app) - .then(() => { - const initialState = apolloClient.cache.extract(); - expect(initialState).toBeTruthy(); - expect( - initialState.ROOT_QUERY!['currentUser({"id":"1"})'] - ).toBeTruthy(); - resolve(); - }) - .catch(console.error); + await getDataFromTree(app); + + const initialState = apolloClient.cache.extract(); + expect(initialState).toBeTruthy(); + expect(initialState.ROOT_QUERY!['currentUser({"id":"1"})']).toBeTruthy(); }); it("shouldn't run queries if ssr is turned to off", () => { diff --git a/src/react/hooks/__tests__/useMutation.test.tsx b/src/react/hooks/__tests__/useMutation.test.tsx index 8ac66118371..0433d37b339 100644 --- a/src/react/hooks/__tests__/useMutation.test.tsx +++ b/src/react/hooks/__tests__/useMutation.test.tsx @@ -19,7 +19,6 @@ import { } from "../../../core"; import { InMemoryCache } from "../../../cache"; import { - itAsync, MockedProvider, MockSubscriptionLink, mockSingleLink, @@ -1555,7 +1554,7 @@ describe("useMutation Hook", () => { await waitFor(() => expect(variablesMatched).toBe(true)); }); - itAsync("should be called with the provided context", (resolve, reject) => { + it("should be called with the provided context", async () => { const context = { id: 3 }; const variables = { @@ -1599,13 +1598,13 @@ describe("useMutation Hook", () => { ); - return waitFor(() => { + await waitFor(() => { expect(foundContext).toBe(true); - }).then(resolve, reject); + }); }); describe("If context is not provided", () => { - itAsync("should be undefined", (resolve, reject) => { + it("should be undefined", async () => { const variables = { description: "Get milk!", }; @@ -1642,92 +1641,89 @@ describe("useMutation Hook", () => { ); - return waitFor(() => { + await waitFor(() => { expect(checkedContext).toBe(true); - }).then(resolve, reject); + }); }); }); }); describe("Optimistic response", () => { - itAsync( - "should support optimistic response handling", - async (resolve, reject) => { - const optimisticResponse = { - __typename: "Mutation", - createTodo: { - id: 1, - description: "TEMPORARY", - priority: "High", - __typename: "Todo", - }, - }; + it("should support optimistic response handling", async () => { + const optimisticResponse = { + __typename: "Mutation", + createTodo: { + id: 1, + description: "TEMPORARY", + priority: "High", + __typename: "Todo", + }, + }; - const variables = { - description: "Get milk!", - }; + const variables = { + description: "Get milk!", + }; - const mocks = [ - { - request: { - query: CREATE_TODO_MUTATION, - variables, - }, - result: { data: CREATE_TODO_RESULT }, + const mocks = [ + { + request: { + query: CREATE_TODO_MUTATION, + variables, }, - ]; + result: { data: CREATE_TODO_RESULT }, + }, + ]; - const link = mockSingleLink(...mocks).setOnError(reject); - const cache = new InMemoryCache(); - const client = new ApolloClient({ - cache, - link, - }); + const link = mockSingleLink(...mocks); + const cache = new InMemoryCache(); + const client = new ApolloClient({ + cache, + link, + }); - let renderCount = 0; - const Component = () => { - const [createTodo, { loading, data }] = useMutation( - CREATE_TODO_MUTATION, - { optimisticResponse } - ); + let renderCount = 0; + const Component = () => { + const [createTodo, { loading, data }] = useMutation( + CREATE_TODO_MUTATION, + { optimisticResponse } + ); - switch (renderCount) { - case 0: - expect(loading).toBeFalsy(); - expect(data).toBeUndefined(); - void createTodo({ variables }); - - const dataInStore = client.cache.extract(true); - expect(dataInStore["Todo:1"]).toEqual( - optimisticResponse.createTodo - ); - - break; - case 1: - expect(loading).toBeTruthy(); - expect(data).toBeUndefined(); - break; - case 2: - expect(loading).toBeFalsy(); - expect(data).toEqual(CREATE_TODO_RESULT); - break; - default: - } - renderCount += 1; - return null; - }; + switch (renderCount) { + case 0: + expect(loading).toBeFalsy(); + expect(data).toBeUndefined(); + void createTodo({ variables }); - render( - - - - ); + const dataInStore = client.cache.extract(true); + expect(dataInStore["Todo:1"]).toEqual( + optimisticResponse.createTodo + ); + + break; + case 1: + expect(loading).toBeTruthy(); + expect(data).toBeUndefined(); + break; + case 2: + expect(loading).toBeFalsy(); + expect(data).toEqual(CREATE_TODO_RESULT); + break; + default: + } + renderCount += 1; + return null; + }; - return waitFor(() => { - expect(renderCount).toBe(3); - }).then(resolve, reject); - } - ); + render( + + + + ); + + await waitFor(() => { + expect(renderCount).toBe(3); + }); + }); it("should be called with the provided context", async () => { const optimisticResponse = { diff --git a/src/react/hooks/__tests__/useReactiveVar.test.tsx b/src/react/hooks/__tests__/useReactiveVar.test.tsx index c78d7e6ec80..84cb17cb309 100644 --- a/src/react/hooks/__tests__/useReactiveVar.test.tsx +++ b/src/react/hooks/__tests__/useReactiveVar.test.tsx @@ -1,7 +1,6 @@ import React, { StrictMode, useEffect } from "react"; import { screen, render, waitFor, act } from "@testing-library/react"; -import { itAsync } from "../../../testing"; import { makeVar } from "../../../core"; import { useReactiveVar } from "../useReactiveVar"; @@ -47,92 +46,87 @@ describe("useReactiveVar Hook", () => { }); }); - itAsync( - "works when two components share a variable", - async (resolve, reject) => { - const counterVar = makeVar(0); - - let parentRenderCount = 0; - function Parent() { - const count = useReactiveVar(counterVar); + it("works when two components share a variable", async () => { + const counterVar = makeVar(0); - switch (++parentRenderCount) { - case 1: - expect(count).toBe(0); - break; - case 2: - expect(count).toBe(1); - break; - case 3: - expect(count).toBe(11); - break; - default: - reject(`too many (${parentRenderCount}) parent renders`); - } + let parentRenderCount = 0; + function Parent() { + const count = useReactiveVar(counterVar); - return ; + switch (++parentRenderCount) { + case 1: + expect(count).toBe(0); + break; + case 2: + expect(count).toBe(1); + break; + case 3: + expect(count).toBe(11); + break; + default: + throw new Error(`too many (${parentRenderCount}) parent renders`); } - let childRenderCount = 0; - function Child() { - const count = useReactiveVar(counterVar); + return ; + } - switch (++childRenderCount) { - case 1: - expect(count).toBe(0); - break; - case 2: - expect(count).toBe(1); - break; - case 3: - expect(count).toBe(11); - break; - default: - reject(`too many (${childRenderCount}) child renders`); - } + let childRenderCount = 0; + function Child() { + const count = useReactiveVar(counterVar); - return null; + switch (++childRenderCount) { + case 1: + expect(count).toBe(0); + break; + case 2: + expect(count).toBe(1); + break; + case 3: + expect(count).toBe(11); + break; + default: + throw new Error(`too many (${childRenderCount}) child renders`); } - render(); + return null; + } - await waitFor(() => { - expect(parentRenderCount).toBe(1); - }); + render(); - await waitFor(() => { - expect(childRenderCount).toBe(1); - }); + await waitFor(() => { + expect(parentRenderCount).toBe(1); + }); - expect(counterVar()).toBe(0); - act(() => { - counterVar(1); - }); + await waitFor(() => { + expect(childRenderCount).toBe(1); + }); - await waitFor(() => { - expect(parentRenderCount).toBe(2); - }); - await waitFor(() => { - expect(childRenderCount).toBe(2); - }); + expect(counterVar()).toBe(0); + act(() => { + counterVar(1); + }); - expect(counterVar()).toBe(1); - act(() => { - counterVar(counterVar() + 10); - }); + await waitFor(() => { + expect(parentRenderCount).toBe(2); + }); + await waitFor(() => { + expect(childRenderCount).toBe(2); + }); - await waitFor(() => { - expect(parentRenderCount).toBe(3); - }); - await waitFor(() => { - expect(childRenderCount).toBe(3); - }); + expect(counterVar()).toBe(1); + act(() => { + counterVar(counterVar() + 10); + }); - expect(counterVar()).toBe(11); + await waitFor(() => { + expect(parentRenderCount).toBe(3); + }); + await waitFor(() => { + expect(childRenderCount).toBe(3); + }); - resolve(); - } - ); + expect(counterVar()).toBe(11); + }); it("does not update if component has been unmounted", async () => { const counterVar = makeVar(0); @@ -252,7 +246,7 @@ describe("useReactiveVar Hook", () => { }); }); - itAsync("works with strict mode", async (resolve, reject) => { + it("works with strict mode", async () => { const counterVar = makeVar(0); const mock = jest.fn(); @@ -289,94 +283,84 @@ describe("useReactiveVar Hook", () => { expect(mock).toHaveBeenNthCalledWith(2, 1); } }); - - resolve(); }); - itAsync( - "works with multiple synchronous calls", - async (resolve, reject) => { - const counterVar = makeVar(0); - function Component() { - const count = useReactiveVar(counterVar); + it("works with multiple synchronous calls", async () => { + const counterVar = makeVar(0); + function Component() { + const count = useReactiveVar(counterVar); - return
{count}
; - } + return
{count}
; + } - render(); - void Promise.resolve().then(() => { - counterVar(1); - counterVar(2); - counterVar(3); - counterVar(4); - counterVar(5); - counterVar(6); - counterVar(7); - counterVar(8); - counterVar(9); - counterVar(10); - }); - - await waitFor(() => { - expect(screen.getAllByText("10")).toHaveLength(1); - }); - - resolve(); + render(); + void Promise.resolve().then(() => { + counterVar(1); + counterVar(2); + counterVar(3); + counterVar(4); + counterVar(5); + counterVar(6); + counterVar(7); + counterVar(8); + counterVar(9); + counterVar(10); + }); + + await waitFor(() => { + expect(screen.getAllByText("10")).toHaveLength(1); + }); + }); + + it("should survive many rerenderings despite racing asynchronous updates", (done) => { + const rv = makeVar(0); + + function App() { + const value = useReactiveVar(rv); + return ( +
+

{value}

+
+ ); } - ); - - itAsync( - "should survive many rerenderings despite racing asynchronous updates", - (resolve, reject) => { - const rv = makeVar(0); - - function App() { - const value = useReactiveVar(rv); - return ( -
-

{value}

-
- ); - } - const goalCount = 1000; - let updateCount = 0; - let stopped = false; - - function spam() { - if (stopped) return; - try { - if (++updateCount <= goalCount) { - act(() => { - rv(updateCount); - setTimeout(spam, Math.random() * 10); - }); - } else { - stopped = true; - expect(rv()).toBe(goalCount); - screen - .findByText(String(goalCount)) - .then((element) => { - expect(element.nodeName.toLowerCase()).toBe("h1"); - }) - .then(resolve, reject); - } - } catch (e) { + const goalCount = 1000; + let updateCount = 0; + let stopped = false; + + function spam() { + if (stopped) return; + try { + if (++updateCount <= goalCount) { + act(() => { + rv(updateCount); + setTimeout(spam, Math.random() * 10); + }); + } else { stopped = true; - reject(e); + expect(rv()).toBe(goalCount); + void screen + .findByText(String(goalCount)) + .then((element) => { + expect(element.nodeName.toLowerCase()).toBe("h1"); + }) + .then(done); } + } catch (e) { + stopped = true; + throw e; } - spam(); - spam(); - spam(); - spam(); - - render( - - - - ); } - ); + spam(); + spam(); + spam(); + spam(); + + render( + + + + ); + }); }); }); diff --git a/src/testing/internal/ObservableStream.ts b/src/testing/internal/ObservableStream.ts index 63f550827c6..f6c53169b87 100644 --- a/src/testing/internal/ObservableStream.ts +++ b/src/testing/internal/ObservableStream.ts @@ -1,4 +1,7 @@ -import type { Observable } from "../../utilities/index.js"; +import type { + Observable, + ObservableSubscription, +} from "../../utilities/index.js"; import { ReadableStream } from "node:stream/web"; export interface TakeOptions { @@ -11,10 +14,12 @@ type ObservableEvent = export class ObservableStream { private reader: ReadableStreamDefaultReader>; + private subscription!: ObservableSubscription; + constructor(observable: Observable) { this.reader = new ReadableStream>({ - start(controller) { - observable.subscribe( + start: (controller) => { + this.subscription = observable.subscribe( (value) => controller.enqueue({ type: "next", value }), (error) => controller.enqueue({ type: "error", error }), () => controller.enqueue({ type: "complete" }) @@ -36,6 +41,10 @@ export class ObservableStream { ]); } + unsubscribe() { + this.subscription.unsubscribe(); + } + async takeNext(options?: TakeOptions): Promise { const event = await this.take(options); expect(event).toEqual({ type: "next", value: expect.anything() }); diff --git a/src/testing/matchers/toEmitError.ts b/src/testing/matchers/toEmitError.ts index 75e93aa56f2..f488e6f0de4 100644 --- a/src/testing/matchers/toEmitError.ts +++ b/src/testing/matchers/toEmitError.ts @@ -1,7 +1,15 @@ -import type { MatcherFunction } from "expect"; +import type { MatcherFunction, MatcherContext } from "expect"; import type { ObservableStream } from "../internal/index.js"; import type { TakeOptions } from "../internal/ObservableStream.js"; +function isErrorEqual(this: MatcherContext, expected: any, actual: any) { + if (typeof expected === "string" && actual instanceof Error) { + return actual.message === expected; + } + + return this.equals(expected, actual, this.customTesters); +} + export const toEmitError: MatcherFunction< [value?: any, options?: TakeOptions] > = async function (actual, expected, options) { @@ -15,9 +23,7 @@ export const toEmitError: MatcherFunction< try { const error = await stream.takeError(options); const pass = - expected === undefined ? true : ( - this.equals(expected, error, this.customTesters) - ); + expected === undefined ? true : isErrorEqual.call(this, expected, error); return { pass, @@ -37,7 +43,7 @@ export const toEmitError: MatcherFunction< "\n\n" + this.utils.printDiffOrStringify( expected, - error, + typeof expected === "string" ? error.message : error, "Expected", "Recieved", true diff --git a/src/utilities/index.ts b/src/utilities/index.ts index 300cafb0d56..f4e7705deb6 100644 --- a/src/utilities/index.ts +++ b/src/utilities/index.ts @@ -139,6 +139,7 @@ export type { OnlyRequiredProperties } from "./types/OnlyRequiredProperties.js"; export type { Prettify } from "./types/Prettify.js"; export type { UnionToIntersection } from "./types/UnionToIntersection.js"; export type { NoInfer } from "./types/NoInfer.js"; +export type { RemoveIndexSignature } from "./types/RemoveIndexSignature.js"; export { AutoCleanedStrongCache, diff --git a/src/utilities/observables/__tests__/asyncMap.ts b/src/utilities/observables/__tests__/asyncMap.ts index ce4227be45b..8f9d53071cd 100644 --- a/src/utilities/observables/__tests__/asyncMap.ts +++ b/src/utilities/observables/__tests__/asyncMap.ts @@ -1,6 +1,5 @@ import { Observable } from "../Observable"; import { asyncMap } from "../asyncMap"; -import { itAsync } from "../../../testing"; import { ObservableStream } from "../../../testing/internal"; const wait = (delayMs: number) => new Promise((resolve) => setTimeout(resolve, delayMs)); @@ -19,106 +18,66 @@ function make1234Observable() { }); } -function rejectExceptions( - reject: (reason: any) => any, - fn: (...args: Args) => Ret -) { - return function () { - try { - // @ts-expect-error - return fn.apply(this, arguments); - } catch (error) { - reject(error); - } - } as typeof fn; -} - describe("asyncMap", () => { - itAsync("keeps normal results in order", (resolve, reject) => { + it("keeps normal results in order", async () => { const values: number[] = []; - const mapped: number[] = []; - asyncMap(make1234Observable(), (value) => { + const observable = asyncMap(make1234Observable(), (value) => { values.push(value); // Make earlier results take longer than later results. const delay = 100 - value * 10; return wait(delay).then(() => value * 2); - }).subscribe({ - next(mappedValue) { - mapped.push(mappedValue); - }, - error: reject, - complete: rejectExceptions(reject, () => { - expect(values).toEqual([1, 2, 3, 4]); - expect(mapped).toEqual([2, 4, 6, 8]); - resolve(); - }), }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue(2); + await expect(stream).toEmitValue(4); + await expect(stream).toEmitValue(6); + await expect(stream).toEmitValue(8); + await expect(stream).toComplete(); + + expect(values).toEqual([1, 2, 3, 4]); }); - itAsync("handles exceptions from mapping functions", (resolve, reject) => { - const triples: number[] = []; - asyncMap(make1234Observable(), (num) => { + it("handles exceptions from mapping functions", async () => { + const observable = asyncMap(make1234Observable(), (num) => { if (num === 3) throw new Error("expected"); return num * 3; - }).subscribe({ - next: rejectExceptions(reject, (triple) => { - expect(triple).toBeLessThan(9); - triples.push(triple); - }), - error: rejectExceptions(reject, (error) => { - expect(error.message).toBe("expected"); - expect(triples).toEqual([3, 6]); - resolve(); - }), }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue(3); + await expect(stream).toEmitValue(6); + await expect(stream).toEmitError(new Error("expected")); + }); + + it("handles rejected promises from mapping functions", async () => { + const observable = asyncMap(make1234Observable(), (num) => { + if (num === 3) return Promise.reject(new Error("expected")); + return num * 3; + }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue(3); + await expect(stream).toEmitValue(6); + await expect(stream).toEmitError(new Error("expected")); }); - itAsync( - "handles rejected promises from mapping functions", - (resolve, reject) => { - const triples: number[] = []; - asyncMap(make1234Observable(), (num) => { - if (num === 3) return Promise.reject(new Error("expected")); + it("handles async exceptions from mapping functions", async () => { + const observable = asyncMap(make1234Observable(), (num) => + wait(10).then(() => { + if (num === 3) throw new Error("expected"); return num * 3; - }).subscribe({ - next: rejectExceptions(reject, (triple) => { - expect(triple).toBeLessThan(9); - triples.push(triple); - }), - error: rejectExceptions(reject, (error) => { - expect(error.message).toBe("expected"); - expect(triples).toEqual([3, 6]); - resolve(); - }), - }); - } - ); + }) + ); + const stream = new ObservableStream(observable); - itAsync( - "handles async exceptions from mapping functions", - (resolve, reject) => { - const triples: number[] = []; - asyncMap(make1234Observable(), (num) => - wait(10).then(() => { - if (num === 3) throw new Error("expected"); - return num * 3; - }) - ).subscribe({ - next: rejectExceptions(reject, (triple) => { - expect(triple).toBeLessThan(9); - triples.push(triple); - }), - error: rejectExceptions(reject, (error) => { - expect(error.message).toBe("expected"); - expect(triples).toEqual([3, 6]); - resolve(); - }), - }); - } - ); + await expect(stream).toEmitValue(3); + await expect(stream).toEmitValue(6); + await expect(stream).toEmitError(new Error("expected")); + }); - itAsync("handles exceptions from next functions", (resolve, reject) => { + it("handles exceptions from next functions", (done) => { const triples: number[] = []; asyncMap(make1234Observable(), (num) => { return num * 3; @@ -136,10 +95,10 @@ describe("asyncMap", () => { // expect(triples).toEqual([3, 6, 9]); // resolve(); // }), - complete: rejectExceptions(reject, () => { + complete: () => { expect(triples).toEqual([3, 6, 9, 12]); - resolve(); - }), + done(); + }, }); }); diff --git a/src/utilities/types/RemoveIndexSignature.ts b/src/utilities/types/RemoveIndexSignature.ts new file mode 100644 index 00000000000..4aad90687d9 --- /dev/null +++ b/src/utilities/types/RemoveIndexSignature.ts @@ -0,0 +1,6 @@ +export type RemoveIndexSignature = { + [K in keyof T as string extends K ? never + : number extends K ? never + : symbol extends K ? never + : K]: T[K]; +};