diff --git a/.api-reports/api-report-cache.api.md b/.api-reports/api-report-cache.api.md index 908176a26cf..6c52897efbb 100644 --- a/.api-reports/api-report-cache.api.md +++ b/.api-reports/api-report-cache.api.md @@ -764,16 +764,18 @@ export function makeReference(id: string): Reference; // @public (undocumented) export function makeVar(value: T): ReactiveVar; -// 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 "RemoveMaskedMarker" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "ContainsFragmentsRefs" needs to be exported by the entry point index.d.ts // // @public -type MaybeMasked = TData extends any ? true extends IsAny ? TData : TData extends { +type MaybeMasked = DataMasking extends { + mode: "unmask"; +} ? TData extends any ? true extends IsAny ? TData : TData extends { __masked?: true; -} ? Prettify> : DataMasking extends { - enabled: true; -} ? TData : true extends ContainsFragmentsRefs ? Unmasked : TData : never; +} ? Prettify> : true extends ContainsFragmentsRefs ? Unmasked : TData : never : DataMasking extends { + mode: "preserveTypes"; +} ? TData : TData; // @public (undocumented) export interface MergeInfo { diff --git a/.api-reports/api-report-core.api.md b/.api-reports/api-report-core.api.md index 81836428979..f1d74960f19 100644 --- a/.api-reports/api-report-core.api.md +++ b/.api-reports/api-report-core.api.md @@ -1412,11 +1412,13 @@ type MaybeAsync = T | PromiseLike; // Warning: (ae-forgotten-export) The symbol "ContainsFragmentsRefs" needs to be exported by the entry point index.d.ts // // @public -export type MaybeMasked = TData extends any ? true extends IsAny ? TData : TData extends { +export type MaybeMasked = DataMasking extends { + mode: "unmask"; +} ? TData extends any ? true extends IsAny ? TData : TData extends { __masked?: true; -} ? Prettify> : DataMasking extends { - enabled: true; -} ? TData : true extends ContainsFragmentsRefs ? Unmasked : TData : never; +} ? Prettify> : true extends ContainsFragmentsRefs ? Unmasked : TData : never : DataMasking extends { + mode: "preserveTypes"; +} ? TData : TData; // @public (undocumented) export interface MergeInfo { diff --git a/.api-reports/api-report-masking.api.md b/.api-reports/api-report-masking.api.md index fffd2efb2e1..04786e31bbd 100644 --- a/.api-reports/api-report-masking.api.md +++ b/.api-reports/api-report-masking.api.md @@ -440,11 +440,13 @@ export function maskOperation(data: TData, document: DocumentNo // Warning: (ae-forgotten-export) The symbol "ContainsFragmentsRefs" needs to be exported by the entry point index.d.ts // // @public -export type MaybeMasked = TData extends any ? true extends IsAny ? TData : TData extends { +export type MaybeMasked = DataMasking extends { + mode: "unmask"; +} ? TData extends any ? true extends IsAny ? TData : TData extends { __masked?: true; -} ? Prettify> : DataMasking extends { - enabled: true; -} ? TData : true extends ContainsFragmentsRefs ? Unmasked : TData : never; +} ? Prettify> : true extends ContainsFragmentsRefs ? Unmasked : TData : never : DataMasking extends { + mode: "preserveTypes"; +} ? TData : TData; // Warning: (ae-forgotten-export) The symbol "CombineIntersection" needs to be exported by the entry point index.d.ts // diff --git a/.api-reports/api-report-react.api.md b/.api-reports/api-report-react.api.md index 16e002d9106..904d5977115 100644 --- a/.api-reports/api-report-react.api.md +++ b/.api-reports/api-report-react.api.md @@ -1153,16 +1153,18 @@ interface MaskOperationOptions { // @public (undocumented) type MaybeAsync = T | PromiseLike; -// Warning: (ae-forgotten-export) The symbol "RemoveMaskedMarker" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "DataMasking" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "RemoveMaskedMarker" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "ContainsFragmentsRefs" needs to be exported by the entry point index.d.ts // // @public -type MaybeMasked = TData extends any ? true extends IsAny ? TData : TData extends { +type MaybeMasked = DataMasking extends { + mode: "unmask"; +} ? TData extends any ? true extends IsAny ? TData : TData extends { __masked?: true; -} ? Prettify> : DataMasking extends { - enabled: true; -} ? TData : true extends ContainsFragmentsRefs ? Unmasked : TData : never; +} ? Prettify> : true extends ContainsFragmentsRefs ? Unmasked : TData : never : DataMasking extends { + mode: "preserveTypes"; +} ? TData : TData; // Warning: (ae-forgotten-export) The symbol "CombineIntersection" needs to be exported by the entry point index.d.ts // @@ -2411,7 +2413,7 @@ export interface UseReadQueryResult { export function useSubscription(subscription: DocumentNode | TypedDocumentNode, options?: SubscriptionHookOptions, NoInfer_2>): { restart: () => void; loading: boolean; - data?: MaybeMasked | undefined; + data?: TData | undefined; error?: ApolloError; variables?: TVariables | undefined; }; diff --git a/.api-reports/api-report-react_components.api.md b/.api-reports/api-report-react_components.api.md index 051470cb154..1238640825e 100644 --- a/.api-reports/api-report-react_components.api.md +++ b/.api-reports/api-report-react_components.api.md @@ -1016,16 +1016,18 @@ interface MaskOperationOptions { // @public (undocumented) type MaybeAsync = T | PromiseLike; -// Warning: (ae-forgotten-export) The symbol "RemoveMaskedMarker" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "DataMasking" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "RemoveMaskedMarker" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "ContainsFragmentsRefs" needs to be exported by the entry point index.d.ts // // @public -type MaybeMasked = TData extends any ? true extends IsAny ? TData : TData extends { +type MaybeMasked = DataMasking extends { + mode: "unmask"; +} ? TData extends any ? true extends IsAny ? TData : TData extends { __masked?: true; -} ? Prettify> : DataMasking extends { - enabled: true; -} ? TData : true extends ContainsFragmentsRefs ? Unmasked : TData : never; +} ? Prettify> : true extends ContainsFragmentsRefs ? Unmasked : TData : never : DataMasking extends { + mode: "preserveTypes"; +} ? TData : TData; // Warning: (ae-forgotten-export) The symbol "CombineIntersection" needs to be exported by the entry point index.d.ts // diff --git a/.api-reports/api-report-react_context.api.md b/.api-reports/api-report-react_context.api.md index 703cfb4825d..3fdc6fa3414 100644 --- a/.api-reports/api-report-react_context.api.md +++ b/.api-reports/api-report-react_context.api.md @@ -1013,16 +1013,18 @@ interface MaskOperationOptions { // @public (undocumented) type MaybeAsync = T | PromiseLike; -// Warning: (ae-forgotten-export) The symbol "RemoveMaskedMarker" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "DataMasking" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "RemoveMaskedMarker" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "ContainsFragmentsRefs" needs to be exported by the entry point index.d.ts // // @public -type MaybeMasked = TData extends any ? true extends IsAny ? TData : TData extends { +type MaybeMasked = DataMasking extends { + mode: "unmask"; +} ? TData extends any ? true extends IsAny ? TData : TData extends { __masked?: true; -} ? Prettify> : DataMasking extends { - enabled: true; -} ? TData : true extends ContainsFragmentsRefs ? Unmasked : TData : never; +} ? Prettify> : true extends ContainsFragmentsRefs ? Unmasked : TData : never : DataMasking extends { + mode: "preserveTypes"; +} ? TData : TData; // Warning: (ae-forgotten-export) The symbol "CombineIntersection" needs to be exported by the entry point index.d.ts // diff --git a/.api-reports/api-report-react_hoc.api.md b/.api-reports/api-report-react_hoc.api.md index 3f867350519..6fe263dd146 100644 --- a/.api-reports/api-report-react_hoc.api.md +++ b/.api-reports/api-report-react_hoc.api.md @@ -1020,16 +1020,18 @@ interface MaskOperationOptions { // @public (undocumented) type MaybeAsync = T | PromiseLike; -// Warning: (ae-forgotten-export) The symbol "RemoveMaskedMarker" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "DataMasking" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "RemoveMaskedMarker" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "ContainsFragmentsRefs" needs to be exported by the entry point index.d.ts // // @public -type MaybeMasked = TData extends any ? true extends IsAny ? TData : TData extends { +type MaybeMasked = DataMasking extends { + mode: "unmask"; +} ? TData extends any ? true extends IsAny ? TData : TData extends { __masked?: true; -} ? Prettify> : DataMasking extends { - enabled: true; -} ? TData : true extends ContainsFragmentsRefs ? Unmasked : TData : never; +} ? Prettify> : true extends ContainsFragmentsRefs ? Unmasked : TData : never : DataMasking extends { + mode: "preserveTypes"; +} ? TData : TData; // Warning: (ae-forgotten-export) The symbol "CombineIntersection" needs to be exported by the entry point index.d.ts // diff --git a/.api-reports/api-report-react_hooks.api.md b/.api-reports/api-report-react_hooks.api.md index b252b2a672f..1c5b8fb2058 100644 --- a/.api-reports/api-report-react_hooks.api.md +++ b/.api-reports/api-report-react_hooks.api.md @@ -1102,16 +1102,18 @@ interface MaskOperationOptions { // @public (undocumented) type MaybeAsync = T | PromiseLike; -// Warning: (ae-forgotten-export) The symbol "RemoveMaskedMarker" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "DataMasking" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "RemoveMaskedMarker" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "ContainsFragmentsRefs" needs to be exported by the entry point index.d.ts // // @public -type MaybeMasked = TData extends any ? true extends IsAny ? TData : TData extends { +type MaybeMasked = DataMasking extends { + mode: "unmask"; +} ? TData extends any ? true extends IsAny ? TData : TData extends { __masked?: true; -} ? Prettify> : DataMasking extends { - enabled: true; -} ? TData : true extends ContainsFragmentsRefs ? Unmasked : TData : never; +} ? Prettify> : true extends ContainsFragmentsRefs ? Unmasked : TData : never : DataMasking extends { + mode: "preserveTypes"; +} ? TData : TData; // Warning: (ae-forgotten-export) The symbol "CombineIntersection" needs to be exported by the entry point index.d.ts // @@ -2244,7 +2246,7 @@ export interface UseReadQueryResult { export function useSubscription(subscription: DocumentNode | TypedDocumentNode, options?: SubscriptionHookOptions, NoInfer_2>): { restart: () => void; loading: boolean; - data?: MaybeMasked | undefined; + data?: TData | undefined; error?: ApolloError; variables?: TVariables | undefined; }; diff --git a/.api-reports/api-report-react_internal.api.md b/.api-reports/api-report-react_internal.api.md index 3e43a1dafbb..92a7a3747e6 100644 --- a/.api-reports/api-report-react_internal.api.md +++ b/.api-reports/api-report-react_internal.api.md @@ -956,7 +956,7 @@ export class InternalQueryReference { // Warning: (ae-forgotten-export) The symbol "FetchMoreOptions" needs to be exported by the entry point index.d.ts // // (undocumented) - fetchMore(options: FetchMoreOptions): Promise>>; + fetchMore(options: FetchMoreOptions): Promise>; // (undocumented) readonly key: QueryKey; // Warning: (ae-forgotten-export) The symbol "Listener" needs to be exported by the entry point index.d.ts @@ -968,7 +968,7 @@ export class InternalQueryReference { // (undocumented) promise: QueryRefPromise; // (undocumented) - refetch(variables: OperationVariables | undefined): Promise>>; + refetch(variables: OperationVariables | undefined): Promise>; // (undocumented) reinitialize(): void; // (undocumented) @@ -1112,16 +1112,18 @@ interface MaskOperationOptions { // @public (undocumented) type MaybeAsync = T | PromiseLike; -// Warning: (ae-forgotten-export) The symbol "RemoveMaskedMarker" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "DataMasking" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "RemoveMaskedMarker" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "ContainsFragmentsRefs" needs to be exported by the entry point index.d.ts // // @public -type MaybeMasked = TData extends any ? true extends IsAny ? TData : TData extends { +type MaybeMasked = DataMasking extends { + mode: "unmask"; +} ? TData extends any ? true extends IsAny ? TData : TData extends { __masked?: true; -} ? Prettify> : DataMasking extends { - enabled: true; -} ? TData : true extends ContainsFragmentsRefs ? Unmasked : TData : never; +} ? Prettify> : true extends ContainsFragmentsRefs ? Unmasked : TData : never : DataMasking extends { + mode: "preserveTypes"; +} ? TData : TData; // Warning: (ae-forgotten-export) The symbol "CombineIntersection" needs to be exported by the entry point index.d.ts // diff --git a/.api-reports/api-report-react_ssr.api.md b/.api-reports/api-report-react_ssr.api.md index d0560e817dd..83958f92452 100644 --- a/.api-reports/api-report-react_ssr.api.md +++ b/.api-reports/api-report-react_ssr.api.md @@ -998,16 +998,18 @@ interface MaskOperationOptions { // @public (undocumented) type MaybeAsync = T | PromiseLike; -// Warning: (ae-forgotten-export) The symbol "RemoveMaskedMarker" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "DataMasking" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "RemoveMaskedMarker" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "ContainsFragmentsRefs" needs to be exported by the entry point index.d.ts // // @public -type MaybeMasked = TData extends any ? true extends IsAny ? TData : TData extends { +type MaybeMasked = DataMasking extends { + mode: "unmask"; +} ? TData extends any ? true extends IsAny ? TData : TData extends { __masked?: true; -} ? Prettify> : DataMasking extends { - enabled: true; -} ? TData : true extends ContainsFragmentsRefs ? Unmasked : TData : never; +} ? Prettify> : true extends ContainsFragmentsRefs ? Unmasked : TData : never : DataMasking extends { + mode: "preserveTypes"; +} ? TData : TData; // Warning: (ae-forgotten-export) The symbol "CombineIntersection" needs to be exported by the entry point index.d.ts // diff --git a/.api-reports/api-report-testing.api.md b/.api-reports/api-report-testing.api.md index 0c7b64d8eee..18bbb81892d 100644 --- a/.api-reports/api-report-testing.api.md +++ b/.api-reports/api-report-testing.api.md @@ -987,16 +987,18 @@ interface MaskOperationOptions { // @public (undocumented) type MaybeAsync = T | PromiseLike; -// Warning: (ae-forgotten-export) The symbol "RemoveMaskedMarker" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "DataMasking" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "RemoveMaskedMarker" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "ContainsFragmentsRefs" needs to be exported by the entry point index.d.ts // // @public -type MaybeMasked = TData extends any ? true extends IsAny ? TData : TData extends { +type MaybeMasked = DataMasking extends { + mode: "unmask"; +} ? TData extends any ? true extends IsAny ? TData : TData extends { __masked?: true; -} ? Prettify> : DataMasking extends { - enabled: true; -} ? TData : true extends ContainsFragmentsRefs ? Unmasked : TData : never; +} ? Prettify> : true extends ContainsFragmentsRefs ? Unmasked : TData : never : DataMasking extends { + mode: "preserveTypes"; +} ? TData : TData; // Warning: (ae-forgotten-export) The symbol "CombineIntersection" needs to be exported by the entry point index.d.ts // diff --git a/.api-reports/api-report-testing_core.api.md b/.api-reports/api-report-testing_core.api.md index d46a9f55d1f..9b98b8e17de 100644 --- a/.api-reports/api-report-testing_core.api.md +++ b/.api-reports/api-report-testing_core.api.md @@ -986,16 +986,18 @@ interface MaskOperationOptions { // @public (undocumented) type MaybeAsync = T | PromiseLike; -// Warning: (ae-forgotten-export) The symbol "RemoveMaskedMarker" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "DataMasking" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "RemoveMaskedMarker" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "ContainsFragmentsRefs" needs to be exported by the entry point index.d.ts // // @public -type MaybeMasked = TData extends any ? true extends IsAny ? TData : TData extends { +type MaybeMasked = DataMasking extends { + mode: "unmask"; +} ? TData extends any ? true extends IsAny ? TData : TData extends { __masked?: true; -} ? Prettify> : DataMasking extends { - enabled: true; -} ? TData : true extends ContainsFragmentsRefs ? Unmasked : TData : never; +} ? Prettify> : true extends ContainsFragmentsRefs ? Unmasked : TData : never : DataMasking extends { + mode: "preserveTypes"; +} ? TData : TData; // Warning: (ae-forgotten-export) The symbol "CombineIntersection" needs to be exported by the entry point index.d.ts // diff --git a/.api-reports/api-report-utilities.api.md b/.api-reports/api-report-utilities.api.md index a09ee2ed138..6905e929046 100644 --- a/.api-reports/api-report-utilities.api.md +++ b/.api-reports/api-report-utilities.api.md @@ -1695,16 +1695,18 @@ type MaybeAsync = T | PromiseLike; // @public (undocumented) export function maybeDeepFreeze(obj: T): T; -// 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 "RemoveMaskedMarker" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "ContainsFragmentsRefs" needs to be exported by the entry point index.d.ts // // @public -type MaybeMasked = TData extends any ? true extends IsAny ? TData : TData extends { +type MaybeMasked = DataMasking extends { + mode: "unmask"; +} ? TData extends any ? true extends IsAny ? TData : TData extends { __masked?: true; -} ? Prettify> : DataMasking extends { - enabled: true; -} ? TData : true extends ContainsFragmentsRefs ? Unmasked : TData : never; +} ? Prettify> : true extends ContainsFragmentsRefs ? Unmasked : TData : never : DataMasking extends { + mode: "preserveTypes"; +} ? TData : TData; // @public (undocumented) export function mergeDeep(...sources: T): TupleToIntersection; diff --git a/.api-reports/api-report.api.md b/.api-reports/api-report.api.md index 850d6e603b9..42e41d0e105 100644 --- a/.api-reports/api-report.api.md +++ b/.api-reports/api-report.api.md @@ -1593,11 +1593,13 @@ type MaybeAsync = T | PromiseLike; // Warning: (ae-forgotten-export) The symbol "ContainsFragmentsRefs" needs to be exported by the entry point index.d.ts // // @public -export type MaybeMasked = TData extends any ? true extends IsAny ? TData : TData extends { +export type MaybeMasked = DataMasking extends { + mode: "unmask"; +} ? TData extends any ? true extends IsAny ? TData : TData extends { __masked?: true; -} ? Prettify> : DataMasking extends { - enabled: true; -} ? TData : true extends ContainsFragmentsRefs ? Unmasked : TData : never; +} ? Prettify> : true extends ContainsFragmentsRefs ? Unmasked : TData : never : DataMasking extends { + mode: "preserveTypes"; +} ? TData : TData; // @public (undocumented) export interface MergeInfo { @@ -3082,7 +3084,7 @@ export interface UseReadQueryResult { export function useSubscription(subscription: DocumentNode | TypedDocumentNode, options?: SubscriptionHookOptions, NoInfer_2>): { restart: () => void; loading: boolean; - data?: MaybeMasked | undefined; + data?: TData | undefined; error?: ApolloError; variables?: TVariables | undefined; }; diff --git a/.size-limits.json b/.size-limits.json index 5790c408cde..54621796c0c 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 41640, + "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 adc45c38443..6f94749fc14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,46 @@ # @apollo/client +## 3.12.5 + +### Patch Changes + +- [#12252](https://github.com/apollographql/apollo-client/pull/12252) [`cb9cd4e`](https://github.com/apollographql/apollo-client/commit/cb9cd4ea251aab225adf5e4e4f3f69e1bbacee52) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Changes the default behavior of the `MaybeMasked` type to preserve types unless otherwise specified. This change makes it easier to upgrade from older versions of the client where types could have unexpectedly changed in the application due to the default of trying to unwrap types into unmasked types. This change also fixes the compilation performance regression experienced when simply upgrading the client since types are now preserved by default. + + A new `mode` option has now been introduced to allow for the old behavior. See the next section on migrating if you wish to maintain the old default behavior after upgrading to this version. + + ### Migrating from <= v3.12.4 + + If you've adopted data masking and have opted in to using masked types by setting the `enabled` property to `true`, you can remove this configuration entirely: + + ```diff + -declare module "@apollo/client" { + - interface DataMasking { + - mode: "unmask" + - } + -} + ``` + + If you prefer to specify the behavior explicitly, change the property from `enabled: true`, to `mode: "preserveTypes"`: + + ```diff + declare module "@apollo/client" { + interface DataMasking { + - enabled: true + + mode: "preserveTypes" + } + } + ``` + + If you rely on the default behavior in 3.12.4 or below and would like to continue to use unmasked types by default, set the `mode` to `unmask`: + + ```ts + declare module "@apollo/client" { + interface DataMasking { + mode: "unmask"; + } + } + ``` + ## 3.12.4 ### Patch Changes diff --git a/docs/source/data/fragments.mdx b/docs/source/data/fragments.mdx index 0faf4fcca73..ee6ccd596f0 100644 --- a/docs/source/data/fragments.mdx +++ b/docs/source/data/fragments.mdx @@ -837,12 +837,12 @@ It's important that the parent query or fragment selects any [`keyFields`](../ca ### Nesting fragments in other fragments +As your UI grows in complexity, it is common to split up components into smaller, more reusable chunks. As a result you may end up with more deeply nested components that have their own data requirements. Much like queries, we can nest fragments within other fragments. + -You can nest fragments with or without data masking (for an example, see the section on [colocating fragments](#colocating-fragments).) The following section describes how you can use masking in components with `useFragment`. +You can nest fragments with or without data masking (for an example, see the section on [colocating fragments](#colocating-fragments).) This section describes how you can use masking in components with `useFragment`. -As your UI grows in complexity, it is common to split up components into smaller, more reusable chunks. As a result you may end up with more deeply nested components that have their own data requirements. Much like queries, we can nest fragments within other fragments. - Let's add a `Comment` component that will be used by `PostDetails` to render the `topComment` for the post. ```jsx title="Comment.jsx" @@ -1104,9 +1104,7 @@ type GetCurrentUserQuery = { #### Generating masked types -You generate masked types with either the [`typescript-operations` plugin](https://the-guild.dev/graphql/codegen/plugins/typescript/typescript-operations) or the [client preset](https://the-guild.dev/graphql/codegen/plugins/presets/preset-client). You can generate masked types at any stage in the adoption process, even before enabling `dataMasking` in your client instance. Until you [opt in to use masked types](#opting-in-to-use-masked-types), the client unwraps them and provides the full operation type. - -The following sections show how to configure GraphQL Codegen to output masked types. +You generate masked types with either the [`typescript-operations` plugin](https://the-guild.dev/graphql/codegen/plugins/typescript/typescript-operations) or the [client preset](https://the-guild.dev/graphql/codegen/plugins/presets/preset-client). The following sections show how to configure GraphQL Codegen to output masked types. ##### With the `typescript-operations` plugin @@ -1140,53 +1138,47 @@ const config: CodegenConfig = { Support for the `@unmask` directive was introduced with `@graphql-codegen/client-preset` [v4.5.1](https://github.com/dotansimha/graphql-code-generator/releases/tag/release-1732308151614) -Add the following configuration to your GraphQL Codegen config. - - -The [Fragment Masking](https://the-guild.dev/graphql/codegen/plugins/presets/preset-client#fragment-masking) feature in the client preset is incompatible with Apollo Client's data masking feature. You need to [turn off Fragment Masking](https://the-guild.dev/graphql/codegen/plugins/presets/preset-client#how-to-disable-fragment-masking) in your configuration (included below). If you use the [generated `useFragment` function](https://the-guild.dev/graphql/codegen/plugins/presets/preset-client#the-usefragment-helper), you must use Apollo Client's [`useFragment`](#usefragment) hook instead. - - -```ts title="codegen.ts" -const config: CodegenConfig = { - // ... - generates: { - "path/to/gql/": { - preset: "client", - presetConfig: { - // ... - // disables the incompatible GraphQL Codegen fragment masking feature - fragmentMasking: false, - customDirectives: { - apolloUnmask: true +You can't use the `client-preset` [Fragment Masking](https://the-guild.dev/graphql/codegen/plugins/presets/preset-client#fragment-masking) and Apollo Client's data masking features simultaneously. +The incompatibility between the features is in runtime behavior only. +Apollo's data masking uses the same type output generated by CodeGen's Fragment Masking feature. + +To migrate from CodeGen's fragment masking feature to Apollo Client's data masking, follow these steps: + +1. Replace the [generated `useFragment` function](https://the-guild.dev/graphql/codegen/plugins/presets/preset-client#the-usefragment-helper), with Apollo Client's [`useFragment`](#usefragment) hook. +2. [Turn off Fragment Masking](https://the-guild.dev/graphql/codegen/plugins/presets/preset-client#how-to-disable-fragment-masking) in your GraphQL Codegen config, along with these additions: + + ```ts title="codegen.ts" + const config: CodegenConfig = { + // ... + generates: { + "path/to/gql/": { + preset: "client", + presetConfig: { + // ... + // disables the incompatible GraphQL Codegen fragment masking feature + fragmentMasking: false, + customDirectives: { + apolloUnmask: true + } + }, + config: { + inlineFragmentTypes: "mask", + } } - }, - config: { - inlineFragmentTypes: "mask", } } - } -} -``` - -#### Opting in to use masked types - - -We recommend that you opt in to use masked types only after you've [enabled `dataMasking`](#enabling-data-masking) in your `ApolloClient` instance. - - -By default, the client unwraps operation types and provides the full operation result type. To prevent this behavior and have the client use masked types, you need to opt in. This strategy allows for [incremental adoption](#incremental-adoption-in-an-existing-application) in your application and avoids the need to update large parts of your application to satisfy the change in types. + ``` -You can opt in to use the masked types in one of two ways. -##### Opting in globally +3. [Enable data masking](#enabling-data-masking) in Apollo Client. -To turn on masked types for your whole application at once, modify the `DataMasking` exported type from `@apollo/client` using TypeScript's [declaration merging](https://www.typescriptlang.org/docs/handbook/declaration-merging.html) ability. + +#### Setting a types mode for masked types + - -We recommend this approach for most cases. Use this approach with the [`@unmask` directive](./directives#unmask) for the best path to [incremental adoption](#incremental-adoption-in-an-existing-application). - +Apollo Client provides different modes to work with operation types throughout your application. You change the mode by modifying the `DataMasking` exported type from the `@apollo/client` package using TypeScript's [declaration merging](https://www.typescriptlang.org/docs/handbook/declaration-merging.html) ability. Specifying a mode is optional and only needed when you prefer to use a different mode other than the default. -First, create a TypeScript file that will be used to modify the `DataMasking` type. +To modify the data masking mode used in the client, first create a TypeScript file that will be used to modify the `DataMasking` type. ```ts title="apollo-client.d.ts" // This import is necessary to ensure all Apollo Client imports @@ -1195,7 +1187,7 @@ import '@apollo/client'; declare module "@apollo/client" { interface DataMasking { - enabled: true; + mode: "preserveTypes"; } } ``` @@ -1204,9 +1196,25 @@ declare module "@apollo/client" { This example uses `apollo-client.d.ts` as the file name to make it easily identifiable. You can name this file as you wish. -With `enabled` set to `true`, any request-based API will type `data` using the masked type and prevent the client from unwrapping it. +##### Modes -##### Opting in per operation + +Prior to Apollo Client version 3.12.5, the default was to [unmask](#unmask) the types. If you upgrade to 3.12.5 or later and wish to maintain the same default as 3.12.4 or below, set the mode to [`unmask`](#unmask). See pull request [#12252](https://github.com/apollographql/apollo-client/pull/12252) for more details on this change, including a section on migrating from {"<="} 3.12.4. + + +###### `preserveTypes` (default) + +The default `preserveTypes` mode makes no modification to the operation types regardless of whether the type definitions are masked or unmasked. This provides a simpler upgrade path when you're ready to [incrementally adopt](#incremental-adoption-in-an-existing-application) data masking. + +###### `unmask` + +Setting the mode to `unmask` will unwrap masked types and provide the full result type. Use this mode when you [generate masked types](#generating-masked-types) but need to maintain access to the full result type, such as using this with [per-operation masked types](#using-per-operation-masked-types). + +##### Using per-operation masked types + + +This is only useful if you have changed the types mode to `unmask`. Using this with `preserveTypes` has no effect. + If you prefer an incremental approach, you can opt in to use masked types per operation. This can be useful when your application creates multiple Apollo Client instances where only a subset enables data masking. @@ -1315,7 +1323,7 @@ In this example, the `GetPosts` query selects enough fields to satisfy the `Post On rare occasions, you may need access to the unmasked type of a particular operation. Apollo Client provides the `Unmasked` helper type that unwraps masked types and removes meta information on the type. -This is the same helper type the client uses when unwrapping types while data masking is turned off or for APIs that use the full result. +This is the same helper type the client uses when unwrapping types while the TypeScript data masking `mode` is set to `unmask` or for APIs that use the full result. ```ts @@ -1419,9 +1427,9 @@ new ApolloClient({ > Enabling data masking early in the adoption process makes it much easier to adopt for newly added queries and fragments since masking becomes the default behavior. Ideally data masking is enabled in the same pull request as the `@unmask` changes to ensure that no new queries and fragments are introduced to the codebase without the `@unmask` modifications applied. -#### 3. Generate and opt in to use masked types +#### 3. Generate masked types -If you are using TypeScript in your application, you will need to update your GraphQL Codegen configuration to [generate masked types](#generating-masked-types). Once you generate masked types, [opt in](#opting-in-to-use-masked-types) to use the masking types with your client. +If you are using TypeScript in your application, you will need to update your GraphQL Codegen configuration to [generate masked types](#generating-masked-types). Learn more about using TypeScript with data masking in the ["Using with TypeScript"](#using-with-typescript) section. diff --git a/package-lock.json b/package-lock.json index 6503dfb9530..0c256410e09 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@apollo/client", - "version": "3.12.4", + "version": "3.12.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@apollo/client", - "version": "3.12.4", + "version": "3.12.5", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 7538bd3a651..70f0c82c577 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/client", - "version": "3.12.4", + "version": "3.12.5", "description": "A fully-featured caching GraphQL client.", "private": true, "keywords": [ diff --git a/src/__tests__/graphqlSubscriptions.ts b/src/__tests__/graphqlSubscriptions.ts index d666ed22e65..2008c92285f 100644 --- a/src/__tests__/graphqlSubscriptions.ts +++ b/src/__tests__/graphqlSubscriptions.ts @@ -4,9 +4,9 @@ import { ApolloClient, FetchResult } from "../core"; import { InMemoryCache } from "../cache"; import { ApolloError, PROTOCOL_ERRORS_SYMBOL } from "../errors"; import { QueryManager } from "../core/QueryManager"; -import { itAsync, mockObservableLink } from "../testing"; +import { mockObservableLink } from "../testing"; import { GraphQLError } from "graphql"; -import { spyOnConsole } from "../testing/internal"; +import { ObservableStream, spyOnConsole } from "../testing/internal"; import { getDefaultOptionsForQueryManagerTests } from "../testing/core/mocking/mockQueryManager"; describe("GraphQL Subscriptions", () => { @@ -47,36 +47,23 @@ describe("GraphQL Subscriptions", () => { }; }); - itAsync( - "should start a subscription on network interface and unsubscribe", - (resolve, reject) => { - const link = mockObservableLink(); - // This test calls directly through Apollo Client - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ addTypename: false }), - }); + it("should start a subscription on network interface and unsubscribe", async () => { + const link = mockObservableLink(); + // This test calls directly through Apollo Client + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ addTypename: false }), + }); - let count = 0; - const sub = client.subscribe(defaultOptions).subscribe({ - next(result) { - count++; - expect(result).toEqual(results[0].result); + const stream = new ObservableStream(client.subscribe(defaultOptions)); + link.simulateResult(results[0]); - // Test unsubscribing - if (count > 1) { - throw new Error("next fired after unsubscribing"); - } - sub.unsubscribe(); - resolve(); - }, - }); + await expect(stream).toEmitValue(results[0].result); - link.simulateResult(results[0]); - } - ); + stream.unsubscribe(); + }); - itAsync("should subscribe with default values", (resolve, reject) => { + it("should subscribe with default values", async () => { const link = mockObservableLink(); // This test calls directly through Apollo Client const client = new ApolloClient({ @@ -84,25 +71,16 @@ describe("GraphQL Subscriptions", () => { cache: new InMemoryCache({ addTypename: false }), }); - let count = 0; - const sub = client.subscribe(options).subscribe({ - next(result) { - expect(result).toEqual(results[0].result); + const stream = new ObservableStream(client.subscribe(options)); - // Test unsubscribing - if (count > 1) { - throw new Error("next fired after unsubscribing"); - } - sub.unsubscribe(); + link.simulateResult(results[0]); - resolve(); - }, - }); + await expect(stream).toEmitValue(results[0].result); - link.simulateResult(results[0]); + stream.unsubscribe(); }); - itAsync("should multiplex subscriptions", (resolve, reject) => { + it("should multiplex subscriptions", async () => { const link = mockObservableLink(); const queryManager = new QueryManager( getDefaultOptionsForQueryManagerTests({ @@ -112,88 +90,57 @@ describe("GraphQL Subscriptions", () => { ); const obs = queryManager.startGraphQLSubscription(options); - - let counter = 0; - - // tslint:disable-next-line - obs.subscribe({ - next(result) { - expect(result).toEqual(results[0].result); - counter++; - if (counter === 2) { - resolve(); - } - }, - }) as any; - - // Subscribe again. Should also receive the same result. - // tslint:disable-next-line - obs.subscribe({ - next(result) { - expect(result).toEqual(results[0].result); - counter++; - if (counter === 2) { - resolve(); - } - }, - }) as any; + const stream1 = new ObservableStream(obs); + const stream2 = new ObservableStream(obs); link.simulateResult(results[0]); + + await expect(stream1).toEmitValue(results[0].result); + await expect(stream2).toEmitValue(results[0].result); }); - itAsync( - "should receive multiple results for a subscription", - (resolve, reject) => { - const link = mockObservableLink(); - let numResults = 0; - const queryManager = new QueryManager( - getDefaultOptionsForQueryManagerTests({ - link, - cache: new InMemoryCache({ addTypename: false }), - }) - ); - - // tslint:disable-next-line - queryManager.startGraphQLSubscription(options).subscribe({ - next(result) { - expect(result).toEqual(results[numResults].result); - numResults++; - if (numResults === 4) { - resolve(); - } - }, - }) as any; + it("should receive multiple results for a subscription", async () => { + const link = mockObservableLink(); + const queryManager = new QueryManager( + getDefaultOptionsForQueryManagerTests({ + link, + cache: new InMemoryCache({ addTypename: false }), + }) + ); - for (let i = 0; i < 4; i++) { - link.simulateResult(results[i]); - } + const stream = new ObservableStream( + queryManager.startGraphQLSubscription(options) + ); + + for (let i = 0; i < 4; i++) { + link.simulateResult(results[i]); } - ); - - itAsync( - "should not cache subscription data if a `no-cache` fetch policy is used", - (resolve, reject) => { - const link = mockObservableLink(); - const cache = new InMemoryCache({ addTypename: false }); - const client = new ApolloClient({ - link, - cache, - }); - expect(cache.extract()).toEqual({}); + await expect(stream).toEmitValue(results[0].result); + await expect(stream).toEmitValue(results[1].result); + await expect(stream).toEmitValue(results[2].result); + await expect(stream).toEmitValue(results[3].result); + await expect(stream).not.toEmitAnything(); + }); - options.fetchPolicy = "no-cache"; - const sub = client.subscribe(options).subscribe({ - next() { - expect(cache.extract()).toEqual({}); - sub.unsubscribe(); - resolve(); - }, - }); + it("should not cache subscription data if a `no-cache` fetch policy is used", async () => { + const link = mockObservableLink(); + const cache = new InMemoryCache({ addTypename: false }); + const client = new ApolloClient({ + link, + cache, + }); - link.simulateResult(results[0]); - } - ); + expect(cache.extract()).toEqual({}); + + options.fetchPolicy = "no-cache"; + const stream = new ObservableStream(client.subscribe(options)); + + link.simulateResult(results[0]); + + await expect(stream).toEmitNext(); + expect(cache.extract()).toEqual({}); + }); it("should throw an error if the result has errors on it", () => { const link = mockObservableLink(); @@ -492,27 +439,22 @@ describe("GraphQL Subscriptions", () => { }); }); - itAsync( - "should pass a context object through the link execution chain", - (resolve, reject) => { - const link = mockObservableLink(); - const client = new ApolloClient({ - cache: new InMemoryCache(), - link, - }); + it("should pass a context object through the link execution chain", async () => { + const link = mockObservableLink(); + const client = new ApolloClient({ + cache: new InMemoryCache(), + link, + }); - client.subscribe(options).subscribe({ - next() { - expect(link.operation?.getContext().someVar).toEqual( - options.context.someVar - ); - resolve(); - }, - }); + const stream = new ObservableStream(client.subscribe(options)); - link.simulateResult(results[0]); - } - ); + link.simulateResult(results[0]); + + await expect(stream).toEmitNext(); + expect(link.operation?.getContext().someVar).toEqual( + options.context.someVar + ); + }); it("should throw an error if the result has protocolErrors on it", async () => { const link = mockObservableLink(); diff --git a/src/__tests__/local-state/export.ts b/src/__tests__/local-state/export.ts index ea3fb15ae5b..f0e5daf4d60 100644 --- a/src/__tests__/local-state/export.ts +++ b/src/__tests__/local-state/export.ts @@ -2,183 +2,164 @@ import gql from "graphql-tag"; import { print } from "graphql"; import { Observable } from "../../utilities"; -import { itAsync } from "../../testing"; import { ApolloLink } from "../../link/core"; import { ApolloClient } from "../../core"; import { InMemoryCache } from "../../cache"; -import { spyOnConsole } from "../../testing/internal"; +import { ObservableStream, spyOnConsole } from "../../testing/internal"; describe("@client @export tests", () => { - itAsync( - "should not break @client only queries when the @export directive is " + - "used", - (resolve, reject) => { - const query = gql` - { - field @client @export(as: "someVar") - } - `; + it("should not break @client only queries when the @export directive is used", async () => { + const query = gql` + { + field @client @export(as: "someVar") + } + `; - 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: { field: 1 }, - }); + cache.writeQuery({ + query, + data: { field: 1 }, + }); - return client.query({ query }).then(({ data }: any) => { - expect({ ...data }).toMatchObject({ field: 1 }); - resolve(); - }); - } - ); + const { data } = await client.query({ query }); - itAsync( - "should not break @client only queries when the @export directive is " + - "used on nested fields", - (resolve, reject) => { - const query = gql` - { - car @client { - engine { - torque @export(as: "torque") - } + expect(data).toEqual({ field: 1 }); + }); + + it("should not break @client only queries when the @export directive is used on nested fields", async () => { + const query = gql` + { + car @client { + engine { + torque @export(as: "torque") } } - `; + } + `; - 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: { - car: { - engine: { - cylinders: 8, - torque: 7200, - __typename: "Engine", - }, - __typename: "Car", + cache.writeQuery({ + query, + data: { + car: { + engine: { + cylinders: 8, + torque: 7200, + __typename: "Engine", }, + __typename: "Car", }, - }); + }, + }); - return client.query({ query }).then(({ data }: any) => { - expect({ ...data }).toMatchObject({ - car: { - engine: { - torque: 7200, - }, - }, - }); - resolve(); - }); - } - ); + const { data } = await client.query({ query }); - itAsync( - "should store the @client field value in the specified @export " + - "variable, and make it available to a subsequent resolver", - (resolve, reject) => { - const query = gql` - query currentAuthorPostCount($authorId: Int!) { - currentAuthorId @client @export(as: "authorId") - postCount(authorId: $authorId) @client - } - `; + expect(data).toEqual({ + car: { + __typename: "Car", + engine: { + __typename: "Engine", + torque: 7200, + }, + }, + }); + }); - const testAuthorId = 100; - const testPostCount = 200; + it("should store the @client field value in the specified @export variable, and make it available to a subsequent resolver", async () => { + const query = gql` + query currentAuthorPostCount($authorId: Int!) { + currentAuthorId @client @export(as: "authorId") + postCount(authorId: $authorId) @client + } + `; - const cache = new InMemoryCache(); - const client = new ApolloClient({ - cache, - resolvers: { - Query: { - postCount(_, { authorId }) { - return authorId === testAuthorId ? testPostCount : 0; - }, + const testAuthorId = 100; + const testPostCount = 200; + + const cache = new InMemoryCache(); + const client = new ApolloClient({ + cache, + resolvers: { + Query: { + postCount(_, { authorId }) { + return authorId === testAuthorId ? testPostCount : 0; }, }, - }); + }, + }); - cache.writeQuery({ - query, - data: { - currentAuthorId: testAuthorId, - }, - }); + cache.writeQuery({ + query, + data: { + currentAuthorId: testAuthorId, + }, + }); - return client.query({ query }).then(({ data }: any) => { - expect({ ...data }).toMatchObject({ - currentAuthorId: testAuthorId, - postCount: testPostCount, - }); - resolve(); - }); - } - ); + const { data } = await client.query({ query }); - itAsync( - "should store the @client nested field value in the specified @export " + - "variable, and make it avilable to a subsequent resolver", - (resolve, reject) => { - const query = gql` - query currentAuthorPostCount($authorId: Int!) { - currentAuthor @client { - name - authorId @export(as: "authorId") - } - postCount(authorId: $authorId) @client + expect(data).toEqual({ + currentAuthorId: testAuthorId, + postCount: testPostCount, + }); + }); + + it("should store the @client nested field value in the specified @export variable, and make it avilable to a subsequent resolver", async () => { + const query = gql` + query currentAuthorPostCount($authorId: Int!) { + currentAuthor @client { + name + authorId @export(as: "authorId") } - `; + postCount(authorId: $authorId) @client + } + `; - const testAuthor = { - name: "John Smith", - authorId: 100, - __typename: "Author", - }; + const testAuthor = { + name: "John Smith", + authorId: 100, + __typename: "Author", + }; - const testPostCount = 200; + const testPostCount = 200; - const cache = new InMemoryCache(); - const client = new ApolloClient({ - cache, - resolvers: { - Query: { - postCount(_, { authorId }) { - return authorId === testAuthor.authorId ? testPostCount : 0; - }, + const cache = new InMemoryCache(); + const client = new ApolloClient({ + cache, + resolvers: { + Query: { + postCount(_, { authorId }) { + return authorId === testAuthor.authorId ? testPostCount : 0; }, }, - }); + }, + }); - cache.writeQuery({ - query, - data: { - currentAuthor: testAuthor, - }, - }); + cache.writeQuery({ + query, + data: { + currentAuthor: testAuthor, + }, + }); - return client.query({ query }).then(({ data }: any) => { - expect({ ...data }).toMatchObject({ - currentAuthor: testAuthor, - postCount: testPostCount, - }); - resolve(); - }); - } - ); + const { data } = await client.query({ query }); + + expect({ ...data }).toMatchObject({ + currentAuthor: testAuthor, + postCount: testPostCount, + }); + }); it("should allow @client @export variables to be used with remote queries", async () => { using _consoleSpies = spyOnConsole.takeSnapshots("error"); @@ -233,160 +214,154 @@ describe("@client @export tests", () => { }); }); - itAsync( - "should support @client @export variables that are nested multiple " + - "levels deep", - (resolve, reject) => { - const query = gql` - query currentAuthorPostCount($authorId: Int!) { - appContainer @client { - systemDetails { - currentAuthor { - name - authorId @export(as: "authorId") - } + it("should support @client @export variables that are nested multiple levels deep", async () => { + const query = gql` + query currentAuthorPostCount($authorId: Int!) { + appContainer @client { + systemDetails { + currentAuthor { + name + authorId @export(as: "authorId") } } - postCount(authorId: $authorId) } - `; + postCount(authorId: $authorId) + } + `; - const appContainer = { - systemDetails: { - currentAuthor: { - name: "John Smith", - authorId: 100, - __typename: "Author", - }, - __typename: "SystemDetails", + const appContainer = { + systemDetails: { + currentAuthor: { + name: "John Smith", + authorId: 100, + __typename: "Author", }, - __typename: "AppContainer", - }; + __typename: "SystemDetails", + }, + __typename: "AppContainer", + }; - const testPostCount = 200; + const testPostCount = 200; - const link = new ApolloLink(() => - Observable.of({ - data: { - postCount: testPostCount, - }, - }) - ); + const link = new ApolloLink(() => + Observable.of({ + data: { + postCount: testPostCount, + }, + }) + ); - const cache = new InMemoryCache(); - const client = new ApolloClient({ - cache, - link, - resolvers: {}, - }); + const cache = new InMemoryCache(); + const client = new ApolloClient({ + cache, + link, + resolvers: {}, + }); + { + using _ = spyOnConsole("error"); cache.writeQuery({ query, data: { appContainer, }, }); - - return client.query({ query }).then(({ data }: any) => { - expect({ ...data }).toMatchObject({ - appContainer, - postCount: testPostCount, - }); - resolve(); - }); } - ); - itAsync( - "should ignore @export directives if not used with @client", - (resolve, reject) => { - const query = gql` - query currentAuthorPostCount($authorId: Int!) { - currentAuthor { - name - authorId @export(as: "authorId") - } - postCount(authorId: $authorId) - } - `; + const { data } = await client.query({ query }); - const testAuthor = { - name: "John Smith", - authorId: 100, - __typename: "Author", - }; - const testPostCount = 200; + expect(data).toEqual({ + appContainer, + postCount: testPostCount, + }); + }); - const link = new ApolloLink(() => - Observable.of({ - data: { - currentAuthor: testAuthor, - postCount: testPostCount, - }, - }) - ); + it("should ignore @export directives if not used with @client", async () => { + const query = gql` + query currentAuthorPostCount($authorId: Int!) { + currentAuthor { + name + authorId @export(as: "authorId") + } + postCount(authorId: $authorId) + } + `; - const client = new ApolloClient({ - cache: new InMemoryCache(), - link, - resolvers: {}, - }); + const testAuthor = { + name: "John Smith", + authorId: 100, + __typename: "Author", + }; + const testPostCount = 200; - return client.query({ query }).then(({ data }: any) => { - expect({ ...data }).toMatchObject({ + const link = new ApolloLink(() => + Observable.of({ + data: { currentAuthor: testAuthor, postCount: testPostCount, - }); - resolve(); - }); - } - ); + }, + }) + ); - itAsync( - "should support setting an @client @export variable, loaded from the " + - "cache, on a virtual field that is combined into a remote query.", - (resolve, reject) => { - const query = gql` - query postRequiringReview($reviewerId: Int!) { - postRequiringReview { - id - title - loggedInReviewerId @client @export(as: "reviewerId") - } - reviewerDetails(reviewerId: $reviewerId) { - name - } + const client = new ApolloClient({ + cache: new InMemoryCache(), + link, + resolvers: {}, + }); + + const { data } = await client.query({ query }); + + expect(data).toEqual({ + currentAuthor: testAuthor, + postCount: testPostCount, + }); + }); + + it("should support setting an @client @export variable, loaded from the cache, on a virtual field that is combined into a remote query.", async () => { + const query = gql` + query postRequiringReview($reviewerId: Int!) { + postRequiringReview { + id + title + loggedInReviewerId @client @export(as: "reviewerId") } - `; + reviewerDetails(reviewerId: $reviewerId) { + name + } + } + `; - const postRequiringReview = { - id: 10, - title: "The Local State Conundrum", - __typename: "Post", - }; - const reviewerDetails = { - name: "John Smith", - __typename: "Reviewer", - }; - const loggedInReviewerId = 100; + const postRequiringReview = { + id: 10, + title: "The Local State Conundrum", + __typename: "Post", + }; + const reviewerDetails = { + name: "John Smith", + __typename: "Reviewer", + }; + const loggedInReviewerId = 100; - const link = new ApolloLink(({ variables }) => { - expect(variables).toMatchObject({ reviewerId: loggedInReviewerId }); - return Observable.of({ - data: { - postRequiringReview, - reviewerDetails, - }, - }); - }).setOnError(reject); + const link = new ApolloLink(({ variables }) => { + expect(variables).toMatchObject({ reviewerId: loggedInReviewerId }); - const cache = new InMemoryCache(); - const client = new ApolloClient({ - cache, - link, - resolvers: {}, + return Observable.of({ + data: { + postRequiringReview, + reviewerDetails, + }, }); + }); + const cache = new InMemoryCache(); + const client = new ApolloClient({ + cache, + link, + resolvers: {}, + }); + + { + using _ = spyOnConsole("error"); cache.writeQuery({ query, data: { @@ -397,79 +372,76 @@ describe("@client @export tests", () => { }, }, }); - - return client - .query({ query }) - .then(({ data }: any) => { - expect({ ...data }).toMatchObject({ - postRequiringReview: { - id: postRequiringReview.id, - title: postRequiringReview.title, - loggedInReviewerId, - }, - reviewerDetails, - }); - }) - .then(resolve, reject); } - ); - itAsync( - "should support setting a @client @export variable, loaded via a " + - "local resolver, on a virtual field that is combined into a remote query.", - (resolve, reject) => { - const query = gql` - query postRequiringReview($reviewerId: Int!) { - postRequiringReview { - id - title - currentReviewer @client { - id @export(as: "reviewerId") - } - } - reviewerDetails(reviewerId: $reviewerId) { - name + const { data } = await client.query({ query }); + + expect(data).toEqual({ + postRequiringReview: { + __typename: "Post", + id: postRequiringReview.id, + title: postRequiringReview.title, + loggedInReviewerId, + }, + reviewerDetails, + }); + }); + + it("should support setting a @client @export variable, loaded via a local resolver, on a virtual field that is combined into a remote query.", async () => { + const query = gql` + query postRequiringReview($reviewerId: Int!) { + postRequiringReview { + id + title + currentReviewer @client { + id @export(as: "reviewerId") } } - `; + reviewerDetails(reviewerId: $reviewerId) { + name + } + } + `; - const postRequiringReview = { - id: 10, - title: "The Local State Conundrum", - __typename: "Post", - }; - const reviewerDetails = { - name: "John Smith", - __typename: "Reviewer", - }; - const currentReviewer = { - id: 100, - __typename: "CurrentReviewer", - }; + const postRequiringReview = { + id: 10, + title: "The Local State Conundrum", + __typename: "Post", + }; + const reviewerDetails = { + name: "John Smith", + __typename: "Reviewer", + }; + const currentReviewer = { + id: 100, + __typename: "CurrentReviewer", + }; - const link = new ApolloLink(({ variables }) => { - expect(variables).toMatchObject({ reviewerId: currentReviewer.id }); - return Observable.of({ - data: { - postRequiringReview, - reviewerDetails, - }, - }); + const link = new ApolloLink(({ variables }) => { + expect(variables).toMatchObject({ reviewerId: currentReviewer.id }); + return Observable.of({ + data: { + postRequiringReview, + reviewerDetails, + }, }); - - const cache = new InMemoryCache(); - const client = new ApolloClient({ - cache, - link, - resolvers: { - Post: { - currentReviewer() { - return currentReviewer; - }, + }); + + const cache = new InMemoryCache(); + const client = new ApolloClient({ + cache, + link, + resolvers: { + Post: { + currentReviewer() { + return currentReviewer; }, }, - }); + }, + }); + { + using _ = spyOnConsole("error"); cache.writeQuery({ query, data: { @@ -478,130 +450,120 @@ describe("@client @export tests", () => { }, }, }); - - return client.query({ query }).then(({ data }: any) => { - expect({ ...data }).toMatchObject({ - postRequiringReview: { - id: postRequiringReview.id, - title: postRequiringReview.title, - currentReviewer, - }, - reviewerDetails, - }); - resolve(); - }); } - ); - - itAsync( - "should support combining @client @export variables, calculated by a " + - "local resolver, with remote mutations", - (resolve, reject) => { - const mutation = gql` - mutation upvotePost($postId: Int!) { - topPost @client @export(as: "postId") - upvotePost(postId: $postId) { - title - votes - } + + const { data } = await client.query({ query }); + + expect(data).toMatchObject({ + postRequiringReview: { + id: postRequiringReview.id, + title: postRequiringReview.title, + currentReviewer, + }, + reviewerDetails, + }); + }); + + it("should support combining @client @export variables, calculated by a local resolver, with remote mutations", async () => { + const mutation = gql` + mutation upvotePost($postId: Int!) { + topPost @client @export(as: "postId") + upvotePost(postId: $postId) { + title + votes } - `; + } + `; - const testPostId = 100; - const testPost = { - title: "The Day of the Jackal", - votes: 10, - __typename: "post", - }; + const testPostId = 100; + const testPost = { + title: "The Day of the Jackal", + votes: 10, + __typename: "post", + }; - const link = new ApolloLink(({ variables }) => { - expect(variables).toMatchObject({ postId: testPostId }); - return Observable.of({ - data: { - upvotePost: testPost, - }, - }); + const link = new ApolloLink(({ variables }) => { + expect(variables).toMatchObject({ postId: testPostId }); + return Observable.of({ + data: { + upvotePost: testPost, + }, }); + }); - const client = new ApolloClient({ - cache: new InMemoryCache(), - link, - resolvers: { - Mutation: { - topPost() { - return testPostId; - }, + const client = new ApolloClient({ + cache: new InMemoryCache(), + link, + resolvers: { + Mutation: { + topPost() { + return testPostId; }, }, - }); + }, + }); - return client.mutate({ mutation }).then(({ data }: any) => { - expect({ ...data }).toMatchObject({ - upvotePost: testPost, - }); - resolve(); - }); - } - ); - - itAsync( - "should support combining @client @export variables, calculated by " + - "reading from the cache, with remote mutations", - (resolve, reject) => { - const mutation = gql` - mutation upvotePost($postId: Int!) { - topPost @client @export(as: "postId") - upvotePost(postId: $postId) { - title - votes - } - } - `; + const { data } = await client.mutate({ mutation }); - const testPostId = 100; - const testPost = { - title: "The Day of the Jackal", - votes: 10, - __typename: "post", - }; + expect(data).toEqual({ + topPost: 100, + upvotePost: testPost, + }); + }); - const link = new ApolloLink(({ variables }) => { - expect(variables).toMatchObject({ postId: testPostId }); - return Observable.of({ - data: { - upvotePost: testPost, - }, - }); - }); + it("should support combining @client @export variables, calculated by reading from the cache, with remote mutations", async () => { + const mutation = gql` + mutation upvotePost($postId: Int!) { + topPost @client @export(as: "postId") + upvotePost(postId: $postId) { + title + votes + } + } + `; - const cache = new InMemoryCache(); - const client = new ApolloClient({ - cache, - link, - resolvers: {}, - }); + const testPostId = 100; + const testPost = { + title: "The Day of the Jackal", + votes: 10, + __typename: "post", + }; - cache.writeQuery({ - query: gql` - { - topPost - } - `, + const link = new ApolloLink(({ variables }) => { + expect(variables).toMatchObject({ postId: testPostId }); + return Observable.of({ data: { - topPost: testPostId, + upvotePost: testPost, }, }); + }); - return client.mutate({ mutation }).then(({ data }: any) => { - expect({ ...data }).toMatchObject({ - upvotePost: testPost, - }); - resolve(); - }); - } - ); + const cache = new InMemoryCache(); + const client = new ApolloClient({ + cache, + link, + resolvers: {}, + }); + + cache.writeQuery({ + query: gql` + { + topPost + } + `, + data: { + topPost: testPostId, + }, + }); + + const { data } = await client.mutate({ mutation }); - it("should not add __typename to @export-ed objects (#4691)", () => { + expect(data).toEqual({ + upvotePost: testPost, + }); + }); + + it("should not add __typename to @export-ed objects (#4691)", async () => { const query = gql` query GetListItems($where: LessonFilter) { currentFilter @client @export(as: "where") { @@ -666,51 +628,50 @@ describe("@client @export tests", () => { }, }); - return client.query({ query }).then((result) => { - expect(result.data).toEqual({ - currentFilter, - ...data, - }); + const result = await client.query({ query }); + + expect(result.data).toEqual({ + currentFilter, + ...data, }); }); - itAsync( - "should use the value of the last @export variable defined, if multiple " + - "variables are defined with the same name", - (resolve, reject) => { - const query = gql` - query reviewerPost($reviewerId: Int!) { - primaryReviewerId @client @export(as: "reviewerId") - secondaryReviewerId @client @export(as: "reviewerId") - post(reviewerId: $reviewerId) { - title - } + it("should use the value of the last @export variable defined, if multiple variables are defined with the same name", async () => { + const query = gql` + query reviewerPost($reviewerId: Int!) { + primaryReviewerId @client @export(as: "reviewerId") + secondaryReviewerId @client @export(as: "reviewerId") + post(reviewerId: $reviewerId) { + title } - `; + } + `; - const post = { - title: "The One Post to Rule Them All", - __typename: "Post", - }; - const primaryReviewerId = 100; - const secondaryReviewerId = 200; + const post = { + title: "The One Post to Rule Them All", + __typename: "Post", + }; + const primaryReviewerId = 100; + const secondaryReviewerId = 200; - const link = new ApolloLink(({ variables }) => { - expect(variables).toMatchObject({ reviewerId: secondaryReviewerId }); - return Observable.of({ - data: { - post, - }, - }); + const link = new ApolloLink(({ variables }) => { + expect(variables).toMatchObject({ reviewerId: secondaryReviewerId }); + return Observable.of({ + data: { + post, + }, }); + }); - const cache = new InMemoryCache(); - const client = new ApolloClient({ - cache, - link, - resolvers: {}, - }); + const cache = new InMemoryCache(); + const client = new ApolloClient({ + cache, + link, + resolvers: {}, + }); + { + using _ = spyOnConsole("error"); cache.writeQuery({ query, data: { @@ -718,304 +679,268 @@ describe("@client @export tests", () => { secondaryReviewerId, }, }); - - return client.query({ query }).then(({ data }: any) => { - expect({ ...data }).toMatchObject({ - post, - }); - resolve(); - }); } - ); - - it( - "should refetch if an @export variable changes, the current fetch " + - "policy is not cache-only, and the query includes fields that need to " + - "be resolved remotely", - async () => { - using _consoleSpies = spyOnConsole.takeSnapshots("error"); - await new Promise((resolve, reject) => { - const query = gql` - query currentAuthorPostCount($authorId: Int!) { - currentAuthorId @client @export(as: "authorId") - postCount(authorId: $authorId) - } - `; - const testAuthorId1 = 100; - const testPostCount1 = 200; + const { data } = await client.query({ query }); + + expect(data).toEqual({ + post, + primaryReviewerId, + secondaryReviewerId, + }); + }); + + it("should refetch if an @export variable changes, the current fetch policy is not cache-only, and the query includes fields that need to be resolved remotely", async () => { + using _consoleSpies = spyOnConsole.takeSnapshots("error"); + const query = gql` + query currentAuthorPostCount($authorId: Int!) { + currentAuthorId @client @export(as: "authorId") + postCount(authorId: $authorId) + } + `; - const testAuthorId2 = 101; - const testPostCount2 = 201; + const testAuthorId1 = 100; + const testPostCount1 = 200; - let resultCount = 0; + const testAuthorId2 = 101; + const testPostCount2 = 201; - const link = new ApolloLink(() => - Observable.of({ - data: { - postCount: resultCount === 0 ? testPostCount1 : testPostCount2, - }, - }) - ); + let currentAuthorId = testAuthorId1; - const cache = new InMemoryCache(); - const client = new ApolloClient({ - cache, - link, - resolvers: {}, - }); + const link = new ApolloLink(() => + Observable.of({ + data: { + postCount: + currentAuthorId === testAuthorId1 ? testPostCount1 : testPostCount2, + }, + }) + ); - client.writeQuery({ - query, - data: { currentAuthorId: testAuthorId1 }, - }); + const cache = new InMemoryCache(); + const client = new ApolloClient({ + cache, + link, + resolvers: {}, + }); - const obs = client.watchQuery({ query }); - obs.subscribe({ - next({ data }) { - if (resultCount === 0) { - expect({ ...data }).toMatchObject({ - currentAuthorId: testAuthorId1, - postCount: testPostCount1, - }); - client.writeQuery({ - query, - data: { currentAuthorId: testAuthorId2 }, - }); - } else if (resultCount === 1) { - expect({ ...data }).toMatchObject({ - currentAuthorId: testAuthorId2, - postCount: testPostCount2, - }); - resolve(); - } - resultCount += 1; - }, - }); - }); - } - ); - - it( - "should NOT refetch if an @export variable has not changed, the " + - "current fetch policy is not cache-only, and the query includes fields " + - "that need to be resolved remotely", - async () => { - using _consoleSpies = spyOnConsole.takeSnapshots("error"); - await new Promise((resolve, reject) => { - const query = gql` - query currentAuthorPostCount($authorId: Int!) { - currentAuthorId @client @export(as: "authorId") - postCount(authorId: $authorId) - } - `; + client.writeQuery({ + query, + data: { currentAuthorId }, + }); - const testAuthorId1 = 100; - const testPostCount1 = 200; + const obs = client.watchQuery({ query }); + const stream = new ObservableStream(obs); - const testPostCount2 = 201; + await expect(stream).toEmitMatchedValue({ + data: { + currentAuthorId: testAuthorId1, + postCount: testPostCount1, + }, + }); - let resultCount = 0; + currentAuthorId = testAuthorId2; + client.writeQuery({ + query, + data: { currentAuthorId }, + }); - let fetchCount = 0; - const link = new ApolloLink(() => { - fetchCount += 1; - return Observable.of({ - data: { - postCount: testPostCount1, - }, - }); - }); + await expect(stream).toEmitMatchedValue({ + data: { + currentAuthorId: testAuthorId2, + postCount: testPostCount2, + }, + }); + }); - const cache = new InMemoryCache(); - const client = new ApolloClient({ - cache, - link, - resolvers: {}, - }); + it("should NOT refetch if an @export variable has not changed, the current fetch policy is not cache-only, and the query includes fields that need to be resolved remotely", async () => { + using _consoleSpies = spyOnConsole.takeSnapshots("error"); + const query = gql` + query currentAuthorPostCount($authorId: Int!) { + currentAuthorId @client @export(as: "authorId") + postCount(authorId: $authorId) + } + `; - client.writeQuery({ - query, - data: { currentAuthorId: testAuthorId1 }, - }); + const testAuthorId1 = 100; + const testPostCount1 = 200; - const obs = client.watchQuery({ query }); - obs.subscribe({ - next(result) { - if (resultCount === 0) { - expect(fetchCount).toBe(1); - expect(result.data).toMatchObject({ - currentAuthorId: testAuthorId1, - postCount: testPostCount1, - }); - - client.writeQuery({ - query, - variables: { authorId: testAuthorId1 }, - data: { postCount: testPostCount2 }, - }); - } else if (resultCount === 1) { - // Should not have refetched - expect(fetchCount).toBe(1); - resolve(); - } + const testPostCount2 = 201; - resultCount += 1; - }, - }); + let fetchCount = 0; + const link = new ApolloLink(() => { + fetchCount += 1; + return Observable.of({ + data: { + postCount: testPostCount1, + }, }); - } - ); - - itAsync( - "should NOT attempt to refetch over the network if an @export variable " + - "has changed, the current fetch policy is cache-first, and the remote " + - "part of the query (that leverages the @export variable) can be fully " + - "found in the cache.", - (resolve, reject) => { - const query = gql` - query currentAuthorPostCount($authorId: Int!) { - currentAuthorId @client @export(as: "authorId") - postCount(authorId: $authorId) - } - `; + }); - const testAuthorId1 = 1; - const testPostCount1 = 100; + const cache = new InMemoryCache(); + const client = new ApolloClient({ + cache, + link, + resolvers: {}, + }); - const testAuthorId2 = 2; - const testPostCount2 = 200; + client.writeQuery({ + query, + data: { currentAuthorId: testAuthorId1 }, + }); - let fetchCount = 0; - const link = new ApolloLink(() => { - fetchCount += 1; - return Observable.of({ - data: { - postCount: testPostCount1, - }, - }); - }); + const obs = client.watchQuery({ query }); + const stream = new ObservableStream(obs); - const cache = new InMemoryCache(); - const client = new ApolloClient({ - cache, - link, - resolvers: {}, - }); + await expect(stream).toEmitMatchedValue({ + data: { + currentAuthorId: testAuthorId1, + postCount: testPostCount1, + }, + }); + expect(fetchCount).toBe(1); - client.writeQuery({ - query: gql` - { - currentAuthorId - } - `, - data: { currentAuthorId: testAuthorId1 }, - }); + client.writeQuery({ + query, + variables: { authorId: testAuthorId1 }, + data: { postCount: testPostCount2 }, + }); - let resultCount = 0; - const obs = client.watchQuery({ query, fetchPolicy: "cache-first" }); - obs.subscribe({ - next(result) { - if (resultCount === 0) { - // The initial result is fetched over the network. - expect(fetchCount).toBe(1); - expect(result.data).toMatchObject({ - currentAuthorId: testAuthorId1, - postCount: testPostCount1, - }); - - client.writeQuery({ - query, - variables: { authorId: testAuthorId2 }, - data: { postCount: testPostCount2 }, - }); - client.writeQuery({ - query: gql` - { - currentAuthorId - } - `, - data: { currentAuthorId: testAuthorId2 }, - }); - } else if (resultCount === 1) { - // The updated result should not have been fetched over the - // network, as it can be found in the cache. - expect(fetchCount).toBe(1); - expect(result.data).toMatchObject({ - currentAuthorId: testAuthorId2, - postCount: testPostCount2, - }); - resolve(); - } + await expect(stream).toEmitNext(); + expect(fetchCount).toBe(1); + }); + + it("should NOT attempt to refetch over the network if an @export variable has changed, the current fetch policy is cache-first, and the remote part of the query (that leverages the @export variable) can be fully found in the cache.", async () => { + const query = gql` + query currentAuthorPostCount($authorId: Int!) { + currentAuthorId @client @export(as: "authorId") + postCount(authorId: $authorId) + } + `; + + const testAuthorId1 = 1; + const testPostCount1 = 100; - resultCount += 1; + const testAuthorId2 = 2; + const testPostCount2 = 200; + + let fetchCount = 0; + const link = new ApolloLink(() => { + fetchCount += 1; + return Observable.of({ + data: { + postCount: testPostCount1, }, }); - } - ); + }); - itAsync( - "should update @client @export variables on each broadcast if they've " + - "changed", - (resolve, reject) => { - const cache = new InMemoryCache(); + const cache = new InMemoryCache(); + const client = new ApolloClient({ + cache, + link, + resolvers: {}, + }); - const widgetCountQuery = gql` + client.writeQuery({ + query: gql` { - widgetCount @client + currentAuthorId } - `; - cache.writeQuery({ - query: widgetCountQuery, - data: { - widgetCount: 100, - }, - }); + `, + data: { currentAuthorId: testAuthorId1 }, + }); - const client = new ApolloClient({ - cache, - resolvers: { - Query: { - doubleWidgets(_, { widgetCount }) { - return widgetCount ? widgetCount * 2 : 0; - }, - }, - }, - }); + const obs = client.watchQuery({ query, fetchPolicy: "cache-first" }); + const stream = new ObservableStream(obs); - const doubleWidgetsQuery = gql` - query DoubleWidgets($widgetCount: Int!) { - widgetCount @client @export(as: "widgetCount") - doubleWidgets(widgetCount: $widgetCount) @client + await expect(stream).toEmitMatchedValue({ + data: { + currentAuthorId: testAuthorId1, + postCount: testPostCount1, + }, + }); + // The initial result is fetched over the network. + expect(fetchCount).toBe(1); + + client.writeQuery({ + query, + variables: { authorId: testAuthorId2 }, + data: { postCount: testPostCount2 }, + }); + client.writeQuery({ + query: gql` + { + currentAuthorId } - `; + `, + data: { currentAuthorId: testAuthorId2 }, + }); - let count = 0; - const obs = client.watchQuery({ query: doubleWidgetsQuery }); - obs.subscribe({ - next({ data }) { - switch (count) { - case 0: - expect(data.widgetCount).toEqual(100); - expect(data.doubleWidgets).toEqual(200); - - client.writeQuery({ - query: widgetCountQuery, - data: { - widgetCount: 500, - }, - }); - break; - case 1: - expect(data.widgetCount).toEqual(500); - expect(data.doubleWidgets).toEqual(1000); - resolve(); - break; - default: - } - count += 1; + await expect(stream).toEmitMatchedValue({ + data: { + currentAuthorId: testAuthorId2, + postCount: testPostCount2, + }, + }); + // The updated result should not have been fetched over the + // network, as it can be found in the cache. + expect(fetchCount).toBe(1); + }); + + it("should update @client @export variables on each broadcast if they've changed", async () => { + const cache = new InMemoryCache(); + + const widgetCountQuery = gql` + { + widgetCount @client + } + `; + cache.writeQuery({ + query: widgetCountQuery, + data: { + widgetCount: 100, + }, + }); + + const client = new ApolloClient({ + cache, + resolvers: { + Query: { + doubleWidgets(_, { widgetCount }) { + return widgetCount ? widgetCount * 2 : 0; + }, }, - }); - } - ); + }, + }); + + const doubleWidgetsQuery = gql` + query DoubleWidgets($widgetCount: Int!) { + widgetCount @client @export(as: "widgetCount") + doubleWidgets(widgetCount: $widgetCount) @client + } + `; + + const obs = client.watchQuery({ query: doubleWidgetsQuery }); + const stream = new ObservableStream(obs); + + await expect(stream).toEmitMatchedValue({ + data: { + widgetCount: 100, + doubleWidgets: 200, + }, + }); + + client.writeQuery({ + query: widgetCountQuery, + data: { + widgetCount: 500, + }, + }); + + await expect(stream).toEmitMatchedValue({ + data: { + widgetCount: 500, + doubleWidgets: 1000, + }, + }); + }); }); diff --git a/src/__tests__/local-state/subscriptions.ts b/src/__tests__/local-state/subscriptions.ts index d331cd4fb42..9a3c94edd49 100644 --- a/src/__tests__/local-state/subscriptions.ts +++ b/src/__tests__/local-state/subscriptions.ts @@ -4,10 +4,10 @@ import { Observable } from "../../utilities"; import { ApolloLink } from "../../link/core"; import { ApolloClient } from "../../core"; import { InMemoryCache } from "../../cache"; -import { itAsync } from "../../testing"; +import { ObservableStream } from "../../testing/internal"; describe("Basic functionality", () => { - itAsync("should not break subscriptions", (resolve, reject) => { + it("should not break subscriptions", async () => { const query = gql` subscription { field @@ -28,65 +28,43 @@ describe("Basic functionality", () => { }, }); - let counter = 0; - expect.assertions(2); - client.subscribe({ query }).forEach((item) => { - expect(item).toMatchObject({ data: { field: ++counter } }); - if (counter === 2) { - resolve(); - } - }); + const stream = new ObservableStream(client.subscribe({ query })); + + await expect(stream).toEmitValue({ data: { field: 1 } }); + await expect(stream).toEmitValue({ data: { field: 2 } }); + await expect(stream).toComplete(); }); - itAsync( - "should be able to mix @client fields with subscription results", - (resolve, reject) => { - const query = gql` - subscription { - field - count @client - } - `; + it("should be able to mix @client fields with subscription results", async () => { + const query = gql` + subscription { + field + count @client + } + `; - const link = new ApolloLink(() => - Observable.of({ data: { field: 1 } }, { data: { field: 2 } }) - ); + const link = new ApolloLink(() => + Observable.of({ data: { field: 1 } }, { data: { field: 2 } }) + ); - let subCounter = 0; - const client = new ApolloClient({ - cache: new InMemoryCache(), - link, - resolvers: { - Subscription: { - count: () => { - subCounter += 1; - return subCounter; - }, + let subCounter = 0; + const client = new ApolloClient({ + cache: new InMemoryCache(), + link, + resolvers: { + Subscription: { + count: () => { + subCounter += 1; + return subCounter; }, }, - }); + }, + }); - expect.assertions(2); - const obs = client.subscribe({ query }); - let resultCounter = 1; - obs.subscribe({ - next(result) { - try { - expect(result).toMatchObject({ - data: { - field: resultCounter, - count: resultCounter, - }, - }); - } catch (error) { - reject(error); - } - resultCounter += 1; - }, - complete() { - resolve(); - }, - }); - } - ); + const stream = new ObservableStream(client.subscribe({ query })); + + await expect(stream).toEmitValue({ data: { field: 1, count: 1 } }); + await expect(stream).toEmitValue({ data: { field: 2, count: 2 } }); + await expect(stream).toComplete(); + }); }); diff --git a/src/__tests__/optimistic.ts b/src/__tests__/optimistic.ts index ed53dc8cf9c..f2b20a94e84 100644 --- a/src/__tests__/optimistic.ts +++ b/src/__tests__/optimistic.ts @@ -10,19 +10,17 @@ import { ApolloCache, MutationQueryReducersMap, TypedDocumentNode, + ApolloError, } from "../core"; import { QueryManager } from "../core/QueryManager"; import { Cache, InMemoryCache } from "../cache"; -import { - Observable, - ObservableSubscription as Subscription, - addTypenameToDocument, -} from "../utilities"; +import { Observable, addTypenameToDocument } from "../utilities"; -import { itAsync, mockSingleLink } from "../testing"; +import { MockedResponse, mockSingleLink } from "../testing"; +import { ObservableStream } from "../testing/internal"; describe("optimistic mutation results", () => { const query = gql` @@ -108,10 +106,7 @@ describe("optimistic mutation results", () => { }, }; - async function setup( - reject: (reason: any) => any, - ...mockedResponses: any[] - ) { + async function setup(...mockedResponses: MockedResponse[]) { const link = mockSingleLink( { request: { query }, @@ -213,223 +208,196 @@ describe("optimistic mutation results", () => { }, }; - itAsync( - "handles a single error for a single mutation", - async (resolve, reject) => { - expect.assertions(6); - const client = await setup(reject, { - request: { query: mutation }, - error: new Error("forbidden (test error)"), - }); - try { - const promise = client.mutate({ - mutation, - optimisticResponse, - updateQueries, - }); + it("handles a single error for a single mutation", async () => { + expect.assertions(5); + const client = await setup({ + request: { query: mutation }, + error: new Error("forbidden (test error)"), + }); + const promise = client.mutate({ + mutation, + optimisticResponse, + updateQueries, + }); - const dataInStore = (client.cache as InMemoryCache).extract(true); - expect((dataInStore["TodoList5"] as any).todos.length).toBe(4); - expect((dataInStore["Todo99"] as any).text).toBe( - "Optimistically generated" - ); - await promise; - } catch (err) { - expect(err).toBeInstanceOf(Error); - expect((err as Error).message).toBe("forbidden (test error)"); + { + const dataInStore = (client.cache as InMemoryCache).extract(true); + expect((dataInStore["TodoList5"] as any).todos.length).toBe(4); + expect((dataInStore["Todo99"] as any).text).toBe( + "Optimistically generated" + ); + } - const dataInStore = (client.cache as InMemoryCache).extract(true); - expect((dataInStore["TodoList5"] as any).todos.length).toBe(3); - expect(dataInStore).not.toHaveProperty("Todo99"); - } + await expect(promise).rejects.toThrow( + new ApolloError({ networkError: new Error("forbidden (test error)") }) + ); - resolve(); + { + const dataInStore = (client.cache as InMemoryCache).extract(true); + expect((dataInStore["TodoList5"] as any).todos.length).toBe(3); + expect(dataInStore).not.toHaveProperty("Todo99"); } - ); + }); - itAsync( - "handles errors produced by one mutation in a series", - async (resolve, reject) => { - expect.assertions(10); - let subscriptionHandle: Subscription; - const client = await setup( - reject, - { - request: { query: mutation }, - error: new Error("forbidden (test error)"), - }, - { - request: { query: mutation }, - result: mutationResult2, - } - ); + it("handles errors produced by one mutation in a series", async () => { + expect.assertions(11); + const client = await setup( + { + request: { query: mutation }, + error: new Error("forbidden (test error)"), + }, + { + request: { query: mutation }, + result: mutationResult2, + } + ); - // we have to actually subscribe to the query to be able to update it - await new Promise((resolve) => { - const handle = client.watchQuery({ query }); - subscriptionHandle = handle.subscribe({ - next(res: any) { - resolve(res); - }, - }); - }); + const stream = new ObservableStream(client.watchQuery({ query })); - const promise = client - .mutate({ - mutation, - optimisticResponse, - updateQueries, - }) - .catch((err: any) => { - // it is ok to fail here - expect(err).toBeInstanceOf(Error); - expect(err.message).toBe("forbidden (test error)"); - return null; - }); + await expect(stream).toEmitNext(); - const promise2 = client.mutate({ + const promise = client + .mutate({ mutation, - optimisticResponse: optimisticResponse2, + optimisticResponse, updateQueries, + }) + .catch((err: any) => { + // it is ok to fail here + expect(err).toBeInstanceOf(Error); + expect(err.message).toBe("forbidden (test error)"); + return null; }); + const promise2 = client.mutate({ + mutation, + optimisticResponse: optimisticResponse2, + updateQueries, + }); + + const dataInStore = (client.cache as InMemoryCache).extract(true); + expect((dataInStore["TodoList5"] as any).todos.length).toBe(5); + expect((dataInStore["Todo99"] as any).text).toBe( + "Optimistically generated" + ); + expect((dataInStore["Todo66"] as any).text).toBe( + "Optimistically generated 2" + ); + + await Promise.all([promise, promise2]); + + stream.unsubscribe(); + + { const dataInStore = (client.cache as InMemoryCache).extract(true); - expect((dataInStore["TodoList5"] as any).todos.length).toBe(5); - expect((dataInStore["Todo99"] as any).text).toBe( - "Optimistically generated" + expect((dataInStore["TodoList5"] as any).todos.length).toBe(4); + expect(dataInStore).not.toHaveProperty("Todo99"); + expect(dataInStore).toHaveProperty("Todo66"); + expect((dataInStore["TodoList5"] as any).todos).toContainEqual( + makeReference("Todo66") ); - expect((dataInStore["Todo66"] as any).text).toBe( - "Optimistically generated 2" + expect((dataInStore["TodoList5"] as any).todos).not.toContainEqual( + makeReference("Todo99") ); + } + }); - await Promise.all([promise, promise2]); - - subscriptionHandle!.unsubscribe(); + it("can run 2 mutations concurrently and handles all intermediate states well", async () => { + expect.assertions(35); + function checkBothMutationsAreApplied( + expectedText1: any, + expectedText2: any + ) { + const dataInStore = (client.cache as InMemoryCache).extract(true); + expect((dataInStore["TodoList5"] as any).todos.length).toBe(5); + expect(dataInStore).toHaveProperty("Todo99"); + expect(dataInStore).toHaveProperty("Todo66"); + // can be removed once @types/chai adds deepInclude + expect((dataInStore["TodoList5"] as any).todos).toContainEqual( + makeReference("Todo66") + ); + expect((dataInStore["TodoList5"] as any).todos).toContainEqual( + makeReference("Todo99") + ); + expect((dataInStore["Todo99"] as any).text).toBe(expectedText1); + expect((dataInStore["Todo66"] as any).text).toBe(expectedText2); + } + const client = await setup( { - const dataInStore = (client.cache as InMemoryCache).extract(true); - expect((dataInStore["TodoList5"] as any).todos.length).toBe(4); - expect(dataInStore).not.toHaveProperty("Todo99"); - expect(dataInStore).toHaveProperty("Todo66"); - expect((dataInStore["TodoList5"] as any).todos).toContainEqual( - makeReference("Todo66") - ); - expect((dataInStore["TodoList5"] as any).todos).not.toContainEqual( - makeReference("Todo99") - ); - resolve(); + request: { query: mutation }, + result: mutationResult, + }, + { + request: { query: mutation }, + result: mutationResult2, + // make sure it always happens later + delay: 100, } - } - ); + ); + const stream = new ObservableStream(client.watchQuery({ query })); - itAsync( - "can run 2 mutations concurrently and handles all intermediate states well", - async (resolve, reject) => { - expect.assertions(34); - function checkBothMutationsAreApplied( - expectedText1: any, - expectedText2: any - ) { - const dataInStore = (client.cache as InMemoryCache).extract(true); - expect((dataInStore["TodoList5"] as any).todos.length).toBe(5); - expect(dataInStore).toHaveProperty("Todo99"); - expect(dataInStore).toHaveProperty("Todo66"); - // can be removed once @types/chai adds deepInclude - expect((dataInStore["TodoList5"] as any).todos).toContainEqual( - makeReference("Todo66") - ); - expect((dataInStore["TodoList5"] as any).todos).toContainEqual( - makeReference("Todo99") + await expect(stream).toEmitNext(); + + const queryManager: QueryManager = (client as any).queryManager; + + const promise = client + .mutate({ + mutation, + optimisticResponse, + updateQueries, + }) + .then((res: any) => { + checkBothMutationsAreApplied( + "This one was created with a mutation.", + "Optimistically generated 2" ); - expect((dataInStore["Todo99"] as any).text).toBe(expectedText1); - expect((dataInStore["Todo66"] as any).text).toBe(expectedText2); - } - let subscriptionHandle: Subscription; - const client = await setup( - reject, - { - request: { query: mutation }, - result: mutationResult, - }, - { - request: { query: mutation }, - result: mutationResult2, - // make sure it always happens later - delay: 100, - } - ); - // we have to actually subscribe to the query to be able to update it - await new Promise((resolve) => { - const handle = client.watchQuery({ query }); - subscriptionHandle = handle.subscribe({ - next(res: any) { - resolve(res); - }, - }); - }); - const queryManager: QueryManager = (client as any).queryManager; + // @ts-ignore + const latestState = queryManager.mutationStore!; + expect(latestState[1].loading).toBe(false); + expect(latestState[2].loading).toBe(true); - const promise = client - .mutate({ - mutation, - optimisticResponse, - updateQueries, - }) - .then((res: any) => { - checkBothMutationsAreApplied( - "This one was created with a mutation.", - "Optimistically generated 2" - ); - - // @ts-ignore - const latestState = queryManager.mutationStore!; - expect(latestState[1].loading).toBe(false); - expect(latestState[2].loading).toBe(true); - - return res; - }); + return res; + }); - const promise2 = client - .mutate({ - mutation, - optimisticResponse: optimisticResponse2, - updateQueries, - }) - .then((res: any) => { - checkBothMutationsAreApplied( - "This one was created with a mutation.", - "Second mutation." - ); - - // @ts-ignore - const latestState = queryManager.mutationStore!; - expect(latestState[1].loading).toBe(false); - expect(latestState[2].loading).toBe(false); - - return res; - }); + const promise2 = client + .mutate({ + mutation, + optimisticResponse: optimisticResponse2, + updateQueries, + }) + .then((res: any) => { + checkBothMutationsAreApplied( + "This one was created with a mutation.", + "Second mutation." + ); - // @ts-ignore - const mutationsState = queryManager.mutationStore!; - expect(mutationsState[1].loading).toBe(true); - expect(mutationsState[2].loading).toBe(true); + // @ts-ignore + const latestState = queryManager.mutationStore!; + expect(latestState[1].loading).toBe(false); + expect(latestState[2].loading).toBe(false); - checkBothMutationsAreApplied( - "Optimistically generated", - "Optimistically generated 2" - ); + return res; + }); - await Promise.all([promise, promise2]); + // @ts-ignore + const mutationsState = queryManager.mutationStore!; + expect(mutationsState[1].loading).toBe(true); + expect(mutationsState[2].loading).toBe(true); - subscriptionHandle!.unsubscribe(); - checkBothMutationsAreApplied( - "This one was created with a mutation.", - "Second mutation." - ); + checkBothMutationsAreApplied( + "Optimistically generated", + "Optimistically generated 2" + ); - resolve(); - } - ); + await Promise.all([promise, promise2]); + + stream.unsubscribe(); + checkBothMutationsAreApplied( + "This one was created with a mutation.", + "Second mutation." + ); + }); }); describe("with `update`", () => { @@ -464,225 +432,195 @@ describe("optimistic mutation results", () => { }); }; - itAsync( - "handles a single error for a single mutation", - async (resolve, reject) => { - expect.assertions(6); - - const client = await setup(reject, { - request: { query: mutation }, - error: new Error("forbidden (test error)"), - }); + it("handles a single error for a single mutation", async () => { + expect.assertions(5); - try { - const promise = client.mutate({ - mutation, - optimisticResponse, - update, - }); + const client = await setup({ + request: { query: mutation }, + error: new Error("forbidden (test error)"), + }); - const dataInStore = (client.cache as InMemoryCache).extract(true); - expect((dataInStore["TodoList5"] as any).todos.length).toBe(4); - expect((dataInStore["Todo99"] as any).text).toBe( - "Optimistically generated" - ); + const promise = client.mutate({ + mutation, + optimisticResponse, + update, + }); - await promise; - } catch (err) { - expect(err).toBeInstanceOf(Error); - expect((err as Error).message).toBe("forbidden (test error)"); + { + const dataInStore = (client.cache as InMemoryCache).extract(true); + expect((dataInStore["TodoList5"] as any).todos.length).toBe(4); + expect((dataInStore["Todo99"] as any).text).toBe( + "Optimistically generated" + ); + } - const dataInStore = (client.cache as InMemoryCache).extract(true); - expect((dataInStore["TodoList5"] as any).todos.length).toBe(3); - expect(dataInStore).not.toHaveProperty("Todo99"); - } + await expect(promise).rejects.toThrow( + new ApolloError({ networkError: new Error("forbidden (test error)") }) + ); - resolve(); + { + const dataInStore = (client.cache as InMemoryCache).extract(true); + expect((dataInStore["TodoList5"] as any).todos.length).toBe(3); + expect(dataInStore).not.toHaveProperty("Todo99"); } - ); + }); - itAsync( - "handles errors produced by one mutation in a series", - async (resolve, reject) => { - expect.assertions(10); - let subscriptionHandle: Subscription; - const client = await setup( - reject, - { - request: { query: mutation }, - error: new Error("forbidden (test error)"), - }, - { - request: { query: mutation }, - result: mutationResult2, - } - ); + it("handles errors produced by one mutation in a series", async () => { + expect.assertions(11); + const client = await setup( + { + request: { query: mutation }, + error: new Error("forbidden (test error)"), + }, + { + request: { query: mutation }, + result: mutationResult2, + } + ); - // we have to actually subscribe to the query to be able to update it - await new Promise((resolve) => { - const handle = client.watchQuery({ query }); - subscriptionHandle = handle.subscribe({ - next(res: any) { - resolve(res); - }, - }); - }); + const stream = new ObservableStream(client.watchQuery({ query })); - const promise = client - .mutate({ - mutation, - optimisticResponse, - update, - }) - .catch((err: any) => { - // it is ok to fail here - expect(err).toBeInstanceOf(Error); - expect(err.message).toBe("forbidden (test error)"); - return null; - }); + await expect(stream).toEmitNext(); - const promise2 = client.mutate({ + const promise = client + .mutate({ mutation, - optimisticResponse: optimisticResponse2, + optimisticResponse, update, + }) + .catch((err: any) => { + // it is ok to fail here + expect(err).toBeInstanceOf(Error); + expect(err.message).toBe("forbidden (test error)"); + return null; }); + const promise2 = client.mutate({ + mutation, + optimisticResponse: optimisticResponse2, + update, + }); + + const dataInStore = (client.cache as InMemoryCache).extract(true); + expect((dataInStore["TodoList5"] as any).todos.length).toBe(5); + expect((dataInStore["Todo99"] as any).text).toBe( + "Optimistically generated" + ); + expect((dataInStore["Todo66"] as any).text).toBe( + "Optimistically generated 2" + ); + + await Promise.all([promise, promise2]); + + stream.unsubscribe(); + { const dataInStore = (client.cache as InMemoryCache).extract(true); - expect((dataInStore["TodoList5"] as any).todos.length).toBe(5); - expect((dataInStore["Todo99"] as any).text).toBe( - "Optimistically generated" + expect((dataInStore["TodoList5"] as any).todos.length).toBe(4); + expect(dataInStore).not.toHaveProperty("Todo99"); + expect(dataInStore).toHaveProperty("Todo66"); + expect((dataInStore["TodoList5"] as any).todos).toContainEqual( + makeReference("Todo66") ); - expect((dataInStore["Todo66"] as any).text).toBe( - "Optimistically generated 2" + expect((dataInStore["TodoList5"] as any).todos).not.toContainEqual( + makeReference("Todo99") ); + } + }); - await Promise.all([promise, promise2]); + it("can run 2 mutations concurrently and handles all intermediate states well", async () => { + expect.assertions(35); + function checkBothMutationsAreApplied( + expectedText1: any, + expectedText2: any + ) { + const dataInStore = (client.cache as InMemoryCache).extract(true); + expect((dataInStore["TodoList5"] as any).todos.length).toBe(5); + expect(dataInStore).toHaveProperty("Todo99"); + expect(dataInStore).toHaveProperty("Todo66"); + expect((dataInStore["TodoList5"] as any).todos).toContainEqual( + makeReference("Todo66") + ); + expect((dataInStore["TodoList5"] as any).todos).toContainEqual( + makeReference("Todo99") + ); + expect((dataInStore["Todo99"] as any).text).toBe(expectedText1); + expect((dataInStore["Todo66"] as any).text).toBe(expectedText2); + } - subscriptionHandle!.unsubscribe(); + const client = await setup( { - const dataInStore = (client.cache as InMemoryCache).extract(true); - expect((dataInStore["TodoList5"] as any).todos.length).toBe(4); - expect(dataInStore).not.toHaveProperty("Todo99"); - expect(dataInStore).toHaveProperty("Todo66"); - expect((dataInStore["TodoList5"] as any).todos).toContainEqual( - makeReference("Todo66") - ); - expect((dataInStore["TodoList5"] as any).todos).not.toContainEqual( - makeReference("Todo99") - ); - resolve(); + request: { query: mutation }, + result: mutationResult, + }, + { + request: { query: mutation }, + result: mutationResult2, + // make sure it always happens later + delay: 100, } - } - ); + ); + const stream = new ObservableStream(client.watchQuery({ query })); - itAsync( - "can run 2 mutations concurrently and handles all intermediate states well", - async (resolve, reject) => { - expect.assertions(34); - function checkBothMutationsAreApplied( - expectedText1: any, - expectedText2: any - ) { - const dataInStore = (client.cache as InMemoryCache).extract(true); - expect((dataInStore["TodoList5"] as any).todos.length).toBe(5); - expect(dataInStore).toHaveProperty("Todo99"); - expect(dataInStore).toHaveProperty("Todo66"); - expect((dataInStore["TodoList5"] as any).todos).toContainEqual( - makeReference("Todo66") - ); - expect((dataInStore["TodoList5"] as any).todos).toContainEqual( - makeReference("Todo99") + await expect(stream).toEmitNext(); + + const promise = client + .mutate({ + mutation, + optimisticResponse, + update, + }) + .then((res: any) => { + checkBothMutationsAreApplied( + "This one was created with a mutation.", + "Optimistically generated 2" ); - expect((dataInStore["Todo99"] as any).text).toBe(expectedText1); - expect((dataInStore["Todo66"] as any).text).toBe(expectedText2); - } - let subscriptionHandle: Subscription; - const client = await setup( - reject, - { - request: { query: mutation }, - result: mutationResult, - }, - { - request: { query: mutation }, - result: mutationResult2, - // make sure it always happens later - delay: 100, - } - ); + // @ts-ignore + const latestState = client.queryManager.mutationStore!; + expect(latestState[1].loading).toBe(false); + expect(latestState[2].loading).toBe(true); - // we have to actually subscribe to the query to be able to update it - await new Promise((resolve) => { - const handle = client.watchQuery({ query }); - subscriptionHandle = handle.subscribe({ - next(res: any) { - resolve(res); - }, - }); + return res; }); - const promise = client - .mutate({ - mutation, - optimisticResponse, - update, - }) - .then((res: any) => { - checkBothMutationsAreApplied( - "This one was created with a mutation.", - "Optimistically generated 2" - ); - - // @ts-ignore - const latestState = client.queryManager.mutationStore!; - expect(latestState[1].loading).toBe(false); - expect(latestState[2].loading).toBe(true); - - return res; - }); + const promise2 = client + .mutate({ + mutation, + optimisticResponse: optimisticResponse2, + update, + }) + .then((res: any) => { + checkBothMutationsAreApplied( + "This one was created with a mutation.", + "Second mutation." + ); - const promise2 = client - .mutate({ - mutation, - optimisticResponse: optimisticResponse2, - update, - }) - .then((res: any) => { - checkBothMutationsAreApplied( - "This one was created with a mutation.", - "Second mutation." - ); - - // @ts-ignore - const latestState = client.queryManager.mutationStore!; - expect(latestState[1].loading).toBe(false); - expect(latestState[2].loading).toBe(false); - - return res; - }); + // @ts-ignore + const latestState = client.queryManager.mutationStore!; + expect(latestState[1].loading).toBe(false); + expect(latestState[2].loading).toBe(false); - // @ts-ignore - const mutationsState = client.queryManager.mutationStore!; - expect(mutationsState[1].loading).toBe(true); - expect(mutationsState[2].loading).toBe(true); + return res; + }); - checkBothMutationsAreApplied( - "Optimistically generated", - "Optimistically generated 2" - ); + // @ts-ignore + const mutationsState = client.queryManager.mutationStore!; + expect(mutationsState[1].loading).toBe(true); + expect(mutationsState[2].loading).toBe(true); - await Promise.all([promise, promise2]); + checkBothMutationsAreApplied( + "Optimistically generated", + "Optimistically generated 2" + ); - subscriptionHandle!.unsubscribe(); - checkBothMutationsAreApplied( - "This one was created with a mutation.", - "Second mutation." - ); + await Promise.all([promise, promise2]); - resolve(); - } - ); + stream.unsubscribe(); + checkBothMutationsAreApplied( + "This one was created with a mutation.", + "Second mutation." + ); + }); }); }); @@ -752,39 +690,34 @@ describe("optimistic mutation results", () => { } `; - itAsync( - "client.readQuery should read the optimistic response of a mutation " + - "only when update function is called optimistically", - (resolve, reject) => { - return setup(reject, { - request: { query: todoListMutation }, - result: todoListMutationResult, - }) - .then((client) => { - let updateCount = 0; - return client.mutate({ - mutation: todoListMutation, - optimisticResponse: todoListOptimisticResponse, - update: (proxy: any, mResult: any) => { - ++updateCount; - const data = proxy.readQuery({ query: todoListQuery }); - const readText = data.todoList.todos[0].text; - if (updateCount === 1) { - const optimisticText = - todoListOptimisticResponse.createTodo.todos[0].text; - expect(readText).toEqual(optimisticText); - } else if (updateCount === 2) { - const incomingText = mResult.data.createTodo.todos[0].text; - expect(readText).toEqual(incomingText); - } else { - reject("too many update calls"); - } - }, - }); - }) - .then(resolve, reject); - } - ); + it("client.readQuery should read the optimistic response of a mutation only when update function is called optimistically", async () => { + expect.assertions(2); + const client = await setup({ + request: { query: todoListMutation }, + result: todoListMutationResult, + }); + + let updateCount = 0; + await client.mutate({ + mutation: todoListMutation, + optimisticResponse: todoListOptimisticResponse, + update: (proxy: any, mResult: any) => { + ++updateCount; + const data = proxy.readQuery({ query: todoListQuery }); + const readText = data.todoList.todos[0].text; + if (updateCount === 1) { + const optimisticText = + todoListOptimisticResponse.createTodo.todos[0].text; + expect(readText).toEqual(optimisticText); + } else if (updateCount === 2) { + const incomingText = mResult.data.createTodo.todos[0].text; + expect(readText).toEqual(incomingText); + } else { + throw new Error("too many update calls"); + } + }, + }); + }); const todoListFragment = gql` fragment todoList on TodoList { @@ -797,79 +730,67 @@ describe("optimistic mutation results", () => { } `; - itAsync( - "should read the optimistic response of a mutation when making an " + - "ApolloClient.readFragment() call, if the `optimistic` param is set " + - "to true", - (resolve, reject) => { - return setup(reject, { - request: { query: todoListMutation }, - result: todoListMutationResult, - }) - .then((client) => { - let updateCount = 0; - return client.mutate({ - mutation: todoListMutation, - optimisticResponse: todoListOptimisticResponse, - update: (proxy: any, mResult: any) => { - ++updateCount; - const data: any = proxy.readFragment( - { - id: "TodoList5", - fragment: todoListFragment, - }, - true - ); - if (updateCount === 1) { - expect(data.todos[0].text).toEqual( - todoListOptimisticResponse.createTodo.todos[0].text - ); - } else if (updateCount === 2) { - expect(data.todos[0].text).toEqual( - mResult.data.createTodo.todos[0].text - ); - expect(data.todos[0].text).toEqual( - todoListMutationResult.data.createTodo.todos[0].text - ); - } else { - reject("too many update calls"); - } - }, - }); - }) - .then(resolve, reject); - } - ); + it("should read the optimistic response of a mutation when making an ApolloClient.readFragment() call, if the `optimistic` param is set to true", async () => { + expect.assertions(3); + const client = await setup({ + request: { query: todoListMutation }, + result: todoListMutationResult, + }); - itAsync( - "should not read the optimistic response of a mutation when making " + - "an ApolloClient.readFragment() call, if the `optimistic` param is " + - "set to false", - (resolve, reject) => { - return setup(reject, { - request: { query: todoListMutation }, - result: todoListMutationResult, - }) - .then((client) => { - return client.mutate({ - mutation: todoListMutation, - optimisticResponse: todoListOptimisticResponse, - update: (proxy: any, mResult: any) => { - const incomingText = mResult.data.createTodo.todos[0].text; - const data: any = proxy.readFragment( - { - id: "TodoList5", - fragment: todoListFragment, - }, - false - ); - expect(data.todos[0].text).toEqual(incomingText); - }, - }); - }) - .then(resolve, reject); - } - ); + let updateCount = 0; + await client.mutate({ + mutation: todoListMutation, + optimisticResponse: todoListOptimisticResponse, + update: (proxy: any, mResult: any) => { + ++updateCount; + const data: any = proxy.readFragment( + { + id: "TodoList5", + fragment: todoListFragment, + }, + true + ); + if (updateCount === 1) { + expect(data.todos[0].text).toEqual( + todoListOptimisticResponse.createTodo.todos[0].text + ); + } else if (updateCount === 2) { + expect(data.todos[0].text).toEqual( + mResult.data.createTodo.todos[0].text + ); + expect(data.todos[0].text).toEqual( + todoListMutationResult.data.createTodo.todos[0].text + ); + } else { + throw new Error("too many update calls"); + } + }, + }); + }); + + it("should not read the optimistic response of a mutation when making an ApolloClient.readFragment() call, if the `optimistic` param is set to false", async () => { + expect.assertions(2); + const client = await setup({ + request: { query: todoListMutation }, + result: todoListMutationResult, + }); + + await client.mutate({ + mutation: todoListMutation, + optimisticResponse: todoListOptimisticResponse, + update: (proxy: any, mResult: any) => { + const incomingText = mResult.data.createTodo.todos[0].text; + const data: any = proxy.readFragment( + { + id: "TodoList5", + fragment: todoListFragment, + }, + false + ); + expect(data.todos[0].text).toEqual(incomingText); + }, + }); + }); }); describe("passing a function to optimisticResponse", () => { @@ -909,187 +830,157 @@ describe("optimistic mutation results", () => { }, }); - itAsync( - "will use a passed variable in optimisticResponse", - async (resolve, reject) => { - expect.assertions(6); - let subscriptionHandle: Subscription; - const client = await setup(reject, { - request: { query: mutation, variables }, - result: mutationResult, - }); - - // we have to actually subscribe to the query to be able to update it - await new Promise((resolve) => { - const handle = client.watchQuery({ query }); - subscriptionHandle = handle.subscribe({ - next(res: any) { - resolve(res); - }, - }); - }); - - const promise = client.mutate({ - mutation, - variables, - optimisticResponse, - update: (proxy: any, mResult: any) => { - expect(mResult.data.createTodo.id).toBe("99"); - - const id = "TodoList5"; - const fragment = gql` - fragment todoList on TodoList { - todos { - id - text - completed - __typename - } - } - `; - - const data: any = proxy.readFragment({ id, fragment }); + it("will use a passed variable in optimisticResponse", async () => { + expect.assertions(7); + const client = await setup({ + request: { query: mutation, variables }, + result: mutationResult, + }); + const stream = new ObservableStream(client.watchQuery({ query })); - proxy.writeFragment({ - data: { - ...data, - todos: [mResult.data.createTodo, ...data.todos], - }, - id, - fragment, - }); - }, - }); + await expect(stream).toEmitNext(); - const dataInStore = (client.cache as InMemoryCache).extract(true); - expect((dataInStore["TodoList5"] as any).todos.length).toEqual(4); - expect((dataInStore["Todo99"] as any).text).toEqual( - "Optimistically generated from variables" - ); + const promise = client.mutate({ + mutation, + variables, + optimisticResponse, + update: (proxy: any, mResult: any) => { + expect(mResult.data.createTodo.id).toBe("99"); - await promise; + const id = "TodoList5"; + const fragment = gql` + fragment todoList on TodoList { + todos { + id + text + completed + __typename + } + } + `; - const newResult: any = await client.query({ query }); + const data: any = proxy.readFragment({ id, fragment }); - subscriptionHandle!.unsubscribe(); - // There should be one more todo item than before - expect(newResult.data.todoList.todos.length).toEqual(4); + proxy.writeFragment({ + data: { + ...data, + todos: [mResult.data.createTodo, ...data.todos], + }, + id, + fragment, + }); + }, + }); - // Since we used `prepend` it should be at the front - expect(newResult.data.todoList.todos[0].text).toEqual( - "This one was created with a mutation." - ); + const dataInStore = (client.cache as InMemoryCache).extract(true); + expect((dataInStore["TodoList5"] as any).todos.length).toEqual(4); + expect((dataInStore["Todo99"] as any).text).toEqual( + "Optimistically generated from variables" + ); - resolve(); - } - ); + await promise; - itAsync( - "will not update optimistically if optimisticResponse returns IGNORE sentinel object", - async (resolve, reject) => { - expect.assertions(5); + const newResult: any = await client.query({ query }); - let subscriptionHandle: Subscription; + stream.unsubscribe(); + // There should be one more todo item than before + expect(newResult.data.todoList.todos.length).toEqual(4); - const client = await setup(reject, { - request: { query: mutation, variables }, - result: mutationResult, - }); + // Since we used `prepend` it should be at the front + expect(newResult.data.todoList.todos[0].text).toEqual( + "This one was created with a mutation." + ); + }); - // we have to actually subscribe to the query to be able to update it - await new Promise((resolve) => { - const handle = client.watchQuery({ query }); - subscriptionHandle = handle.subscribe({ - next(res: any) { - resolve(res); - }, - }); - }); + it("will not update optimistically if optimisticResponse returns IGNORE sentinel object", async () => { + expect.assertions(6); - const id = "TodoList5"; - const isTodoList = ( - list: unknown - ): list is { todos: { text: string }[] } => - typeof initialList === "object" && - initialList !== null && - "todos" in initialList && - Array.isArray(initialList.todos); + const client = await setup({ + request: { query: mutation, variables }, + result: mutationResult, + }); + const stream = new ObservableStream(client.watchQuery({ query })); - const initialList = client.cache.extract(true)[id]; + await expect(stream).toEmitNext(); - if (!isTodoList(initialList)) { - reject(new Error("Expected TodoList")); - return; - } + const id = "TodoList5"; + const isTodoList = ( + list: unknown + ): list is { todos: { text: string }[] } => + typeof initialList === "object" && + initialList !== null && + "todos" in initialList && + Array.isArray(initialList.todos); - expect(initialList.todos.length).toEqual(3); + const initialList = client.cache.extract(true)[id]; - const promise = client.mutate({ - mutation, - variables, - optimisticResponse: (vars, { IGNORE }) => { - return IGNORE; - }, - update: (proxy: any, mResult: any) => { - expect(mResult.data.createTodo.id).toBe("99"); - - const fragment = gql` - fragment todoList on TodoList { - todos { - id - text - completed - __typename - } - } - `; + if (!isTodoList(initialList)) { + throw new Error("Expected TodoList"); + } - const data: any = proxy.readFragment({ id, fragment }); + expect(initialList.todos.length).toEqual(3); - proxy.writeFragment({ - data: { - ...data, - todos: [mResult.data.createTodo, ...data.todos], - }, - id, - fragment, - }); - }, - }); + const promise = client.mutate({ + mutation, + variables, + optimisticResponse: (vars, { IGNORE }) => { + return IGNORE; + }, + update: (proxy: any, mResult: any) => { + expect(mResult.data.createTodo.id).toBe("99"); - const list = client.cache.extract(true)[id]; + const fragment = gql` + fragment todoList on TodoList { + todos { + id + text + completed + __typename + } + } + `; - if (!isTodoList(list)) { - reject(new Error("Expected TodoList")); - return; - } + const data: any = proxy.readFragment({ id, fragment }); - expect(list.todos.length).toEqual(3); + proxy.writeFragment({ + data: { + ...data, + todos: [mResult.data.createTodo, ...data.todos], + }, + id, + fragment, + }); + }, + }); - await promise; + const list = client.cache.extract(true)[id]; - const result = await client.query({ query }); + if (!isTodoList(list)) { + throw new Error("Expected TodoList"); + } - subscriptionHandle!.unsubscribe(); + expect(list.todos.length).toEqual(3); - const newList = result.data.todoList; + await promise; - if (!isTodoList(newList)) { - reject(new Error("Expected TodoList")); - return; - } + const result = await client.query({ query }); - // There should be one more todo item than before - expect(newList.todos.length).toEqual(4); + stream.unsubscribe(); - // Since we used `prepend` it should be at the front - expect(newList.todos[0].text).toBe( - "This one was created with a mutation." - ); + const newList = result.data.todoList; - resolve(); + if (!isTodoList(newList)) { + throw new Error("Expected TodoList"); } - ); + + // There should be one more todo item than before + expect(newList.todos.length).toEqual(4); + + // Since we used `prepend` it should be at the front + expect(newList.todos[0].text).toBe( + "This one was created with a mutation." + ); + }); it("allows IgnoreModifier as return value when inferring from a TypedDocumentNode mutation", () => { const mutation: TypedDocumentNode<{ bar: string }> = gql` @@ -1176,72 +1067,57 @@ describe("optimistic mutation results", () => { }, }; - itAsync( - "will insert a single itemAsync to the beginning", - async (resolve, reject) => { - expect.assertions(7); - let subscriptionHandle: Subscription; - const client = await setup(reject, { - request: { query: mutation }, - result: mutationResult, - }); + it("will insert a single itemAsync to the beginning", async () => { + expect.assertions(8); + const client = await setup({ + request: { query: mutation }, + result: mutationResult, + }); + const stream = new ObservableStream(client.watchQuery({ query })); - // we have to actually subscribe to the query to be able to update it - await new Promise((resolve) => { - const handle = client.watchQuery({ query }); - subscriptionHandle = handle.subscribe({ - next(res: any) { - resolve(res); - }, - }); - }); + await expect(stream).toEmitNext(); - const promise = client.mutate({ - mutation, - optimisticResponse, - updateQueries: { - todoList(prev: any, options: any) { - const mResult = options.mutationResult as any; - expect(mResult.data.createTodo.id).toEqual("99"); - return { - ...prev, - todoList: { - ...prev.todoList, - todos: [mResult.data.createTodo, ...prev.todoList.todos], - }, - }; - }, + const promise = client.mutate({ + mutation, + optimisticResponse, + updateQueries: { + todoList(prev: any, options: any) { + const mResult = options.mutationResult as any; + expect(mResult.data.createTodo.id).toEqual("99"); + return { + ...prev, + todoList: { + ...prev.todoList, + todos: [mResult.data.createTodo, ...prev.todoList.todos], + }, + }; }, - }); - - const dataInStore = (client.cache as InMemoryCache).extract(true); - expect((dataInStore["TodoList5"] as any).todos.length).toEqual(4); - expect((dataInStore["Todo99"] as any).text).toEqual( - "Optimistically generated" - ); + }, + }); - await promise; + const dataInStore = (client.cache as InMemoryCache).extract(true); + expect((dataInStore["TodoList5"] as any).todos.length).toEqual(4); + expect((dataInStore["Todo99"] as any).text).toEqual( + "Optimistically generated" + ); - const newResult: any = await client.query({ query }); + await promise; - subscriptionHandle!.unsubscribe(); - // There should be one more todo item than before - expect(newResult.data.todoList.todos.length).toEqual(4); + const newResult: any = await client.query({ query }); - // Since we used `prepend` it should be at the front - expect(newResult.data.todoList.todos[0].text).toEqual( - "This one was created with a mutation." - ); + stream.unsubscribe(); + // There should be one more todo item than before + expect(newResult.data.todoList.todos.length).toEqual(4); - resolve(); - } - ); + // Since we used `prepend` it should be at the front + expect(newResult.data.todoList.todos[0].text).toEqual( + "This one was created with a mutation." + ); + }); - itAsync("two array insert like mutations", async (resolve, reject) => { - expect.assertions(9); - let subscriptionHandle: Subscription; + it("two array insert like mutations", async () => { + expect.assertions(10); const client = await setup( - reject, { request: { query: mutation }, result: mutationResult, @@ -1252,16 +1128,9 @@ describe("optimistic mutation results", () => { delay: 50, } ); + const stream = new ObservableStream(client.watchQuery({ query })); - // we have to actually subscribe to the query to be able to update it - await new Promise((resolve) => { - const handle = client.watchQuery({ query }); - subscriptionHandle = handle.subscribe({ - next(res: any) { - resolve(res); - }, - }); - }); + await expect(stream).toEmitNext(); const updateQueries = { todoList: (prev, options) => { @@ -1317,7 +1186,7 @@ describe("optimistic mutation results", () => { const newResult: any = await client.query({ query }); - subscriptionHandle!.unsubscribe(); + stream.unsubscribe(); // There should be one more todo item than before expect(newResult.data.todoList.todos.length).toEqual(5); @@ -1326,15 +1195,11 @@ describe("optimistic mutation results", () => { expect(newResult.data.todoList.todos[1].text).toEqual( "This one was created with a mutation." ); - - resolve(); }); - itAsync("two mutations, one fails", async (resolve, reject) => { - expect.assertions(10); - let subscriptionHandle: Subscription; + it("two mutations, one fails", async () => { + expect.assertions(11); const client = await setup( - reject, { request: { query: mutation }, error: new Error("forbidden (test error)"), @@ -1351,16 +1216,9 @@ describe("optimistic mutation results", () => { // delay: 50, } ); + const stream = new ObservableStream(client.watchQuery({ query })); - // we have to actually subscribe to the query to be able to update it - await new Promise((resolve) => { - const handle = client.watchQuery({ query }); - subscriptionHandle = handle.subscribe({ - next(res: any) { - resolve(res); - }, - }); - }); + await expect(stream).toEmitNext(); const updateQueries = { todoList: (prev, options) => { @@ -1405,7 +1263,7 @@ describe("optimistic mutation results", () => { await Promise.all([promise, promise2]); - subscriptionHandle!.unsubscribe(); + stream.unsubscribe(); { const dataInStore = (client.cache as InMemoryCache).extract(true); expect((dataInStore["TodoList5"] as any).todos.length).toEqual(4); @@ -1417,11 +1275,10 @@ describe("optimistic mutation results", () => { expect((dataInStore["TodoList5"] as any).todos).not.toContainEqual( makeReference("Todo99") ); - resolve(); } }); - itAsync("will handle dependent updates", async (resolve, reject) => { + it("will handle dependent updates", async () => { expect.assertions(1); const link = mockSingleLink( { @@ -1438,7 +1295,7 @@ describe("optimistic mutation results", () => { result: mutationResult2, delay: 20, } - ).setOnError(reject); + ); const customOptimisticResponse1 = { __typename: "Mutation", @@ -1502,13 +1359,13 @@ describe("optimistic mutation results", () => { // https://github.com/apollographql/apollo-client/issues/3723 await new Promise((resolve) => setTimeout(resolve)); - client.mutate({ + void client.mutate({ mutation, optimisticResponse: customOptimisticResponse1, updateQueries, }); - client.mutate({ + void client.mutate({ mutation, optimisticResponse: customOptimisticResponse2, updateQueries, @@ -1536,8 +1393,6 @@ describe("optimistic mutation results", () => { ...defaultTodos, ], ]); - - resolve(); }); }); @@ -1596,82 +1451,67 @@ describe("optimistic mutation results", () => { }, }; - itAsync( - "will insert a single itemAsync to the beginning", - async (resolve, reject) => { - expect.assertions(6); - let subscriptionHandle: Subscription; - const client = await setup(reject, { - request: { query: mutation }, - delay: 300, - result: mutationResult, - }); + it("will insert a single itemAsync to the beginning", async () => { + expect.assertions(7); + const client = await setup({ + request: { query: mutation }, + delay: 300, + result: mutationResult, + }); + const stream = new ObservableStream(client.watchQuery({ query })); - // we have to actually subscribe to the query to be able to update it - await new Promise((resolve) => { - const handle = client.watchQuery({ query }); - subscriptionHandle = handle.subscribe({ - next(res: any) { - resolve(res); - }, - }); - }); + await expect(stream).toEmitNext(); - let firstTime = true; - let before = Date.now(); - const promise = client.mutate({ - mutation, - optimisticResponse, - update: (proxy: any, mResult: any) => { - const after = Date.now(); - const duration = after - before; - if (firstTime) { - expect(duration < 300).toBe(true); - firstTime = false; - } else { - expect(duration > 300).toBe(true); - } - let data = proxy.readQuery({ query }); + let firstTime = true; + let before = Date.now(); + const promise = client.mutate({ + mutation, + optimisticResponse, + update: (proxy: any, mResult: any) => { + const after = Date.now(); + const duration = after - before; + if (firstTime) { + expect(duration < 300).toBe(true); + firstTime = false; + } else { + expect(duration > 300).toBe(true); + } + let data = proxy.readQuery({ query }); - proxy.writeQuery({ - query, - data: { - ...data, - todoList: { - ...data.todoList, - todos: [mResult.data.createTodo, ...data.todoList.todos], - }, + proxy.writeQuery({ + query, + data: { + ...data, + todoList: { + ...data.todoList, + todos: [mResult.data.createTodo, ...data.todoList.todos], }, - }); - }, - }); + }, + }); + }, + }); - const dataInStore = (client.cache as InMemoryCache).extract(true); - expect((dataInStore["TodoList5"] as any).todos.length).toBe(4); - expect((dataInStore["Todo99"] as any).text).toBe( - "Optimistically generated" - ); - await promise; - await client.query({ query }).then((newResult: any) => { - subscriptionHandle!.unsubscribe(); - // There should be one more todo item than before - expect(newResult.data.todoList.todos.length).toBe(4); - - // Since we used `prepend` it should be at the front - expect(newResult.data.todoList.todos[0].text).toBe( - "This one was created with a mutation." - ); - }); + const dataInStore = (client.cache as InMemoryCache).extract(true); + expect((dataInStore["TodoList5"] as any).todos.length).toBe(4); + expect((dataInStore["Todo99"] as any).text).toBe( + "Optimistically generated" + ); + await promise; + const newResult = await client.query({ query }); - resolve(); - } - ); + stream.unsubscribe(); + // There should be one more todo item than before + expect(newResult.data.todoList.todos.length).toBe(4); + + // Since we used `prepend` it should be at the front + expect(newResult.data.todoList.todos[0].text).toBe( + "This one was created with a mutation." + ); + }); - itAsync("two array insert like mutations", async (resolve, reject) => { - expect.assertions(9); - let subscriptionHandle: Subscription; + it("two array insert like mutations", async () => { + expect.assertions(10); const client = await setup( - reject, { request: { query: mutation }, result: mutationResult, @@ -1682,16 +1522,9 @@ describe("optimistic mutation results", () => { delay: 50, } ); + const stream = new ObservableStream(client.watchQuery({ query })); - // we have to actually subscribe to the query to be able to update it - await new Promise((resolve) => { - const handle = client.watchQuery({ query }); - subscriptionHandle = handle.subscribe({ - next(res: any) { - resolve(res); - }, - }); - }); + await expect(stream).toEmitNext(); const update = (proxy: any, mResult: any) => { const data: any = proxy.readFragment({ @@ -1765,7 +1598,7 @@ describe("optimistic mutation results", () => { const newResult: any = await client.query({ query }); - subscriptionHandle!.unsubscribe(); + stream.unsubscribe(); // There should be one more todo item than before expect(newResult.data.todoList.todos.length).toBe(5); @@ -1774,15 +1607,11 @@ describe("optimistic mutation results", () => { expect(newResult.data.todoList.todos[1].text).toBe( "This one was created with a mutation." ); - - resolve(); }); - itAsync("two mutations, one fails", async (resolve, reject) => { - expect.assertions(10); - let subscriptionHandle: Subscription; + it("two mutations, one fails", async () => { + expect.assertions(11); const client = await setup( - reject, { request: { query: mutation }, error: new Error("forbidden (test error)"), @@ -1799,16 +1628,9 @@ describe("optimistic mutation results", () => { // delay: 50, } ); + const stream = new ObservableStream(client.watchQuery({ query })); - // we have to actually subscribe to the query to be able to update it - await new Promise((resolve) => { - const handle = client.watchQuery({ query }); - subscriptionHandle = handle.subscribe({ - next(res: any) { - resolve(res); - }, - }); - }); + await expect(stream).toEmitNext(); const update = (proxy: any, mResult: any) => { const data: any = proxy.readFragment({ @@ -1873,7 +1695,7 @@ describe("optimistic mutation results", () => { await Promise.all([promise, promise2]); - subscriptionHandle!.unsubscribe(); + stream.unsubscribe(); { const dataInStore = (client.cache as InMemoryCache).extract(true); expect((dataInStore["TodoList5"] as any).todos.length).toBe(4); @@ -1885,11 +1707,10 @@ describe("optimistic mutation results", () => { expect((dataInStore["TodoList5"] as any).todos).not.toContainEqual( makeReference("Todo99") ); - resolve(); } }); - itAsync("will handle dependent updates", async (resolve, reject) => { + it("will handle dependent updates", async () => { expect.assertions(1); const link = mockSingleLink( { @@ -1906,7 +1727,7 @@ describe("optimistic mutation results", () => { result: mutationResult2, delay: 20, } - ).setOnError(reject); + ); const customOptimisticResponse1 = { __typename: "Mutation", @@ -1983,13 +1804,13 @@ describe("optimistic mutation results", () => { await new Promise((resolve) => setTimeout(resolve)); - client.mutate({ + void client.mutate({ mutation, optimisticResponse: customOptimisticResponse1, update, }); - client.mutate({ + void client.mutate({ mutation, optimisticResponse: customOptimisticResponse2, update, @@ -2016,11 +1837,9 @@ describe("optimistic mutation results", () => { ...defaultTodos, ], ]); - - resolve(); }); - itAsync("final update ignores optimistic data", (resolve, reject) => { + it("final update ignores optimistic data", async () => { const cache = new InMemoryCache(); const client = new ApolloClient({ cache, @@ -2143,209 +1962,190 @@ describe("optimistic mutation results", () => { const optimisticItem = makeItem("optimistic"); const mutationItem = makeItem("mutation"); - const wrapReject = ( - fn: (...args: TArgs) => TResult - ): typeof fn => { - return function (this: unknown, ...args: TArgs) { - try { - return fn.apply(this, args); - } catch (e) { - reject(e); - throw e; - } - }; - }; + const result = await client.mutate({ + mutation, + optimisticResponse: { + addItem: optimisticItem, + }, + variables: { + item: mutationItem, + }, + update: (cache, mutationResult) => { + ++updateCount; + if (updateCount === 1) { + expect(mutationResult).toEqual({ + data: { + addItem: optimisticItem, + }, + }); - return client - .mutate({ - mutation, - optimisticResponse: { - addItem: optimisticItem, - }, - variables: { - item: mutationItem, - }, - update: wrapReject((cache, mutationResult) => { - ++updateCount; - if (updateCount === 1) { - expect(mutationResult).toEqual({ - data: { - addItem: optimisticItem, + append(cache, optimisticItem); + + const expected = { + ROOT_QUERY: { + __typename: "Query", + items: [manualItem1, manualItem2, optimisticItem], + }, + ROOT_MUTATION: { + __typename: "Mutation", + // Although ROOT_MUTATION field data gets removed immediately + // after the mutation finishes, it is still temporarily visible + // to the update function. + 'addItem({"item":{"__typename":"Item","text":"mutation 4"}})': { + __typename: "Item", + text: "optimistic 3", }, - }); + }, + }; + + // Since we're in an optimistic update function, reading + // non-optimistically still returns optimistic data. + expect(cache.extract(false)).toEqual(expected); + expect(cache.extract(true)).toEqual(expected); + } else if (updateCount === 2) { + expect(mutationResult).toEqual({ + data: { + addItem: mutationItem, + }, + }); - append(cache, optimisticItem); + append(cache, mutationItem); - const expected = { - ROOT_QUERY: { - __typename: "Query", - items: [manualItem1, manualItem2, optimisticItem], - }, - ROOT_MUTATION: { - __typename: "Mutation", - // Although ROOT_MUTATION field data gets removed immediately - // after the mutation finishes, it is still temporarily visible - // to the update function. - 'addItem({"item":{"__typename":"Item","text":"mutation 4"}})': - { - __typename: "Item", - text: "optimistic 3", - }, - }, - }; - - // Since we're in an optimistic update function, reading - // non-optimistically still returns optimistic data. - expect(cache.extract(false)).toEqual(expected); - expect(cache.extract(true)).toEqual(expected); - } else if (updateCount === 2) { - expect(mutationResult).toEqual({ - data: { - addItem: mutationItem, + const expected = { + ROOT_QUERY: { + __typename: "Query", + items: [mutationItem], + }, + ROOT_MUTATION: { + __typename: "Mutation", + 'addItem({"item":{"__typename":"Item","text":"mutation 4"}})': { + __typename: "Item", + text: "mutation 4", }, - }); + }, + }; + + // Since we're in the final (non-optimistic) update function, + // optimistic data is invisible, even if we try to read + // optimistically. + expect(cache.extract(false)).toEqual(expected); + expect(cache.extract(true)).toEqual(expected); + } else { + throw new Error("too many updates"); + } + }, + }); - append(cache, mutationItem); + expect(result).toEqual({ + data: { + addItem: mutationItem, + }, + }); - const expected = { - ROOT_QUERY: { - __typename: "Query", - items: [mutationItem], - }, - ROOT_MUTATION: { - __typename: "Mutation", - 'addItem({"item":{"__typename":"Item","text":"mutation 4"}})': - { - __typename: "Item", - text: "mutation 4", - }, - }, - }; - - // Since we're in the final (non-optimistic) update function, - // optimistic data is invisible, even if we try to read - // optimistically. - expect(cache.extract(false)).toEqual(expected); - expect(cache.extract(true)).toEqual(expected); - } else { - throw new Error("too many updates"); - } - }), - }) - .then((result) => { - expect(result).toEqual({ - data: { - addItem: mutationItem, - }, - }); + // Only the final update function ever touched non-optimistic + // cache data. + expect(cache.extract(false)).toEqual({ + ROOT_QUERY: { + __typename: "Query", + items: [mutationItem], + }, + ROOT_MUTATION: { + __typename: "Mutation", + }, + }); - // Only the final update function ever touched non-optimistic - // cache data. - expect(cache.extract(false)).toEqual({ - ROOT_QUERY: { - __typename: "Query", - items: [mutationItem], - }, - ROOT_MUTATION: { - __typename: "Mutation", - }, - }); + // Now that the mutation is finished, reading optimistically from + // the cache should return the manually added items again. + expect(cache.extract(true)).toEqual({ + ROOT_QUERY: { + __typename: "Query", + items: [ + // If we wanted to keep optimistic data as up-to-date as + // possible, we could rerun all optimistic transactions + // after writing to the root (non-optimistic) layer of the + // cache, which would result in mutationItem appearing in + // this list along with manualItem1 and manualItem2 + // (presumably in that order). However, rerunning those + // optimistic transactions would trigger additional + // broadcasts for optimistic query watches, with + // intermediate results that (re)combine optimistic and + // non-optimistic data. Since rerendering the UI tends to be + // expensive, we should prioritize broadcasting states that + // matter most, and in this case that means broadcasting the + // initial optimistic state (for perceived performance), + // followed by the final, authoritative, non-optimistic + // state. Other intermediate states are a distraction, as + // they will probably soon be superseded by another (more + // authoritative) update. This particular state is visible + // only because we haven't rolled back this manual Layer + // just yet (see cache.removeOptimistic below). + manualItem1, + manualItem2, + ], + }, + ROOT_MUTATION: { + __typename: "Mutation", + }, + }); - // Now that the mutation is finished, reading optimistically from - // the cache should return the manually added items again. - expect(cache.extract(true)).toEqual({ - ROOT_QUERY: { - __typename: "Query", - items: [ - // If we wanted to keep optimistic data as up-to-date as - // possible, we could rerun all optimistic transactions - // after writing to the root (non-optimistic) layer of the - // cache, which would result in mutationItem appearing in - // this list along with manualItem1 and manualItem2 - // (presumably in that order). However, rerunning those - // optimistic transactions would trigger additional - // broadcasts for optimistic query watches, with - // intermediate results that (re)combine optimistic and - // non-optimistic data. Since rerendering the UI tends to be - // expensive, we should prioritize broadcasting states that - // matter most, and in this case that means broadcasting the - // initial optimistic state (for perceived performance), - // followed by the final, authoritative, non-optimistic - // state. Other intermediate states are a distraction, as - // they will probably soon be superseded by another (more - // authoritative) update. This particular state is visible - // only because we haven't rolled back this manual Layer - // just yet (see cache.removeOptimistic below). - manualItem1, - manualItem2, - ], - }, - ROOT_MUTATION: { - __typename: "Mutation", - }, - }); + cache.removeOptimistic("manual"); - cache.removeOptimistic("manual"); + // After removing the manual optimistic layer, only the + // non-optimistic data remains. + expect(cache.extract(true)).toEqual({ + ROOT_QUERY: { + __typename: "Query", + items: [mutationItem], + }, + ROOT_MUTATION: { + __typename: "Mutation", + }, + }); - // After removing the manual optimistic layer, only the - // non-optimistic data remains. - expect(cache.extract(true)).toEqual({ - ROOT_QUERY: { - __typename: "Query", - items: [mutationItem], - }, - ROOT_MUTATION: { - __typename: "Mutation", - }, - }); - }) - .then(() => { - cancelFns.forEach((cancel) => cancel()); + cancelFns.forEach((cancel) => cancel()); - expect(optimisticDiffs).toEqual([ - { - complete: true, - fromOptimisticTransaction: true, - result: { - items: manualItems, - }, - }, - { - complete: true, - fromOptimisticTransaction: true, - result: { - items: [...manualItems, optimisticItem], - }, - }, - { - complete: true, - result: { - items: manualItems, - }, - }, - { - complete: true, - result: { - items: [mutationItem], - }, - }, - ]); + expect(optimisticDiffs).toEqual([ + { + complete: true, + fromOptimisticTransaction: true, + result: { + items: manualItems, + }, + }, + { + complete: true, + fromOptimisticTransaction: true, + result: { + items: [...manualItems, optimisticItem], + }, + }, + { + complete: true, + result: { + items: manualItems, + }, + }, + { + complete: true, + result: { + items: [mutationItem], + }, + }, + ]); - expect(realisticDiffs).toEqual([ - { - complete: false, - missing: [expect.anything()], - result: {}, - }, - { - complete: true, - result: { - items: [mutationItem], - }, - }, - ]); - }) - .then(resolve, reject); + expect(realisticDiffs).toEqual([ + { + complete: false, + missing: [expect.anything()], + result: {}, + }, + { + complete: true, + result: { + items: [mutationItem], + }, + }, + ]); }); }); }); @@ -2403,10 +2203,7 @@ describe("optimistic mutation - githunt comments", () => { }, }; - async function setup( - reject: (reason: any) => any, - ...mockedResponses: any[] - ) { + async function setup(...mockedResponses: MockedResponse[]) { const link = mockSingleLink( { request: { @@ -2423,7 +2220,7 @@ describe("optimistic mutation - githunt comments", () => { result, }, ...mockedResponses - ).setOnError(reject); + ); const client = new ApolloClient({ link, @@ -2501,31 +2298,25 @@ describe("optimistic mutation - githunt comments", () => { }, }; - itAsync("can post a new comment", async (resolve, reject) => { - expect.assertions(1); + it("can post a new comment", async () => { + expect.assertions(2); const mutationVariables = { repoFullName: "org/repo", commentContent: "New Comment", }; - let subscriptionHandle: Subscription; - const client = await setup(reject, { + const client = await setup({ request: { query: addTypenameToDocument(mutation), variables: mutationVariables, }, result: mutationResult, }); + const stream = new ObservableStream( + client.watchQuery({ query, variables }) + ); - // we have to actually subscribe to the query to be able to update it - await new Promise((resolve) => { - const handle = client.watchQuery({ query, variables }); - subscriptionHandle = handle.subscribe({ - next(res: any) { - resolve(res); - }, - }); - }); + await expect(stream).toEmitNext(); await client.mutate({ mutation, @@ -2536,9 +2327,7 @@ describe("optimistic mutation - githunt comments", () => { const newResult: any = await client.query({ query, variables }); - subscriptionHandle!.unsubscribe(); + stream.unsubscribe(); expect(newResult.data.entry.comments.length).toBe(2); - - resolve(); }); }); diff --git a/src/__tests__/refetchQueries.ts b/src/__tests__/refetchQueries.ts index 4b843c61165..fbeb52c6012 100644 --- a/src/__tests__/refetchQueries.ts +++ b/src/__tests__/refetchQueries.ts @@ -1,6 +1,5 @@ import { Subscription } from "zen-observable-ts"; -import { itAsync } from "../testing"; import { ApolloClient, ApolloLink, @@ -10,29 +9,31 @@ import { TypedDocumentNode, ObservableQuery, } from "../core"; +import { ObservableStream } from "../testing/internal"; describe("client.refetchQueries", () => { - itAsync("is public and callable", (resolve, reject) => { + it("is public and callable", async () => { + expect.assertions(6); const client = new ApolloClient({ cache: new InMemoryCache(), }); expect(typeof client.refetchQueries).toBe("function"); + const onQueryUpdated = jest.fn(); const result = client.refetchQueries({ updateCache(cache) { expect(cache).toBe(client.cache); expect(cache.extract()).toEqual({}); }, - onQueryUpdated() { - reject("should not have called onQueryUpdated"); - return false; - }, + onQueryUpdated, }); expect(result.queries).toEqual([]); expect(result.results).toEqual([]); - result.then(resolve, reject); + await result; + + expect(onQueryUpdated).not.toHaveBeenCalled(); }); const aQuery: TypedDocumentNode<{ a: string }> = gql` @@ -113,917 +114,858 @@ describe("client.refetchQueries", () => { }); } - itAsync( - "includes watched queries affected by updateCache", - async (resolve, reject) => { - const client = makeClient(); - const [aObs, bObs, abObs] = await setup(client); + it("includes watched queries affected by updateCache", async () => { + expect.assertions(9); + const client = makeClient(); + const [aObs, bObs, abObs] = await setup(client); - const ayyResults = await client.refetchQueries({ - updateCache(cache) { - cache.writeQuery({ - query: aQuery, - data: { - a: "Ayy", - }, - }); - }, + const ayyResults = await client.refetchQueries({ + updateCache(cache) { + cache.writeQuery({ + query: aQuery, + data: { + a: "Ayy", + }, + }); + }, - onQueryUpdated(obs, diff) { - if (obs === aObs) { - expect(diff.result).toEqual({ a: "Ayy" }); - } else if (obs === bObs) { - reject("bQuery should not have been updated"); - } else if (obs === abObs) { - expect(diff.result).toEqual({ a: "Ayy", b: "B" }); - } else { - reject( - `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` - ); - } - return Promise.resolve(diff.result); - }, - }); + onQueryUpdated(obs, diff) { + if (obs === aObs) { + expect(diff.result).toEqual({ a: "Ayy" }); + } else if (obs === bObs) { + throw new Error("bQuery should not have been updated"); + } else if (obs === abObs) { + expect(diff.result).toEqual({ a: "Ayy", b: "B" }); + } else { + throw new Error( + `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` + ); + } + return Promise.resolve(diff.result); + }, + }); - sortObjects(ayyResults); + sortObjects(ayyResults); - expect(ayyResults).toEqual([ - { a: "Ayy" }, - { a: "Ayy", b: "B" }, - // Note that no bQuery result is included here. - ]); + expect(ayyResults).toEqual([ + { a: "Ayy" }, + { a: "Ayy", b: "B" }, + // Note that no bQuery result is included here. + ]); - const beeResults = await client.refetchQueries({ - updateCache(cache) { - cache.writeQuery({ - query: bQuery, - data: { - b: "Bee", - }, - }); - }, + const beeResults = await client.refetchQueries({ + updateCache(cache) { + cache.writeQuery({ + query: bQuery, + data: { + b: "Bee", + }, + }); + }, - onQueryUpdated(obs, diff) { - if (obs === aObs) { - reject("aQuery should not have been updated"); - } else if (obs === bObs) { - expect(diff.result).toEqual({ b: "Bee" }); - } else if (obs === abObs) { - expect(diff.result).toEqual({ a: "Ayy", b: "Bee" }); - } else { - reject( - `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` - ); - } - return diff.result; - }, - }); + onQueryUpdated(obs, diff) { + if (obs === aObs) { + throw new Error("aQuery should not have been updated"); + } else if (obs === bObs) { + expect(diff.result).toEqual({ b: "Bee" }); + } else if (obs === abObs) { + expect(diff.result).toEqual({ a: "Ayy", b: "Bee" }); + } else { + throw new Error( + `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` + ); + } + return diff.result; + }, + }); - sortObjects(beeResults); + sortObjects(beeResults); - expect(beeResults).toEqual([ - // Note that no aQuery result is included here. - { a: "Ayy", b: "Bee" }, - { b: "Bee" }, - ]); + expect(beeResults).toEqual([ + // Note that no aQuery result is included here. + { a: "Ayy", b: "Bee" }, + { b: "Bee" }, + ]); - unsubscribe(); - resolve(); - } - ); + unsubscribe(); + }); - itAsync( - "includes watched queries named in options.include", - async (resolve, reject) => { - const client = makeClient(); - const [aObs, bObs, abObs] = await setup(client); + it("includes watched queries named in options.include", async () => { + expect.assertions(11); + const client = makeClient(); + const [aObs, bObs, abObs] = await setup(client); - const ayyResults = await client.refetchQueries({ - updateCache(cache) { - cache.writeQuery({ - query: aQuery, - data: { - a: "Ayy", - }, - }); - }, + const ayyResults = await client.refetchQueries({ + updateCache(cache) { + cache.writeQuery({ + query: aQuery, + data: { + a: "Ayy", + }, + }); + }, - // This is the options.include array mentioned in the test description. - include: ["B"], + // This is the options.include array mentioned in the test description. + include: ["B"], + + onQueryUpdated(obs, diff) { + if (obs === aObs) { + expect(diff.result).toEqual({ a: "Ayy" }); + } else if (obs === bObs) { + expect(diff.result).toEqual({ b: "B" }); + } else if (obs === abObs) { + expect(diff.result).toEqual({ a: "Ayy", b: "B" }); + } else { + throw new Error( + `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` + ); + } + return Promise.resolve(diff.result); + }, + }); - onQueryUpdated(obs, diff) { - if (obs === aObs) { - expect(diff.result).toEqual({ a: "Ayy" }); - } else if (obs === bObs) { - expect(diff.result).toEqual({ b: "B" }); - } else if (obs === abObs) { - expect(diff.result).toEqual({ a: "Ayy", b: "B" }); - } else { - reject( - `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` - ); - } - return Promise.resolve(diff.result); - }, - }); + sortObjects(ayyResults); - sortObjects(ayyResults); + expect(ayyResults).toEqual([ + { a: "Ayy" }, + { a: "Ayy", b: "B" }, + // Included this time! + { b: "B" }, + ]); - expect(ayyResults).toEqual([ - { a: "Ayy" }, - { a: "Ayy", b: "B" }, - // Included this time! - { b: "B" }, - ]); + const beeResults = await client.refetchQueries({ + updateCache(cache) { + cache.writeQuery({ + query: bQuery, + data: { + b: "Bee", + }, + }); + }, - const beeResults = await client.refetchQueries({ - updateCache(cache) { - cache.writeQuery({ - query: bQuery, - data: { - b: "Bee", - }, - }); - }, + // The "A" here causes aObs to be included, but the "AB" should be + // redundant because that query is already included. + include: ["A", "AB"], + + onQueryUpdated(obs, diff) { + if (obs === aObs) { + expect(diff.result).toEqual({ a: "Ayy" }); + } else if (obs === bObs) { + expect(diff.result).toEqual({ b: "Bee" }); + } else if (obs === abObs) { + expect(diff.result).toEqual({ a: "Ayy", b: "Bee" }); + } else { + throw new Error( + `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` + ); + } + return diff.result; + }, + }); - // The "A" here causes aObs to be included, but the "AB" should be - // redundant because that query is already included. - include: ["A", "AB"], - - onQueryUpdated(obs, diff) { - if (obs === aObs) { - expect(diff.result).toEqual({ a: "Ayy" }); - } else if (obs === bObs) { - expect(diff.result).toEqual({ b: "Bee" }); - } else if (obs === abObs) { - expect(diff.result).toEqual({ a: "Ayy", b: "Bee" }); - } else { - reject( - `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` - ); - } - return diff.result; - }, - }); + sortObjects(beeResults); - sortObjects(beeResults); + expect(beeResults).toEqual([ + { a: "Ayy" }, // Included this time! + { a: "Ayy", b: "Bee" }, + { b: "Bee" }, + ]); - expect(beeResults).toEqual([ - { a: "Ayy" }, // Included this time! - { a: "Ayy", b: "Bee" }, - { b: "Bee" }, - ]); + unsubscribe(); + }); - unsubscribe(); - resolve(); - } - ); + it("includes query DocumentNode objects specified in options.include", async () => { + expect.assertions(11); + const client = makeClient(); + const [aObs, bObs, abObs] = await setup(client); - itAsync( - "includes query DocumentNode objects specified in options.include", - async (resolve, reject) => { - const client = makeClient(); - const [aObs, bObs, abObs] = await setup(client); + const ayyResults = await client.refetchQueries({ + updateCache(cache) { + cache.writeQuery({ + query: aQuery, + data: { + a: "Ayy", + }, + }); + }, - const ayyResults = await client.refetchQueries({ - updateCache(cache) { - cache.writeQuery({ - query: aQuery, - data: { - a: "Ayy", - }, - }); - }, + // Note that we're passing query DocumentNode objects instead of query + // name strings, in this test. + include: [bQuery, abQuery], + + onQueryUpdated(obs, diff) { + if (obs === aObs) { + expect(diff.result).toEqual({ a: "Ayy" }); + } else if (obs === bObs) { + expect(diff.result).toEqual({ b: "B" }); + } else if (obs === abObs) { + expect(diff.result).toEqual({ a: "Ayy", b: "B" }); + } else { + throw new Error( + `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` + ); + } + return Promise.resolve(diff.result); + }, + }); - // Note that we're passing query DocumentNode objects instead of query - // name strings, in this test. - include: [bQuery, abQuery], - - onQueryUpdated(obs, diff) { - if (obs === aObs) { - expect(diff.result).toEqual({ a: "Ayy" }); - } else if (obs === bObs) { - expect(diff.result).toEqual({ b: "B" }); - } else if (obs === abObs) { - expect(diff.result).toEqual({ a: "Ayy", b: "B" }); - } else { - reject( - `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` - ); - } - return Promise.resolve(diff.result); - }, - }); + sortObjects(ayyResults); - sortObjects(ayyResults); + expect(ayyResults).toEqual([ + { a: "Ayy" }, + { a: "Ayy", b: "B" }, + // Included this time! + { b: "B" }, + ]); - expect(ayyResults).toEqual([ - { a: "Ayy" }, - { a: "Ayy", b: "B" }, - // Included this time! - { b: "B" }, - ]); + const beeResults = await client.refetchQueries({ + updateCache(cache) { + cache.writeQuery({ + query: bQuery, + data: { + b: "Bee", + }, + }); + }, - const beeResults = await client.refetchQueries({ - updateCache(cache) { - cache.writeQuery({ - query: bQuery, - data: { - b: "Bee", - }, - }); - }, + // The abQuery and "AB" should be redundant, but the aQuery here is + // important for aObs to be included. + include: [abQuery, "AB", aQuery], + + onQueryUpdated(obs, diff) { + if (obs === aObs) { + expect(diff.result).toEqual({ a: "Ayy" }); + } else if (obs === bObs) { + expect(diff.result).toEqual({ b: "Bee" }); + } else if (obs === abObs) { + expect(diff.result).toEqual({ a: "Ayy", b: "Bee" }); + } else { + throw new Error( + `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` + ); + } + return diff.result; + }, + }); - // The abQuery and "AB" should be redundant, but the aQuery here is - // important for aObs to be included. - include: [abQuery, "AB", aQuery], - - onQueryUpdated(obs, diff) { - if (obs === aObs) { - expect(diff.result).toEqual({ a: "Ayy" }); - } else if (obs === bObs) { - expect(diff.result).toEqual({ b: "Bee" }); - } else if (obs === abObs) { - expect(diff.result).toEqual({ a: "Ayy", b: "Bee" }); - } else { - reject( - `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` - ); - } - return diff.result; - }, - }); + sortObjects(beeResults); - sortObjects(beeResults); + expect(beeResults).toEqual([ + { a: "Ayy" }, // Included this time! + { a: "Ayy", b: "Bee" }, + { b: "Bee" }, + ]); - expect(beeResults).toEqual([ - { a: "Ayy" }, // Included this time! - { a: "Ayy", b: "Bee" }, - { b: "Bee" }, - ]); + unsubscribe(); + }); - unsubscribe(); - resolve(); - } - ); + it('includes all queries when options.include === "all"', async () => { + expect.assertions(11); + const client = makeClient(); + const [aObs, bObs, abObs] = await setup(client); - itAsync( - 'includes all queries when options.include === "all"', - async (resolve, reject) => { - const client = makeClient(); - const [aObs, bObs, abObs] = await setup(client); + const ayyResults = await client.refetchQueries({ + include: "all", - const ayyResults = await client.refetchQueries({ - include: "all", + updateCache(cache) { + cache.writeQuery({ + query: aQuery, + data: { + a: "Ayy", + }, + }); + }, - updateCache(cache) { - cache.writeQuery({ - query: aQuery, - data: { - a: "Ayy", - }, - }); - }, + onQueryUpdated(obs, diff) { + if (obs === aObs) { + expect(diff.result).toEqual({ a: "Ayy" }); + } else if (obs === bObs) { + expect(diff.result).toEqual({ b: "B" }); + } else if (obs === abObs) { + expect(diff.result).toEqual({ a: "Ayy", b: "B" }); + } else { + throw new Error( + `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` + ); + } + return Promise.resolve(diff.result); + }, + }); - onQueryUpdated(obs, diff) { - if (obs === aObs) { - expect(diff.result).toEqual({ a: "Ayy" }); - } else if (obs === bObs) { - expect(diff.result).toEqual({ b: "B" }); - } else if (obs === abObs) { - expect(diff.result).toEqual({ a: "Ayy", b: "B" }); - } else { - reject( - `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` - ); - } - return Promise.resolve(diff.result); - }, - }); + sortObjects(ayyResults); - sortObjects(ayyResults); + expect(ayyResults).toEqual([ + { a: "Ayy" }, + { a: "Ayy", b: "B" }, + { b: "B" }, + ]); - expect(ayyResults).toEqual([ - { a: "Ayy" }, - { a: "Ayy", b: "B" }, - { b: "B" }, - ]); + const beeResults = await client.refetchQueries({ + include: "all", - const beeResults = await client.refetchQueries({ - include: "all", + updateCache(cache) { + cache.writeQuery({ + query: bQuery, + data: { + b: "Bee", + }, + }); + }, - updateCache(cache) { - cache.writeQuery({ - query: bQuery, - data: { - b: "Bee", - }, - }); - }, + onQueryUpdated(obs, diff) { + if (obs === aObs) { + expect(diff.result).toEqual({ a: "Ayy" }); + } else if (obs === bObs) { + expect(diff.result).toEqual({ b: "Bee" }); + } else if (obs === abObs) { + expect(diff.result).toEqual({ a: "Ayy", b: "Bee" }); + } else { + throw new Error( + `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` + ); + } + return diff.result; + }, + }); - onQueryUpdated(obs, diff) { - if (obs === aObs) { - expect(diff.result).toEqual({ a: "Ayy" }); - } else if (obs === bObs) { - expect(diff.result).toEqual({ b: "Bee" }); - } else if (obs === abObs) { - expect(diff.result).toEqual({ a: "Ayy", b: "Bee" }); - } else { - reject( - `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` - ); - } - return diff.result; - }, - }); + sortObjects(beeResults); - sortObjects(beeResults); + expect(beeResults).toEqual([ + { a: "Ayy" }, + { a: "Ayy", b: "Bee" }, + { b: "Bee" }, + ]); - expect(beeResults).toEqual([ - { a: "Ayy" }, - { a: "Ayy", b: "Bee" }, - { b: "Bee" }, - ]); + unsubscribe(); + }); - unsubscribe(); - resolve(); - } - ); - - itAsync( - 'includes all active queries when options.include === "active"', - async (resolve, reject) => { - const client = makeClient(); - const [aObs, bObs, abObs] = await setup(client); - - const extraObs = client.watchQuery({ query: abQuery }); - expect(extraObs.hasObservers()).toBe(false); - - const activeResults = await client.refetchQueries({ - include: "active", - - onQueryUpdated(obs, diff) { - if (obs === aObs) { - expect(diff.result).toEqual({ a: "A" }); - } else if (obs === bObs) { - expect(diff.result).toEqual({ b: "B" }); - } else if (obs === abObs) { - expect(diff.result).toEqual({ a: "A", b: "B" }); - } else { - reject( - `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` - ); - } - return Promise.resolve(diff.result); - }, - }); + it('includes all active queries when options.include === "active"', async () => { + expect.assertions(15); + const client = makeClient(); + const [aObs, bObs, abObs] = await setup(client); + + const extraObs = client.watchQuery({ query: abQuery }); + expect(extraObs.hasObservers()).toBe(false); + + const activeResults = await client.refetchQueries({ + include: "active", + + onQueryUpdated(obs, diff) { + if (obs === aObs) { + expect(diff.result).toEqual({ a: "A" }); + } else if (obs === bObs) { + expect(diff.result).toEqual({ b: "B" }); + } else if (obs === abObs) { + expect(diff.result).toEqual({ a: "A", b: "B" }); + } else { + throw new Error( + `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` + ); + } + return Promise.resolve(diff.result); + }, + }); - sortObjects(activeResults); + sortObjects(activeResults); - expect(activeResults).toEqual([ - { a: "A" }, - { a: "A", b: "B" }, - { b: "B" }, - ]); + expect(activeResults).toEqual([{ a: "A" }, { a: "A", b: "B" }, { b: "B" }]); - subs.push( - extraObs.subscribe({ - next(result) { - expect(result).toEqual({ a: "A", b: "B" }); - }, - }) - ); - expect(extraObs.hasObservers()).toBe(true); - - const resultsAfterSubscribe = await client.refetchQueries({ - include: "active", - - onQueryUpdated(obs, diff) { - if (obs === aObs) { - expect(diff.result).toEqual({ a: "A" }); - } else if (obs === bObs) { - expect(diff.result).toEqual({ b: "B" }); - } else if (obs === abObs) { - expect(diff.result).toEqual({ a: "A", b: "B" }); - } else if (obs === extraObs) { - expect(diff.result).toEqual({ a: "A", b: "B" }); - } else { - reject( - `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` - ); - } - return Promise.resolve(diff.result); + subs.push( + extraObs.subscribe({ + next(result) { + expect(result).toEqual({ a: "A", b: "B" }); }, - }); - - sortObjects(resultsAfterSubscribe); + }) + ); + expect(extraObs.hasObservers()).toBe(true); + + const resultsAfterSubscribe = await client.refetchQueries({ + include: "active", + + onQueryUpdated(obs, diff) { + if (obs === aObs) { + expect(diff.result).toEqual({ a: "A" }); + } else if (obs === bObs) { + expect(diff.result).toEqual({ b: "B" }); + } else if (obs === abObs) { + expect(diff.result).toEqual({ a: "A", b: "B" }); + } else if (obs === extraObs) { + expect(diff.result).toEqual({ a: "A", b: "B" }); + } else { + throw new Error( + `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` + ); + } + return Promise.resolve(diff.result); + }, + }); - expect(resultsAfterSubscribe).toEqual([ - { a: "A" }, - { a: "A", b: "B" }, - // Included thanks to extraObs this time. - { a: "A", b: "B" }, - // Sorted last by sortObjects. - { b: "B" }, - ]); + sortObjects(resultsAfterSubscribe); - unsubscribe(); - resolve(); - } - ); - - itAsync( - "includes queries named in refetchQueries even if they have no observers", - async (resolve, reject) => { - const client = makeClient(); - - const aObs = client.watchQuery({ query: aQuery }); - const bObs = client.watchQuery({ query: bQuery }); - const abObs = client.watchQuery({ query: abQuery }); - - // These ObservableQuery objects have no observers yet, but should - // nevertheless be refetched if identified explicitly in an options.include - // array passed to client.refetchQueries. - expect(aObs.hasObservers()).toBe(false); - expect(bObs.hasObservers()).toBe(false); - expect(abObs.hasObservers()).toBe(false); - - const activeResults = await client.refetchQueries({ - include: ["A", abQuery], - - onQueryUpdated(obs, diff) { - if (obs === aObs) { - expect(diff.complete).toBe(false); - expect(diff.result).toEqual({}); - } else if (obs === abObs) { - expect(diff.complete).toBe(false); - expect(diff.result).toEqual({}); - } else { - reject( - `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` - ); - } - return Promise.resolve(diff.result); - }, - }); + expect(resultsAfterSubscribe).toEqual([ + { a: "A" }, + { a: "A", b: "B" }, + // Included thanks to extraObs this time. + { a: "A", b: "B" }, + // Sorted last by sortObjects. + { b: "B" }, + ]); - sortObjects(activeResults); - expect(activeResults).toEqual([{}, {}]); - - subs.push( - abObs.subscribe({ - next(result) { - expect(result.data).toEqual({ a: "A", b: "B" }); - - client - .refetchQueries({ - include: [aQuery, "B"], - - onQueryUpdated(obs, diff) { - if (obs === aObs) { - expect(diff.result).toEqual({ a: "A" }); - } else if (obs === bObs) { - expect(diff.result).toEqual({ b: "B" }); - } else if (obs === abObs) { - expect(diff.result).toEqual({ a: "A", b: "B" }); - } else { - reject( - `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` - ); - } - return Promise.resolve(diff.result); - }, - }) - .then((resultsAfterSubscribe) => { - sortObjects(resultsAfterSubscribe); - expect(resultsAfterSubscribe).toEqual([{ a: "A" }, { b: "B" }]); - - unsubscribe(); - }) - .then(resolve, reject); - }, - }) - ); + unsubscribe(); + }); - expect(abObs.hasObservers()).toBe(true); - } - ); - - itAsync( - "should not include unwatched single queries", - async (resolve, reject) => { - const client = makeClient(); - const [aObs, bObs, abObs] = await setup(client); - - const delayedQuery = gql` - query DELAYED { - d - e - l - a - y - e - d + it("includes queries named in refetchQueries even if they have no observers", async () => { + expect.assertions(13); + const client = makeClient(); + + const aObs = client.watchQuery({ query: aQuery }); + const bObs = client.watchQuery({ query: bQuery }); + const abObs = client.watchQuery({ query: abQuery }); + + // These ObservableQuery objects have no observers yet, but should + // nevertheless be refetched if identified explicitly in an options.include + // array passed to client.refetchQueries. + expect(aObs.hasObservers()).toBe(false); + expect(bObs.hasObservers()).toBe(false); + expect(abObs.hasObservers()).toBe(false); + + const activeResults = await client.refetchQueries({ + include: ["A", abQuery], + + onQueryUpdated(obs, diff) { + if (obs === aObs) { + expect(diff.complete).toBe(false); + expect(diff.result).toEqual({}); + } else if (obs === abObs) { + expect(diff.complete).toBe(false); + expect(diff.result).toEqual({}); + } else { + throw new Error( + `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` + ); } - `; - - client - .query({ - query: delayedQuery, - variables: { - // Delay this query by 10 seconds so it stays in-flight. - delay: 10000, - }, - }) - .catch(reject); - - const queries = client["queryManager"]["queries"]; - expect(queries.size).toBe(4); - - queries.forEach((queryInfo, queryId) => { - if (queryId === "1" || queryId === "2" || queryId === "3") { - expect(queryInfo.observableQuery).toBeInstanceOf(ObservableQuery); - } else if (queryId === "4") { - // One-off client.query-style queries never get an ObservableQuery, so - // they should not be included by include: "active". - expect(queryInfo.observableQuery).toBe(null); - expect(queryInfo.document).toBe(delayedQuery); + return Promise.resolve(diff.result); + }, + }); + + sortObjects(activeResults); + expect(activeResults).toEqual([{}, {}]); + + const stream = new ObservableStream(abObs); + subs.push(stream as unknown as Subscription); + expect(abObs.hasObservers()).toBe(true); + + await expect(stream).toEmitMatchedValue({ data: { a: "A", b: "B" } }); + + const resultsAfterSubscribe = await client.refetchQueries({ + include: [aQuery, "B"], + + onQueryUpdated(obs, diff) { + if (obs === aObs) { + expect(diff.result).toEqual({ a: "A" }); + } else if (obs === bObs) { + expect(diff.result).toEqual({ b: "B" }); + } else { + throw new Error( + `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` + ); } - }); + return Promise.resolve(diff.result); + }, + }); - const activeResults = await client.refetchQueries({ - include: "active", - - onQueryUpdated(obs, diff) { - if (obs === aObs) { - expect(diff.result).toEqual({ a: "A" }); - } else if (obs === bObs) { - expect(diff.result).toEqual({ b: "B" }); - } else if (obs === abObs) { - expect(diff.result).toEqual({ a: "A", b: "B" }); - } else { - reject( - `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` - ); - } - return Promise.resolve(diff.result); - }, - }); + sortObjects(resultsAfterSubscribe); + expect(resultsAfterSubscribe).toEqual([{ a: "A" }, { b: "B" }]); - sortObjects(activeResults); + unsubscribe(); + }); - expect(activeResults).toEqual([ - { a: "A" }, - { a: "A", b: "B" }, - { b: "B" }, - ]); + it("should not include unwatched single queries", async () => { + expect.assertions(18); + const client = makeClient(); + const [aObs, bObs, abObs] = await setup(client); + + const delayedQuery = gql` + query DELAYED { + d + e + l + a + y + e + d + } + `; + + void client.query({ + query: delayedQuery, + variables: { + // Delay this query by 10 seconds so it stays in-flight. + delay: 10000, + }, + }); - const allResults = await client.refetchQueries({ - include: "all", - - onQueryUpdated(obs, diff) { - if (obs === aObs) { - expect(diff.result).toEqual({ a: "A" }); - } else if (obs === bObs) { - expect(diff.result).toEqual({ b: "B" }); - } else if (obs === abObs) { - expect(diff.result).toEqual({ a: "A", b: "B" }); - } else { - reject( - `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` - ); - } - return Promise.resolve(diff.result); - }, - }); + const queries = client["queryManager"]["queries"]; + expect(queries.size).toBe(4); + + queries.forEach((queryInfo, queryId) => { + if (queryId === "1" || queryId === "2" || queryId === "3") { + expect(queryInfo.observableQuery).toBeInstanceOf(ObservableQuery); + } else if (queryId === "4") { + // One-off client.query-style queries never get an ObservableQuery, so + // they should not be included by include: "active". + expect(queryInfo.observableQuery).toBe(null); + expect(queryInfo.document).toBe(delayedQuery); + } + }); - sortObjects(allResults); + const activeResults = await client.refetchQueries({ + include: "active", + + onQueryUpdated(obs, diff) { + if (obs === aObs) { + expect(diff.result).toEqual({ a: "A" }); + } else if (obs === bObs) { + expect(diff.result).toEqual({ b: "B" }); + } else if (obs === abObs) { + expect(diff.result).toEqual({ a: "A", b: "B" }); + } else { + throw new Error( + `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` + ); + } + return Promise.resolve(diff.result); + }, + }); - expect(allResults).toEqual([{ a: "A" }, { a: "A", b: "B" }, { b: "B" }]); + sortObjects(activeResults); - unsubscribe(); - client.stop(); + expect(activeResults).toEqual([{ a: "A" }, { a: "A", b: "B" }, { b: "B" }]); - expect(queries.size).toBe(0); + const allResults = await client.refetchQueries({ + include: "all", - resolve(); - } - ); - - itAsync( - "refetches watched queries if onQueryUpdated not provided", - async (resolve, reject) => { - const client = makeClient(); - const [aObs, bObs, abObs] = await setup(client); - - const aSpy = jest.spyOn(aObs, "refetch"); - const bSpy = jest.spyOn(bObs, "refetch"); - const abSpy = jest.spyOn(abObs, "refetch"); - - const ayyResults = ( - await client.refetchQueries({ - include: ["B"], - updateCache(cache) { - cache.writeQuery({ - query: aQuery, - data: { - a: "Ayy", - }, - }); - }, - }) - ).map((result) => result.data as object); + onQueryUpdated(obs, diff) { + if (obs === aObs) { + expect(diff.result).toEqual({ a: "A" }); + } else if (obs === bObs) { + expect(diff.result).toEqual({ b: "B" }); + } else if (obs === abObs) { + expect(diff.result).toEqual({ a: "A", b: "B" }); + } else { + throw new Error( + `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` + ); + } + return Promise.resolve(diff.result); + }, + }); - sortObjects(ayyResults); + sortObjects(allResults); - // These results have reverted back to what the ApolloLink returns ("A" - // rather than "Ayy"), because we let them be refetched (by not providing - // an onQueryUpdated function). - expect(ayyResults).toEqual([{ a: "A" }, { a: "A", b: "B" }, { b: "B" }]); + expect(allResults).toEqual([{ a: "A" }, { a: "A", b: "B" }, { b: "B" }]); - expect(aSpy).toHaveBeenCalledTimes(1); - expect(bSpy).toHaveBeenCalledTimes(1); - expect(abSpy).toHaveBeenCalledTimes(1); + unsubscribe(); + client.stop(); - unsubscribe(); - resolve(); - } - ); - - itAsync( - "can run updateQuery function against optimistic cache layer", - async (resolve, reject) => { - const client = makeClient(); - const [aObs, bObs, abObs] = await setup(client); - - client.cache.watch({ - query: abQuery, - optimistic: false, - callback(diff) { - reject("should not have notified non-optimistic watcher"); - }, - }); + expect(queries.size).toBe(0); + }); - expect(client.cache.extract(true)).toEqual({ - ROOT_QUERY: { - __typename: "Query", - a: "A", - b: "B", - }, - }); + it("refetches watched queries if onQueryUpdated not provided", async () => { + expect.assertions(7); + const client = makeClient(); + const [aObs, bObs, abObs] = await setup(client); - const results = await client.refetchQueries({ - // This causes the update to run against a temporary optimistic layer. - optimistic: true, + const aSpy = jest.spyOn(aObs, "refetch"); + const bSpy = jest.spyOn(bObs, "refetch"); + const abSpy = jest.spyOn(abObs, "refetch"); + const ayyResults = ( + await client.refetchQueries({ + include: ["B"], updateCache(cache) { - const modified = cache.modify({ - fields: { - a(value, { DELETE }) { - expect(value).toEqual("A"); - return DELETE; - }, + cache.writeQuery({ + query: aQuery, + data: { + a: "Ayy", }, }); - expect(modified).toBe(true); }, + }) + ).map((result) => result.data as object); - onQueryUpdated(obs, diff) { - expect(diff.complete).toBe(true); - - // Even though we evicted the Query.a field in the updateCache function, - // that optimistic layer was discarded before broadcasting results, so - // we're back to the original (non-optimistic) data. - if (obs === aObs) { - expect(diff.result).toEqual({ a: "A" }); - } else if (obs === bObs) { - reject("bQuery should not have been updated"); - } else if (obs === abObs) { - expect(diff.result).toEqual({ a: "A", b: "B" }); - } else { - reject( - `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` - ); - } - - return diff.result; - }, - }); + sortObjects(ayyResults); - sortObjects(results); + // These results have reverted back to what the ApolloLink returns ("A" + // rather than "Ayy"), because we let them be refetched (by not providing + // an onQueryUpdated function). + expect(ayyResults).toEqual([{ a: "A" }, { a: "A", b: "B" }, { b: "B" }]); - expect(results).toEqual([{ a: "A" }, { a: "A", b: "B" }]); + expect(aSpy).toHaveBeenCalledTimes(1); + expect(bSpy).toHaveBeenCalledTimes(1); + expect(abSpy).toHaveBeenCalledTimes(1); - expect(client.cache.extract(true)).toEqual({ - ROOT_QUERY: { - __typename: "Query", - a: "A", - b: "B", - }, - }); + unsubscribe(); + }); - resolve(); - } - ); - - itAsync( - "can return true from onQueryUpdated to choose default refetching behavior", - async (resolve, reject) => { - const client = makeClient(); - const [aObs, bObs, abObs] = await setup(client); - - const refetchResult = client.refetchQueries({ - include: ["A", "B"], - onQueryUpdated(obs, diff) { - if (obs === aObs) { - expect(diff.result).toEqual({ a: "A" }); - } else if (obs === bObs) { - expect(diff.result).toEqual({ b: "B" }); - } else if (obs === abObs) { - reject("abQuery should not have been updated"); - } else { - reject( - `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` - ); - } - return true; - }, - }); + it("can run updateQuery function against optimistic cache layer", async () => { + expect.assertions(12); + const client = makeClient(); + const [aObs, bObs, abObs] = await setup(client); - expect(refetchResult.results.length).toBe(2); - refetchResult.results.forEach((result) => { - expect(result).toBeInstanceOf(Promise); - }); + client.cache.watch({ + query: abQuery, + optimistic: false, + callback(diff) { + throw new Error("should not have notified non-optimistic watcher"); + }, + }); - expect( - refetchResult.queries - .map((obs) => { - expect(obs).toBeInstanceOf(ObservableQuery); - return obs.queryName; - }) - .sort() - ).toEqual(["A", "B"]); - - const results = (await refetchResult).map((result) => { - // These results are ApolloQueryResult, as inferred by TypeScript. - expect(Object.keys(result).sort()).toEqual([ - "data", - "loading", - "networkStatus", - ]); - return result.data; - }); + expect(client.cache.extract(true)).toEqual({ + ROOT_QUERY: { + __typename: "Query", + a: "A", + b: "B", + }, + }); - sortObjects(results); + const results = await client.refetchQueries({ + // This causes the update to run against a temporary optimistic layer. + optimistic: true, - expect(results).toEqual([{ a: "A" }, { b: "B" }]); + updateCache(cache) { + const modified = cache.modify({ + fields: { + a(value, { DELETE }) { + expect(value).toEqual("A"); + return DELETE; + }, + }, + }); + expect(modified).toBe(true); + }, - resolve(); - } - ); + onQueryUpdated(obs, diff) { + expect(diff.complete).toBe(true); + + // Even though we evicted the Query.a field in the updateCache function, + // that optimistic layer was discarded before broadcasting results, so + // we're back to the original (non-optimistic) data. + if (obs === aObs) { + expect(diff.result).toEqual({ a: "A" }); + } else if (obs === bObs) { + throw new Error("bQuery should not have been updated"); + } else if (obs === abObs) { + expect(diff.result).toEqual({ a: "A", b: "B" }); + } else { + throw new Error( + `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` + ); + } - itAsync( - "can return true from onQueryUpdated when using options.updateCache", - async (resolve, reject) => { - const client = makeClient(); - const [aObs, bObs, abObs] = await setup(client); + return diff.result; + }, + }); - const refetchResult = client.refetchQueries({ - updateCache(cache) { - cache.writeQuery({ - query: bQuery, - data: { - b: "Beetlejuice", - }, - }); - }, + sortObjects(results); - onQueryUpdated(obs, diff) { - if (obs === aObs) { - reject("aQuery should not have been updated"); - } else if (obs === bObs) { - expect(diff.result).toEqual({ b: "Beetlejuice" }); - } else if (obs === abObs) { - expect(diff.result).toEqual({ a: "A", b: "Beetlejuice" }); - } else { - reject( - `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` - ); - } - - expect(client.cache.extract()).toEqual({ - ROOT_QUERY: { - __typename: "Query", - a: "A", - b: "Beetlejuice", - }, - }); + expect(results).toEqual([{ a: "A" }, { a: "A", b: "B" }]); - return true; - }, - }); + expect(client.cache.extract(true)).toEqual({ + ROOT_QUERY: { + __typename: "Query", + a: "A", + b: "B", + }, + }); + }); - expect(refetchResult.results.length).toBe(2); - refetchResult.results.forEach((result) => { - expect(result).toBeInstanceOf(Promise); - }); + it("can return true from onQueryUpdated to choose default refetching behavior", async () => { + expect.assertions(14); + const client = makeClient(); + const [aObs, bObs, abObs] = await setup(client); + + const refetchResult = client.refetchQueries({ + include: ["A", "B"], + onQueryUpdated(obs, diff) { + if (obs === aObs) { + expect(diff.result).toEqual({ a: "A" }); + } else if (obs === bObs) { + expect(diff.result).toEqual({ b: "B" }); + } else if (obs === abObs) { + throw new Error("abQuery should not have been updated"); + } else { + throw new Error( + `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` + ); + } + return true; + }, + }); - expect( - refetchResult.queries - .map((obs) => { - expect(obs).toBeInstanceOf(ObservableQuery); - return obs.queryName; - }) - .sort() - ).toEqual(["AB", "B"]); - - const results = (await refetchResult).map((result) => { - // These results are ApolloQueryResult, as inferred by TypeScript. - expect(Object.keys(result).sort()).toEqual([ - "data", - "loading", - "networkStatus", - ]); - return result.data; - }); + expect(refetchResult.results.length).toBe(2); + refetchResult.results.forEach((result) => { + expect(result).toBeInstanceOf(Promise); + }); + + expect( + refetchResult.queries + .map((obs) => { + expect(obs).toBeInstanceOf(ObservableQuery); + return obs.queryName; + }) + .sort() + ).toEqual(["A", "B"]); + + const results = (await refetchResult).map((result) => { + // These results are ApolloQueryResult, as inferred by TypeScript. + expect(Object.keys(result).sort()).toEqual([ + "data", + "loading", + "networkStatus", + ]); + return result.data; + }); + + sortObjects(results); + + expect(results).toEqual([{ a: "A" }, { b: "B" }]); + }); - sortObjects(results); + it("can return true from onQueryUpdated when using options.updateCache", async () => { + expect.assertions(17); + const client = makeClient(); + const [aObs, bObs, abObs] = await setup(client); - expect(results).toEqual([ - // Since we returned true from onQueryUpdated, the results were refetched, - // replacing "Beetlejuice" with "B" again. - { a: "A", b: "B" }, - { b: "B" }, + const refetchResult = client.refetchQueries({ + updateCache(cache) { + cache.writeQuery({ + query: bQuery, + data: { + b: "Beetlejuice", + }, + }); + }, + + onQueryUpdated(obs, diff) { + if (obs === aObs) { + throw new Error("aQuery should not have been updated"); + } else if (obs === bObs) { + expect(diff.result).toEqual({ b: "Beetlejuice" }); + } else if (obs === abObs) { + expect(diff.result).toEqual({ a: "A", b: "Beetlejuice" }); + } else { + throw new Error( + `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` + ); + } + + expect(client.cache.extract()).toEqual({ + ROOT_QUERY: { + __typename: "Query", + a: "A", + b: "Beetlejuice", + }, + }); + + return true; + }, + }); + + expect(refetchResult.results.length).toBe(2); + refetchResult.results.forEach((result) => { + expect(result).toBeInstanceOf(Promise); + }); + + expect( + refetchResult.queries + .map((obs) => { + expect(obs).toBeInstanceOf(ObservableQuery); + return obs.queryName; + }) + .sort() + ).toEqual(["AB", "B"]); + + const results = (await refetchResult).map((result) => { + // These results are ApolloQueryResult, as inferred by TypeScript. + expect(Object.keys(result).sort()).toEqual([ + "data", + "loading", + "networkStatus", ]); + return result.data; + }); - expect(client.cache.extract()).toEqual({ - ROOT_QUERY: { - __typename: "Query", - a: "A", - b: "B", - }, - }); + sortObjects(results); - resolve(); - } - ); - - itAsync( - "can return false from onQueryUpdated to skip/ignore a query", - async (resolve, reject) => { - const client = makeClient(); - const [aObs, bObs, abObs] = await setup(client); - - const refetchResult = client.refetchQueries({ - include: ["A", "B"], - onQueryUpdated(obs, diff) { - if (obs === aObs) { - expect(diff.result).toEqual({ a: "A" }); - } else if (obs === bObs) { - expect(diff.result).toEqual({ b: "B" }); - } else if (obs === abObs) { - reject("abQuery should not have been updated"); - } else { - reject( - `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` - ); - } - // Skip refetching all but the B query. - return obs.queryName === "B"; - }, - }); + expect(results).toEqual([ + // Since we returned true from onQueryUpdated, the results were refetched, + // replacing "Beetlejuice" with "B" again. + { a: "A", b: "B" }, + { b: "B" }, + ]); - expect(refetchResult.results.length).toBe(1); - refetchResult.results.forEach((result) => { - expect(result).toBeInstanceOf(Promise); - }); + expect(client.cache.extract()).toEqual({ + ROOT_QUERY: { + __typename: "Query", + a: "A", + b: "B", + }, + }); + }); - expect( - refetchResult.queries - .map((obs) => { - expect(obs).toBeInstanceOf(ObservableQuery); - return obs.queryName; - }) - .sort() - ).toEqual(["B"]); - - const results = (await refetchResult).map((result) => { - // These results are ApolloQueryResult, as inferred by TypeScript. - expect(Object.keys(result).sort()).toEqual([ - "data", - "loading", - "networkStatus", - ]); - return result.data; - }); + it("can return false from onQueryUpdated to skip/ignore a query", async () => { + expect.assertions(11); + const client = makeClient(); + const [aObs, bObs, abObs] = await setup(client); + + const refetchResult = client.refetchQueries({ + include: ["A", "B"], + onQueryUpdated(obs, diff) { + if (obs === aObs) { + expect(diff.result).toEqual({ a: "A" }); + } else if (obs === bObs) { + expect(diff.result).toEqual({ b: "B" }); + } else if (obs === abObs) { + throw new Error("abQuery should not have been updated"); + } else { + throw new Error( + `unexpected ObservableQuery ${obs.queryId} with name ${obs.queryName}` + ); + } + // Skip refetching all but the B query. + return obs.queryName === "B"; + }, + }); - sortObjects(results); + expect(refetchResult.results.length).toBe(1); + refetchResult.results.forEach((result) => { + expect(result).toBeInstanceOf(Promise); + }); - expect(results).toEqual([{ b: "B" }]); + expect( + refetchResult.queries + .map((obs) => { + expect(obs).toBeInstanceOf(ObservableQuery); + return obs.queryName; + }) + .sort() + ).toEqual(["B"]); + + const results = (await refetchResult).map((result) => { + // These results are ApolloQueryResult, as inferred by TypeScript. + expect(Object.keys(result).sort()).toEqual([ + "data", + "loading", + "networkStatus", + ]); + return result.data; + }); - resolve(); - } - ); + sortObjects(results); + + expect(results).toEqual([{ b: "B" }]); + }); it("can refetch no-cache queries", () => { // TODO The options.updateCache function won't work for these queries, but diff --git a/src/__tests__/subscribeToMore.ts b/src/__tests__/subscribeToMore.ts index 419b0b696d0..6216eaa85c9 100644 --- a/src/__tests__/subscribeToMore.ts +++ b/src/__tests__/subscribeToMore.ts @@ -4,7 +4,8 @@ import { DocumentNode, OperationDefinitionNode } from "graphql"; import { ApolloClient } from "../core"; import { InMemoryCache } from "../cache"; import { ApolloLink, Operation } from "../link/core"; -import { itAsync, mockSingleLink, mockObservableLink } from "../testing"; +import { mockSingleLink, mockObservableLink, wait } from "../testing"; +import { ObservableStream, spyOnConsole } from "../testing/internal"; const isSub = (operation: Operation) => (operation.query as DocumentNode).definitions @@ -57,13 +58,11 @@ describe("subscribeToMore", () => { name: string; } - itAsync("triggers new result from subscription data", (resolve, reject) => { - let latestResult: any = null; + it("triggers new result from subscription data", async () => { const wSLink = mockObservableLink(); - const httpLink = mockSingleLink(req1).setOnError(reject); + const httpLink = mockSingleLink(req1); const link = ApolloLink.split(isSub, wSLink, httpLink); - let counter = 0; const client = new ApolloClient({ cache: new InMemoryCache({ addTypename: false }), @@ -71,21 +70,7 @@ describe("subscribeToMore", () => { }); const obsHandle = client.watchQuery({ query }); - - const sub = obsHandle.subscribe({ - next(queryResult: any) { - latestResult = queryResult; - if (++counter === 3) { - sub.unsubscribe(); - expect(latestResult).toEqual({ - data: { entry: { value: "Amanda Liu" } }, - loading: false, - networkStatus: 7, - }); - resolve(); - } - }, - }); + const stream = new ObservableStream(obsHandle); obsHandle.subscribeToMore({ document: gql` @@ -98,26 +83,35 @@ describe("subscribeToMore", () => { }, }); - let i = 0; - function simulate() { - const result = results[i++]; - if (result) { - wSLink.simulateResult(result); - setTimeout(simulate, 10); - } - } - simulate(); + await expect(stream).toEmitValue({ + data: { entry: { value: "1" } }, + loading: false, + networkStatus: 7, + }); + + wSLink.simulateResult(results[0]); + + await expect(stream).toEmitValue({ + data: { entry: { value: "Dahivat Pandya" } }, + loading: false, + networkStatus: 7, + }); + + await wait(10); + wSLink.simulateResult(results[1]); + + await expect(stream).toEmitValue({ + data: { entry: { value: "Amanda Liu" } }, + loading: false, + networkStatus: 7, + }); }); - itAsync("calls error callback on error", (resolve, reject) => { - let latestResult: any = null; + it("calls error callback on error", async () => { const wSLink = mockObservableLink(); - const httpLink = mockSingleLink(req1).setOnError(reject); - + const httpLink = mockSingleLink(req1); const link = ApolloLink.split(isSub, wSLink, httpLink); - let counter = 0; - const client = new ApolloClient({ link, cache: new InMemoryCache({ addTypename: false }), @@ -126,14 +120,9 @@ describe("subscribeToMore", () => { const obsHandle = client.watchQuery<(typeof req1)["result"]["data"]>({ query, }); - const sub = obsHandle.subscribe({ - next(queryResult: any) { - latestResult = queryResult; - counter++; - }, - }); + const stream = new ObservableStream(obsHandle); - let errorCount = 0; + const onError = jest.fn(); obsHandle.subscribeToMore({ document: gql` @@ -144,98 +133,84 @@ describe("subscribeToMore", () => { updateQuery: (_, { subscriptionData }) => { return { entry: { value: subscriptionData.data.name } }; }, - onError: () => { - errorCount += 1; - }, + onError, }); - setTimeout(() => { - sub.unsubscribe(); - expect(latestResult).toEqual({ - data: { entry: { value: "Amanda Liu" } }, - loading: false, - networkStatus: 7, - }); - expect(counter).toBe(2); - expect(errorCount).toBe(1); - resolve(); - }, 15); - - for (let i = 0; i < 2; i++) { - wSLink.simulateResult(results2[i]); + for (const result of results2) { + wSLink.simulateResult(result); } - }); - itAsync( - "prints unhandled subscription errors to the console", - (resolve, reject) => { - let latestResult: any = null; + await expect(stream).toEmitValue({ + data: { entry: { value: "1" } }, + loading: false, + networkStatus: 7, + }); - const wSLink = mockObservableLink(); - const httpLink = mockSingleLink(req1).setOnError(reject); + await expect(stream).toEmitValue({ + data: { entry: { value: "Amanda Liu" } }, + loading: false, + networkStatus: 7, + }); - const link = ApolloLink.split(isSub, wSLink, httpLink); + await wait(15); - let counter = 0; + expect(onError).toHaveBeenCalledTimes(1); + expect(onError).toHaveBeenCalledWith(new Error("You cant touch this")); + }); - const client = new ApolloClient({ - link, - cache: new InMemoryCache({ addTypename: false }), - }); + it("prints unhandled subscription errors to the console", async () => { + using _ = spyOnConsole("error"); - const obsHandle = client.watchQuery({ - query, - }); - const sub = obsHandle.subscribe({ - next(queryResult: any) { - latestResult = queryResult; - counter++; - }, - }); + const wSLink = mockObservableLink(); + const httpLink = mockSingleLink(req1); + const link = ApolloLink.split(isSub, wSLink, httpLink); - let errorCount = 0; - const consoleErr = console.error; - console.error = (_: Error) => { - errorCount += 1; - }; + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ addTypename: false }), + }); - obsHandle.subscribeToMore({ - document: gql` - subscription newValues { - name - } - `, - updateQuery: () => { - throw new Error("should not be called because of initial error"); - }, - }); - - setTimeout(() => { - sub.unsubscribe(); - expect(latestResult).toEqual({ - data: { entry: { value: "1" } }, - loading: false, - networkStatus: 7, - }); - expect(counter).toBe(1); - expect(errorCount).toBe(1); - console.error = consoleErr; - resolve(); - }, 15); - - for (let i = 0; i < 2; i++) { - wSLink.simulateResult(results3[i]); - } + const obsHandle = client.watchQuery({ + query, + }); + const stream = new ObservableStream(obsHandle); + + obsHandle.subscribeToMore({ + document: gql` + subscription newValues { + name + } + `, + updateQuery: () => { + throw new Error("should not be called because of initial error"); + }, + }); + + for (const result of results3) { + wSLink.simulateResult(result); } - ); - itAsync("should not corrupt the cache (#3062)", async (resolve, reject) => { - let latestResult: any = null; - const wSLink = mockObservableLink(); - const httpLink = mockSingleLink(req4).setOnError(reject); + await expect(stream).toEmitValue({ + data: { entry: { value: "1" } }, + loading: false, + networkStatus: 7, + }); + + await wait(15); + + expect(console.error).toHaveBeenCalledTimes(1); + expect(console.error).toHaveBeenCalledWith( + "Unhandled GraphQL subscription error", + new Error("You cant touch this") + ); + + await expect(stream).not.toEmitAnything(); + }); + it("should not corrupt the cache (#3062)", async () => { + const wSLink = mockObservableLink(); + const httpLink = mockSingleLink(req4); const link = ApolloLink.split(isSub, wSLink, httpLink); - let counter = 0; const client = new ApolloClient({ cache: new InMemoryCache({ addTypename: false }).restore({ @@ -256,13 +231,7 @@ describe("subscribeToMore", () => { const obsHandle = client.watchQuery<(typeof req4)["result"]["data"]>({ query, }); - - const sub = obsHandle.subscribe({ - next(queryResult: any) { - latestResult = queryResult; - counter++; - }, - }); + const stream = new ObservableStream(obsHandle); let nextMutation: { value: string }; obsHandle.subscribeToMore({ @@ -279,9 +248,6 @@ describe("subscribeToMore", () => { }, }); - const wait = (dur: any) => - new Promise((resolve) => setTimeout(resolve, dur)); - for (let i = 0; i < 2; i++) { // init optimistic mutation let data = client.cache.readQuery<(typeof req4)["result"]["data"]>( @@ -302,105 +268,104 @@ describe("subscribeToMore", () => { client.cache.removeOptimistic(i.toString()); // note: we don't complete mutation with performTransaction because a real example would detect duplicates } - sub.unsubscribe(); - expect(counter).toBe(3); - expect(latestResult).toEqual({ + + await expect(stream).toEmitValue({ + data: { entry: [{ value: 1 }, { value: 2 }] }, + loading: false, + networkStatus: 7, + }); + + await expect(stream).toEmitValue({ + data: { + entry: [{ value: 1 }, { value: 2 }, { value: "Dahivat Pandya" }], + }, + loading: false, + networkStatus: 7, + }); + + await expect(stream).toEmitValue({ data: { entry: [ - { - value: 1, - }, - { - value: 2, - }, - { - value: "Dahivat Pandya", - }, - { - value: "Amanda Liu", - }, + { value: 1 }, + { value: 2 }, + { value: "Dahivat Pandya" }, + { value: "Amanda Liu" }, ], }, loading: false, networkStatus: 7, }); - resolve(); + + await expect(stream).not.toEmitAnything(); }); // TODO add a test that checks that subscriptions are cancelled when obs is unsubscribed from. - itAsync( - "allows specification of custom types for variables and payload (#4246)", - (resolve, reject) => { - interface TypedOperation extends Operation { - variables: { - someNumber: number; - }; - } - const typedReq = { - request: { query, variables: { someNumber: 1 } } as TypedOperation, - result, + it("allows specification of custom types for variables and payload (#4246)", async () => { + interface TypedOperation extends Operation { + variables: { + someNumber: number; }; - interface TypedSubscriptionVariables { - someString: string; - } + } + const typedReq = { + request: { query, variables: { someNumber: 1 } } as TypedOperation, + result, + }; + interface TypedSubscriptionVariables { + someString: string; + } - let latestResult: any = null; - const wSLink = mockObservableLink(); - const httpLink = mockSingleLink(typedReq).setOnError(reject); - - const link = ApolloLink.split(isSub, wSLink, httpLink); - let counter = 0; - - const client = new ApolloClient({ - cache: new InMemoryCache({ addTypename: false }), - link, - }); - - type TData = (typeof typedReq)["result"]["data"]; - type TVars = (typeof typedReq)["request"]["variables"]; - const obsHandle = client.watchQuery({ - query, - variables: { someNumber: 1 }, - }); - - const sub = obsHandle.subscribe({ - next(queryResult: any) { - latestResult = queryResult; - if (++counter === 3) { - sub.unsubscribe(); - expect(latestResult).toEqual({ - data: { entry: { value: "Amanda Liu" } }, - loading: false, - networkStatus: 7, - }); - resolve(); - } - }, - }); - - obsHandle.subscribeToMore({ - document: gql` - subscription newValues { - name - } - `, - variables: { - someString: "foo", - }, - updateQuery: (_, { subscriptionData }) => { - return { entry: { value: subscriptionData.data.name } }; - }, - }); - - let i = 0; - function simulate() { - const result = results[i++]; - if (result) { - wSLink.simulateResult(result); - setTimeout(simulate, 10); + const wSLink = mockObservableLink(); + const httpLink = mockSingleLink(typedReq); + const link = ApolloLink.split(isSub, wSLink, httpLink); + + const client = new ApolloClient({ + cache: new InMemoryCache({ addTypename: false }), + link, + }); + + type TData = (typeof typedReq)["result"]["data"]; + type TVars = (typeof typedReq)["request"]["variables"]; + const obsHandle = client.watchQuery({ + query, + variables: { someNumber: 1 }, + }); + const stream = new ObservableStream(obsHandle); + + obsHandle.subscribeToMore({ + document: gql` + subscription newValues { + name } - } - simulate(); - } - ); + `, + variables: { + someString: "foo", + }, + updateQuery: (_, { subscriptionData }) => { + return { entry: { value: subscriptionData.data.name } }; + }, + }); + + await expect(stream).toEmitValue({ + data: { entry: { value: "1" } }, + loading: false, + networkStatus: 7, + }); + + wSLink.simulateResult(results[0]); + + await expect(stream).toEmitValue({ + data: { entry: { value: "Dahivat Pandya" } }, + loading: false, + networkStatus: 7, + }); + + await wait(10); + wSLink.simulateResult(results[1]); + + await expect(stream).toEmitValue({ + data: { entry: { value: "Amanda Liu" } }, + loading: false, + networkStatus: 7, + }); + }); }); diff --git a/src/core/__tests__/QueryManager/index.ts b/src/core/__tests__/QueryManager/index.ts index 1edd4e2c2f1..32bf53b64ee 100644 --- a/src/core/__tests__/QueryManager/index.ts +++ b/src/core/__tests__/QueryManager/index.ts @@ -4,7 +4,7 @@ import { map } from "rxjs/operators"; import { assign } from "lodash"; import gql from "graphql-tag"; import { DocumentNode, GraphQLError } from "graphql"; -import { setVerbosity } from "ts-invariant"; +import { InvariantError, setVerbosity } from "ts-invariant"; import { Observable, @@ -28,32 +28,22 @@ import { } from "../../../testing/core/mocking/mockLink"; // core -import { ApolloQueryResult } from "../../types"; import { NetworkStatus } from "../../networkStatus"; import { ObservableQuery } from "../../ObservableQuery"; -import { - MutationBaseOptions, - MutationOptions, - WatchQueryOptions, -} from "../../watchQueryOptions"; +import { WatchQueryOptions } from "../../watchQueryOptions"; import { QueryManager } from "../../QueryManager"; import { ApolloError } from "../../../errors"; // testing utils import { waitFor } from "@testing-library/react"; -import wrap from "../../../testing/core/wrap"; -import observableToPromise, { - observableToPromiseAndSubscription, -} from "../../../testing/core/observableToPromise"; -import { itAsync } from "../../../testing/core"; +import { wait } from "../../../testing/core"; import { ApolloClient } from "../../../core"; import { mockFetchQuery } from "../ObservableQuery"; import { Concast, print } from "../../../utilities"; -import { ObservableStream } from "../../../testing/internal"; +import { ObservableStream, spyOnConsole } from "../../../testing/internal"; interface MockedMutation { - reject: (reason: any) => any; mutation: DocumentNode; data?: Object; errors?: GraphQLError[]; @@ -106,24 +96,20 @@ describe("QueryManager", () => { // Helper method that sets up a mockQueryManager and then passes on the // results to an observer. - const assertWithObserver = ({ - reject, + const getObservableStream = ({ query, variables = {}, queryOptions = {}, result, error, delay, - observer, }: { - reject: (reason: any) => any; query: DocumentNode; variables?: Object; queryOptions?: Object; error?: Error; result?: FetchResult; delay?: number; - observer: Observer>; }) => { const queryManager = mockQueryManager({ request: { query, variables }, @@ -131,18 +117,13 @@ describe("QueryManager", () => { error, delay, }); - const finalOptions = assign( - { query, variables }, - queryOptions - ) as WatchQueryOptions; - return queryManager.watchQuery(finalOptions).subscribe({ - next: wrap(reject, observer.next!), - error: observer.error, - }); + + return new ObservableStream( + queryManager.watchQuery(assign({ query, variables }, queryOptions)) + ); }; const mockMutation = ({ - reject, mutation, data, errors, @@ -152,7 +133,7 @@ describe("QueryManager", () => { const link = mockSingleLink({ request: { query: mutation, variables }, result: { data, errors }, - }).setOnError(reject); + }); const queryManager = createQueryManager({ link, @@ -174,18 +155,6 @@ describe("QueryManager", () => { }); }; - const assertMutationRoundtrip = ( - resolve: (result: any) => any, - opts: MockedMutation - ) => { - const { reject } = opts; - return mockMutation(opts) - .then(({ result }) => { - expect(result.data).toEqual(opts.data); - }) - .then(resolve, reject); - }; - // Helper method that takes a query with a first response and a second response. // Used to assert stuff about refetches. const mockRefetch = ({ @@ -230,9 +199,8 @@ describe("QueryManager", () => { }; } - itAsync("handles GraphQL errors", (resolve, reject) => { - assertWithObserver({ - reject, + it("handles GraphQL errors", async () => { + const stream = getObservableStream({ query: gql` query people { allPeople(first: 1) { @@ -246,24 +214,17 @@ describe("QueryManager", () => { result: { errors: [new GraphQLError("This is an error message.")], }, - observer: { - next() { - reject( - new Error("Returned a result when it was supposed to error out") - ); - }, - - error(apolloError) { - expect(apolloError).toBeDefined(); - resolve(); - }, - }, }); + + await expect(stream).toEmitError( + new ApolloError({ + graphQLErrors: [{ message: "This is an error message." }], + }) + ); }); - itAsync("handles GraphQL errors as data", (resolve, reject) => { - assertWithObserver({ - reject, + it("handles GraphQL errors as data", async () => { + const stream = getObservableStream({ query: gql` query people { allPeople(first: 1) { @@ -280,26 +241,18 @@ describe("QueryManager", () => { result: { errors: [new GraphQLError("This is an error message.")], }, - observer: { - next({ errors }) { - expect(errors).toBeDefined(); - expect(errors![0].message).toBe("This is an error message."); - resolve(); - }, - error(apolloError) { - reject( - new Error( - "Called observer.error instead of passing errors to observer.next" - ) - ); - }, - }, + }); + + await expect(stream).toEmitValue({ + data: undefined, + loading: false, + networkStatus: 8, + errors: [{ message: "This is an error message." }], }); }); - itAsync("handles GraphQL errors with data returned", (resolve, reject) => { - assertWithObserver({ - reject, + it("handles GraphQL errors with data returned", async () => { + const stream = getObservableStream({ query: gql` query people { allPeople(first: 1) { @@ -319,90 +272,56 @@ describe("QueryManager", () => { }, errors: [new GraphQLError("This is an error message.")], }, - observer: { - next() { - reject(new Error("Returned data when it was supposed to error out.")); - }, - - error(apolloError) { - expect(apolloError).toBeDefined(); - resolve(); - }, - }, }); + + await expect(stream).toEmitError( + new ApolloError({ + graphQLErrors: [{ message: "This is an error message." }], + }) + ); }); - itAsync( - "empty error array (handle non-spec-compliant server) #156", - (resolve, reject) => { - assertWithObserver({ - reject, - query: gql` - query people { - allPeople(first: 1) { - people { - name - } + it("empty error array (handle non-spec-compliant server) #156", async () => { + const stream = getObservableStream({ + query: gql` + query people { + allPeople(first: 1) { + people { + name } } - `, - result: { - data: { - allPeople: { - people: { - name: "Ada Lovelace", - }, + } + `, + result: { + data: { + allPeople: { + people: { + name: "Ada Lovelace", }, }, - errors: [], }, - observer: { - next(result) { - expect(result.data["allPeople"].people.name).toBe("Ada Lovelace"); - expect(result["errors"]).toBeUndefined(); - resolve(); + errors: [], + }, + }); + + await expect(stream).toEmitValue({ + errors: undefined, + data: { + allPeople: { + people: { + name: "Ada Lovelace", }, }, - }); - } - ); + }, + networkStatus: 7, + loading: false, + }); + }); // Easy to get into this state if you write an incorrect `formatError` // function with graphql-server or express-graphql - itAsync( - "error array with nulls (handle non-spec-compliant server) #1185", - (resolve, reject) => { - assertWithObserver({ - reject, - query: gql` - query people { - allPeople(first: 1) { - people { - name - } - } - } - `, - result: { - errors: [null as any], - }, - observer: { - next() { - reject(new Error("Should not fire next for an error")); - }, - error(error) { - expect((error as any).graphQLErrors).toEqual([null]); - expect(error.message).toBe("Error message not found."); - resolve(); - }, - }, - }); - } - ); - - itAsync("handles network errors", (resolve, reject) => { - assertWithObserver({ - reject, + it("error array with nulls (handle non-spec-compliant server) #1185", async () => { + const stream = getObservableStream({ query: gql` query people { allPeople(first: 1) { @@ -412,30 +331,20 @@ describe("QueryManager", () => { } } `, - error: new Error("Network error"), - observer: { - next: () => { - reject(new Error("Should not deliver result")); - }, - error: (error) => { - const apolloError = error as ApolloError; - expect(apolloError.networkError).toBeDefined(); - expect(apolloError.networkError!.message).toMatch("Network error"); - resolve(); - }, + result: { + errors: [null as any], }, }); - }); - itAsync("uses console.error to log unhandled errors", (resolve, reject) => { - const oldError = console.error; - let printed: any; - console.error = (...args: any[]) => { - printed = args; - }; + await expect(stream).toEmitError( + new ApolloError({ + graphQLErrors: [null as any], + }) + ); + }); - assertWithObserver({ - reject, + it("handles network errors", async () => { + const stream = getObservableStream({ query: gql` query people { allPeople(first: 1) { @@ -446,53 +355,71 @@ describe("QueryManager", () => { } `, error: new Error("Network error"), - observer: { - next: () => { - reject(new Error("Should not deliver result")); - }, + }); + + await expect(stream).toEmitError( + new ApolloError({ + networkError: new Error("Network error"), + }) + ); + }); + + it("uses console.error to log unhandled errors", async () => { + using _ = spyOnConsole("error"); + const query = gql` + query people { + allPeople(first: 1) { + people { + name + } + } + } + `; + const error = new Error("Network error"); + + const queryManager = mockQueryManager({ + request: { query }, + error, + }); + + const observable = queryManager.watchQuery({ query }); + observable.subscribe({ + next: () => { + throw new Error("Should not have been called"); }, }); - setTimeout(() => { - expect(printed[0]).toMatch(/error/); - console.error = oldError; - resolve(); - }, 10); + await wait(10); + + expect(console.error).toHaveBeenCalledTimes(1); + expect(console.error).toHaveBeenCalledWith( + "Unhandled error", + "Network error", + expect.anything() + ); }); // XXX this looks like a bug in zen-observable but we should figure // out a solution for it - itAsync.skip( - "handles an unsubscribe action that happens before data returns", - (resolve, reject) => { - const subscription = assertWithObserver({ - reject, - query: gql` - query people { - allPeople(first: 1) { - people { - name - } + it.skip("handles an unsubscribe action that happens before data returns", async () => { + const stream = getObservableStream({ + query: gql` + query people { + allPeople(first: 1) { + people { + name } } - `, - delay: 1000, - observer: { - next: () => { - reject(new Error("Should not deliver result")); - }, - error: () => { - reject(new Error("Should not deliver result")); - }, - }, - }); + } + `, + delay: 1000, + }); - expect(subscription.unsubscribe).not.toThrow(); - } - ); + expect(stream.unsubscribe).not.toThrow(); + }); // Query should be aborted on last .unsubscribe() - itAsync("causes link unsubscription if unsubscribed", (resolve, reject) => { + it("causes link unsubscription if unsubscribed", async () => { const expResult = { data: { allPeople: { @@ -557,25 +484,17 @@ describe("QueryManager", () => { notifyOnNetworkStatusChange: false, }); - const observerCallback = wrap(reject, () => { - reject(new Error("Link subscription should have been cancelled")); - }); + const stream = new ObservableStream(observableQuery); - const subscription = observableQuery.subscribe({ - next: observerCallback, - error: observerCallback, - complete: observerCallback, - }); + stream.unsubscribe(); - subscription.unsubscribe(); + await wait(10); - return waitFor(() => { - expect(onRequestUnsubscribe).toHaveBeenCalledTimes(1); - expect(onRequestSubscribe).toHaveBeenCalledTimes(1); - }).then(resolve, reject); + expect(onRequestUnsubscribe).toHaveBeenCalledTimes(1); + expect(onRequestSubscribe).toHaveBeenCalledTimes(1); }); - itAsync("causes link unsubscription after reobserve", (resolve, reject) => { + it("causes link unsubscription after reobserve", async () => { const expResult = { data: { allPeople: { @@ -654,86 +573,72 @@ describe("QueryManager", () => { variables: request.variables, }); - const observerCallback = wrap(reject, () => { - reject(new Error("Link subscription should have been cancelled")); - }); - - const subscription = observableQuery.subscribe({ - next: observerCallback, - error: observerCallback, - complete: observerCallback, - }); + const stream = new ObservableStream(observableQuery); expect(onRequestSubscribe).toHaveBeenCalledTimes(1); // This is the most important part of this test // Check that reobserve cancels the previous connection while watchQuery remains active - observableQuery.reobserve({ variables: { offset: 20 } }); + void observableQuery.reobserve({ variables: { offset: 20 } }); - return waitFor(() => { + await waitFor(() => { // Verify that previous connection was aborted by reobserve expect(onRequestUnsubscribe).toHaveBeenCalledTimes(1); - }) - .then(async () => { - subscription.unsubscribe(); - await waitFor(() => { - expect(onRequestSubscribe).toHaveBeenCalledTimes(2); - }); - await waitFor(() => { - expect(onRequestUnsubscribe).toHaveBeenCalledTimes(2); - }); - }) - .then(resolve, reject); + }); + + stream.unsubscribe(); + + await wait(10); + + expect(onRequestSubscribe).toHaveBeenCalledTimes(2); + expect(onRequestUnsubscribe).toHaveBeenCalledTimes(2); }); - itAsync( - "supports interoperability with other Observable implementations like RxJS", - (resolve, reject) => { - const expResult = { - data: { - allPeople: { - people: [ - { - name: "Luke Skywalker", - }, - ], - }, + it("supports interoperability with other Observable implementations like RxJS", async () => { + const expResult = { + data: { + allPeople: { + people: [ + { + name: "Luke Skywalker", + }, + ], }, - }; + }, + }; - const handle = mockWatchQuery({ - request: { - query: gql` - query people { - allPeople(first: 1) { - people { - name - } + const handle = mockWatchQuery({ + request: { + query: gql` + query people { + allPeople(first: 1) { + people { + name } } - `, - }, - result: expResult, - }); + } + `, + }, + result: expResult, + }); - const observable = from(handle as any); + const observable = from(handle as any); - observable - .pipe(map((result) => assign({ fromRx: true }, result))) - .subscribe({ - next: wrap(reject, (newResult) => { - const expectedResult = assign( - { fromRx: true, loading: false, networkStatus: 7 }, - expResult - ); - expect(newResult).toEqual(expectedResult); - resolve(); - }), - }); - } - ); + const stream = new ObservableStream( + observable.pipe( + map((result) => assign({ fromRx: true }, result)) + ) as unknown as Observable + ); + + await expect(stream).toEmitValue({ + fromRx: true, + loading: false, + networkStatus: 7, + ...expResult, + }); + }); - itAsync("allows you to subscribe twice to one query", (resolve, reject) => { + it("allows you to subscribe twice to one query", async () => { const request = { query: gql` query fetchLuke($id: String) { @@ -782,59 +687,29 @@ describe("QueryManager", () => { } ); - let subOneCount = 0; - // pre populate data to avoid contention - queryManager.query(request).then(() => { - const handle = queryManager.watchQuery(request); + await queryManager.query(request); - const subOne = handle.subscribe({ - next(result) { - subOneCount++; + const handle = queryManager.watchQuery(request); - if (subOneCount === 1) { - expect(result.data).toEqual(data1); - } else if (subOneCount === 2) { - expect(result.data).toEqual(data2); - } - }, - }); + const stream1 = new ObservableStream(handle); + const stream2 = new ObservableStream(handle); - let subTwoCount = 0; - handle.subscribe({ - next(result) { - subTwoCount++; - if (subTwoCount === 1) { - expect(result.data).toEqual(data1); - handle.refetch(); - } else if (subTwoCount === 2) { - expect(result.data).toEqual(data2); - setTimeout(() => { - try { - expect(subOneCount).toBe(2); - - subOne.unsubscribe(); - handle.refetch(); - } catch (e) { - reject(e); - } - }, 0); - } else if (subTwoCount === 3) { - setTimeout(() => { - try { - expect(subOneCount).toBe(2); - resolve(); - } catch (e) { - reject(e); - } - }, 0); - } - }, - }); - }); + await expect(stream1).toEmitMatchedValue({ data: data1 }); + await expect(stream2).toEmitMatchedValue({ data: data1 }); + + void handle.refetch(); + + await expect(stream1).toEmitMatchedValue({ data: data2 }); + await expect(stream2).toEmitMatchedValue({ data: data2 }); + + stream1.unsubscribe(); + void handle.refetch(); + + await expect(stream2).toEmitMatchedValue({ data: data3 }); }); - itAsync("resolves all queries when one finishes after another", (resolve) => { + it("resolves all queries when one finishes after another", async () => { const request = { query: gql` query fetchLuke($id: String) { @@ -914,23 +789,16 @@ describe("QueryManager", () => { const ob2 = queryManager.watchQuery(request2); const ob3 = queryManager.watchQuery(request3); - let finishCount = 0; - ob1.subscribe((result) => { - expect(result.data).toEqual(data1); - finishCount++; - }); - ob2.subscribe((result) => { - expect(result.data).toEqual(data2); - expect(finishCount).toBe(2); - resolve(); - }); - ob3.subscribe((result) => { - expect(result.data).toEqual(data3); - finishCount++; - }); + const stream1 = new ObservableStream(ob1); + const stream2 = new ObservableStream(ob2); + const stream3 = new ObservableStream(ob3); + + await expect(stream1).toEmitMatchedValue({ data: data1 }); + await expect(stream2).toEmitMatchedValue({ data: data2 }); + await expect(stream3).toEmitMatchedValue({ data: data3 }); }); - itAsync("allows you to refetch queries", (resolve, reject) => { + it("allows you to refetch queries", async () => { const request = { query: gql` query fetchLuke($id: String) { @@ -963,336 +831,220 @@ describe("QueryManager", () => { }); const observable = queryManager.watchQuery(request); - return observableToPromise( - { observable }, - (result) => { - expect(result.data).toEqual(data1); - observable.refetch(); - }, - (result) => expect(result.data).toEqual(data2) - ).then(resolve, reject); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitMatchedValue({ data: data1 }); + void observable.refetch(); + await expect(stream).toEmitMatchedValue({ data: data2 }); }); - itAsync( - "will return referentially equivalent data if nothing changed in a refetch", - (resolve, reject) => { - const request: WatchQueryOptions = { - query: gql` - { - a - b { - c - } - d { - e - f { - g - } + it("will return referentially equivalent data if nothing changed in a refetch", async () => { + const request: WatchQueryOptions = { + query: gql` + { + a + b { + c + } + d { + e + f { + g } } - `, - notifyOnNetworkStatusChange: false, - canonizeResults: true, - }; + } + `, + notifyOnNetworkStatusChange: false, + canonizeResults: true, + }; - const data1 = { - a: 1, - b: { c: 2 }, - d: { e: 3, f: { g: 4 } }, - }; + const data1 = { + a: 1, + b: { c: 2 }, + d: { e: 3, f: { g: 4 } }, + }; - const data2 = { - a: 1, - b: { c: 2 }, - d: { e: 30, f: { g: 4 } }, - }; + const data2 = { + a: 1, + b: { c: 2 }, + d: { e: 30, f: { g: 4 } }, + }; - const data3 = { - a: 1, - b: { c: 2 }, - d: { e: 3, f: { g: 4 } }, - }; + const data3 = { + a: 1, + b: { c: 2 }, + d: { e: 3, f: { g: 4 } }, + }; - const queryManager = mockRefetch({ - request, - firstResult: { data: data1 }, - secondResult: { data: data2 }, - thirdResult: { data: data3 }, - }); + const queryManager = mockRefetch({ + request, + firstResult: { data: data1 }, + secondResult: { data: data2 }, + thirdResult: { data: data3 }, + }); - const observable = queryManager.watchQuery(request); + const observable = queryManager.watchQuery(request); + const stream = new ObservableStream(observable); - let count = 0; - let firstResultData: any; + const { data: firstResultData } = await stream.takeNext(); + expect(firstResultData).toEqual(data1); - observable.subscribe({ - next: (result) => { - try { - switch (count++) { - case 0: - expect(result.data).toEqual(data1); - firstResultData = result.data; - observable.refetch(); - break; - case 1: - expect(result.data).toEqual(data2); - expect(result.data).not.toEqual(firstResultData); - expect(result.data.b).toEqual(firstResultData.b); - expect(result.data.d).not.toEqual(firstResultData.d); - expect(result.data.d.f).toEqual(firstResultData.d.f); - observable.refetch(); - break; - case 2: - expect(result.data).toEqual(data3); - expect(result.data).toBe(firstResultData); - resolve(); - break; - default: - throw new Error("Next run too many times."); - } - } catch (error) { - reject(error); - } - }, - error: reject, - }); - } - ); + void observable.refetch(); - itAsync( - "will return referentially equivalent data in getCurrentResult if nothing changed", - (resolve, reject) => { - const request = { - query: gql` - { - a - b { - c - } - d { - e - f { - g - } - } - } - `, - notifyOnNetworkStatusChange: false, - }; + { + const result = await stream.takeNext(); - const data1 = { - a: 1, - b: { c: 2 }, - d: { e: 3, f: { g: 4 } }, - }; + expect(result.data).toEqual(data2); + expect(result.data).not.toEqual(firstResultData); + expect(result.data.b).toEqual(firstResultData.b); + expect(result.data.d).not.toEqual(firstResultData.d); + expect(result.data.d.f).toEqual(firstResultData.d.f); + } - const queryManager = mockQueryManager({ - request, - result: { data: data1 }, - }); + void observable.refetch(); - const observable = queryManager.watchQuery(request); + { + const result = await stream.takeNext(); - observable.subscribe({ - next: (result) => { - try { - expect(result.data).toEqual(data1); - expect(result.data).toEqual(observable.getCurrentResult().data); - resolve(); - } catch (error) { - reject(error); - } - }, - error: reject, - }); + expect(result.data).toEqual(data3); + expect(result.data).toBe(firstResultData); } - ); + }); - itAsync( - "sets networkStatus to `refetch` when refetching", - (resolve, reject) => { - const request: WatchQueryOptions = { - query: gql` - query fetchLuke($id: String) { - people_one(id: $id) { - name + it("will return referentially equivalent data in getCurrentResult if nothing changed", async () => { + const request = { + query: gql` + { + a + b { + c + } + d { + e + f { + g } } - `, - variables: { - id: "1", - }, - notifyOnNetworkStatusChange: true, - // This causes a loading:true result to be delivered from the cache - // before the final data2 result is delivered. - fetchPolicy: "cache-and-network", - }; - const data1 = { - people_one: { - name: "Luke Skywalker", - }, - }; - - const data2 = { - people_one: { - name: "Luke Skywalker has a new name", - }, - }; - - const queryManager = mockRefetch({ - request, - firstResult: { data: data1 }, - secondResult: { data: data2 }, - }); - - const observable = queryManager.watchQuery(request); - return observableToPromise( - { observable }, - (result) => { - expect(result.data).toEqual(data1); - observable.refetch(); - }, - (result) => expect(result.networkStatus).toBe(NetworkStatus.refetch), - (result) => { - expect(result.networkStatus).toBe(NetworkStatus.ready); - expect(result.data).toEqual(data2); } - ).then(resolve, reject); - } - ); + `, + notifyOnNetworkStatusChange: false, + }; - itAsync( - "allows you to refetch queries with promises", - async (resolve, reject) => { - const request = { - query: gql` - { - people_one(id: 1) { - name - } - } - `, - }; - const data1 = { - people_one: { - name: "Luke Skywalker", - }, - }; + const data1 = { + a: 1, + b: { c: 2 }, + d: { e: 3, f: { g: 4 } }, + }; - const data2 = { - people_one: { - name: "Luke Skywalker has a new name", - }, - }; + const queryManager = mockQueryManager({ + request, + result: { data: data1 }, + }); - const queryManager = mockRefetch({ - request, - firstResult: { data: data1 }, - secondResult: { data: data2 }, - }); + const observable = queryManager.watchQuery(request); + const stream = new ObservableStream(observable); - const handle = queryManager.watchQuery(request); - handle.subscribe({}); + const { data } = await stream.takeNext(); - return handle - .refetch() - .then((result) => expect(result.data).toEqual(data2)) - .then(resolve, reject); - } - ); + expect(data).toEqual(data1); + expect(data).toBe(observable.getCurrentResult().data); + }); - itAsync( - "allows you to refetch queries with new variables", - (resolve, reject) => { - const query = gql` - { - people_one(id: 1) { + it("sets networkStatus to `refetch` when refetching", async () => { + const request: WatchQueryOptions = { + query: gql` + query fetchLuke($id: String) { + people_one(id: $id) { name } } - `; + `, + variables: { + id: "1", + }, + notifyOnNetworkStatusChange: true, + // This causes a loading:true result to be delivered from the cache + // before the final data2 result is delivered. + fetchPolicy: "cache-and-network", + }; + const data1 = { + people_one: { + name: "Luke Skywalker", + }, + }; - const data1 = { - people_one: { - name: "Luke Skywalker", - }, - }; + const data2 = { + people_one: { + name: "Luke Skywalker has a new name", + }, + }; - const data2 = { - people_one: { - name: "Luke Skywalker has a new name", - }, - }; + const queryManager = mockRefetch({ + request, + firstResult: { data: data1 }, + secondResult: { data: data2 }, + }); - const data3 = { - people_one: { - name: "Luke Skywalker has a new name and age", - }, - }; + const observable = queryManager.watchQuery(request); + const stream = new ObservableStream(observable); - const data4 = { - people_one: { - name: "Luke Skywalker has a whole new bag", - }, - }; + await expect(stream).toEmitValue({ + data: data1, + loading: false, + networkStatus: NetworkStatus.ready, + }); - const variables1 = { - test: "I am your father", - }; + void observable.refetch(); - const variables2 = { - test: "No. No! That's not true! That's impossible!", - }; + await expect(stream).toEmitValue({ + data: data1, + loading: true, + networkStatus: NetworkStatus.refetch, + }); + await expect(stream).toEmitValue({ + data: data2, + loading: false, + networkStatus: NetworkStatus.ready, + }); + }); - const queryManager = mockQueryManager( - { - request: { query: query }, - result: { data: data1 }, - }, - { - request: { query: query }, - result: { data: data2 }, - }, - { - request: { query: query, variables: variables1 }, - result: { data: data3 }, - }, + it("allows you to refetch queries with promises", async () => { + const request = { + query: gql` { - request: { query: query, variables: variables2 }, - result: { data: data4 }, + people_one(id: 1) { + name + } } - ); + `, + }; + const data1 = { + people_one: { + name: "Luke Skywalker", + }, + }; - const observable = queryManager.watchQuery({ - query, - notifyOnNetworkStatusChange: false, - }); - return observableToPromise( - { observable }, - (result) => { - expect(result.loading).toBe(false); - expect(result.data).toEqual(data1); - return observable.refetch(); - }, - (result) => { - expect(result.loading).toBe(false); - expect(result.data).toEqual(data2); - return observable.refetch(variables1); - }, - (result) => { - expect(result.loading).toBe(false); - expect(result.data).toEqual(data3); - return observable.refetch(variables2); - }, - (result) => { - expect(result.loading).toBe(false); - expect(result.data).toEqual(data4); - } - ).then(resolve, reject); - } - ); + const data2 = { + people_one: { + name: "Luke Skywalker has a new name", + }, + }; + + const queryManager = mockRefetch({ + request, + firstResult: { data: data1 }, + secondResult: { data: data2 }, + }); + + const handle = queryManager.watchQuery(request); + handle.subscribe({}); - itAsync("only modifies varaibles when refetching", (resolve, reject) => { + const result = await handle.refetch(); + + expect(result.data).toEqual(data2); + }); + + it("allows you to refetch queries with new variables", async () => { const query = gql` { people_one(id: 1) { @@ -1313,6 +1065,26 @@ describe("QueryManager", () => { }, }; + const data3 = { + people_one: { + name: "Luke Skywalker has a new name and age", + }, + }; + + const data4 = { + people_one: { + name: "Luke Skywalker has a whole new bag", + }, + }; + + const variables1 = { + test: "I am your father", + }; + + const variables2 = { + test: "No. No! That's not true! That's impossible!", + }; + const queryManager = mockQueryManager( { request: { query: query }, @@ -1321,6 +1093,14 @@ describe("QueryManager", () => { { request: { query: query }, result: { data: data2 }, + }, + { + request: { query: query, variables: variables1 }, + result: { data: data3 }, + }, + { + request: { query: query, variables: variables2 }, + result: { data: data4 }, } ); @@ -1328,24 +1108,40 @@ describe("QueryManager", () => { query, notifyOnNetworkStatusChange: false, }); - const originalOptions = assign({}, observable.options); - return observableToPromise( - { observable }, - (result) => { - expect(result.data).toEqual(data1); - observable.refetch(); - }, - (result) => { - expect(result.data).toEqual(data2); - const updatedOptions = assign({}, observable.options); - delete originalOptions.variables; - delete updatedOptions.variables; - expect(updatedOptions).toEqual(originalOptions); - } - ).then(resolve, reject); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue({ + data: data1, + loading: false, + networkStatus: NetworkStatus.ready, + }); + + void observable.refetch(); + + await expect(stream).toEmitValue({ + data: data2, + loading: false, + networkStatus: NetworkStatus.ready, + }); + + void observable.refetch(variables1); + + await expect(stream).toEmitValue({ + data: data3, + loading: false, + networkStatus: NetworkStatus.ready, + }); + + void observable.refetch(variables2); + + await expect(stream).toEmitValue({ + data: data4, + loading: false, + networkStatus: NetworkStatus.ready, + }); }); - itAsync("continues to poll after refetch", (resolve, reject) => { + it("only modifies varaibles when refetching", async () => { const query = gql` { people_one(id: 1) { @@ -1366,115 +1162,184 @@ describe("QueryManager", () => { }, }; - const data3 = { - people_one: { - name: "Patsy", - }, - }; - const queryManager = mockQueryManager( { - request: { query }, + request: { query: query }, result: { data: data1 }, }, { - request: { query }, + request: { query: query }, result: { data: data2 }, - }, - { - request: { query }, - result: { data: data3 }, } ); const observable = queryManager.watchQuery({ query, - pollInterval: 200, notifyOnNetworkStatusChange: false, }); + const stream = new ObservableStream(observable); + const originalOptions = assign({}, observable.options); - return observableToPromise( - { observable }, - (result) => { - expect(result.data).toEqual(data1); - observable.refetch(); - }, - (result) => expect(result.data).toEqual(data2), - (result) => { - expect(result.data).toEqual(data3); - observable.stopPolling(); - } - ).then(resolve, reject); - }); + await expect(stream).toEmitValue({ + data: data1, + loading: false, + networkStatus: NetworkStatus.ready, + }); - itAsync( - "sets networkStatus to `poll` if a polling query is in flight", - (resolve) => { - const query = gql` - { - people_one(id: 1) { - name - } + void observable.refetch(); + + await expect(stream).toEmitValue({ + data: data2, + loading: false, + networkStatus: NetworkStatus.ready, + }); + + const updatedOptions = assign({}, observable.options); + delete originalOptions.variables; + delete updatedOptions.variables; + expect(updatedOptions).toEqual(originalOptions); + }); + + it("continues to poll after refetch", async () => { + const query = gql` + { + people_one(id: 1) { + name } - `; + } + `; - const data1 = { - people_one: { - name: "Luke Skywalker", - }, - }; + const data1 = { + people_one: { + name: "Luke Skywalker", + }, + }; - const data2 = { - people_one: { - name: "Luke Skywalker has a new name", - }, - }; + const data2 = { + people_one: { + name: "Luke Skywalker has a new name", + }, + }; - const data3 = { - people_one: { - name: "Patsy", - }, - }; + const data3 = { + people_one: { + name: "Patsy", + }, + }; - const queryManager = mockQueryManager( - { - request: { query }, - result: { data: data1 }, - }, - { - request: { query }, - result: { data: data2 }, - }, - { - request: { query }, - result: { data: data3 }, + const queryManager = mockQueryManager( + { + request: { query }, + result: { data: data1 }, + }, + { + request: { query }, + result: { data: data2 }, + }, + { + request: { query }, + result: { data: data3 }, + } + ); + + const observable = queryManager.watchQuery({ + query, + pollInterval: 200, + notifyOnNetworkStatusChange: false, + }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue({ + data: data1, + loading: false, + networkStatus: NetworkStatus.ready, + }); + + void observable.refetch(); + + await expect(stream).toEmitValue({ + data: data2, + loading: false, + networkStatus: NetworkStatus.ready, + }); + + await expect(stream).toEmitValue( + { + data: data3, + loading: false, + networkStatus: NetworkStatus.ready, + }, + { timeout: 250 } + ); + + observable.stopPolling(); + }); + + it("sets networkStatus to `poll` if a polling query is in flight", async () => { + const query = gql` + { + people_one(id: 1) { + name } - ); + } + `; - const observable = queryManager.watchQuery({ - query, - pollInterval: 30, - notifyOnNetworkStatusChange: true, - }); + const data1 = { + people_one: { + name: "Luke Skywalker", + }, + }; - let counter = 0; - const handle = observable.subscribe({ - next(result) { - counter += 1; - - if (counter === 1) { - expect(result.networkStatus).toBe(NetworkStatus.ready); - } else if (counter === 2) { - expect(result.networkStatus).toBe(NetworkStatus.poll); - handle.unsubscribe(); - resolve(); - } - }, - }); - } - ); + const data2 = { + people_one: { + name: "Luke Skywalker has a new name", + }, + }; + + const data3 = { + people_one: { + name: "Patsy", + }, + }; + + const queryManager = mockQueryManager( + { + request: { query }, + result: { data: data1 }, + }, + { + request: { query }, + result: { data: data2 }, + }, + { + request: { query }, + result: { data: data3 }, + } + ); + + const observable = queryManager.watchQuery({ + query, + pollInterval: 30, + notifyOnNetworkStatusChange: true, + }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue({ + data: data1, + loading: false, + networkStatus: NetworkStatus.ready, + }); + + await expect(stream).toEmitValue({ + data: data1, + loading: true, + networkStatus: NetworkStatus.poll, + }); + + stream.unsubscribe(); + }); - itAsync("can handle null values in arrays (#1551)", (resolve) => { + it("can handle null values in arrays (#1551)", async () => { const query = gql` { list { @@ -1488,72 +1353,61 @@ describe("QueryManager", () => { result: { data }, }); const observable = queryManager.watchQuery({ query }); + const stream = new ObservableStream(observable); - observable.subscribe({ - next: (result) => { - expect(result.data).toEqual(data); - expect(observable.getCurrentResult().data).toEqual(data); - resolve(); - }, - }); + await expect(stream).toEmitMatchedValue({ data }); + expect(observable.getCurrentResult().data).toEqual(data); }); - itAsync( - "supports cache-only fetchPolicy fetching only cached data", - (resolve, reject) => { - const primeQuery = gql` - query primeQuery { - luke: people_one(id: 1) { - name - } + it("supports cache-only fetchPolicy fetching only cached data", async () => { + const primeQuery = gql` + query primeQuery { + luke: people_one(id: 1) { + name } - `; + } + `; - const complexQuery = gql` - query complexQuery { - luke: people_one(id: 1) { - name - } - vader: people_one(id: 4) { - name - } + const complexQuery = gql` + query complexQuery { + luke: people_one(id: 1) { + name } - `; + vader: people_one(id: 4) { + name + } + } + `; - const data1 = { - luke: { - name: "Luke Skywalker", - }, - }; + const data1 = { + luke: { + name: "Luke Skywalker", + }, + }; - const queryManager = mockQueryManager({ - request: { query: primeQuery }, - result: { data: data1 }, - }); + const queryManager = mockQueryManager({ + request: { query: primeQuery }, + result: { data: data1 }, + }); - // First, prime the cache - return queryManager - .query({ - query: primeQuery, - }) - .then(() => { - const handle = queryManager.watchQuery({ - query: complexQuery, - fetchPolicy: "cache-only", - }); + // First, prime the cache + await queryManager.query({ + query: primeQuery, + }); - return handle.result().then((result) => { - expect(result.data["luke"].name).toBe("Luke Skywalker"); - expect(result.data).not.toHaveProperty("vader"); - }); - }) - .then(resolve, reject); - } - ); + const handle = queryManager.watchQuery({ + query: complexQuery, + fetchPolicy: "cache-only", + }); + + const result = await handle.result(); + + expect(result.data["luke"].name).toBe("Luke Skywalker"); + expect(result.data).not.toHaveProperty("vader"); + }); - itAsync("runs a mutation", (resolve, reject) => { - return assertMutationRoundtrip(resolve, { - reject, + it("runs a mutation", async () => { + const { result } = await mockMutation({ mutation: gql` mutation makeListPrivate { makeListPrivate(id: "5") @@ -1561,31 +1415,29 @@ describe("QueryManager", () => { `, data: { makeListPrivate: true }, }); + + expect(result.data).toEqual({ makeListPrivate: true }); }); - itAsync( - "runs a mutation even when errors is empty array #2912", - (resolve, reject) => { - return assertMutationRoundtrip(resolve, { - reject, - mutation: gql` - mutation makeListPrivate { - makeListPrivate(id: "5") - } - `, - errors: [], - data: { makeListPrivate: true }, - }); - } - ); + it("runs a mutation even when errors is empty array #2912", async () => { + const { result } = await mockMutation({ + mutation: gql` + mutation makeListPrivate { + makeListPrivate(id: "5") + } + `, + errors: [], + data: { makeListPrivate: true }, + }); + + expect(result.data).toEqual({ makeListPrivate: true }); + }); - itAsync( - 'runs a mutation with default errorPolicy equal to "none"', - (resolve, reject) => { - const errors = [new GraphQLError("foo")]; + it('runs a mutation with default errorPolicy equal to "none"', async () => { + const errors = [new GraphQLError("foo")]; - return mockMutation({ - reject, + await expect( + mockMutation({ mutation: gql` mutation makeListPrivate { makeListPrivate(id: "5") @@ -1593,23 +1445,15 @@ describe("QueryManager", () => { `, errors, }) - .then( - (result) => { - throw new Error( - "Mutation should not be successful with default errorPolicy" - ); - }, - (error) => { - expect(error.graphQLErrors).toEqual(errors); - } - ) - .then(resolve, reject); - } - ); + ).rejects.toThrow( + expect.objectContaining({ + graphQLErrors: errors, + }) + ); + }); - itAsync("runs a mutation with variables", (resolve, reject) => { - return assertMutationRoundtrip(resolve, { - reject, + it("runs a mutation with variables", async () => { + const { result } = await mockMutation({ mutation: gql` mutation makeListPrivate($listId: ID!) { makeListPrivate(id: $listId) @@ -1618,126 +1462,108 @@ describe("QueryManager", () => { variables: { listId: "1" }, data: { makeListPrivate: true }, }); + + expect(result.data).toEqual({ makeListPrivate: true }); }); const getIdField = (obj: any) => obj.id; - itAsync( - "runs a mutation with object parameters and puts the result in the store", - (resolve, reject) => { - const data = { - makeListPrivate: { - id: "5", - isPrivate: true, - }, - }; - return mockMutation({ - reject, - mutation: gql` - mutation makeListPrivate { - makeListPrivate(input: { id: "5" }) { - id - isPrivate - } + it("runs a mutation with object parameters and puts the result in the store", async () => { + const data = { + makeListPrivate: { + id: "5", + isPrivate: true, + }, + }; + const { result, queryManager } = await mockMutation({ + mutation: gql` + mutation makeListPrivate { + makeListPrivate(input: { id: "5" }) { + id + isPrivate } - `, - data, - config: { dataIdFromObject: getIdField }, - }) - .then(({ result, queryManager }) => { - expect(result.data).toEqual(data); + } + `, + data, + config: { dataIdFromObject: getIdField }, + }); - // Make sure we updated the store with the new data - expect(queryManager.cache.extract()["5"]).toEqual({ - id: "5", - isPrivate: true, - }); - }) - .then(resolve, reject); - } - ); + expect(result.data).toEqual(data); - itAsync( - "runs a mutation and puts the result in the store", - (resolve, reject) => { - const data = { - makeListPrivate: { - id: "5", - isPrivate: true, - }, - }; + // Make sure we updated the store with the new data + expect(queryManager.cache.extract()["5"]).toEqual({ + id: "5", + isPrivate: true, + }); + }); - return mockMutation({ - reject, - mutation: gql` - mutation makeListPrivate { - makeListPrivate(id: "5") { - id - isPrivate - } - } - `, - data, - config: { dataIdFromObject: getIdField }, - }) - .then(({ result, queryManager }) => { - expect(result.data).toEqual(data); - - // Make sure we updated the store with the new data - expect(queryManager.cache.extract()["5"]).toEqual({ - id: "5", - isPrivate: true, - }); - }) - .then(resolve, reject); - } - ); + it("runs a mutation and puts the result in the store", async () => { + const data = { + makeListPrivate: { + id: "5", + isPrivate: true, + }, + }; - itAsync( - "runs a mutation and puts the result in the store with root key", - (resolve, reject) => { - const mutation = gql` + const { result, queryManager } = await mockMutation({ + mutation: gql` mutation makeListPrivate { makeListPrivate(id: "5") { id isPrivate } } - `; + `, + data, + config: { dataIdFromObject: getIdField }, + }); - const data = { - makeListPrivate: { - id: "5", - isPrivate: true, - }, - }; + expect(result.data).toEqual(data); - const queryManager = createQueryManager({ - link: mockSingleLink({ - request: { query: mutation }, - result: { data }, - }).setOnError(reject), - config: { dataIdFromObject: getIdField }, - }); + // Make sure we updated the store with the new data + expect(queryManager.cache.extract()["5"]).toEqual({ + id: "5", + isPrivate: true, + }); + }); - return queryManager - .mutate({ - mutation, - }) - .then((result) => { - expect(result.data).toEqual(data); + it("runs a mutation and puts the result in the store with root key", async () => { + const mutation = gql` + mutation makeListPrivate { + makeListPrivate(id: "5") { + id + isPrivate + } + } + `; - // Make sure we updated the store with the new data - expect(queryManager.cache.extract()["5"]).toEqual({ - id: "5", - isPrivate: true, - }); - }) - .then(resolve, reject); - } - ); + const data = { + makeListPrivate: { + id: "5", + isPrivate: true, + }, + }; + + const queryManager = createQueryManager({ + link: mockSingleLink({ + request: { query: mutation }, + result: { data }, + }), + config: { dataIdFromObject: getIdField }, + }); + + const result = await queryManager.mutate({ mutation }); + + expect(result.data).toEqual(data); + + // Make sure we updated the store with the new data + expect(queryManager.cache.extract()["5"]).toEqual({ + id: "5", + isPrivate: true, + }); + }); - itAsync(`doesn't return data while query is loading`, (resolve, reject) => { + it(`doesn't return data while query is loading`, async () => { const query1 = gql` { people_one(id: 1) { @@ -1781,14 +1607,11 @@ describe("QueryManager", () => { const observable1 = queryManager.watchQuery({ query: query1 }); const observable2 = queryManager.watchQuery({ query: query2 }); - return Promise.all([ - observableToPromise({ observable: observable1 }, (result) => - expect(result.data).toEqual(data1) - ), - observableToPromise({ observable: observable2 }, (result) => - expect(result.data).toEqual(data2) - ), - ]).then(resolve, reject); + const stream1 = new ObservableStream(observable1); + const stream2 = new ObservableStream(observable2); + + await expect(stream1).toEmitMatchedValue({ data: data1 }); + await expect(stream2).toEmitMatchedValue({ data: data2 }); }); it("updates result of previous query if the result of a new query overlaps", async () => { @@ -1867,7 +1690,7 @@ describe("QueryManager", () => { await expect(stream).not.toEmitAnything(); }); - itAsync("warns if you forget the template literal tag", async (resolve) => { + it("warns if you forget the template literal tag", async () => { const queryManager = mockQueryManager(); expect(() => { void queryManager.query({ @@ -1889,57 +1712,49 @@ describe("QueryManager", () => { query: "string" as any as DocumentNode, }); }).toThrowError(/wrap the query string in a "gql" tag/); - - resolve(); }); - itAsync( - "should transform queries correctly when given a QueryTransformer", - (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } + it("should transform queries correctly when given a QueryTransformer", 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 transformedQueryResult = { - author: { - firstName: "John", - lastName: "Smith", - __typename: "Author", - }, - }; + const transformedQueryResult = { + author: { + firstName: "John", + lastName: "Smith", + __typename: "Author", + }, + }; - //make sure that the query is transformed within the query - //manager - createQueryManager({ - link: mockSingleLink({ - request: { query: transformedQuery }, - result: { data: transformedQueryResult }, - }).setOnError(reject), - config: { addTypename: true }, - }) - .query({ query: query }) - .then((result) => { - expect(result.data).toEqual(transformedQueryResult); - }) - .then(resolve, reject); - } - ); + //make sure that the query is transformed within the query + //manager + const result = await createQueryManager({ + link: mockSingleLink({ + request: { query: transformedQuery }, + result: { data: transformedQueryResult }, + }), + config: { addTypename: true }, + }).query({ query: query }); + + expect(result.data).toEqual(transformedQueryResult); + }); - itAsync("should transform mutations correctly", (resolve, reject) => { + it("should transform mutations correctly", async () => { const mutation = gql` mutation { createAuthor(firstName: "John", lastName: "Smith") { @@ -1966,476 +1781,380 @@ describe("QueryManager", () => { }, }; - createQueryManager({ + const result = await createQueryManager({ link: mockSingleLink({ request: { query: transformedMutation }, result: { data: transformedMutationResult }, - }).setOnError(reject), + }), config: { addTypename: true }, - }) - .mutate({ mutation: mutation }) - .then((result) => { - expect(result.data).toEqual(transformedMutationResult); - resolve(); - }); + }).mutate({ mutation: mutation }); + + expect(result.data).toEqual(transformedMutationResult); }); - itAsync( - "should reject a query promise given a network error", - (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } + it("should reject a query promise given a network error", async () => { + const query = gql` + query { + author { + firstName + lastName } - `; - const networkError = new Error("Network error"); + } + `; + const networkError = new Error("Network error"); + + await expect( mockQueryManager({ request: { query }, error: networkError, - }) - .query({ query }) - .then(() => { - reject(new Error("Returned result on an errored fetchQuery")); - }) - .catch((error) => { - const apolloError = error as ApolloError; - - expect(apolloError.message).toBeDefined(); - expect(apolloError.networkError).toBe(networkError); - expect(apolloError.graphQLErrors).toEqual([]); - resolve(); - }) - .then(resolve, reject); - } - ); + }).query({ query }) + ).rejects.toEqual(new ApolloError({ networkError })); + }); - itAsync( - "should reject a query promise given a GraphQL error", - (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } + it("should reject a query promise given a GraphQL error", async () => { + const query = gql` + query { + author { + firstName + lastName } - `; - const graphQLErrors = [new GraphQLError("GraphQL error")]; - return mockQueryManager({ + } + `; + const graphQLErrors = [new GraphQLError("GraphQL error")]; + await expect( + mockQueryManager({ request: { query }, result: { errors: graphQLErrors }, - }) - .query({ query }) - .then( - () => { - throw new Error("Returned result on an errored fetchQuery"); - }, - // don't use .catch() for this or it will catch the above error - (error) => { - const apolloError = error as ApolloError; - expect(apolloError.graphQLErrors).toEqual(graphQLErrors); - expect(!apolloError.networkError).toBeTruthy(); - } - ) - .then(resolve, reject); - } - ); + }).query({ query }) + ).rejects.toEqual(new ApolloError({ graphQLErrors })); + }); - itAsync( - "should not empty the store when a non-polling query fails due to a network error", - (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } - } - `; - const data = { - author: { - firstName: "Dhaivat", - lastName: "Pandya", - }, - }; - const queryManager = mockQueryManager( - { - request: { query }, - result: { data }, - }, - { - request: { query }, - error: new Error("Network error ocurred"), + it("should not empty the store when a non-polling query fails due to a network error", async () => { + const query = gql` + query { + author { + firstName + lastName } - ); - queryManager - .query({ query }) - .then((result) => { - expect(result.data).toEqual(data); - - queryManager - .query({ query, fetchPolicy: "network-only" }) - .then(() => { - reject( - new Error("Returned a result when it was not supposed to.") - ); - }) - .catch(() => { - // make that the error thrown doesn't empty the state - expect(queryManager.cache.extract().ROOT_QUERY!.author).toEqual( - data.author - ); - resolve(); - }); - }) - .catch(() => { - reject(new Error("Threw an error on the first query.")); - }); - } - ); + } + `; + const data = { + author: { + firstName: "Dhaivat", + lastName: "Pandya", + }, + }; + const queryManager = mockQueryManager( + { + request: { query }, + result: { data }, + }, + { + request: { query }, + error: new Error("Network error ocurred"), + } + ); + const result = await queryManager.query({ query }); - itAsync( - "should be able to unsubscribe from a polling query subscription", - (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } + expect(result.data).toEqual(data); + + await expect( + queryManager.query({ query, fetchPolicy: "network-only" }) + ).rejects.toThrow(); + + expect(queryManager.cache.extract().ROOT_QUERY!.author).toEqual( + data.author + ); + }); + + it("should be able to unsubscribe from a polling query subscription", async () => { + const query = gql` + query { + author { + firstName + lastName } - `; - const data = { - author: { - firstName: "John", - lastName: "Smith", - }, - }; + } + `; + const data = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; - const observable = mockQueryManager({ + const observable = mockQueryManager( + { request: { query }, result: { data }, - }).watchQuery({ query, pollInterval: 20 }); - - const { promise, subscription } = observableToPromiseAndSubscription( - { - observable, - wait: 60, + }, + { + request: { query }, + result: () => { + throw new Error("Should not again"); }, - (result: any) => { - expect(result.data).toEqual(data); - subscription.unsubscribe(); - } - ); + } + ).watchQuery({ query, pollInterval: 20 }); + const stream = new ObservableStream(observable); - return promise.then(resolve, reject); - } - ); + await expect(stream).toEmitMatchedValue({ data }); - itAsync( - "should not empty the store when a polling query fails due to a network error", - (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } + stream.unsubscribe(); + + // Ensure polling has stopped by ensuring the error is not thrown from the mocks + await wait(30); + }); + + it("should not empty the store when a polling query fails due to a network error", async () => { + const query = gql` + query { + author { + firstName + lastName } - `; - const data = { - author: { - firstName: "John", - lastName: "Smith", - }, - }; - const queryManager = mockQueryManager( - { - request: { query }, - result: { data }, - }, - { - request: { query }, - error: new Error("Network error occurred."), - } - ); - const observable = queryManager.watchQuery({ - query, - pollInterval: 20, - notifyOnNetworkStatusChange: false, - }); + } + `; + const data = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; + const queryManager = mockQueryManager( + { + request: { query }, + result: { data }, + }, + { + request: { query }, + error: new Error("Network error occurred."), + } + ); + const observable = queryManager.watchQuery({ + query, + pollInterval: 20, + notifyOnNetworkStatusChange: false, + }); + const stream = new ObservableStream(observable); - return observableToPromise( - { - observable, - errorCallbacks: [ - () => { - expect(queryManager.cache.extract().ROOT_QUERY!.author).toEqual( - data.author - ); - }, - ], - }, - (result) => { - expect(result.data).toEqual(data); - expect(queryManager.cache.extract().ROOT_QUERY!.author).toEqual( - data.author - ); + await expect(stream).toEmitMatchedValue({ data }); + expect(queryManager.cache.extract().ROOT_QUERY!.author).toEqual( + data.author + ); + + await expect(stream).toEmitError( + new ApolloError({ networkError: new Error("Network error occurred.") }) + ); + expect(queryManager.cache.extract().ROOT_QUERY!.author).toEqual( + data.author + ); + }); + + it("should not fire next on an observer if there is no change in the result", async () => { + const query = gql` + query { + author { + firstName + lastName } - ).then(resolve, reject); - } - ); + } + `; - itAsync( - "should not fire next on an observer if there is no change in the result", - (resolve, reject) => { - const query = gql` - query { - author { + const data = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; + const queryManager = mockQueryManager( + { + request: { query }, + result: { data }, + }, + { + request: { query }, + result: { data }, + } + ); + + const observable = queryManager.watchQuery({ query }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitMatchedValue({ data }); + + const result = await queryManager.query({ query }); + expect(result.data).toEqual(data); + + await expect(stream).not.toEmitAnything(); + }); + + it("should not return stale data when we orphan a real-id node in the store with a real-id node", async () => { + const query1 = gql` + query { + author { + name { firstName lastName } + age + id + __typename + } + } + `; + const query2 = gql` + query { + author { + name { + firstName + } + id + __typename } - `; - - const data = { - author: { + } + `; + const data1 = { + author: { + name: { firstName: "John", lastName: "Smith", }, - }; - const queryManager = mockQueryManager( + age: 18, + id: "187", + __typename: "Author", + }, + }; + const data2 = { + author: { + name: { + firstName: "John", + }, + id: "197", + __typename: "Author", + }, + }; + const reducerConfig = { dataIdFromObject }; + const queryManager = createQueryManager({ + link: mockSingleLink( { - request: { query }, - result: { data }, + request: { query: query1 }, + result: { data: data1 }, }, { - request: { query }, - result: { data }, + request: { query: query2 }, + result: { data: data2 }, + }, + { + request: { query: query1 }, + result: { data: data1 }, } - ); + ), + config: reducerConfig, + }); - const observable = queryManager.watchQuery({ query }); - return Promise.all([ - // we wait for a little bit to ensure the result of the second query - // don't trigger another subscription event - observableToPromise({ observable, wait: 100 }, (result) => { - expect(result.data).toEqual(data); - }), - queryManager.query({ query }).then((result) => { - expect(result.data).toEqual(data); - }), - ]).then(resolve, reject); - } - ); + const observable1 = queryManager.watchQuery({ query: query1 }); + const observable2 = queryManager.watchQuery({ query: query2 }); - itAsync( - "should not return stale data when we orphan a real-id node in the store with a real-id node", - (resolve, reject) => { - const query1 = gql` - query { - author { - name { - firstName - lastName - } - age - id - __typename + const stream1 = new ObservableStream(observable1); + const stream2 = new ObservableStream(observable2); + + await expect(stream1).toEmitValue({ + data: data1, + loading: false, + networkStatus: NetworkStatus.ready, + }); + await expect(stream2).toEmitValue({ + data: data2, + loading: false, + networkStatus: NetworkStatus.ready, + }); + }); + + it("should return partial data when configured when we orphan a real-id node in the store with a real-id node", async () => { + const query1 = gql` + query { + author { + name { + firstName + lastName } + age + id + __typename } - `; - const query2 = gql` - query { - author { - name { - firstName - } - id - __typename + } + `; + const query2 = gql` + query { + author { + name { + firstName } + id + __typename } - `; - const data1 = { - author: { - name: { - firstName: "John", - lastName: "Smith", - }, - age: 18, - id: "187", - __typename: "Author", + } + `; + const data1 = { + author: { + name: { + firstName: "John", + lastName: "Smith", }, - }; - const data2 = { - author: { - name: { - firstName: "John", - }, - id: "197", - __typename: "Author", + age: 18, + id: "187", + __typename: "Author", + }, + }; + const data2 = { + author: { + name: { + firstName: "John", }, - }; - const reducerConfig = { dataIdFromObject }; - const queryManager = createQueryManager({ - link: mockSingleLink( - { - request: { query: query1 }, - result: { data: data1 }, - }, - { - request: { query: query2 }, - result: { data: data2 }, - }, - { - request: { query: query1 }, - result: { data: data1 }, - } - ).setOnError(reject), - config: reducerConfig, - }); + id: "197", + __typename: "Author", + }, + }; - const observable1 = queryManager.watchQuery({ query: query1 }); - const observable2 = queryManager.watchQuery({ query: query2 }); + const queryManager = createQueryManager({ + link: mockSingleLink( + { + request: { query: query1 }, + result: { data: data1 }, + }, + { + request: { query: query2 }, + result: { data: data2 }, + } + ), + }); - // I'm not sure the waiting 60 here really is required, but the test used to do it - return Promise.all([ - observableToPromise( - { - observable: observable1, - wait: 60, - }, - (result) => { - expect(result).toEqual({ - data: data1, - loading: false, - networkStatus: NetworkStatus.ready, - }); - } - ), - observableToPromise( - { - observable: observable2, - wait: 60, - }, - (result) => { - expect(result).toEqual({ - data: data2, - loading: false, - networkStatus: NetworkStatus.ready, - }); - } - ), - ]).then(resolve, reject); - } - ); + const observable1 = queryManager.watchQuery({ + query: query1, + returnPartialData: true, + }); + const observable2 = queryManager.watchQuery({ query: query2 }); - itAsync( - "should return partial data when configured when we orphan a real-id node in the store with a real-id node", - (resolve, reject) => { - const query1 = gql` - query { - author { - name { - firstName - lastName - } - age - id - __typename - } - } - `; - const query2 = gql` - query { - author { - name { - firstName - } - id - __typename - } - } - `; - const data1 = { - author: { - name: { - firstName: "John", - lastName: "Smith", - }, - age: 18, - id: "187", - __typename: "Author", - }, - }; - const data2 = { - author: { - name: { - firstName: "John", - }, - id: "197", - __typename: "Author", - }, - }; + const stream1 = new ObservableStream(observable1); + const stream2 = new ObservableStream(observable2); - const queryManager = createQueryManager({ - link: mockSingleLink( - { - request: { query: query1 }, - result: { data: data1 }, - }, - { - request: { query: query2 }, - result: { data: data2 }, - } - ).setOnError(reject), - }); - - const observable1 = queryManager.watchQuery({ - query: query1, - returnPartialData: true, - }); - const observable2 = queryManager.watchQuery({ query: query2 }); - - return Promise.all([ - observableToPromise( - { - observable: observable1, - }, - (result) => { - expect(result).toEqual({ - data: {}, - loading: true, - networkStatus: NetworkStatus.loading, - partial: true, - }); - }, - (result) => { - expect(result).toEqual({ - data: data1, - loading: false, - networkStatus: NetworkStatus.ready, - }); - } - ), - observableToPromise( - { - observable: observable2, - }, - (result) => { - expect(result).toEqual({ - data: data2, - loading: false, - networkStatus: NetworkStatus.ready, - }); - } - ), - ]).then(resolve, reject); - } - ); + await expect(stream1).toEmitValue({ + data: {}, + loading: true, + networkStatus: NetworkStatus.loading, + partial: true, + }); + await expect(stream2).toEmitValue({ + data: data2, + loading: false, + networkStatus: NetworkStatus.ready, + }); + await expect(stream1).toEmitValue({ + data: data1, + loading: false, + networkStatus: NetworkStatus.ready, + }); + }); it("should not write unchanged network results to cache", async () => { const cache = new InMemoryCache({ @@ -2656,254 +2375,233 @@ describe("QueryManager", () => { await expect(stream).not.toEmitAnything(); }); - itAsync( - "should not error when replacing unidentified data with a normalized ID", - (resolve, reject) => { - const queryWithoutId = gql` - query { - author { - name { - firstName - lastName - } - age - __typename + it("should not error when replacing unidentified data with a normalized ID", async () => { + const queryWithoutId = gql` + query { + author { + name { + firstName + lastName } + age + __typename } - `; + } + `; - const queryWithId = gql` - query { - author { - name { - firstName - } - id - __typename + const queryWithId = gql` + query { + author { + name { + firstName } + id + __typename } - `; + } + `; - const dataWithoutId = { - author: { - name: { - firstName: "John", - lastName: "Smith", - }, - age: "124", - __typename: "Author", + const dataWithoutId = { + author: { + name: { + firstName: "John", + lastName: "Smith", }, - }; + age: "124", + __typename: "Author", + }, + }; - const dataWithId = { - author: { - name: { - firstName: "Jane", - }, - id: "129", - __typename: "Author", + const dataWithId = { + author: { + name: { + firstName: "Jane", }, - }; + id: "129", + __typename: "Author", + }, + }; - let mergeCount = 0; - const queryManager = createQueryManager({ - link: mockSingleLink( - { - request: { query: queryWithoutId }, - result: { data: dataWithoutId }, - }, - { - request: { query: queryWithId }, - result: { data: dataWithId }, - } - ).setOnError(reject), - config: { - typePolicies: { - Query: { - fields: { - author: { - merge(existing, incoming, { isReference, readField }) { - switch (++mergeCount) { - case 1: - expect(existing).toBeUndefined(); - expect(isReference(incoming)).toBe(false); - expect(incoming).toEqual(dataWithoutId.author); - break; - case 2: - expect(existing).toEqual(dataWithoutId.author); - expect(isReference(incoming)).toBe(true); - expect(readField("id", incoming)).toBe("129"); - expect(readField("name", incoming)).toEqual( - dataWithId.author.name - ); - break; - default: - fail("unreached"); - } - return incoming; - }, + let mergeCount = 0; + const queryManager = createQueryManager({ + link: mockSingleLink( + { + request: { query: queryWithoutId }, + result: { data: dataWithoutId }, + }, + { + request: { query: queryWithId }, + result: { data: dataWithId }, + } + ), + config: { + typePolicies: { + Query: { + fields: { + author: { + merge(existing, incoming, { isReference, readField }) { + switch (++mergeCount) { + case 1: + expect(existing).toBeUndefined(); + expect(isReference(incoming)).toBe(false); + expect(incoming).toEqual(dataWithoutId.author); + break; + case 2: + expect(existing).toEqual(dataWithoutId.author); + expect(isReference(incoming)).toBe(true); + expect(readField("id", incoming)).toBe("129"); + expect(readField("name", incoming)).toEqual( + dataWithId.author.name + ); + break; + default: + fail("unreached"); + } + return incoming; }, }, }, }, }, - }); + }, + }); - const observableWithId = queryManager.watchQuery({ - query: queryWithId, - }); + const observableWithId = queryManager.watchQuery({ + query: queryWithId, + }); - const observableWithoutId = queryManager.watchQuery({ - query: queryWithoutId, - }); + const observableWithoutId = queryManager.watchQuery({ + query: queryWithoutId, + }); - return Promise.all([ - observableToPromise({ observable: observableWithoutId }, (result) => - expect(result.data).toEqual(dataWithoutId) - ), - observableToPromise({ observable: observableWithId }, (result) => - expect(result.data).toEqual(dataWithId) - ), - ]).then(resolve, reject); - } - ); + const stream1 = new ObservableStream(observableWithoutId); + const stream2 = new ObservableStream(observableWithId); - itAsync( - "exposes errors on a refetch as a rejection", - async (resolve, reject) => { - const request = { - query: gql` - { - people_one(id: 1) { - name - } + await expect(stream1).toEmitMatchedValue({ data: dataWithoutId }); + await expect(stream2).toEmitMatchedValue({ data: dataWithId }); + }); + + it("exposes errors on a refetch as a rejection", async () => { + const request = { + query: gql` + { + people_one(id: 1) { + name } - `, - }; - const firstResult = { - data: { - people_one: { - name: "Luke Skywalker", - }, + } + `, + }; + const firstResult = { + data: { + people_one: { + name: "Luke Skywalker", }, - }; - const secondResult = { - errors: [ - new GraphQLError("This is not the person you are looking for."), - ], - }; + }, + }; + const secondResult = { + errors: [new GraphQLError("This is not the person you are looking for.")], + }; - const queryManager = mockRefetch({ - request, - firstResult, - secondResult, - }); + const queryManager = mockRefetch({ + request, + firstResult, + secondResult, + }); - const handle = queryManager.watchQuery(request); + const handle = queryManager.watchQuery(request); + const stream = new ObservableStream(handle); - const checkError = (error: ApolloError) => { - expect(error.graphQLErrors[0].message).toEqual( - "This is not the person you are looking for." - ); - }; + await expect(stream).toEmitValue({ + data: firstResult.data, + loading: false, + networkStatus: NetworkStatus.ready, + }); - handle.subscribe({ - error: checkError, - }); + const expectedError = new ApolloError({ + graphQLErrors: secondResult.errors, + }); - handle - .refetch() - .then(() => { - reject(new Error("Error on refetch should reject promise")); - }) - .catch((error) => { - checkError(error); - }) - .then(resolve, reject); - } - ); - - itAsync( - "does not return incomplete data when two queries for the same item are executed", - (resolve, reject) => { - const queryA = gql` - query queryA { - person(id: "abc") { - __typename - id - firstName - lastName - } + await expect(handle.refetch()).rejects.toThrow(expectedError); + await expect(stream).toEmitError(expectedError); + }); + + it("does not return incomplete data when two queries for the same item are executed", async () => { + const queryA = gql` + query queryA { + person(id: "abc") { + __typename + id + firstName + lastName } - `; - const queryB = gql` - query queryB { - person(id: "abc") { - __typename - id - lastName - age - } + } + `; + const queryB = gql` + query queryB { + person(id: "abc") { + __typename + id + lastName + age } - `; - const dataA = { - person: { - __typename: "Person", - id: "abc", - firstName: "Luke", - lastName: "Skywalker", - }, - }; - const dataB = { - person: { - __typename: "Person", - id: "abc", - lastName: "Skywalker", - age: "32", - }, - }; - const queryManager = new QueryManager( - getDefaultOptionsForQueryManagerTests({ - link: mockSingleLink( - { request: { query: queryA }, result: { data: dataA } }, - { request: { query: queryB }, result: { data: dataB }, delay: 20 } - ).setOnError(reject), - cache: new InMemoryCache({}), - ssrMode: true, - }) - ); + } + `; + const dataA = { + person: { + __typename: "Person", + id: "abc", + firstName: "Luke", + lastName: "Skywalker", + }, + }; + const dataB = { + person: { + __typename: "Person", + id: "abc", + lastName: "Skywalker", + age: "32", + }, + }; + const queryManager = new QueryManager( + getDefaultOptionsForQueryManagerTests({ + link: mockSingleLink( + { request: { query: queryA }, result: { data: dataA } }, + { request: { query: queryB }, result: { data: dataB }, delay: 20 } + ), + cache: new InMemoryCache({}), + ssrMode: true, + }) + ); - const observableA = queryManager.watchQuery({ - query: queryA, - }); - const observableB = queryManager.watchQuery({ - query: queryB, - }); + const observableA = queryManager.watchQuery({ + query: queryA, + }); + const observableB = queryManager.watchQuery({ + query: queryB, + }); + const streamA = new ObservableStream(observableA); + const streamB = new ObservableStream(observableB); - return Promise.all([ - observableToPromise({ observable: observableA }, () => { - expect(getCurrentQueryResult(observableA)).toEqual({ - data: dataA, - partial: false, - }); - expect(getCurrentQueryResult(observableB)).toEqual({ - data: undefined, - partial: true, - }); - }), - observableToPromise({ observable: observableB }, () => { - expect(getCurrentQueryResult(observableA)).toEqual({ - data: dataA, - partial: false, - }); - expect(getCurrentQueryResult(observableB)).toEqual({ - data: dataB, - partial: false, - }); - }), - ]).then(resolve, reject); - } - ); + await expect(streamA).toEmitNext(); + expect(getCurrentQueryResult(observableA)).toEqual({ + data: dataA, + partial: false, + }); + expect(getCurrentQueryResult(observableB)).toEqual({ + data: undefined, + partial: true, + }); + + await expect(streamB).toEmitNext(); + expect(getCurrentQueryResult(observableA)).toEqual({ + data: dataA, + partial: false, + }); + expect(getCurrentQueryResult(observableB)).toEqual({ + data: dataB, + partial: false, + }); + }); it('only increments "queryInfo.lastRequestId" when fetching data from network', async () => { const query = gql` @@ -2966,7 +2664,7 @@ describe("QueryManager", () => { }); describe("polling queries", () => { - itAsync("allows you to poll queries", (resolve, reject) => { + it("allows you to poll queries", async () => { const query = gql` query fetchLuke($id: String) { people_one(id: $id) { @@ -3007,15 +2705,13 @@ describe("QueryManager", () => { pollInterval: 50, notifyOnNetworkStatusChange: false, }); + const stream = new ObservableStream(observable); - return observableToPromise( - { observable }, - (result) => expect(result.data).toEqual(data1), - (result) => expect(result.data).toEqual(data2) - ).then(resolve, reject); + await expect(stream).toEmitMatchedValue({ data: data1 }); + await expect(stream).toEmitMatchedValue({ data: data2 }); }); - itAsync("does not poll during SSR", (resolve, reject) => { + it("does not poll during SSR", async () => { const query = gql` query fetchLuke($id: String) { people_one(id: $id) { @@ -3055,7 +2751,7 @@ describe("QueryManager", () => { request: { query, variables }, result: { data: data2 }, } - ).setOnError(reject), + ), cache: new InMemoryCache({ addTypename: false }), ssrMode: true, }) @@ -3067,286 +2763,250 @@ describe("QueryManager", () => { pollInterval: 10, notifyOnNetworkStatusChange: false, }); + const stream = new ObservableStream(observable); - let count = 1; - const subHandle = observable.subscribe({ - next: (result: any) => { - switch (count) { - case 1: - expect(result.data).toEqual(data1); - setTimeout(() => { - subHandle.unsubscribe(); - resolve(); - }, 15); - count++; - break; - case 2: - default: - reject(new Error("Only expected one result, not multiple")); - } - }, - }); + await expect(stream).toEmitMatchedValue({ data: data1 }); + await expect(stream).not.toEmitAnything(); }); - itAsync( - "should let you handle multiple polled queries and unsubscribe from one of them", - (resolve) => { - const query1 = gql` - query { - author { - firstName - lastName - } + it("should let you handle multiple polled queries and unsubscribe from one of them", async () => { + const query1 = gql` + query { + author { + firstName + lastName } - `; - const query2 = gql` - query { - person { - name - } + } + `; + const query2 = gql` + query { + person { + name } - `; - const data11 = { - author: { - firstName: "John", - lastName: "Smith", - }, - }; - const data12 = { - author: { - firstName: "Jack", - lastName: "Smith", - }, - }; - const data13 = { - author: { - firstName: "Jolly", - lastName: "Smith", - }, - }; - const data14 = { - author: { - firstName: "Jared", - lastName: "Smith", - }, - }; - const data21 = { - person: { - name: "Jane Smith", - }, - }; - const data22 = { - person: { - name: "Josey Smith", - }, - }; - const queryManager = mockQueryManager( - { - request: { query: query1 }, - result: { data: data11 }, - }, - { - request: { query: query1 }, - result: { data: data12 }, - }, - { - request: { query: query1 }, - result: { data: data13 }, - }, - { - request: { query: query1 }, - result: { data: data14 }, - }, - { - request: { query: query2 }, - result: { data: data21 }, + } + `; + const data11 = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; + const data12 = { + author: { + firstName: "Jack", + lastName: "Smith", + }, + }; + const data13 = { + author: { + firstName: "Jolly", + lastName: "Smith", + }, + }; + const data14 = { + author: { + firstName: "Jared", + lastName: "Smith", + }, + }; + const data21 = { + person: { + name: "Jane Smith", + }, + }; + const data22 = { + person: { + name: "Josey Smith", + }, + }; + const queryManager = mockQueryManager( + { + request: { query: query1 }, + result: { data: data11 }, + }, + { + request: { query: query1 }, + result: { data: data12 }, + }, + { + request: { query: query1 }, + result: { data: data13 }, + }, + { + request: { query: query1 }, + result: { data: data14 }, + }, + { + request: { query: query2 }, + result: { data: data21 }, + }, + { + request: { query: query2 }, + result: { data: data22 }, + } + ); + let handle1Count = 0; + let handleCount = 0; + let setMilestone = false; + + const subscription1 = queryManager + .watchQuery({ + query: query1, + pollInterval: 150, + }) + .subscribe({ + next() { + handle1Count++; + handleCount++; + if (handle1Count > 1 && !setMilestone) { + subscription1.unsubscribe(); + setMilestone = true; + } }, - { - request: { query: query2 }, - result: { data: data22 }, - } - ); - let handle1Count = 0; - let handleCount = 0; - let setMilestone = false; - - const subscription1 = queryManager - .watchQuery({ - query: query1, - pollInterval: 150, - }) - .subscribe({ - next() { - handle1Count++; - handleCount++; - if (handle1Count > 1 && !setMilestone) { - subscription1.unsubscribe(); - setMilestone = true; - } - }, - }); + }); - const subscription2 = queryManager - .watchQuery({ - query: query2, - pollInterval: 2000, - }) - .subscribe({ - next() { - handleCount++; - }, - }); + const subscription2 = queryManager + .watchQuery({ + query: query2, + pollInterval: 2000, + }) + .subscribe({ + next() { + handleCount++; + }, + }); - setTimeout(() => { - expect(handleCount).toBe(3); - subscription1.unsubscribe(); - subscription2.unsubscribe(); + await wait(400); - resolve(); - }, 400); - } - ); + expect(handleCount).toBe(3); + subscription1.unsubscribe(); + subscription2.unsubscribe(); + }); - itAsync( - "allows you to unsubscribe from polled queries", - (resolve, reject) => { - const query = gql` - query fetchLuke($id: String) { - people_one(id: $id) { - name - } + it("allows you to unsubscribe from polled queries", async () => { + const query = gql` + query fetchLuke($id: String) { + people_one(id: $id) { + name } - `; + } + `; - const variables = { - id: "1", - }; + const variables = { + id: "1", + }; - const data1 = { - people_one: { - name: "Luke Skywalker", - }, - }; + const data1 = { + people_one: { + name: "Luke Skywalker", + }, + }; - const data2 = { - people_one: { - name: "Luke Skywalker has a new name", - }, - }; + const data2 = { + people_one: { + name: "Luke Skywalker has a new name", + }, + }; - const queryManager = mockQueryManager( - { - request: { query, variables }, - result: { data: data1 }, + const queryManager = mockQueryManager( + { + request: { query, variables }, + result: { data: data1 }, + }, + { + request: { query, variables }, + result: { data: data2 }, + }, + { + request: { query, variables }, + result: () => { + throw new Error("Should not fetch again"); }, - { - request: { query, variables }, - result: { data: data2 }, - } - ); - const observable = queryManager.watchQuery({ - query, - variables, - pollInterval: 50, - notifyOnNetworkStatusChange: false, - }); + } + ); + const observable = queryManager.watchQuery({ + query, + variables, + pollInterval: 50, + notifyOnNetworkStatusChange: false, + }); + const stream = new ObservableStream(observable); - const { promise, subscription } = observableToPromiseAndSubscription( - { - observable, - wait: 60, - }, - (result) => expect(result.data).toEqual(data1), - (result) => { - expect(result.data).toEqual(data2); + await expect(stream).toEmitMatchedValue({ data: data1 }); + await expect(stream).toEmitMatchedValue({ data: data2 }); - // we unsubscribe here manually, rather than waiting for the timeout. - subscription.unsubscribe(); - } - ); + stream.unsubscribe(); - return promise.then(resolve, reject); - } - ); + // Ensure polling has stopped by ensuring the error is not thrown from the mocks + await wait(60); + }); - itAsync( - "allows you to unsubscribe from polled query errors", - (resolve, reject) => { - const query = gql` - query fetchLuke($id: String) { - people_one(id: $id) { - name - } + it("allows you to unsubscribe from polled query errors", async () => { + const query = gql` + query fetchLuke($id: String) { + people_one(id: $id) { + name } - `; + } + `; - const variables = { - id: "1", - }; + const variables = { + id: "1", + }; - const data1 = { - people_one: { - name: "Luke Skywalker", - }, - }; + const data1 = { + people_one: { + name: "Luke Skywalker", + }, + }; - const data2 = { - people_one: { - name: "Luke Skywalker has a new name", - }, - }; + const data2 = { + people_one: { + name: "Luke Skywalker has a new name", + }, + }; - const queryManager = mockQueryManager( - { - request: { query, variables }, - result: { data: data1 }, - }, - { - request: { query, variables }, - error: new Error("Network error"), + const queryManager = mockQueryManager( + { + request: { query, variables }, + result: { data: data1 }, + }, + { + request: { query, variables }, + error: new Error("Network error"), + }, + { + request: { query, variables }, + result: { data: data2 }, + }, + { + request: { query, variables }, + result: () => { + throw new Error("Should not fetch again"); }, - { - request: { query, variables }, - result: { data: data2 }, - } - ); + } + ); - const observable = queryManager.watchQuery({ - query, - variables, - pollInterval: 50, - notifyOnNetworkStatusChange: false, - }); + const observable = queryManager.watchQuery({ + query, + variables, + pollInterval: 50, + notifyOnNetworkStatusChange: false, + }); + const stream = new ObservableStream(observable); - let isFinished = false; - process.once("unhandledRejection", () => { - if (!isFinished) reject("unhandledRejection from network"); - }); + await expect(stream).toEmitMatchedValue({ data: data1 }); + await expect(stream).toEmitError( + new ApolloError({ networkError: new Error("Network error") }) + ); - const { promise, subscription } = observableToPromiseAndSubscription( - { - observable, - wait: 60, - errorCallbacks: [ - (error) => { - expect(error.message).toMatch("Network error"); - subscription.unsubscribe(); - }, - ], - }, - (result) => expect(result.data).toEqual(data1) - ); - - promise.then(() => { - setTimeout(() => { - isFinished = true; - resolve(); - }, 4); - }); - } - ); + stream.unsubscribe(); - itAsync("exposes a way to start a polling query", (resolve, reject) => { + // Ensure polling has stopped by ensuring the error is not thrown from the mocks + await wait(60); + }); + + it("exposes a way to start a polling query", async () => { const query = gql` query fetchLuke($id: String) { people_one(id: $id) { @@ -3388,15 +3048,13 @@ describe("QueryManager", () => { notifyOnNetworkStatusChange: false, }); observable.startPolling(50); + const stream = new ObservableStream(observable); - return observableToPromise( - { observable }, - (result) => expect(result.data).toEqual(data1), - (result) => expect(result.data).toEqual(data2) - ).then(resolve, reject); + await expect(stream).toEmitMatchedValue({ data: data1 }); + await expect(stream).toEmitMatchedValue({ data: data2 }); }); - itAsync("exposes a way to stop a polling query", (resolve, reject) => { + it("exposes a way to stop a polling query", async () => { const query = gql` query fetchLeia($id: String) { people_one(id: $id) { @@ -3436,14 +3094,16 @@ describe("QueryManager", () => { variables, pollInterval: 50, }); + const stream = new ObservableStream(observable); - return observableToPromise({ observable, wait: 60 }, (result) => { - expect(result.data).toEqual(data1); - observable.stopPolling(); - }).then(resolve, reject); + await expect(stream).toEmitMatchedValue({ data: data1 }); + + observable.stopPolling(); + + await expect(stream).not.toEmitAnything(); }); - itAsync("stopped polling queries still get updates", (resolve, reject) => { + it("stopped polling queries still get updates", async () => { const query = gql` query fetchLeia($id: String) { people_one(id: $id) { @@ -3484,148 +3144,123 @@ describe("QueryManager", () => { variables, pollInterval: 50, }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitMatchedValue({ data: data1 }); + + const result = await queryManager.query({ + query, + variables, + fetchPolicy: "network-only", + }); - return Promise.all([ - observableToPromise( - { observable }, - (result) => { - expect(result.data).toEqual(data1); - queryManager - .query({ - query, - variables, - fetchPolicy: "network-only", - }) - .then((result) => { - expect(result.data).toEqual(data2); - }) - .catch(reject); - }, - (result) => { - expect(result.data).toEqual(data2); - } - ), - ]).then(resolve, reject); + expect(result.data).toEqual(data2); + await expect(stream).toEmitMatchedValue({ data: data2 }); }); }); + describe("store resets", () => { - itAsync( - "returns a promise resolving when all queries have been refetched", - (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } + it("returns a promise resolving when all queries have been refetched", async () => { + const query = gql` + query { + author { + firstName + lastName } - `; + } + `; - const data = { - author: { - firstName: "John", - lastName: "Smith", - }, - }; + const data = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; - const dataChanged = { - author: { - firstName: "John changed", - lastName: "Smith", - }, - }; + const dataChanged = { + author: { + firstName: "John changed", + lastName: "Smith", + }, + }; - const query2 = gql` - query { - author2 { - firstName - lastName - } + const query2 = gql` + query { + author2 { + firstName + lastName } - `; + } + `; - const data2 = { - author2: { - firstName: "John", - lastName: "Smith", - }, - }; + const data2 = { + author2: { + firstName: "John", + lastName: "Smith", + }, + }; + + const data2Changed = { + author2: { + firstName: "John changed", + lastName: "Smith", + }, + }; - const data2Changed = { - author2: { - firstName: "John changed", - lastName: "Smith", + const queryManager = createQueryManager({ + link: mockSingleLink( + { + request: { query }, + result: { data }, }, - }; + { + request: { query: query2 }, + result: { data: data2 }, + }, + { + request: { query }, + result: { data: dataChanged }, + }, + { + request: { query: query2 }, + result: { data: data2Changed }, + } + ), + }); - const queryManager = createQueryManager({ - link: mockSingleLink( - { - request: { query }, - result: { data }, - }, - { - request: { query: query2 }, - result: { data: data2 }, - }, - { - request: { query }, - result: { data: dataChanged }, - }, - { - request: { query: query2 }, - result: { data: data2Changed }, - } - ).setOnError(reject), - }); + const observable = queryManager.watchQuery({ query }); + const observable2 = queryManager.watchQuery({ query: query2 }); - const observable = queryManager.watchQuery({ query }); - const observable2 = queryManager.watchQuery({ query: query2 }); + const stream = new ObservableStream(observable); + const stream2 = new ObservableStream(observable2); - return Promise.all([ - observableToPromise({ observable }, (result) => - expect(result.data).toEqual(data) - ), - observableToPromise({ observable: observable2 }, (result) => - expect(result.data).toEqual(data2) - ), - ]) - .then(() => { - observable.subscribe({ next: () => null }); - observable2.subscribe({ next: () => null }); - - return resetStore(queryManager).then(() => { - const result = getCurrentQueryResult(observable); - expect(result.partial).toBe(false); - expect(result.data).toEqual(dataChanged); - - const result2 = getCurrentQueryResult(observable2); - expect(result2.partial).toBe(false); - expect(result2.data).toEqual(data2Changed); - }); - }) - .then(resolve, reject); - } - ); + await expect(stream).toEmitMatchedValue({ data }); + await expect(stream2).toEmitMatchedValue({ data: data2 }); - itAsync( - "should change the store state to an empty state", - (resolve, reject) => { - const queryManager = createQueryManager({ - link: mockSingleLink().setOnError(reject), - }); + await resetStore(queryManager); - resetStore(queryManager); + const result = getCurrentQueryResult(observable); + expect(result.partial).toBe(false); + expect(result.data).toEqual(dataChanged); - expect(queryManager.cache.extract()).toEqual({}); - expect(queryManager.getQueryStore()).toEqual({}); - expect(queryManager.mutationStore).toEqual({}); + const result2 = getCurrentQueryResult(observable2); + expect(result2.partial).toBe(false); + expect(result2.data).toEqual(data2Changed); + }); - resolve(); - } - ); + it("should change the store state to an empty state", () => { + const queryManager = createQueryManager({ + link: mockSingleLink(), + }); + + void resetStore(queryManager); - xit("should only refetch once when we store reset", () => { + expect(queryManager.cache.extract()).toEqual({}); + expect(queryManager.getQueryStore()).toEqual({}); + expect(queryManager.mutationStore).toEqual({}); + }); + + it.skip("should only refetch once when we store reset", async () => { let queryManager: QueryManager; const query = gql` query { @@ -3665,25 +3300,22 @@ describe("QueryManager", () => { ); queryManager = createQueryManager({ link }); const observable = queryManager.watchQuery({ query }); + const stream = new ObservableStream(observable); - // wait just to make sure the observable doesn't fire again - return observableToPromise( - { observable, wait: 0 }, - (result) => { - expect(result.data).toEqual(data); - expect(timesFired).toBe(1); - // reset the store after data has returned - resetStore(queryManager); - }, - (result) => { - // only refetch once and make sure data has changed - expect(result.data).toEqual(data2); - expect(timesFired).toBe(2); - } - ); + await expect(stream).toEmitMatchedValue({ data }); + expect(timesFired).toBe(1); + + // reset the store after data has returned + void resetStore(queryManager); + + // only refetch once and make sure data has changed + await expect(stream).toEmitMatchedValue({ data: data2 }); + expect(timesFired).toBe(2); + + await expect(stream).not.toEmitAnything(); }); - itAsync("should not refetch torn-down queries", (resolve) => { + it("should not refetch torn-down queries", async () => { let queryManager: QueryManager; let observable: ObservableQuery; const query = gql` @@ -3707,31 +3339,26 @@ describe("QueryManager", () => { new Observable((observer) => { timesFired += 1; observer.next({ data }); - return; }), ]); queryManager = createQueryManager({ link }); observable = queryManager.watchQuery({ query }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitMatchedValue({ data }); - observableToPromise({ observable, wait: 0 }, (result) => - expect(result.data).toEqual(data) - ).then(() => { - expect(timesFired).toBe(1); + stream.unsubscribe(); - // at this point the observable query has been torn down - // because observableToPromise unsubscribe before resolving - resetStore(queryManager); + expect(timesFired).toBe(1); - setTimeout(() => { - expect(timesFired).toBe(1); + void resetStore(queryManager); + await wait(50); - resolve(); - }, 50); - }); + expect(timesFired).toBe(1); }); - itAsync("should not error when resetStore called", (resolve, reject) => { + it("should not error when resetStore called", async () => { const query = gql` query { author { @@ -3766,23 +3393,18 @@ describe("QueryManager", () => { query, notifyOnNetworkStatusChange: false, }); + const stream = new ObservableStream(observable); - // wait to make sure store reset happened - return observableToPromise( - { observable, wait: 20 }, - (result) => { - expect(result.data).toEqual(data); - expect(timesFired).toBe(1); - resetStore(queryManager).catch(reject); - }, - (result) => { - expect(result.data).toEqual(data); - expect(timesFired).toBe(2); - } - ).then(resolve, reject); + await expect(stream).toEmitMatchedValue({ data }); + expect(timesFired).toBe(1); + + void resetStore(queryManager); + + await expect(stream).toEmitMatchedValue({ data }); + expect(timesFired).toBe(2); }); - itAsync("should not error on a stopped query()", (resolve, reject) => { + it("should not error on a stopped query()", async () => { let queryManager: QueryManager; const query = gql` query { @@ -3810,403 +3432,184 @@ describe("QueryManager", () => { queryManager = createQueryManager({ link }); const queryId = "1"; - queryManager - .fetchQuery(queryId, { query }) - .catch((e) => reject("Exception thrown for stopped query")); + const promise = queryManager.fetchQuery(queryId, { query }); queryManager.removeQuery(queryId); - resetStore(queryManager).then(resolve, reject); + + await resetStore(queryManager); + // Ensure the promise doesn't reject + await Promise.race([wait(50), promise]); }); - itAsync( - "should throw an error on an inflight fetch query if the store is reset", - (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } + it("should throw an error on an inflight fetch query if the store is reset", async () => { + const query = gql` + query { + author { + firstName + lastName } - `; - const data = { - author: { - firstName: "John", - lastName: "Smith", - }, - }; - const queryManager = mockQueryManager({ - request: { query }, - result: { data }, - delay: 10000, //i.e. forever - }); - queryManager - .fetchQuery("made up id", { query }) - .then(() => { - reject(new Error("Returned a result.")); - }) - .catch((error) => { - expect(error.message).toMatch("Store reset"); - resolve(); - }); - // Need to delay the reset at least until the fetchRequest method - // has had a chance to enter this request into fetchQueryRejectFns. - setTimeout(() => resetStore(queryManager), 100); - } - ); + } + `; + const data = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; + const queryManager = mockQueryManager({ + request: { query }, + result: { data }, + delay: 10000, //i.e. forever + }); + const promise = queryManager.fetchQuery("made up id", { query }); - itAsync( - "should call refetch on a mocked Observable if the store is reset", - (resolve) => { - const query = gql` - query { - author { - firstName - lastName - } - } - `; - const data = { - author: { - firstName: "John", - lastName: "Smith", - }, - }; - const queryManager = mockQueryManager({ - request: { query }, - result: { data }, - }); - const obs = queryManager.watchQuery({ query }); - obs.subscribe({}); - obs.refetch = resolve as any; + // Need to delay the reset at least until the fetchRequest method + // has had a chance to enter this request into fetchQueryRejectFns. + await wait(100); + void resetStore(queryManager); - resetStore(queryManager); - } - ); + await expect(promise).rejects.toThrow( + new InvariantError( + "Store reset while query was in flight (not completed in link chain)" + ) + ); + }); - itAsync( - "should not call refetch on a cache-only Observable if the store is reset", - (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } + it("should call refetch on a mocked Observable if the store is reset", async () => { + const query = gql` + query { + author { + firstName + lastName } - `; - - const queryManager = createQueryManager({ - link: mockSingleLink().setOnError(reject), - }); - - const options = { - query, - fetchPolicy: "cache-only", - } as WatchQueryOptions; - - let refetchCount = 0; + } + `; + const data = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; + const queryManager = mockQueryManager({ + request: { query }, + result: { data }, + }); + const obs = queryManager.watchQuery({ query }); + obs.subscribe({}); + obs.refetch = jest.fn(); - const obs = queryManager.watchQuery(options); - obs.subscribe({}); - obs.refetch = () => { - ++refetchCount; - return null as never; - }; + void resetStore(queryManager); - resetStore(queryManager); + await wait(0); - setTimeout(() => { - expect(refetchCount).toEqual(0); - resolve(); - }, 50); - } - ); + expect(obs.refetch).toHaveBeenCalledTimes(1); + }); - itAsync( - "should not call refetch on a standby Observable if the store is reset", - (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } + it("should not call refetch on a cache-only Observable if the store is reset", async () => { + const query = gql` + query { + author { + firstName + lastName } - `; + } + `; - const queryManager = createQueryManager({ - link: mockSingleLink().setOnError(reject), - }); + const queryManager = createQueryManager({ + link: mockSingleLink(), + }); - const options = { - query, - fetchPolicy: "standby", - } as WatchQueryOptions; + const options = { + query, + fetchPolicy: "cache-only", + } as WatchQueryOptions; - let refetchCount = 0; + let refetchCount = 0; - const obs = queryManager.watchQuery(options); - obs.subscribe({}); - obs.refetch = () => { - ++refetchCount; - return null as never; - }; + const obs = queryManager.watchQuery(options); + obs.subscribe({}); + obs.refetch = () => { + ++refetchCount; + return null as never; + }; - resetStore(queryManager); + void resetStore(queryManager); - setTimeout(() => { - expect(refetchCount).toEqual(0); - resolve(); - }, 50); - } - ); + await wait(50); - itAsync( - "should not call refetch on a non-subscribed Observable if the store is reset", - (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } + expect(refetchCount).toEqual(0); + }); + + it("should not call refetch on a standby Observable if the store is reset", async () => { + const query = gql` + query { + author { + firstName + lastName } - `; + } + `; - const queryManager = createQueryManager({ - link: mockSingleLink().setOnError(reject), - }); + const queryManager = createQueryManager({ + link: mockSingleLink(), + }); - const options = { - query, - } as WatchQueryOptions; + const options = { + query, + fetchPolicy: "standby", + } as WatchQueryOptions; - let refetchCount = 0; + let refetchCount = 0; - const obs = queryManager.watchQuery(options); - obs.refetch = () => { - ++refetchCount; - return null as never; - }; + const obs = queryManager.watchQuery(options); + obs.subscribe({}); + obs.refetch = () => { + ++refetchCount; + return null as never; + }; - resetStore(queryManager); + void resetStore(queryManager); - setTimeout(() => { - expect(refetchCount).toEqual(0); - resolve(); - }, 50); - } - ); + await wait(50); - itAsync( - "should throw an error on an inflight query() if the store is reset", - (resolve, reject) => { - let queryManager: QueryManager; - const query = gql` - query { - author { - firstName - lastName - } + expect(refetchCount).toEqual(0); + }); + + it("should not call refetch on a non-subscribed Observable if the store is reset", async () => { + const query = gql` + query { + author { + firstName + lastName } - `; + } + `; - const data = { - author: { - firstName: "John", - lastName: "Smith", - }, - }; - const link = new ApolloLink( - () => - new Observable((observer) => { - // reset the store as soon as we hear about the query - resetStore(queryManager); - observer.next({ data }); - return; - }) - ); + const queryManager = createQueryManager({ + link: mockSingleLink(), + }); - queryManager = createQueryManager({ link }); - queryManager - .query({ query }) - .then(() => { - reject(new Error("query() gave results on a store reset")); - }) - .catch(() => { - resolve(); - }); - } - ); - }); - describe("refetching observed queries", () => { - itAsync( - "returns a promise resolving when all queries have been refetched", - (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } - } - `; + const options = { + query, + } as WatchQueryOptions; - const data = { - author: { - firstName: "John", - lastName: "Smith", - }, - }; + let refetchCount = 0; - const dataChanged = { - author: { - firstName: "John changed", - lastName: "Smith", - }, - }; + const obs = queryManager.watchQuery(options); + obs.refetch = () => { + ++refetchCount; + return null as never; + }; - const query2 = gql` - query { - author2 { - firstName - lastName - } - } - `; + void resetStore(queryManager); - const data2 = { - author2: { - firstName: "John", - lastName: "Smith", - }, - }; + await wait(50); - const data2Changed = { - author2: { - firstName: "John changed", - lastName: "Smith", - }, - }; + expect(refetchCount).toEqual(0); + }); - const queryManager = createQueryManager({ - link: mockSingleLink( - { - request: { query }, - result: { data }, - }, - { - request: { query: query2 }, - result: { data: data2 }, - }, - { - request: { query }, - result: { data: dataChanged }, - }, - { - request: { query: query2 }, - result: { data: data2Changed }, - } - ).setOnError(reject), - }); - - const observable = queryManager.watchQuery({ query }); - const observable2 = queryManager.watchQuery({ query: query2 }); - - return Promise.all([ - observableToPromise({ observable }, (result) => - expect(result.data).toEqual(data) - ), - observableToPromise({ observable: observable2 }, (result) => - expect(result.data).toEqual(data2) - ), - ]) - .then(() => { - observable.subscribe({ next: () => null }); - observable2.subscribe({ next: () => null }); - - return queryManager.reFetchObservableQueries().then(() => { - const result = getCurrentQueryResult(observable); - expect(result.partial).toBe(false); - expect(result.data).toEqual(dataChanged); - - const result2 = getCurrentQueryResult(observable2); - expect(result2.partial).toBe(false); - expect(result2.data).toEqual(data2Changed); - }); - }) - .then(resolve, reject); - } - ); - - itAsync( - "should only refetch once when we refetch observable queries", - (resolve, reject) => { - let queryManager: QueryManager; - const query = gql` - query { - author { - firstName - lastName - } - } - `; - const data = { - author: { - firstName: "John", - lastName: "Smith", - }, - }; - - const data2 = { - author: { - firstName: "Johnny", - lastName: "Smith", - }, - }; - - let timesFired = 0; - const link: ApolloLink = new ApolloLink( - (op) => - new Observable((observer) => { - timesFired += 1; - if (timesFired > 1) { - observer.next({ data: data2 }); - } else { - observer.next({ data }); - } - observer.complete(); - return; - }) - ); - queryManager = createQueryManager({ link }); - const observable = queryManager.watchQuery({ query }); - - // wait just to make sure the observable doesn't fire again - return observableToPromise( - { observable, wait: 0 }, - (result) => { - expect(result.data).toEqual(data); - expect(timesFired).toBe(1); - // refetch the observed queries after data has returned - queryManager.reFetchObservableQueries(); - }, - (result) => { - // only refetch once and make sure data has changed - expect(result.data).toEqual(data2); - expect(timesFired).toBe(2); - resolve(); - } - ).catch((e) => { - reject(e); - }); - } - ); - - itAsync("should not refetch torn-down queries", (resolve) => { + it("should throw an error on an inflight query() if the store is reset", async () => { let queryManager: QueryManager; - let observable: ObservableQuery; const query = gql` query { author { @@ -4215,582 +3618,1101 @@ describe("QueryManager", () => { } } `; + const data = { author: { firstName: "John", lastName: "Smith", }, }; - - let timesFired = 0; - const link: ApolloLink = ApolloLink.from([ + const link = new ApolloLink( () => new Observable((observer) => { - timesFired += 1; + // reset the store as soon as we hear about the query + void resetStore(queryManager); observer.next({ data }); return; - }), - ]); + }) + ); queryManager = createQueryManager({ link }); - observable = queryManager.watchQuery({ query }); - observableToPromise({ observable, wait: 0 }, (result) => - expect(result.data).toEqual(data) - ).then(() => { - expect(timesFired).toBe(1); + await expect(queryManager.query({ query })).rejects.toBeTruthy(); + }); + }); - // at this point the observable query has been torn down - // because observableToPromise unsubscribe before resolving - queryManager.reFetchObservableQueries(); + describe("refetching observed queries", () => { + it("returns a promise resolving when all queries have been refetched", async () => { + const query = gql` + query { + author { + firstName + lastName + } + } + `; - setTimeout(() => { - expect(timesFired).toBe(1); + const data = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; - resolve(); - }, 50); - }); - }); + const dataChanged = { + author: { + firstName: "John changed", + lastName: "Smith", + }, + }; - itAsync( - "should not error after reFetchObservableQueries", - (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } + const query2 = gql` + query { + author2 { + firstName + lastName } - `; - const data = { - author: { - firstName: "John", - lastName: "Smith", - }, - }; - - let timesFired = 0; - const link = ApolloLink.from([ - () => - new Observable((observer) => { - timesFired += 1; - observer.next({ data }); - observer.complete(); - }), - ]); + } + `; - const queryManager = createQueryManager({ link }); + const data2 = { + author2: { + firstName: "John", + lastName: "Smith", + }, + }; - const observable = queryManager.watchQuery({ - query, - notifyOnNetworkStatusChange: false, - }); + const data2Changed = { + author2: { + firstName: "John changed", + lastName: "Smith", + }, + }; - // wait to make sure store reset happened - return observableToPromise( - { observable, wait: 20 }, - (result) => { - expect(result.data).toEqual(data); - expect(timesFired).toBe(1); - queryManager.reFetchObservableQueries(); + const queryManager = createQueryManager({ + link: mockSingleLink( + { + request: { query }, + result: { data }, }, - (result) => { - expect(result.data).toEqual(data); - expect(timesFired).toBe(2); - } - ).then(resolve, reject); - } - ); - - itAsync( - "should NOT throw an error on an inflight fetch query if the observable queries are refetched", - (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } - } - `; - const data = { - author: { - firstName: "John", - lastName: "Smith", + { + request: { query: query2 }, + result: { data: data2 }, }, - }; - const queryManager = mockQueryManager({ - request: { query }, - result: { data }, - delay: 100, - }); - queryManager - .fetchQuery("made up id", { query }) - .then(resolve) - .catch((error) => { - reject(new Error("Should not return an error")); - }); - queryManager.reFetchObservableQueries(); - } - ); - - itAsync( - "should call refetch on a mocked Observable if the observed queries are refetched", - (resolve) => { - const query = gql` - query { - author { - firstName - lastName - } - } - `; - const data = { - author: { - firstName: "John", - lastName: "Smith", + { + request: { query }, + result: { data: dataChanged }, }, - }; - const queryManager = mockQueryManager({ - request: { query }, - result: { data }, - }); - - const obs = queryManager.watchQuery({ query }); - obs.subscribe({}); - obs.refetch = resolve as any; - - queryManager.reFetchObservableQueries(); - } - ); - - itAsync( - "should not call refetch on a cache-only Observable if the observed queries are refetched", - (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } + { + request: { query: query2 }, + result: { data: data2Changed }, } - `; + ), + }); - const queryManager = createQueryManager({ - link: mockSingleLink().setOnError(reject), - }); + const observable = queryManager.watchQuery({ query }); + const observable2 = queryManager.watchQuery({ query: query2 }); - const options = { - query, - fetchPolicy: "cache-only", - } as WatchQueryOptions; + const stream = new ObservableStream(observable); + const stream2 = new ObservableStream(observable2); - let refetchCount = 0; + await expect(stream).toEmitMatchedValue({ data }); + await expect(stream2).toEmitMatchedValue({ data: data2 }); - const obs = queryManager.watchQuery(options); - obs.subscribe({}); - obs.refetch = () => { - ++refetchCount; - return null as never; - }; + await queryManager.reFetchObservableQueries(); - queryManager.reFetchObservableQueries(); + const result = getCurrentQueryResult(observable); + expect(result.partial).toBe(false); + expect(result.data).toEqual(dataChanged); - setTimeout(() => { - expect(refetchCount).toEqual(0); - resolve(); - }, 50); - } - ); + const result2 = getCurrentQueryResult(observable2); + expect(result2.partial).toBe(false); + expect(result2.data).toEqual(data2Changed); + }); - itAsync( - "should not call refetch on a standby Observable if the observed queries are refetched", - (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } + it("should only refetch once when we refetch observable queries", async () => { + const query = gql` + query { + author { + firstName + lastName } - `; - - const queryManager = createQueryManager({ - link: mockSingleLink().setOnError(reject), - }); + } + `; + const data = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; - const options = { - query, - fetchPolicy: "standby", - } as WatchQueryOptions; + const data2 = { + author: { + firstName: "Johnny", + lastName: "Smith", + }, + }; - let refetchCount = 0; + let timesFired = 0; + const link: ApolloLink = new ApolloLink( + (op) => + new Observable((observer) => { + timesFired += 1; + if (timesFired > 1) { + observer.next({ data: data2 }); + } else { + observer.next({ data }); + } + observer.complete(); + return; + }) + ); + const queryManager = createQueryManager({ link }); + const observable = queryManager.watchQuery({ query }); + const stream = new ObservableStream(observable); - const obs = queryManager.watchQuery(options); - obs.subscribe({}); - obs.refetch = () => { - ++refetchCount; - return null as never; - }; + await expect(stream).toEmitMatchedValue({ data }); + expect(timesFired).toBe(1); - queryManager.reFetchObservableQueries(); + // refetch the observed queries after data has returned + void queryManager.reFetchObservableQueries(); - setTimeout(() => { - expect(refetchCount).toEqual(0); - resolve(); - }, 50); - } - ); + await expect(stream).toEmitMatchedValue({ data: data2 }); + expect(timesFired).toBe(2); + }); - itAsync( - "should refetch on a standby Observable if the observed queries are refetched and the includeStandby parameter is set to true", - (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } + it("should not refetch torn-down queries", async () => { + const query = gql` + query { + author { + firstName + lastName } - `; - - const queryManager = createQueryManager({ - link: mockSingleLink().setOnError(reject), - }); + } + `; + const data = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; - const options = { - query, - fetchPolicy: "standby", - } as WatchQueryOptions; + let timesFired = 0; + const link: ApolloLink = ApolloLink.from([ + () => + new Observable((observer) => { + timesFired += 1; + observer.next({ data }); + return; + }), + ]); - let refetchCount = 0; + const queryManager = createQueryManager({ link }); + const observable = queryManager.watchQuery({ query }); + const stream = new ObservableStream(observable); - const obs = queryManager.watchQuery(options); - obs.subscribe({}); - obs.refetch = () => { - ++refetchCount; - return null as never; - }; + await expect(stream).toEmitMatchedValue({ data }); + expect(timesFired).toBe(1); - const includeStandBy = true; - queryManager.reFetchObservableQueries(includeStandBy); + stream.unsubscribe(); + void queryManager.reFetchObservableQueries(); - setTimeout(() => { - expect(refetchCount).toEqual(1); - resolve(); - }, 50); - } - ); + await wait(50); - itAsync( - "should not call refetch on a non-subscribed Observable", - (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } - } - `; + expect(timesFired).toBe(1); + }); - const queryManager = createQueryManager({ - link: mockSingleLink().setOnError(reject), - }); + it("should not error after reFetchObservableQueries", async () => { + const query = gql` + query { + author { + firstName + lastName + } + } + `; + const data = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; - const options = { - query, - } as WatchQueryOptions; + let timesFired = 0; + const link = ApolloLink.from([ + () => + new Observable((observer) => { + timesFired += 1; + observer.next({ data }); + observer.complete(); + }), + ]); - let refetchCount = 0; + const queryManager = createQueryManager({ link }); - const obs = queryManager.watchQuery(options); - obs.refetch = () => { - ++refetchCount; - return null as never; - }; + const observable = queryManager.watchQuery({ + query, + notifyOnNetworkStatusChange: false, + }); + const stream = new ObservableStream(observable); - queryManager.reFetchObservableQueries(); + await expect(stream).toEmitMatchedValue({ data }); + expect(timesFired).toBe(1); - setTimeout(() => { - expect(refetchCount).toEqual(0); - resolve(); - }, 50); - } - ); + void queryManager.reFetchObservableQueries(); - itAsync( - "should NOT throw an error on an inflight query() if the observed queries are refetched", - (resolve, reject) => { - let queryManager: QueryManager; - const query = gql` - query { - author { - firstName - lastName - } - } - `; + await expect(stream).toEmitMatchedValue({ data }); + expect(timesFired).toBe(2); - const data = { - author: { - firstName: "John", - lastName: "Smith", - }, - }; - const link = new ApolloLink( - () => - new Observable((observer) => { - // refetch observed queries as soon as we hear about the query - queryManager.reFetchObservableQueries(); - observer.next({ data }); - observer.complete(); - }) - ); + await expect(stream).not.toEmitAnything(); + }); - queryManager = createQueryManager({ link }); - queryManager - .query({ query }) - .then(() => { - resolve(); - }) - .catch((e) => { - reject( - new Error( - "query() should not throw error when refetching observed queriest" - ) - ); - }); - } - ); - }); + it("should NOT throw an error on an inflight fetch query if the observable queries are refetched", async () => { + const query = gql` + query { + author { + firstName + lastName + } + } + `; + const data = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; + const queryManager = mockQueryManager({ + request: { query }, + result: { data }, + delay: 100, + }); + const promise = queryManager.fetchQuery("made up id", { query }); + void queryManager.reFetchObservableQueries(); - describe("refetching specified queries", () => { - itAsync( - "returns a promise resolving when all queries have been refetched", - (resolve, reject) => { - const query = gql` - query GetAuthor { - author { - firstName - lastName - } + await expect(promise).resolves.toBeTruthy(); + }); + + it("should call refetch on a mocked Observable if the observed queries are refetched", async () => { + const query = gql` + query { + author { + firstName + lastName } - `; + } + `; + const data = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; + const queryManager = mockQueryManager({ + request: { query }, + result: { data }, + }); - const data = { - author: { - firstName: "John", - lastName: "Smith", - }, - }; + const obs = queryManager.watchQuery({ query }); + obs.subscribe({}); + obs.refetch = jest.fn(); - const dataChanged = { - author: { - firstName: "John changed", - lastName: "Smith", - }, - }; + void queryManager.reFetchObservableQueries(); - const query2 = gql` - query GetAuthor2 { - author2 { - firstName - lastName - } + await wait(0); + + expect(obs.refetch).toHaveBeenCalledTimes(1); + }); + + it("should not call refetch on a cache-only Observable if the observed queries are refetched", async () => { + const query = gql` + query { + author { + firstName + lastName } - `; + } + `; - const data2 = { - author2: { - firstName: "John", - lastName: "Smith", - }, - }; + const queryManager = createQueryManager({ + link: mockSingleLink(), + }); - const data2Changed = { - author2: { - firstName: "John changed", - lastName: "Smith", - }, - }; + const options = { + query, + fetchPolicy: "cache-only", + } as WatchQueryOptions; - const queryManager = createQueryManager({ - link: mockSingleLink( - { - request: { query }, - result: { data }, - }, - { - request: { query: query2 }, - result: { data: data2 }, - }, - { - request: { query }, - result: { data: dataChanged }, - }, - { - request: { query: query2 }, - result: { data: data2Changed }, - } - ).setOnError(reject), - }); + let refetchCount = 0; - const observable = queryManager.watchQuery({ query }); - const observable2 = queryManager.watchQuery({ query: query2 }); + const obs = queryManager.watchQuery(options); + obs.subscribe({}); + obs.refetch = () => { + ++refetchCount; + return null as never; + }; - return Promise.all([ - observableToPromise({ observable }, (result) => - expect(result.data).toEqual(data) - ), - observableToPromise({ observable: observable2 }, (result) => - expect(result.data).toEqual(data2) - ), - ]) - .then(() => { - observable.subscribe({ next: () => null }); - observable2.subscribe({ next: () => null }); - - const results: any[] = []; - queryManager - .refetchQueries({ - include: ["GetAuthor", "GetAuthor2"], - }) - .forEach((result) => results.push(result)); + void queryManager.reFetchObservableQueries(); - return Promise.all(results).then(() => { - const result = getCurrentQueryResult(observable); - expect(result.partial).toBe(false); - expect(result.data).toEqual(dataChanged); + await wait(50); - const result2 = getCurrentQueryResult(observable2); - expect(result2.partial).toBe(false); - expect(result2.data).toEqual(data2Changed); - }); - }) - .then(resolve, reject); - } - ); - }); + expect(refetchCount).toEqual(0); + }); - describe("loading state", () => { - itAsync( - "should be passed as false if we are not watching a query", - (resolve, reject) => { - const query = gql` - query { - fortuneCookie + it("should not call refetch on a standby Observable if the observed queries are refetched", async () => { + const query = gql` + query { + author { + firstName + lastName } - `; - const data = { - fortuneCookie: "Buy it", - }; - return mockQueryManager({ - request: { query }, - result: { data }, - }) - .query({ query }) - .then((result) => { - expect(!result.loading).toBeTruthy(); - expect(result.data).toEqual(data); - }) - .then(resolve, reject); - } - ); + } + `; - itAsync( - "should be passed to the observer as true if we are returning partial data", - (resolve, reject) => { - const fortuneCookie = - "You must stick to your goal but rethink your approach"; - const primeQuery = gql` - query { - fortuneCookie + const queryManager = createQueryManager({ + link: mockSingleLink(), + }); + + const options = { + query, + fetchPolicy: "standby", + } as WatchQueryOptions; + + let refetchCount = 0; + + const obs = queryManager.watchQuery(options); + obs.subscribe({}); + obs.refetch = () => { + ++refetchCount; + return null as never; + }; + + void queryManager.reFetchObservableQueries(); + + await wait(50); + + expect(refetchCount).toEqual(0); + }); + + it("should refetch on a standby Observable if the observed queries are refetched and the includeStandby parameter is set to true", async () => { + const query = gql` + query { + author { + firstName + lastName } - `; - const primeData = { fortuneCookie }; + } + `; - const author = { name: "John" }; - const query = gql` - query { - fortuneCookie - author { - name - } + const queryManager = createQueryManager({ + link: mockSingleLink(), + }); + + const options = { + query, + fetchPolicy: "standby", + } as WatchQueryOptions; + + let refetchCount = 0; + + const obs = queryManager.watchQuery(options); + obs.subscribe({}); + obs.refetch = () => { + ++refetchCount; + return null as never; + }; + + const includeStandBy = true; + void queryManager.reFetchObservableQueries(includeStandBy); + + await wait(50); + + expect(refetchCount).toEqual(1); + }); + + it("should not call refetch on a non-subscribed Observable", async () => { + const query = gql` + query { + author { + firstName + lastName } - `; - const fullData = { fortuneCookie, author }; + } + `; - const queryManager = mockQueryManager( - { - request: { query }, - result: { data: fullData }, - delay: 5, - }, - { - request: { query: primeQuery }, - result: { data: primeData }, + const queryManager = createQueryManager({ + link: mockSingleLink(), + }); + + const options = { + query, + } as WatchQueryOptions; + + let refetchCount = 0; + + const obs = queryManager.watchQuery(options); + obs.refetch = () => { + ++refetchCount; + return null as never; + }; + + void queryManager.reFetchObservableQueries(); + + await wait(50); + + expect(refetchCount).toEqual(0); + }); + + it("should NOT throw an error on an inflight query() if the observed queries are refetched", async () => { + let queryManager: QueryManager; + const query = gql` + query { + author { + firstName + lastName } - ); - - return queryManager - .query({ query: primeQuery }) - .then((primeResult) => { - const observable = queryManager.watchQuery({ - query, - returnPartialData: true, - }); - - return observableToPromise( - { observable }, - (result) => { - expect(result.loading).toBe(true); - expect(result.data).toEqual(primeData); - }, - (result) => { - expect(result.loading).toBe(false); - expect(result.data).toEqual(fullData); - } - ); + } + `; + + const data = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; + const link = new ApolloLink( + () => + new Observable((observer) => { + // refetch observed queries as soon as we hear about the query + void queryManager.reFetchObservableQueries(); + observer.next({ data }); + observer.complete(); }) - .then(resolve, reject); - } - ); + ); + + queryManager = createQueryManager({ link }); + + await expect(queryManager.query({ query })).resolves.toBeTruthy(); + }); + }); + + describe("refetching specified queries", () => { + it("returns a promise resolving when all queries have been refetched", async () => { + const query = gql` + query GetAuthor { + author { + firstName + lastName + } + } + `; + + const data = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; + + const dataChanged = { + author: { + firstName: "John changed", + lastName: "Smith", + }, + }; + + const query2 = gql` + query GetAuthor2 { + author2 { + firstName + lastName + } + } + `; + + const data2 = { + author2: { + firstName: "John", + lastName: "Smith", + }, + }; + + const data2Changed = { + author2: { + firstName: "John changed", + lastName: "Smith", + }, + }; + + const queryManager = createQueryManager({ + link: mockSingleLink( + { + request: { query }, + result: { data }, + }, + { + request: { query: query2 }, + result: { data: data2 }, + }, + { + request: { query }, + result: { data: dataChanged }, + }, + { + request: { query: query2 }, + result: { data: data2Changed }, + } + ), + }); + + const observable = queryManager.watchQuery({ query }); + const observable2 = queryManager.watchQuery({ query: query2 }); + + const stream = new ObservableStream(observable); + const stream2 = new ObservableStream(observable2); + + await expect(stream).toEmitMatchedValue({ data }); + await expect(stream2).toEmitMatchedValue({ data: data2 }); + + const results: any[] = []; + queryManager + .refetchQueries({ + include: ["GetAuthor", "GetAuthor2"], + }) + .forEach((result) => results.push(result)); + + await Promise.all(results); + + const result = getCurrentQueryResult(observable); + expect(result.partial).toBe(false); + expect(result.data).toEqual(dataChanged); + + const result2 = getCurrentQueryResult(observable2); + expect(result2.partial).toBe(false); + expect(result2.data).toEqual(data2Changed); + }); + }); + + describe("loading state", () => { + it("should be passed as false if we are not watching a query", async () => { + const query = gql` + query { + fortuneCookie + } + `; + const data = { + fortuneCookie: "Buy it", + }; + const result = await mockQueryManager({ + request: { query }, + result: { data }, + }).query({ query }); + + expect(result.loading).toBe(false); + expect(result.data).toEqual(data); + }); + + it("should be passed to the observer as true if we are returning partial data", async () => { + const fortuneCookie = + "You must stick to your goal but rethink your approach"; + const primeQuery = gql` + query { + fortuneCookie + } + `; + const primeData = { fortuneCookie }; + + const author = { name: "John" }; + const query = gql` + query { + fortuneCookie + author { + name + } + } + `; + const fullData = { fortuneCookie, author }; + + const queryManager = mockQueryManager( + { + request: { query }, + result: { data: fullData }, + delay: 5, + }, + { + request: { query: primeQuery }, + result: { data: primeData }, + } + ); + + await queryManager.query({ query: primeQuery }); + + const observable = queryManager.watchQuery({ + query, + returnPartialData: true, + }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue({ + data: primeData, + loading: true, + networkStatus: NetworkStatus.loading, + partial: true, + }); + await expect(stream).toEmitValue({ + data: fullData, + loading: false, + networkStatus: NetworkStatus.ready, + }); + }); + + it("should be passed to the observer as false if we are returning all the data", async () => { + const stream = getObservableStream({ + query: gql` + query { + author { + firstName + lastName + } + } + `, + result: { + data: { + author: { + firstName: "John", + lastName: "Smith", + }, + }, + }, + }); + + await expect(stream).toEmitValue({ + data: { author: { firstName: "John", lastName: "Smith" } }, + loading: false, + networkStatus: NetworkStatus.ready, + }); + }); + + it("will update on `resetStore`", async () => { + const testQuery = gql` + query { + author { + firstName + lastName + } + } + `; + const data1 = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; + const data2 = { + author: { + firstName: "John", + lastName: "Smith 2", + }, + }; + const queryManager = mockQueryManager( + { + request: { query: testQuery }, + result: { data: data1 }, + }, + { + request: { query: testQuery }, + result: { data: data2 }, + } + ); + + const stream = new ObservableStream( + queryManager.watchQuery({ + query: testQuery, + notifyOnNetworkStatusChange: false, + }) + ); + + await expect(stream).toEmitValue({ + data: data1, + loading: false, + networkStatus: NetworkStatus.ready, + }); + + await wait(0); + void resetStore(queryManager); + + await expect(stream).toEmitValue({ + data: data2, + loading: false, + networkStatus: NetworkStatus.ready, + }); + + await expect(stream).not.toEmitAnything(); + }); + + it("will be true when partial data may be returned", async () => { + const query1 = gql` + { + a { + x1 + y1 + z1 + } + } + `; + const query2 = gql` + { + a { + x1 + y1 + z1 + } + b { + x2 + y2 + z2 + } + } + `; + const data1 = { + a: { x1: 1, y1: 2, z1: 3 }, + }; + const data2 = { + a: { x1: 1, y1: 2, z1: 3 }, + b: { x2: 3, y2: 2, z2: 1 }, + }; + const queryManager = mockQueryManager( + { + request: { query: query1 }, + result: { data: data1 }, + }, + { + request: { query: query2 }, + result: { data: data2 }, + delay: 5, + } + ); + + const result1 = await queryManager.query({ query: query1 }); + expect(result1.loading).toBe(false); + expect(result1.data).toEqual(data1); + + const observable = queryManager.watchQuery({ + query: query2, + returnPartialData: true, + }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue({ + data: data1, + loading: true, + networkStatus: NetworkStatus.loading, + partial: true, + }); + await expect(stream).toEmitValue({ + data: data2, + loading: false, + networkStatus: NetworkStatus.ready, + }); + + await expect(stream).not.toEmitAnything(); + }); + }); + + describe("refetchQueries", () => { + let consoleWarnSpy: jest.SpyInstance; + beforeEach(() => { + consoleWarnSpy = jest.spyOn(console, "warn").mockImplementation(); + }); + afterEach(() => { + consoleWarnSpy.mockRestore(); + }); + + it("should refetch the right query when a result is successfully returned", async () => { + const mutation = gql` + mutation changeAuthorName { + changeAuthorName(newName: "Jack Smith") { + 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 queryManager = mockQueryManager( + { + request: { query, variables }, + result: { data }, + }, + { + request: { query, variables }, + result: { data: secondReqData }, + }, + { + request: { query: mutation }, + result: { data: mutationData }, + } + ); + const observable = queryManager.watchQuery({ + query, + variables, + notifyOnNetworkStatusChange: false, + }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitMatchedValue({ data }); + + void queryManager.mutate({ mutation, refetchQueries: ["getAuthors"] }); + + await expect(stream).toEmitMatchedValue({ data: secondReqData }); + expect(observable.getCurrentResult().data).toEqual(secondReqData); + }); + + it("should not warn and continue when an unknown query name is asked to refetch", async () => { + 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, + notifyOnNetworkStatusChange: false, + }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitMatchedValue({ data }); + + void queryManager.mutate({ + mutation, + refetchQueries: ["fakeQuery", "getAuthors"], + }); + + await expect(stream).toEmitMatchedValue({ data: secondReqData }); + expect(consoleWarnSpy).toHaveBeenLastCalledWith( + 'Unknown query named "%s" requested in refetchQueries options.include array', + "fakeQuery" + ); + }); + + it("should ignore (with warning) a query named in refetchQueries that has no active subscriptions", async () => { + 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 }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitMatchedValue({ data }); + + stream.unsubscribe(); + await queryManager.mutate({ + mutation, + refetchQueries: ["getAuthors"], + }); + + expect(consoleWarnSpy).toHaveBeenLastCalledWith( + 'Unknown query named "%s" requested in refetchQueries options.include array', + "getAuthors" + ); + }); + + it("should ignore (with warning) a document node in refetchQueries that has no active subscriptions", async () => { + 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 }, + } + ); - itAsync( - "should be passed to the observer as false if we are returning all the data", - (resolve, reject) => { - assertWithObserver({ - reject, - query: gql` - query { - author { - firstName - lastName - } - } - `, - result: { - data: { - author: { - firstName: "John", - lastName: "Smith", - }, - }, - }, - observer: { - next(result) { - expect(!result.loading).toBeTruthy(); - resolve(); - }, - }, - }); - } - ); + const observable = queryManager.watchQuery({ query }); + const stream = new ObservableStream(observable); - itAsync("will update on `resetStore`", (resolve, reject) => { - const testQuery = gql` + await expect(stream).toEmitMatchedValue({ data }); + stream.unsubscribe(); + + // The subscription has been stopped already + await queryManager.mutate({ + mutation, + refetchQueries: [query], + }); + + expect(consoleWarnSpy).toHaveBeenLastCalledWith( + 'Unknown query named "%s" requested in refetchQueries options.include array', + "getAuthors" + ); + }); + + it("should ignore (with warning) a document node containing an anonymous query in refetchQueries that has no active subscriptions", async () => { + const mutation = gql` + mutation changeAuthorName { + changeAuthorName(newName: "Jack Smith") { + firstName + lastName + } + } + `; + const mutationData = { + changeAuthorName: { + firstName: "Jack", + lastName: "Smith", + }, + }; + const query = gql` query { author { firstName @@ -4798,510 +4720,201 @@ describe("QueryManager", () => { } } `; - const data1 = { + const data = { author: { firstName: "John", lastName: "Smith", }, }; - const data2 = { + const secondReqData = { author: { - firstName: "John", - lastName: "Smith 2", + firstName: "Jane", + lastName: "Johnson", }, }; const queryManager = mockQueryManager( { - request: { query: testQuery }, - result: { data: data1 }, + request: { query }, + result: { data }, }, { - request: { query: testQuery }, - result: { data: data2 }, + request: { query }, + result: { data: secondReqData }, + }, + { + request: { query: mutation }, + result: { data: mutationData }, } ); - let count = 0; - queryManager - .watchQuery({ - query: testQuery, - notifyOnNetworkStatusChange: false, - }) - .subscribe({ - next: (result) => { - switch (count++) { - case 0: - expect(result.loading).toBe(false); - expect(result.data).toEqual(data1); - setTimeout(() => { - resetStore(queryManager); - }, 0); - break; - case 1: - expect(result.loading).toBe(false); - expect(result.data).toEqual(data2); - resolve(); - break; - default: - reject(new Error("`next` was called to many times.")); - } - }, - error: (error) => reject(error), - }); - }); + const observable = queryManager.watchQuery({ query }); + const stream = new ObservableStream(observable); - itAsync( - "will be true when partial data may be returned", - (resolve, reject) => { - const query1 = gql` - { - a { - x1 - y1 - z1 - } - } - `; - const query2 = gql` - { - a { - x1 - y1 - z1 - } - b { - x2 - y2 - z2 - } - } - `; - const data1 = { - a: { x1: 1, y1: 2, z1: 3 }, - }; - const data2 = { - a: { x1: 1, y1: 2, z1: 3 }, - b: { x2: 3, y2: 2, z2: 1 }, - }; - const queryManager = mockQueryManager( - { - request: { query: query1 }, - result: { data: data1 }, - }, - { - request: { query: query2 }, - result: { data: data2 }, - delay: 5, - } - ); - - queryManager - .query({ query: query1 }) - .then((result1) => { - expect(result1.loading).toBe(false); - expect(result1.data).toEqual(data1); - - let count = 0; - queryManager - .watchQuery({ query: query2, returnPartialData: true }) - .subscribe({ - next: (result2) => { - switch (count++) { - case 0: - expect(result2.loading).toBe(true); - expect(result2.data).toEqual(data1); - break; - case 1: - expect(result2.loading).toBe(false); - expect(result2.data).toEqual(data2); - resolve(); - break; - default: - reject(new Error("`next` was called to many times.")); - } - }, - error: reject, - }); - }) - .then(resolve, reject); - } - ); - }); + await expect(stream).toEmitMatchedValue({ data }); + stream.unsubscribe(); - describe("refetchQueries", () => { - let consoleWarnSpy: jest.SpyInstance; - beforeEach(() => { - consoleWarnSpy = jest.spyOn(console, "warn").mockImplementation(); - }); - afterEach(() => { - consoleWarnSpy.mockRestore(); + // The subscription has been stopped already + await queryManager.mutate({ + mutation, + refetchQueries: [query], + }); + + expect(consoleWarnSpy).toHaveBeenLastCalledWith( + "Unknown anonymous query requested in refetchQueries options.include array" + ); }); - itAsync( - "should refetch the right query when a result is successfully returned", - (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($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 queryManager = mockQueryManager( - { - request: { query, variables }, - result: { data }, - }, - { - request: { query, variables }, - result: { data: secondReqData }, - }, - { - request: { query: mutation }, - result: { data: mutationData }, + it("also works with a query document and variables", async () => { + const mutation = gql` + mutation changeAuthorName($id: ID!) { + changeAuthorName(newName: "Jack Smith", id: $id) { + firstName + lastName } - ); - const observable = queryManager.watchQuery({ - query, - variables, - notifyOnNetworkStatusChange: false, - }); - return observableToPromise( - { observable }, - (result) => { - expect(result.data).toEqual(data); - queryManager.mutate({ mutation, refetchQueries: ["getAuthors"] }); - }, - (result) => { - expect(observable.getCurrentResult().data).toEqual(secondReqData); - expect(result.data).toEqual(secondReqData); + } + `; + const mutationData = { + changeAuthorName: { + firstName: "Jack", + lastName: "Smith", + }, + }; + const query = gql` + query getAuthors($id: ID!) { + author(id: $id) { + firstName + lastName } - ).then(resolve, reject); - } - ); + } + `; + const data = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; + const secondReqData = { + author: { + firstName: "Jane", + lastName: "Johnson", + }, + }; - itAsync( - "should not warn and continue when an unknown query name is asked to refetch", - (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, - notifyOnNetworkStatusChange: false, - }); - return observableToPromise( - { observable }, - (result) => { - expect(result.data).toEqual(data); - queryManager.mutate({ - mutation, - refetchQueries: ["fakeQuery", "getAuthors"], - }); - }, - (result) => { - expect(result.data).toEqual(secondReqData); - expect(consoleWarnSpy).toHaveBeenLastCalledWith( - 'Unknown query named "%s" requested in refetchQueries options.include array', - "fakeQuery" - ); - } - ).then(resolve, reject); - } - ); + 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); - itAsync( - "should ignore (with warning) a query named 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 }, - } - ); + await expect(stream).toEmitMatchedValue({ data }); + + await queryManager.mutate({ + mutation, + variables: mutationVariables, + refetchQueries: [{ query, variables }], + }); - 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: ["getAuthors"], - }); - }) - .then(() => { - expect(consoleWarnSpy).toHaveBeenLastCalledWith( - 'Unknown query named "%s" requested in refetchQueries options.include array', - "getAuthors" - ); - }) - .then(resolve, reject); - } - ); + await expect(stream).toEmitMatchedValue( + { data: secondReqData }, + { timeout: 150 } + ); + expect(observable.getCurrentResult().data).toEqual(secondReqData); - 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 - } + await expect(stream).not.toEmitAnything(); + }); + + 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 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 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 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); - } - ); + 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); - 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 }, - } - ); + await expect(stream).toEmitMatchedValue({ data }); - 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); - } - ); + await queryManager.mutate({ + mutation, + variables: mutationVariables, + refetchQueries: [query], + }); - it("also works with a query document and variables", async () => { + 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) { @@ -5364,22 +4977,151 @@ describe("QueryManager", () => { await queryManager.mutate({ mutation, variables: mutationVariables, - refetchQueries: [{ query, variables }], + // 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); + expect(observable.getCurrentResult().data).toEqual(secondReqData); + + await expect(stream).not.toEmitAnything(); + }); + + it("also works with a conditional function that returns false", async () => { + 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 }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitMatchedValue({ data }); + + const conditional = jest.fn(() => []); + await queryManager.mutate({ mutation, refetchQueries: conditional }); + + expect(conditional).toHaveBeenCalledTimes(1); + expect(conditional).toHaveBeenCalledWith( + expect.objectContaining({ data: mutationData }) + ); + }); + + it("also works with a conditional function that returns an array of refetches", async () => { + 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 }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitMatchedValue({ data }); + + const conditional = jest.fn(() => [{ query }]); + await queryManager.mutate({ mutation, refetchQueries: conditional }); + + expect(conditional).toHaveBeenCalledTimes(1); + expect(conditional).toHaveBeenCalledWith( + expect.objectContaining({ data: mutationData }) + ); - await expect(stream).not.toEmitAnything(); + await expect(stream).toEmitMatchedValue({ data: secondReqData }); }); - it("also works with a query document node", async () => { + it("should refetch using the original query context (if any)", async () => { const mutation = gql` - mutation changeAuthorName($id: ID!) { - changeAuthorName(newName: "Jack Smith", id: $id) { + mutation changeAuthorName { + changeAuthorName(newName: "Jack Smith") { firstName lastName } @@ -5411,50 +5153,55 @@ describe("QueryManager", () => { 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 }, + request: { query: mutation }, result: { data: mutationData }, - delay: 10, } ); - const observable = queryManager.watchQuery({ query, variables }); + + const headers = { + someHeader: "some value", + }; + const observable = queryManager.watchQuery({ + query, + variables, + context: { + headers, + }, + notifyOnNetworkStatusChange: false, + }); const stream = new ObservableStream(observable); - await expect(stream).toEmitMatchedValue({ data }); + await expect(stream).toEmitNext(); - await queryManager.mutate({ + void queryManager.mutate({ mutation, - variables: mutationVariables, - refetchQueries: [query], + refetchQueries: ["getAuthors"], }); - await expect(stream).toEmitMatchedValue( - { data: secondReqData }, - { timeout: 150 } - ); - expect(observable.getCurrentResult().data).toEqual(secondReqData); + await expect(stream).toEmitNext(); - await expect(stream).not.toEmitAnything(); + const context = ( + queryManager.link as MockApolloLink + ).operation!.getContext(); + expect(context.headers).not.toBeUndefined(); + expect(context.headers.someHeader).toEqual(headers.someHeader); }); - it("also works with different references of a same query document node", async () => { + it("should refetch using the specified context, if provided", async () => { const mutation = gql` - mutation changeAuthorName($id: ID!) { - changeAuthorName(newName: "Jack Smith", id: $id) { + mutation changeAuthorName { + changeAuthorName(newName: "Jack Smith") { firstName lastName } @@ -5486,881 +5233,714 @@ describe("QueryManager", () => { 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 }, + request: { query: mutation }, result: { data: mutationData }, - delay: 10, } ); - const observable = queryManager.watchQuery({ query, variables }); + + const observable = queryManager.watchQuery({ + query, + variables, + notifyOnNetworkStatusChange: false, + }); + const stream = new ObservableStream(observable); + + const headers = { + someHeader: "some value", + }; + + await expect(stream).toEmitNext(); + + void queryManager.mutate({ + mutation, + refetchQueries: [ + { + query, + variables, + context: { + headers, + }, + }, + ], + }); + + await expect(stream).toEmitNext(); + + const context = ( + queryManager.link as MockApolloLink + ).operation!.getContext(); + expect(context.headers).not.toBeUndefined(); + expect(context.headers.someHeader).toEqual(headers.someHeader); + }); + }); + + describe("onQueryUpdated", () => { + const mutation = gql` + mutation changeAuthorName { + changeAuthorName(newName: "Jack Smith") { + 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" }; + + function makeQueryManager() { + return mockQueryManager( + { + request: { query, variables }, + result: { data }, + }, + { + request: { query, variables }, + result: { data: secondReqData }, + }, + { + request: { query: mutation }, + result: { data: mutationData }, + } + ); + } + + it("should refetch the right query when a result is successfully returned", async () => { + const queryManager = makeQueryManager(); + + const observable = queryManager.watchQuery({ + query, + variables, + notifyOnNetworkStatusChange: false, + }); const stream = new ObservableStream(observable); + let finishedRefetch = false; + await expect(stream).toEmitMatchedValue({ data }); await queryManager.mutate({ mutation, - variables: mutationVariables, - // spread the query into a new object to simulate multiple instances - refetchQueries: [{ ...query }], + + update(cache) { + cache.modify({ + fields: { + author(_, { INVALIDATE }) { + return INVALIDATE; + }, + }, + }); + }, + + async onQueryUpdated(obsQuery) { + expect(obsQuery.options.query).toBe(query); + const result = await obsQuery.refetch(); + + // Wait a bit to make sure the mutation really awaited the + // refetching of the query. + await wait(100); + finishedRefetch = true; + return result; + }, }); - await expect(stream).toEmitMatchedValue( - { data: secondReqData }, - { timeout: 150 } - ); + expect(finishedRefetch).toBe(true); + await expect(stream).toEmitMatchedValue({ data: secondReqData }); expect(observable.getCurrentResult().data).toEqual(secondReqData); - - await expect(stream).not.toEmitAnything(); }); - itAsync( - "also works with a conditional function that returns false", - (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 }); - const conditional = (result: FetchResult) => { - expect(result.data).toEqual(mutationData); - return []; - }; + it("should refetch using the original query context (if any)", async () => { + const queryManager = makeQueryManager(); - return observableToPromise({ observable }, (result) => { - expect(result.data).toEqual(data); - queryManager.mutate({ mutation, refetchQueries: conditional }); - }).then(resolve, reject); - } - ); + const headers = { + someHeader: "some value", + }; - itAsync( - "also works with a conditional function that returns an array of refetches", - (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 }); - const conditional = (result: FetchResult) => { - expect(result.data).toEqual(mutationData); - return [{ query }]; - }; + const observable = queryManager.watchQuery({ + query, + variables, + context: { + headers, + }, + notifyOnNetworkStatusChange: false, + }); + const stream = new ObservableStream(observable); - return observableToPromise( - { observable }, - (result) => { - expect(result.data).toEqual(data); - queryManager.mutate({ mutation, refetchQueries: conditional }); - }, - (result) => expect(result.data).toEqual(secondReqData) - ).then(resolve, reject); - } - ); + await expect(stream).toEmitMatchedValue({ data }); - itAsync( - "should refetch using the original query context (if any)", - (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($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 queryManager = mockQueryManager( - { - request: { query, variables }, - result: { data }, - }, - { - request: { query, variables }, - result: { data: secondReqData }, - }, - { - request: { query: mutation }, - result: { data: mutationData }, - } - ); + void queryManager.mutate({ + mutation, + + update(cache) { + cache.modify({ + fields: { + author(_, { INVALIDATE }) { + return INVALIDATE; + }, + }, + }); + }, + + onQueryUpdated(obsQuery) { + expect(obsQuery.options.query).toBe(query); + return obsQuery.refetch(); + }, + }); + + await expect(stream).toEmitMatchedValue({ data: secondReqData }); + + const context = ( + queryManager.link as MockApolloLink + ).operation!.getContext(); + expect(context.headers).not.toBeUndefined(); + expect(context.headers.someHeader).toEqual(headers.someHeader); + }); + + it("should refetch using the specified context, if provided", async () => { + const queryManager = makeQueryManager(); + + const observable = queryManager.watchQuery({ + query, + variables, + notifyOnNetworkStatusChange: false, + }); + const stream = new ObservableStream(observable); + + const headers = { + someHeader: "some value", + }; - const headers = { - someHeader: "some value", - }; - const observable = queryManager.watchQuery({ - query, - variables, - context: { - headers, - }, - notifyOnNetworkStatusChange: false, - }); + await expect(stream).toEmitMatchedValue({ data }); - return observableToPromise( - { observable }, - (result) => { - queryManager.mutate({ - mutation, - refetchQueries: ["getAuthors"], - }); - }, - (result) => { - const context = ( - queryManager.link as MockApolloLink - ).operation!.getContext(); - expect(context.headers).not.toBeUndefined(); - expect(context.headers.someHeader).toEqual(headers.someHeader); - } - ).then(resolve, reject); - } - ); + void queryManager.mutate({ + mutation, - itAsync( - "should refetch using the specified context, if provided", - (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($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 queryManager = mockQueryManager( - { - request: { query, variables }, - result: { data }, - }, - { - request: { query, variables }, - result: { data: secondReqData }, - }, - { - request: { query: mutation }, - result: { data: mutationData }, - } - ); + update(cache) { + cache.evict({ fieldName: "author" }); + }, - const observable = queryManager.watchQuery({ - query, - variables, - notifyOnNetworkStatusChange: false, - }); + onQueryUpdated(obsQuery) { + expect(obsQuery.options.query).toBe(query); + return obsQuery.reobserve({ + fetchPolicy: "network-only", + context: { + ...obsQuery.options.context, + headers, + }, + }); + }, + }); - const headers = { - someHeader: "some value", - }; + await expect(stream).toEmitMatchedValue({ data: secondReqData }); - return observableToPromise( - { observable }, - (result) => { - queryManager.mutate({ - mutation, - refetchQueries: [ - { - query, - variables, - context: { - headers, - }, - }, - ], - }); - }, - (result) => { - const context = ( - queryManager.link as MockApolloLink - ).operation!.getContext(); - expect(context.headers).not.toBeUndefined(); - expect(context.headers.someHeader).toEqual(headers.someHeader); - } - ).then(resolve, reject); - } - ); + const context = ( + queryManager.link as MockApolloLink + ).operation!.getContext(); + expect(context.headers).not.toBeUndefined(); + expect(context.headers.someHeader).toEqual(headers.someHeader); + }); }); - describe("onQueryUpdated", () => { - const mutation = gql` - mutation changeAuthorName { - changeAuthorName(newName: "Jack Smith") { - firstName - lastName + describe("awaitRefetchQueries", () => { + it("should not wait for `refetchQueries` to complete before resolving the mutation, when `awaitRefetchQueries` is undefined", async () => { + const query = gql` + query getAuthors($id: ID!) { + author(id: $id) { + firstName + lastName + } } - } - `; + `; - const mutationData = { - changeAuthorName: { - firstName: "Jack", - lastName: "Smith", - }, - }; + const queryData = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; - const query = gql` - query getAuthors($id: ID!) { - author(id: $id) { - firstName - lastName + const mutation = gql` + mutation changeAuthorName { + changeAuthorName(newName: "Jack Smith") { + firstName + lastName + } } - } - `; + `; - const data = { - author: { - firstName: "John", - lastName: "Smith", - }, - }; + const mutationData = { + changeAuthorName: { + firstName: "Jack", + lastName: "Smith", + }, + }; - const secondReqData = { - author: { - firstName: "Jane", - lastName: "Johnson", - }, - }; + const secondReqData = { + author: { + firstName: "Jane", + lastName: "Johnson", + }, + }; - const variables = { id: "1234" }; + const variables = { id: "1234" }; - function makeQueryManager() { - return mockQueryManager( + const queryManager = mockQueryManager( { request: { query, variables }, - result: { data }, + result: { data: queryData }, + }, + { + request: { query: mutation }, + result: { data: mutationData }, }, { request: { query, variables }, result: { data: secondReqData }, + } + ); + + const observable = queryManager.watchQuery({ + query, + variables, + notifyOnNetworkStatusChange: false, + }); + const stream = new ObservableStream(observable); + let mutationComplete = false; + + await expect(stream).toEmitMatchedValue({ data: queryData }); + + void queryManager + .mutate({ + mutation, + refetchQueries: ["getAuthors"], + awaitRefetchQueries: false, + }) + .then(() => { + mutationComplete = true; + }); + + await expect(stream).toEmitMatchedValue({ data: secondReqData }); + expect(observable.getCurrentResult().data).toEqual(secondReqData); + expect(mutationComplete).toBe(true); + }); + + it("should not wait for `refetchQueries` to complete before resolving the mutation, when `awaitRefetchQueries` is false", async () => { + const query = gql` + query getAuthors($id: ID!) { + author(id: $id) { + firstName + lastName + } + } + `; + + const queryData = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; + + const mutation = gql` + mutation changeAuthorName { + changeAuthorName(newName: "Jack Smith") { + firstName + lastName + } + } + `; + + const mutationData = { + changeAuthorName: { + firstName: "Jack", + lastName: "Smith", + }, + }; + + const secondReqData = { + author: { + firstName: "Jane", + lastName: "Johnson", + }, + }; + + const variables = { id: "1234" }; + + const queryManager = mockQueryManager( + { + request: { query, variables }, + result: { data: queryData }, }, { request: { query: mutation }, result: { data: mutationData }, + }, + { + request: { query, variables }, + result: { data: secondReqData }, } ); - } - itAsync( - "should refetch the right query when a result is successfully returned", - (resolve, reject) => { - const queryManager = makeQueryManager(); + const observable = queryManager.watchQuery({ + query, + variables, + notifyOnNetworkStatusChange: false, + }); + const stream = new ObservableStream(observable); + let mutationComplete = false; + + await expect(stream).toEmitMatchedValue({ data: queryData }); - const observable = queryManager.watchQuery({ - query, - variables, - notifyOnNetworkStatusChange: false, + void queryManager + .mutate({ mutation, refetchQueries: ["getAuthors"] }) + .then(() => { + mutationComplete = true; }); - let finishedRefetch = false; - - return observableToPromise( - { observable }, - (result) => { - expect(result.data).toEqual(data); - - return queryManager - .mutate({ - mutation, - - update(cache) { - cache.modify({ - fields: { - author(_, { INVALIDATE }) { - return INVALIDATE; - }, - }, - }); - }, - - onQueryUpdated(obsQuery) { - expect(obsQuery.options.query).toBe(query); - return obsQuery.refetch().then(async (result) => { - // Wait a bit to make sure the mutation really awaited the - // refetching of the query. - await new Promise((resolve) => setTimeout(resolve, 100)); - finishedRefetch = true; - return result; - }); - }, - }) - .then(() => { - expect(finishedRefetch).toBe(true); - }); - }, + await expect(stream).toEmitMatchedValue({ data: secondReqData }); + expect(observable.getCurrentResult().data).toEqual(secondReqData); + expect(mutationComplete).toBe(true); + }); - (result) => { - expect(observable.getCurrentResult().data).toEqual(secondReqData); - expect(result.data).toEqual(secondReqData); - expect(finishedRefetch).toBe(true); + it("should wait for `refetchQueries` to complete before resolving the mutation, when `awaitRefetchQueries` is `true`", async () => { + const query = gql` + query getAuthors($id: ID!) { + author(id: $id) { + firstName + lastName } - ).then(resolve, reject); - } - ); + } + `; - itAsync( - "should refetch using the original query context (if any)", - (resolve, reject) => { - const queryManager = makeQueryManager(); + const queryData = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; - const headers = { - someHeader: "some value", - }; + const mutation = gql` + mutation changeAuthorName { + changeAuthorName(newName: "Jack Smith") { + firstName + lastName + } + } + `; - const observable = queryManager.watchQuery({ - query, - variables, - context: { - headers, - }, - notifyOnNetworkStatusChange: false, - }); + const mutationData = { + changeAuthorName: { + firstName: "Jack", + lastName: "Smith", + }, + }; - return observableToPromise( - { observable }, - (result) => { - expect(result.data).toEqual(data); - - queryManager.mutate({ - mutation, - - update(cache) { - cache.modify({ - fields: { - author(_, { INVALIDATE }) { - return INVALIDATE; - }, - }, - }); - }, + const secondReqData = { + author: { + firstName: "Jane", + lastName: "Johnson", + }, + }; - onQueryUpdated(obsQuery) { - expect(obsQuery.options.query).toBe(query); - return obsQuery.refetch(); - }, - }); - }, + const variables = { id: "1234" }; + + const queryManager = mockQueryManager( + { + request: { query, variables }, + result: { data: queryData }, + }, + { + request: { query: mutation }, + result: { data: mutationData }, + }, + { + request: { query, variables }, + result: { data: secondReqData }, + } + ); - (result) => { - expect(result.data).toEqual(secondReqData); - const context = ( - queryManager.link as MockApolloLink - ).operation!.getContext(); - expect(context.headers).not.toBeUndefined(); - expect(context.headers.someHeader).toEqual(headers.someHeader); - } - ).then(resolve, reject); - } - ); + const observable = queryManager.watchQuery({ + query, + variables, + notifyOnNetworkStatusChange: false, + }); + const stream = new ObservableStream(observable); + let mutationComplete = false; - itAsync( - "should refetch using the specified context, if provided", - (resolve, reject) => { - const queryManager = makeQueryManager(); + await expect(stream).toEmitMatchedValue({ data: queryData }); - const observable = queryManager.watchQuery({ - query, - variables, - notifyOnNetworkStatusChange: false, + void queryManager + .mutate({ + mutation, + refetchQueries: ["getAuthors"], + awaitRefetchQueries: true, + }) + .then(() => { + mutationComplete = true; }); - const headers = { - someHeader: "some value", - }; - - return observableToPromise( - { observable }, - (result) => { - expect(result.data).toEqual(data); - - queryManager.mutate({ - mutation, - - update(cache) { - cache.evict({ fieldName: "author" }); - }, - - onQueryUpdated(obsQuery) { - expect(obsQuery.options.query).toBe(query); - return obsQuery.reobserve({ - fetchPolicy: "network-only", - context: { - ...obsQuery.options.context, - headers, - }, - }); - }, - }); - }, - - (result) => { - expect(result.data).toEqual(secondReqData); - const context = ( - queryManager.link as MockApolloLink - ).operation!.getContext(); - expect(context.headers).not.toBeUndefined(); - expect(context.headers.someHeader).toEqual(headers.someHeader); - } - ).then(resolve, reject); - } - ); - }); + await expect(stream).toEmitMatchedValue({ data: secondReqData }); + expect(observable.getCurrentResult().data).toEqual(secondReqData); + expect(mutationComplete).toBe(false); + }); - describe("awaitRefetchQueries", () => { - const awaitRefetchTest = ({ - awaitRefetchQueries, - testQueryError = false, - }: MutationBaseOptions & { testQueryError?: boolean }) => - new Promise((resolve, reject) => { - const query = gql` - query getAuthors($id: ID!) { - author(id: $id) { - firstName - lastName - } + it("should allow catching errors from `refetchQueries` when `awaitRefetchQueries` is `true`", async () => { + const query = gql` + query getAuthors($id: ID!) { + author(id: $id) { + firstName + lastName } - `; + } + `; - const queryData = { - author: { - firstName: "John", - lastName: "Smith", - }, - }; + const queryData = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; - const mutation = gql` - mutation changeAuthorName { - changeAuthorName(newName: "Jack Smith") { - firstName - lastName - } + const mutation = gql` + mutation changeAuthorName { + changeAuthorName(newName: "Jack Smith") { + firstName + lastName } - `; + } + `; - const mutationData = { - changeAuthorName: { - firstName: "Jack", - lastName: "Smith", - }, - }; + const mutationData = { + changeAuthorName: { + firstName: "Jack", + lastName: "Smith", + }, + }; - const secondReqData = { - author: { - firstName: "Jane", - lastName: "Johnson", - }, - }; + const secondReqData = { + author: { + firstName: "Jane", + lastName: "Johnson", + }, + }; - const variables = { id: "1234" }; + const variables = { id: "1234" }; + const refetchError = new Error("Refetch failed"); - const refetchError = - testQueryError ? new Error("Refetch failed") : undefined; + const queryManager = mockQueryManager( + { + request: { query, variables }, + result: { data: queryData }, + }, + { + request: { query: mutation }, + result: { data: mutationData }, + }, + { + request: { query, variables }, + result: { data: secondReqData }, + error: refetchError, + } + ); - const queryManager = mockQueryManager( - { - request: { query, variables }, - result: { data: queryData }, - }, - { - request: { query: mutation }, - result: { data: mutationData }, - }, - { - request: { query, variables }, - result: { data: secondReqData }, - error: refetchError, - } - ); + const observable = queryManager.watchQuery({ + query, + variables, + notifyOnNetworkStatusChange: false, + }); + const stream = new ObservableStream(observable); + let isRefetchErrorCaught = false; - const observable = queryManager.watchQuery({ - query, - variables, - notifyOnNetworkStatusChange: false, + await expect(stream).toEmitMatchedValue({ data: queryData }); + + void queryManager + .mutate({ + mutation, + refetchQueries: ["getAuthors"], + awaitRefetchQueries: true, + }) + .catch((error) => { + expect(error).toBeDefined(); + isRefetchErrorCaught = true; }); - let isRefetchErrorCaught = false; - let mutationComplete = false; - return observableToPromise( - { observable }, - (result) => { - expect(result.data).toEqual(queryData); - const mutateOptions: MutationOptions = { - mutation, - refetchQueries: ["getAuthors"], - }; - if (awaitRefetchQueries) { - mutateOptions.awaitRefetchQueries = awaitRefetchQueries; - } - queryManager - .mutate(mutateOptions) - .then(() => { - mutationComplete = true; - }) - .catch((error) => { - expect(error).toBeDefined(); - isRefetchErrorCaught = true; - }); - }, - (result) => { - if (awaitRefetchQueries) { - expect(mutationComplete).not.toBeTruthy(); - } else { - expect(mutationComplete).toBeTruthy(); - } - expect(observable.getCurrentResult().data).toEqual(secondReqData); - expect(result.data).toEqual(secondReqData); - } - ) - .then(() => resolve()) - .catch((error) => { - const isRefetchError = - awaitRefetchQueries && - testQueryError && - error.message.includes(refetchError?.message); - - if (isRefetchError) { - return setTimeout(() => { - expect(isRefetchErrorCaught).toBe(true); - resolve(); - }, 10); - } + await expect(stream).toEmitError( + new ApolloError({ networkError: refetchError }) + ); + expect(isRefetchErrorCaught).toBe(true); + }); + }); - reject(error); - }); - }); + describe("store watchers", () => { + it("does not fill up the store on resolved queries", async () => { + const query1 = gql` + query One { + one + } + `; + const query2 = gql` + query Two { + two + } + `; + const query3 = gql` + query Three { + three + } + `; + const query4 = gql` + query Four { + four + } + `; - it( - "should not wait for `refetchQueries` to complete before resolving " + - "the mutation, when `awaitRefetchQueries` is undefined", - () => awaitRefetchTest({ awaitRefetchQueries: void 0 }) - ); + const link = mockSingleLink( + { request: { query: query1 }, result: { data: { one: 1 } } }, + { request: { query: query2 }, result: { data: { two: 2 } } }, + { request: { query: query3 }, result: { data: { three: 3 } } }, + { request: { query: query4 }, result: { data: { four: 4 } } } + ); + const cache = new InMemoryCache(); - it( - "should not wait for `refetchQueries` to complete before resolving " + - "the mutation, when `awaitRefetchQueries` is false", - () => awaitRefetchTest({ awaitRefetchQueries: false }) - ); + const queryManager = new QueryManager( + getDefaultOptionsForQueryManagerTests({ + link, + cache, + }) + ); - it( - "should wait for `refetchQueries` to complete before resolving " + - "the mutation, when `awaitRefetchQueries` is `true`", - () => awaitRefetchTest({ awaitRefetchQueries: true }) - ); + await queryManager.query({ query: query1 }); + await queryManager.query({ query: query2 }); + await queryManager.query({ query: query3 }); + await queryManager.query({ query: query4 }); + await wait(10); - it( - "should allow catching errors from `refetchQueries` when " + - "`awaitRefetchQueries` is `true`", - () => - awaitRefetchTest({ awaitRefetchQueries: true, testQueryError: true }) - ); + expect(cache["watches"].size).toBe(0); + }); }); - describe("store watchers", () => { - itAsync( - "does not fill up the store on resolved queries", - (resolve, reject) => { - const query1 = gql` - query One { - one - } - `; - const query2 = gql` - query Two { - two - } - `; - const query3 = gql` - query Three { - three - } - `; - const query4 = gql` - query Four { - four + describe("`no-cache` handling", () => { + it("should return a query result (if one exists) when a `no-cache` fetch policy is used", async () => { + const query = gql` + query { + author { + firstName + lastName } - `; - - const link = mockSingleLink( - { request: { query: query1 }, result: { data: { one: 1 } } }, - { request: { query: query2 }, result: { data: { two: 2 } } }, - { request: { query: query3 }, result: { data: { three: 3 } } }, - { request: { query: query4 }, result: { data: { four: 4 } } } - ).setOnError(reject); - const cache = new InMemoryCache(); - - const queryManager = new QueryManager( - getDefaultOptionsForQueryManagerTests({ - link, - cache, - }) - ); + } + `; - return queryManager - .query({ query: query1 }) - .then((one) => { - return queryManager.query({ query: query2 }); - }) - .then(() => { - return queryManager.query({ query: query3 }); - }) - .then(() => { - return queryManager.query({ query: query4 }); - }) - .then(() => { - return new Promise((r) => { - setTimeout(r, 10); - }); - }) - .then(() => { - // @ts-ignore - expect(cache.watches.size).toBe(0); - }) - .then(resolve, reject); - } - ); - }); + const data = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; - describe("`no-cache` handling", () => { - itAsync( - "should return a query result (if one exists) when a `no-cache` fetch policy is used", - (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } - } - `; + const queryManager = createQueryManager({ + link: mockSingleLink({ + request: { query }, + result: { data }, + }), + }); - const data = { - author: { - firstName: "John", - lastName: "Smith", - }, - }; + const observable = queryManager.watchQuery({ + query, + fetchPolicy: "no-cache", + }); + const stream = new ObservableStream(observable); - const queryManager = createQueryManager({ - link: mockSingleLink({ - request: { query }, - result: { data }, - }).setOnError(reject), - }); + await expect(stream).toEmitMatchedValue({ data }); - const observable = queryManager.watchQuery({ - query, - fetchPolicy: "no-cache", - }); - observableToPromise({ observable }, (result) => { - expect(result.data).toEqual(data); - const currentResult = getCurrentQueryResult(observable); - expect(currentResult.data).toEqual(data); - resolve(); - }); - } - ); + const currentResult = getCurrentQueryResult(observable); + expect(currentResult.data).toEqual(data); + }); }); describe("client awareness", () => { - itAsync( - "should pass client awareness settings into the link chain via context", - (resolve, reject) => { - const query = gql` - query { - author { - firstName - lastName - } + it("should pass client awareness settings into the link chain via context", async () => { + const query = gql` + query { + author { + firstName + lastName } - `; + } + `; - const data = { - author: { - firstName: "John", - lastName: "Smith", - }, - }; + const data = { + author: { + firstName: "John", + lastName: "Smith", + }, + }; - const link = mockSingleLink({ - request: { query }, - result: { data }, - }).setOnError(reject); + const link = mockSingleLink({ + request: { query }, + result: { data }, + }); - const clientAwareness = { - name: "Test", - version: "1.0.0", - }; + const clientAwareness = { + name: "Test", + version: "1.0.0", + }; - const queryManager = createQueryManager({ - link, - clientAwareness, - }); + const queryManager = createQueryManager({ + link, + clientAwareness, + }); - const observable = queryManager.watchQuery({ - query, - fetchPolicy: "no-cache", - }); + const observable = queryManager.watchQuery({ + query, + fetchPolicy: "no-cache", + }); + const stream = new ObservableStream(observable); - observableToPromise({ observable }, (result) => { - const context = link.operation!.getContext(); - expect(context.clientAwareness).toBeDefined(); - expect(context.clientAwareness).toEqual(clientAwareness); - resolve(); - }); - } - ); + await expect(stream).toEmitNext(); + + const context = link.operation!.getContext(); + expect(context.clientAwareness).toBeDefined(); + expect(context.clientAwareness).toEqual(clientAwareness); + }); }); describe("queryDeduplication", () => { @@ -6383,7 +5963,7 @@ describe("QueryManager", () => { }), }); - queryManager.query({ query, context: { queryDeduplication: true } }); + void queryManager.query({ query, context: { queryDeduplication: true } }); expect( queryManager["inFlightLinkObservables"].peek(print(query), "{}") @@ -6434,11 +6014,9 @@ describe("QueryManager", () => { spy.mockRestore(); }); - function validateWarnings( - resolve: (result?: any) => void, - reject: (reason?: any) => void, - returnPartialData = false, - expectedWarnCount = 1 + async function validateWarnings( + returnPartialData: boolean, + expectedWarnCount: number ) { const query1 = gql` query { @@ -6484,38 +6062,34 @@ describe("QueryManager", () => { returnPartialData, }); - return observableToPromise({ observable: observable1 }, (result) => { - expect(result).toEqual({ - loading: false, - data: data1, - networkStatus: NetworkStatus.ready, - }); - }).then(() => { - observableToPromise({ observable: observable2 }, (result) => { - expect(result).toEqual({ - data: data1, - loading: false, - networkStatus: NetworkStatus.ready, - partial: true, - }); - expect(spy).toHaveBeenCalledTimes(expectedWarnCount); - }).then(resolve, reject); + const stream1 = new ObservableStream(observable1); + + await expect(stream1).toEmitValue({ + data: data1, + loading: false, + networkStatus: NetworkStatus.ready, + }); + + stream1.unsubscribe(); + + const stream2 = new ObservableStream(observable2); + + await expect(stream2).toEmitMatchedValue({ + data: data1, + loading: false, + networkStatus: NetworkStatus.ready, + partial: true, }); + expect(spy).toHaveBeenCalledTimes(expectedWarnCount); } - itAsync( - "should show missing cache result fields warning when returnPartialData is false", - (resolve, reject) => { - validateWarnings(resolve, reject, false, 1); - } - ); + it("should show missing cache result fields warning when returnPartialData is false", async () => { + await validateWarnings(false, 1); + }); - itAsync( - "should not show missing cache result fields warning when returnPartialData is true", - (resolve, reject) => { - validateWarnings(resolve, reject, true, 0); - } - ); + it("should not show missing cache result fields warning when returnPartialData is true", async () => { + await validateWarnings(true, 0); + }); }); describe("defaultContext", () => { diff --git a/src/core/__tests__/QueryManager/multiple-results.ts b/src/core/__tests__/QueryManager/multiple-results.ts index 1d49bbb770b..a8458d0ff13 100644 --- a/src/core/__tests__/QueryManager/multiple-results.ts +++ b/src/core/__tests__/QueryManager/multiple-results.ts @@ -3,15 +3,17 @@ import gql from "graphql-tag"; import { InMemoryCache } from "../../../cache/inmemory/inMemoryCache"; // mocks -import { itAsync, MockSubscriptionLink } from "../../../testing/core"; +import { MockSubscriptionLink, wait } from "../../../testing/core"; // core import { QueryManager } from "../../QueryManager"; import { GraphQLError } from "graphql"; import { getDefaultOptionsForQueryManagerTests } from "../../../testing/core/mocking/mockQueryManager"; +import { ObservableStream } from "../../../testing/internal"; +import { ApolloError } from "../../../errors"; describe("mutiple results", () => { - itAsync("allows multiple query results from link", (resolve, reject) => { + it("allows multiple query results from link", async () => { const query = gql` query LazyLoadLuke { people_one(id: 1) { @@ -49,102 +51,162 @@ describe("mutiple results", () => { query, variables: {}, }); + const stream = new ObservableStream(observable); - let count = 0; - observable.subscribe({ - next: (result) => { - count++; - if (count === 1) { - link.simulateResult({ result: { data: laterData } }); - } - if (count === 2) { - resolve(); + // fire off first result + link.simulateResult({ result: { data: initialData } }); + + await expect(stream).toEmitValue({ + data: initialData, + loading: false, + networkStatus: 7, + }); + + link.simulateResult({ result: { data: laterData } }); + + await expect(stream).toEmitValue({ + data: laterData, + loading: false, + networkStatus: 7, + }); + }); + + it("allows multiple query results from link with ignored errors", async () => { + const query = gql` + query LazyLoadLuke { + people_one(id: 1) { + name + friends @defer { + name + } } + } + `; + + const initialData = { + people_one: { + name: "Luke Skywalker", + friends: null, }, - error: (e) => { - console.error(e); + }; + + const laterData = { + people_one: { + // XXX true defer's wouldn't send this + 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: {}, + errorPolicy: "ignore", }); + const stream = new ObservableStream(observable); // fire off first result link.simulateResult({ result: { data: initialData } }); + + await expect(stream).toEmitValue({ + data: initialData, + loading: false, + networkStatus: 7, + }); + + link.simulateResult({ + result: { errors: [new GraphQLError("defer failed")] }, + }); + + await expect(stream).toEmitValueStrict({ + data: undefined, + loading: false, + networkStatus: 7, + }); + + await wait(20); + link.simulateResult({ result: { data: laterData } }); + + await expect(stream).toEmitValue({ + data: laterData, + loading: false, + networkStatus: 7, + }); }); - itAsync( - "allows multiple query results from link with ignored errors", - (resolve, reject) => { - const query = gql` - query LazyLoadLuke { - people_one(id: 1) { + it("strips errors from a result if ignored", async () => { + const query = gql` + query LazyLoadLuke { + people_one(id: 1) { + name + friends @defer { name - friends @defer { - name - } } } - `; - - const initialData = { - people_one: { - name: "Luke Skywalker", - friends: null, - }, - }; - - const laterData = { - people_one: { - // XXX true defer's wouldn't send this - name: "Luke Skywalker", - friends: [{ name: "Leia Skywalker" }], - }, - }; - const link = new MockSubscriptionLink(); - const queryManager = new QueryManager( - getDefaultOptionsForQueryManagerTests({ - cache: new InMemoryCache({ addTypename: false }), - link, - }) - ); - - const observable = queryManager.watchQuery({ - query, - variables: {}, - errorPolicy: "ignore", - }); - - let count = 0; - observable.subscribe({ - next: (result) => { - // errors should never be passed since they are ignored - expect(result.errors).toBeUndefined(); - count++; - if (count === 1) { - // this shouldn't fire the next event again - link.simulateResult({ - result: { errors: [new GraphQLError("defer failed")] }, - }); - setTimeout(() => { - link.simulateResult({ result: { data: laterData } }); - }, 20); - } - if (count === 2) { - // make sure the count doesn't go up by accident - setTimeout(() => { - if (count === 3) throw new Error("error was not ignored"); - resolve(); - }); - } - }, - error: (e) => { - console.error(e); - }, - }); - - // fire off first result - link.simulateResult({ result: { data: initialData } }); - } - ); - itAsync("strips errors from a result if ignored", (resolve, reject) => { + } + `; + + const initialData = { + people_one: { + name: "Luke Skywalker", + friends: null, + }, + }; + + const laterData = { + people_one: { + // XXX true defer's wouldn't send this + name: "Luke Skywalker", + friends: [{ name: "Leia Skywalker" }], + }, + }; + const link = new MockSubscriptionLink(); + const queryManager = new QueryManager( + getDefaultOptionsForQueryManagerTests({ + cache: new InMemoryCache({ addTypename: false }), + link, + }) + ); + + const observable = queryManager.watchQuery({ + query, + variables: {}, + errorPolicy: "ignore", + }); + const stream = new ObservableStream(observable); + + // fire off first result + link.simulateResult({ result: { data: initialData } }); + + await expect(stream).toEmitValue({ + data: initialData, + loading: false, + networkStatus: 7, + }); + + // this should fire the `next` event without this error + link.simulateResult({ + result: { + errors: [new GraphQLError("defer failed")], + data: laterData, + }, + }); + + await expect(stream).toEmitValueStrict({ + data: laterData, + loading: false, + networkStatus: 7, + }); + }); + + it.skip("allows multiple query results from link with all errors", async () => { const query = gql` query LazyLoadLuke { people_one(id: 1) { @@ -181,185 +243,105 @@ describe("mutiple results", () => { const observable = queryManager.watchQuery({ query, variables: {}, - errorPolicy: "ignore", + errorPolicy: "all", + }); + const stream = new ObservableStream(observable); + + // fire off first result + link.simulateResult({ result: { data: initialData } }); + + await expect(stream).toEmitValue({ + data: initialData, + loading: false, + networkStatus: 7, + }); + + // this should fire the next event again + link.simulateResult({ + error: new Error("defer failed"), + }); + + await expect(stream).toEmitValue({ + data: initialData, + loading: false, + networkStatus: 7, + errors: [new Error("defer failed")], + }); + + link.simulateResult({ result: { data: laterData } }); + + await expect(stream).toEmitValueStrict({ + data: laterData, + loading: false, + networkStatus: 7, + }); + }); + + it("closes the observable if an error is set with the none policy", async () => { + const query = gql` + query LazyLoadLuke { + people_one(id: 1) { + name + friends @defer { + name + } + } + } + `; + + const initialData = { + people_one: { + name: "Luke Skywalker", + friends: null, + }, + }; + + const link = new MockSubscriptionLink(); + const queryManager = new QueryManager( + getDefaultOptionsForQueryManagerTests({ + cache: new InMemoryCache({ addTypename: false }), + link, + }) + ); + + const observable = queryManager.watchQuery({ + query, + variables: {}, + // errorPolicy: 'none', // this is the default }); + const stream = new ObservableStream(observable); let count = 0; observable.subscribe({ next: (result) => { // errors should never be passed since they are ignored - expect(result.errors).toBeUndefined(); count++; - if (count === 1) { - expect(result.data).toEqual(initialData); - // this should fire the `next` event without this error - link.simulateResult({ - result: { - errors: [new GraphQLError("defer failed")], - data: laterData, - }, - }); + expect(result.errors).toBeUndefined(); } if (count === 2) { - expect(result.data).toEqual(laterData); - expect(result.errors).toBeUndefined(); - // make sure the count doesn't go up by accident - setTimeout(() => { - if (count === 3) reject(new Error("error was not ignored")); - resolve(); - }, 10); + console.log(new Error("result came after an error")); } }, error: (e) => { - console.error(e); + expect(e).toBeDefined(); + expect(e.graphQLErrors).toBeDefined(); }, }); // fire off first result link.simulateResult({ result: { data: initialData } }); - }); - itAsync.skip( - "allows multiple query results from link with all errors", - (resolve, reject) => { - const query = gql` - query LazyLoadLuke { - people_one(id: 1) { - name - friends @defer { - name - } - } - } - `; - - const initialData = { - people_one: { - name: "Luke Skywalker", - friends: null, - }, - }; - - const laterData = { - people_one: { - // XXX true defer's wouldn't send this - name: "Luke Skywalker", - friends: [{ name: "Leia Skywalker" }], - }, - }; - const link = new MockSubscriptionLink(); - const queryManager = new QueryManager( - getDefaultOptionsForQueryManagerTests({ - cache: new InMemoryCache({ addTypename: false }), - link, - }) - ); - - const observable = queryManager.watchQuery({ - query, - variables: {}, - errorPolicy: "all", - }); - - let count = 0; - observable.subscribe({ - next: (result) => { - try { - // errors should never be passed since they are ignored - count++; - if (count === 1) { - expect(result.errors).toBeUndefined(); - // this should fire the next event again - link.simulateResult({ - error: new Error("defer failed"), - }); - } - if (count === 2) { - expect(result.errors).toBeDefined(); - link.simulateResult({ result: { data: laterData } }); - } - if (count === 3) { - expect(result.errors).toBeUndefined(); - // make sure the count doesn't go up by accident - setTimeout(() => { - if (count === 4) reject(new Error("error was not ignored")); - resolve(); - }); - } - } catch (e) { - reject(e); - } - }, - error: (e) => { - reject(e); - }, - }); - - // fire off first result - link.simulateResult({ result: { data: initialData } }); - } - ); - itAsync( - "closes the observable if an error is set with the none policy", - (resolve, reject) => { - const query = gql` - query LazyLoadLuke { - people_one(id: 1) { - name - friends @defer { - name - } - } - } - `; - - const initialData = { - people_one: { - name: "Luke Skywalker", - friends: null, - }, - }; - - const link = new MockSubscriptionLink(); - const queryManager = new QueryManager( - getDefaultOptionsForQueryManagerTests({ - cache: new InMemoryCache({ addTypename: false }), - link, - }) - ); - - const observable = queryManager.watchQuery({ - query, - variables: {}, - // errorPolicy: 'none', // this is the default - }); - - let count = 0; - observable.subscribe({ - next: (result) => { - // errors should never be passed since they are ignored - count++; - if (count === 1) { - expect(result.errors).toBeUndefined(); - // this should fire the next event again - link.simulateResult({ - error: new Error("defer failed"), - }); - } - if (count === 2) { - console.log(new Error("result came after an error")); - } - }, - error: (e) => { - expect(e).toBeDefined(); - expect(e.graphQLErrors).toBeDefined(); - resolve(); - }, - }); - - // fire off first result - link.simulateResult({ result: { data: initialData } }); - } - ); + await expect(stream).toEmitValue({ + data: initialData, + loading: false, + networkStatus: 7, + }); + + link.simulateResult({ error: new Error("defer failed") }); + + await expect(stream).toEmitError( + new ApolloError({ networkError: new Error("defer failed") }) + ); + }); }); diff --git a/src/core/__tests__/QueryManager/recycler.ts b/src/core/__tests__/QueryManager/recycler.ts deleted file mode 100644 index fccddc901de..00000000000 --- a/src/core/__tests__/QueryManager/recycler.ts +++ /dev/null @@ -1,111 +0,0 @@ -/* - * This test is used to verify the requirements for how react-apollo - * preserves observables using QueryRecycler. Eventually, QueryRecycler - * will be removed, but this test file should still be valid - */ - -// externals -import gql from "graphql-tag"; - -// core -import { QueryManager } from "../../QueryManager"; -import { ObservableQuery } from "../../ObservableQuery"; -import { ObservableSubscription } from "../../../utilities"; -import { itAsync } from "../../../testing"; -import { InMemoryCache } from "../../../cache"; - -// mocks -import { MockSubscriptionLink } from "../../../testing/core"; -import { getDefaultOptionsForQueryManagerTests } from "../../../testing/core/mocking/mockQueryManager"; - -describe("Subscription lifecycles", () => { - itAsync( - "cleans up and reuses data like QueryRecycler wants", - (resolve, reject) => { - const query = gql` - query Luke { - people_one(id: 1) { - 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, - }) - ); - - // step 1, get some data - const observable = queryManager.watchQuery({ - query, - variables: {}, - fetchPolicy: "cache-and-network", - }); - - const observableQueries: Array<{ - observableQuery: ObservableQuery; - subscription: ObservableSubscription; - }> = []; - - const resubscribe = () => { - const { observableQuery, subscription } = observableQueries.pop()!; - subscription.unsubscribe(); - - observableQuery.setOptions({ - query, - fetchPolicy: "cache-and-network", - }); - - return observableQuery; - }; - - const sub = observable.subscribe({ - next(result: any) { - expect(result.loading).toBe(false); - expect(result.data).toEqual(initialData); - expect(observable.getCurrentResult().data).toEqual(initialData); - - // step 2, recycle it - observable.setOptions({ - fetchPolicy: "standby", - pollInterval: 0, - }); - - observableQueries.push({ - observableQuery: observable, - subscription: observable.subscribe({}), - }); - - // step 3, unsubscribe from observable - sub.unsubscribe(); - - setTimeout(() => { - // step 4, start new Subscription; - const recycled = resubscribe(); - const currentResult = recycled.getCurrentResult(); - expect(currentResult.data).toEqual(initialData); - resolve(); - }, 10); - }, - }); - - setInterval(() => { - // fire off first result - link.simulateResult({ result: { data: initialData } }); - }, 10); - } - ); -}); diff --git a/src/link/core/__tests__/ApolloLink.ts b/src/link/core/__tests__/ApolloLink.ts index 1a97d149c44..506968090dc 100644 --- a/src/link/core/__tests__/ApolloLink.ts +++ b/src/link/core/__tests__/ApolloLink.ts @@ -2,12 +2,12 @@ import gql from "graphql-tag"; import { print } from "graphql"; import { Observable } from "../../../utilities/observables/Observable"; -import { itAsync } from "../../../testing"; import { FetchResult, Operation, NextLink, GraphQLRequest } from "../types"; import { ApolloLink } from "../ApolloLink"; -import { DocumentNode } from "graphql"; +import { ObservableStream } from "../../../testing/internal"; +import { execute } from "../execute"; -export class SetContextLink extends ApolloLink { +class SetContextLink extends ApolloLink { constructor( private setContext: ( context: Record @@ -25,7 +25,7 @@ export class SetContextLink extends ApolloLink { } } -export const sampleQuery = gql` +const sampleQuery = gql` query SampleQuery { stub { id @@ -33,50 +33,11 @@ export const sampleQuery = gql` } `; -function checkCalls(calls: any[] = [], results: Array) { - expect(calls.length).toBe(results.length); - calls.map((call, i) => expect(call.data).toEqual(results[i])); -} - -interface TestResultType { - link: ApolloLink; - results?: any[]; - query?: DocumentNode; - done?: () => void; - context?: any; - variables?: any; -} - -export function testLinkResults(params: TestResultType) { - const { link, context, variables } = params; - const results = params.results || []; - const query = params.query || sampleQuery; - const done = params.done || (() => void 0); - - const spy = jest.fn(); - ApolloLink.execute(link, { query, context, variables }).subscribe({ - next: spy, - error: (error: any) => { - expect(error).toEqual(results.pop()); - checkCalls(spy.mock.calls[0], results); - if (done) { - done(); - } - }, - complete: () => { - checkCalls(spy.mock.calls[0], results); - if (done) { - done(); - } - }, - }); -} - -export const setContext = () => ({ add: 1 }); +const setContext = () => ({ add: 1 }); describe("ApolloClient", () => { describe("context", () => { - itAsync("should merge context when using a function", (resolve, reject) => { + it("should merge context when using a function", async () => { const returnOne = new SetContextLink(setContext); const mock = new ApolloLink((op, forward) => { op.setContext((context: { add: number }) => ({ add: context.add + 2 })); @@ -91,70 +52,68 @@ describe("ApolloClient", () => { }); return Observable.of({ data: op.getContext().add }); }); + const stream = new ObservableStream( + execute(link, { query: sampleQuery }) + ); - testLinkResults({ - link, - results: [3], - done: resolve, - }); + await expect(stream).toEmitValue({ data: 3 }); + await expect(stream).toComplete(); }); - itAsync( - "should merge context when not using a function", - (resolve, reject) => { - const returnOne = new SetContextLink(setContext); - const mock = new ApolloLink((op, forward) => { - op.setContext({ add: 3 }); - op.setContext({ substract: 1 }); + it("should merge context when not using a function", async () => { + const returnOne = new SetContextLink(setContext); + const mock = new ApolloLink((op, forward) => { + op.setContext({ add: 3 }); + op.setContext({ substract: 1 }); - return forward(op); - }); - const link = returnOne.concat(mock).concat((op) => { - expect(op.getContext()).toEqual({ - add: 3, - substract: 1, - }); - return Observable.of({ data: op.getContext().add }); + return forward(op); + }); + const link = returnOne.concat(mock).concat((op) => { + expect(op.getContext()).toEqual({ + add: 3, + substract: 1, }); + return Observable.of({ data: op.getContext().add }); + }); + const stream = new ObservableStream( + execute(link, { query: sampleQuery }) + ); - testLinkResults({ - link, - results: [3], - done: resolve, - }); - } - ); + await expect(stream).toEmitValue({ data: 3 }); + await expect(stream).toComplete(); + }); }); describe("concat", () => { - itAsync("should concat a function", (resolve, reject) => { + it("should concat a function", async () => { const returnOne = new SetContextLink(setContext); const link = returnOne.concat((operation, forward) => { return Observable.of({ data: { count: operation.getContext().add } }); }); + const stream = new ObservableStream( + execute(link, { query: sampleQuery }) + ); - testLinkResults({ - link, - results: [{ count: 1 }], - done: resolve, - }); + await expect(stream).toEmitValue({ data: { count: 1 } }); + await expect(stream).toComplete(); }); - itAsync("should concat a Link", (resolve, reject) => { + it("should concat a Link", async () => { const returnOne = new SetContextLink(setContext); const mock = new ApolloLink((op) => Observable.of({ data: op.getContext().add }) ); const link = returnOne.concat(mock); - testLinkResults({ - link, - results: [1], - done: resolve, - }); + const stream = new ObservableStream( + execute(link, { query: sampleQuery }) + ); + + await expect(stream).toEmitValue({ data: 1 }); + await expect(stream).toComplete(); }); - itAsync("should pass error to observable's error", (resolve, reject) => { + it("should pass error to observable's error", async () => { const error = new Error("thrown"); const returnOne = new SetContextLink(setContext); const mock = new ApolloLink( @@ -166,14 +125,15 @@ describe("ApolloClient", () => { ); const link = returnOne.concat(mock); - testLinkResults({ - link, - results: [1, error], - done: resolve, - }); + const stream = new ObservableStream( + execute(link, { query: sampleQuery }) + ); + + await expect(stream).toEmitValue({ data: 1 }); + await expect(stream).toEmitError(error); }); - itAsync("should concat a Link and function", (resolve, reject) => { + it("should concat a Link and function", async () => { const returnOne = new SetContextLink(setContext); const mock = new ApolloLink((op, forward) => { op.setContext((context: { add: number }) => ({ add: context.add + 2 })); @@ -183,14 +143,15 @@ describe("ApolloClient", () => { return Observable.of({ data: op.getContext().add }); }); - testLinkResults({ - link, - results: [3], - done: resolve, - }); + const stream = new ObservableStream( + execute(link, { query: sampleQuery }) + ); + + await expect(stream).toEmitValue({ data: 3 }); + await expect(stream).toComplete(); }); - itAsync("should concat a function and Link", (resolve, reject) => { + it("should concat a function and Link", async () => { const returnOne = new SetContextLink(setContext); const mock = new ApolloLink((op, forward) => Observable.of({ data: op.getContext().add }) @@ -204,14 +165,16 @@ describe("ApolloClient", () => { return forward(operation); }) .concat(mock); - testLinkResults({ - link, - results: [3], - done: resolve, - }); + + const stream = new ObservableStream( + execute(link, { query: sampleQuery }) + ); + + await expect(stream).toEmitValue({ data: 3 }); + await expect(stream).toComplete(); }); - itAsync("should concat two functions", (resolve, reject) => { + it("should concat two functions", async () => { const returnOne = new SetContextLink(setContext); const link = returnOne .concat((operation, forward) => { @@ -221,14 +184,16 @@ describe("ApolloClient", () => { return forward(operation); }) .concat((op, forward) => Observable.of({ data: op.getContext().add })); - testLinkResults({ - link, - results: [3], - done: resolve, - }); + + const stream = new ObservableStream( + execute(link, { query: sampleQuery }) + ); + + await expect(stream).toEmitValue({ data: 3 }); + await expect(stream).toComplete(); }); - itAsync("should concat two Links", (resolve, reject) => { + it("should concat two Links", async () => { const returnOne = new SetContextLink(setContext); const mock1 = new ApolloLink((operation, forward) => { operation.setContext({ @@ -241,88 +206,93 @@ describe("ApolloClient", () => { ); const link = returnOne.concat(mock1).concat(mock2); - testLinkResults({ - link, - results: [3], - done: resolve, - }); + + const stream = new ObservableStream( + execute(link, { query: sampleQuery }) + ); + + await expect(stream).toEmitValue({ data: 3 }); + await expect(stream).toComplete(); }); - itAsync( - "should return an link that can be concat'd multiple times", - (resolve, reject) => { - const returnOne = new SetContextLink(setContext); - const mock1 = new ApolloLink((operation, forward) => { - operation.setContext({ - add: operation.getContext().add + 2, - }); - return forward(operation); + it("should return an link that can be concat'd multiple times", async () => { + const returnOne = new SetContextLink(setContext); + const mock1 = new ApolloLink((operation, forward) => { + operation.setContext({ + add: operation.getContext().add + 2, }); - const mock2 = new ApolloLink((op, forward) => - Observable.of({ data: op.getContext().add + 2 }) + return forward(operation); + }); + const mock2 = new ApolloLink((op, forward) => + Observable.of({ data: op.getContext().add + 2 }) + ); + const mock3 = new ApolloLink((op, forward) => + Observable.of({ data: op.getContext().add + 3 }) + ); + const link = returnOne.concat(mock1); + + { + const stream = new ObservableStream( + execute(link.concat(mock2), { query: sampleQuery }) ); - const mock3 = new ApolloLink((op, forward) => - Observable.of({ data: op.getContext().add + 3 }) + + await expect(stream).toEmitValue({ data: 5 }); + await expect(stream).toComplete(); + } + + { + const stream = new ObservableStream( + execute(link.concat(mock3), { query: sampleQuery }) ); - const link = returnOne.concat(mock1); - testLinkResults({ - link: link.concat(mock2), - results: [5], - }); - testLinkResults({ - link: link.concat(mock3), - results: [6], - done: resolve, - }); + await expect(stream).toEmitValue({ data: 6 }); + await expect(stream).toComplete(); } - ); + }); }); describe("empty", () => { - itAsync( - "should returns an immediately completed Observable", - (resolve, reject) => { - testLinkResults({ - link: ApolloLink.empty(), - done: resolve, - }); - } - ); + it("should returns an immediately completed Observable", async () => { + const stream = new ObservableStream( + execute(ApolloLink.empty(), { query: sampleQuery }) + ); + + await expect(stream).toComplete(); + }); }); describe("execute", () => { - itAsync( - "transforms an opearation with context into something serlizable", - (resolve, reject) => { - const query = gql` - { - id - } - `; - const link = new ApolloLink((operation) => { - const str = JSON.stringify({ - ...operation, - query: print(operation.query), - }); - - expect(str).toBe( - JSON.stringify({ - variables: { id: 1 }, - extensions: { cache: true }, - query: print(operation.query), - }) - ); - return Observable.of(); + it("transforms an opearation with context into something serlizable", async () => { + const query = gql` + { + id + } + `; + const link = new ApolloLink((operation) => { + const str = JSON.stringify({ + ...operation, + query: print(operation.query), }); - const noop = () => {}; - ApolloLink.execute(link, { + + expect(str).toBe( + JSON.stringify({ + variables: { id: 1 }, + extensions: { cache: true }, + query: print(operation.query), + }) + ); + return Observable.of(); + }); + const stream = new ObservableStream( + execute(link, { query, variables: { id: 1 }, extensions: { cache: true }, - }).subscribe(noop, noop, resolve); - } - ); + }) + ); + + await expect(stream).toComplete(); + }); describe("execute", () => { let _warn: (message?: any, ...originalParams: any[]) => void; @@ -340,92 +310,87 @@ describe("ApolloClient", () => { console.warn = _warn; }); - itAsync( - "should return an empty observable when a link returns null", - (resolve, reject) => { - const link = new ApolloLink(); - link.request = () => null; - testLinkResults({ - link, - results: [], - done: resolve, - }); - } - ); + it("should return an empty observable when a link returns null", async () => { + const link = new ApolloLink(); + link.request = () => null; - itAsync( - "should return an empty observable when a link is empty", - (resolve, reject) => { - testLinkResults({ - link: ApolloLink.empty(), - results: [], - done: resolve, - }); - } - ); + const stream = new ObservableStream( + execute(link, { query: sampleQuery }) + ); - itAsync( - "should return an empty observable when a concat'd link returns null", - (resolve, reject) => { - const link = new ApolloLink((operation, forward) => { - return forward(operation); - }).concat(() => null); - testLinkResults({ - link, - results: [], - done: resolve, - }); - } - ); + await expect(stream).toComplete(); + }); + + it("should return an empty observable when a link is empty", async () => { + const stream = new ObservableStream( + execute(ApolloLink.empty(), { query: sampleQuery }) + ); + + await expect(stream).toComplete(); + }); + + it("should return an empty observable when a concat'd link returns null", async () => { + const link = new ApolloLink((operation, forward) => { + return forward(operation); + }).concat(() => null); + + const stream = new ObservableStream( + execute(link, { query: sampleQuery }) + ); + + await expect(stream).toComplete(); + }); - itAsync( - "should return an empty observable when a split link returns null", - (resolve, reject) => { - let context = { test: true }; - const link = new SetContextLink(() => context).split( - (op) => op.getContext().test, - () => Observable.of(), - () => null + it("should return an empty observable when a split link returns null", async () => { + let context = { test: true }; + const link = new SetContextLink(() => context).split( + (op) => op.getContext().test, + () => Observable.of(), + () => null + ); + + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery }) ); - testLinkResults({ - link, - results: [], - }); - context.test = false; - testLinkResults({ - link, - results: [], - done: resolve, - }); + + await expect(stream).toComplete(); } - ); - itAsync( - "should set a default context, variable, and query on a copy of operation", - (resolve, reject) => { - const operation = { - query: gql` - { - id - } - `, - }; - const link = new ApolloLink((op: Operation) => { - expect((operation as any)["operationName"]).toBeUndefined(); - expect((operation as any)["variables"]).toBeUndefined(); - expect((operation as any)["context"]).toBeUndefined(); - expect((operation as any)["extensions"]).toBeUndefined(); - expect(op["variables"]).toBeDefined(); - expect((op as any)["context"]).toBeUndefined(); - expect(op["extensions"]).toBeDefined(); - return Observable.of(); - }); + context.test = false; - ApolloLink.execute(link, operation).subscribe({ - complete: resolve, - }); + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery }) + ); + + await expect(stream).toComplete(); } - ); + }); + + it("should set a default context, variable, and query on a copy of operation", async () => { + const operation = { + query: gql` + { + id + } + `, + }; + const link = new ApolloLink((op: Operation) => { + expect((operation as any)["operationName"]).toBeUndefined(); + expect((operation as any)["variables"]).toBeUndefined(); + expect((operation as any)["context"]).toBeUndefined(); + expect((operation as any)["extensions"]).toBeUndefined(); + expect(op["variables"]).toBeDefined(); + expect((op as any)["context"]).toBeUndefined(); + expect(op["extensions"]).toBeDefined(); + return Observable.of(); + }); + + const stream = new ObservableStream(execute(link, operation)); + + await expect(stream).toComplete(); + }); }); }); @@ -437,19 +402,14 @@ describe("ApolloClient", () => { extensions: {}, }; - itAsync( - "should create an observable that completes when passed an empty array", - (resolve, reject) => { - const observable = ApolloLink.execute(ApolloLink.from([]), { - query: sampleQuery, - }); - observable.subscribe( - () => expect(false), - () => expect(false), - resolve - ); - } - ); + it("should create an observable that completes when passed an empty array", async () => { + const observable = ApolloLink.execute(ApolloLink.from([]), { + query: sampleQuery, + }); + const stream = new ObservableStream(observable); + + await expect(stream).toComplete(); + }); it("can create chain of one", () => { expect(() => ApolloLink.from([new ApolloLink()])).not.toThrow(); @@ -464,7 +424,7 @@ describe("ApolloClient", () => { ).not.toThrow(); }); - itAsync("should receive result of one link", (resolve, reject) => { + it("should receive result of one link", async () => { const data: FetchResult = { data: { hello: "world", @@ -475,15 +435,10 @@ describe("ApolloClient", () => { ]); // Smoke tests execute as a static method const observable = ApolloLink.execute(chain, uniqueOperation); - observable.subscribe({ - next: (actualData) => { - expect(data).toEqual(actualData); - }, - error: () => { - throw new Error(); - }, - complete: () => resolve(), - }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue(data); + await expect(stream).toComplete(); }); it("should accept AST query and pass AST to link", () => { @@ -497,7 +452,7 @@ describe("ApolloClient", () => { const chain = ApolloLink.from([new ApolloLink(stub)]); ApolloLink.execute(chain, astOperation); - expect(stub).toBeCalledWith({ + expect(stub).toHaveBeenCalledWith({ query: sampleQuery, operationName: "SampleQuery", variables: {}, @@ -505,157 +460,131 @@ describe("ApolloClient", () => { }); }); - itAsync( - "should pass operation from one link to next with modifications", - (resolve, reject) => { - const chain = ApolloLink.from([ - new ApolloLink((op, forward) => - forward({ - ...op, - query: sampleQuery, - }) - ), - new ApolloLink((op) => { - expect({ - extensions: {}, - operationName: "SampleQuery", - query: sampleQuery, - variables: {}, - }).toEqual(op); - - resolve(); - - return new Observable((observer) => { - observer.error("should not have invoked observable"); - }); - }), - ]); - ApolloLink.execute(chain, uniqueOperation); - } - ); + it("should pass operation from one link to next with modifications", async () => { + const chain = ApolloLink.from([ + new ApolloLink((op, forward) => + forward({ + ...op, + query: sampleQuery, + }) + ), + new ApolloLink((op) => { + expect({ + extensions: {}, + operationName: "SampleQuery", + query: sampleQuery, + variables: {}, + }).toEqual(op); + + return new Observable((observer) => { + observer.complete(); + }); + }), + ]); + const observable = ApolloLink.execute(chain, uniqueOperation); + const stream = new ObservableStream(observable); - itAsync( - "should pass result of one link to another with forward", - (resolve, reject) => { - const data: FetchResult = { - data: { - hello: "world", - }, - }; + await expect(stream).toComplete(); + }); + + it("should pass result of one link to another with forward", async () => { + const data: FetchResult = { + data: { + hello: "world", + }, + }; + + const chain = ApolloLink.from([ + new ApolloLink((op, forward) => { + return forward(op); + }), + new ApolloLink(() => Observable.of(data)), + ]); + const observable = ApolloLink.execute(chain, uniqueOperation); + const stream = new ObservableStream(observable); - const chain = ApolloLink.from([ - new ApolloLink((op, forward) => { - const observable = forward(op); + await expect(stream).toEmitValue(data); + await expect(stream).toComplete(); + }); + it("should receive final result of two link chain", async () => { + const data: FetchResult = { + data: { + hello: "world", + }, + }; + + const chain = ApolloLink.from([ + new ApolloLink((op, forward) => { + const observable = forward(op); + + return new Observable((observer) => { observable.subscribe({ next: (actualData) => { expect(data).toEqual(actualData); + observer.next({ + data: { + ...actualData.data, + modification: "unique", + }, + }); }, - error: () => { - throw new Error(); - }, - complete: resolve, + error: (error) => observer.error(error), + complete: () => observer.complete(), }); + }); + }), + new ApolloLink(() => Observable.of(data)), + ]); - return observable; - }), - new ApolloLink(() => Observable.of(data)), - ]); - ApolloLink.execute(chain, uniqueOperation); - } - ); + const result = ApolloLink.execute(chain, uniqueOperation); + const stream = new ObservableStream(result); - itAsync( - "should receive final result of two link chain", - (resolve, reject) => { - const data: FetchResult = { - data: { - hello: "world", - }, - }; + await expect(stream).toEmitValue({ + data: { + ...data.data, + modification: "unique", + }, + }); + await expect(stream).toComplete(); + }); - const chain = ApolloLink.from([ - new ApolloLink((op, forward) => { - const observable = forward(op); - - return new Observable((observer) => { - observable.subscribe({ - next: (actualData) => { - expect(data).toEqual(actualData); - observer.next({ - data: { - ...actualData.data, - modification: "unique", - }, - }); - }, - error: (error) => observer.error(error), - complete: () => observer.complete(), - }); - }); - }), - new ApolloLink(() => Observable.of(data)), - ]); + it("should chain together a function with links", async () => { + const add1 = new ApolloLink((operation: Operation, forward: NextLink) => { + operation.setContext((context: { num: number }) => ({ + num: context.num + 1, + })); + return forward(operation); + }); + const add1Link = new ApolloLink((operation, forward) => { + operation.setContext((context: { num: number }) => ({ + num: context.num + 1, + })); + return forward(operation); + }); - const result = ApolloLink.execute(chain, uniqueOperation); + const link = ApolloLink.from([ + add1, + add1, + add1Link, + add1, + add1Link, + new ApolloLink((operation) => + Observable.of({ data: operation.getContext() }) + ), + ]); - result.subscribe({ - next: (modifiedData) => { - expect({ - data: { - ...data.data, - modification: "unique", - }, - }).toEqual(modifiedData); - }, - error: () => { - throw new Error(); - }, - complete: resolve, - }); - } - ); - - itAsync( - "should chain together a function with links", - (resolve, reject) => { - const add1 = new ApolloLink( - (operation: Operation, forward: NextLink) => { - operation.setContext((context: { num: number }) => ({ - num: context.num + 1, - })); - return forward(operation); - } - ); - const add1Link = new ApolloLink((operation, forward) => { - operation.setContext((context: { num: number }) => ({ - num: context.num + 1, - })); - return forward(operation); - }); + const stream = new ObservableStream( + execute(link, { query: sampleQuery, context: { num: 0 } }) + ); - const link = ApolloLink.from([ - add1, - add1, - add1Link, - add1, - add1Link, - new ApolloLink((operation) => - Observable.of({ data: operation.getContext() }) - ), - ]); - testLinkResults({ - link, - results: [{ num: 5 }], - context: { num: 0 }, - done: resolve, - }); - } - ); + await expect(stream).toEmitValue({ data: { num: 5 } }); + await expect(stream).toComplete(); + }); }); describe("split", () => { - itAsync("should split two functions", (resolve, reject) => { + it("should split two functions", async () => { const context = { add: 1 }; const returnOne = new SetContextLink(() => context); const link1 = returnOne.concat((operation, forward) => @@ -670,21 +599,28 @@ describe("ApolloClient", () => { link2 ); - testLinkResults({ - link, - results: [2], - }); + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery }) + ); + + await expect(stream).toEmitValue({ data: 2 }); + await expect(stream).toComplete(); + } context.add = 2; - testLinkResults({ - link, - results: [4], - done: resolve, - }); + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery }) + ); + + await expect(stream).toEmitValue({ data: 4 }); + await expect(stream).toComplete(); + } }); - itAsync("should split two Links", (resolve, reject) => { + it("should split two Links", async () => { const context = { add: 1 }; const returnOne = new SetContextLink(() => context); const link1 = returnOne.concat( @@ -703,21 +639,28 @@ describe("ApolloClient", () => { link2 ); - testLinkResults({ - link, - results: [2], - }); + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery }) + ); + + await expect(stream).toEmitValue({ data: 2 }); + await expect(stream).toComplete(); + } context.add = 2; - testLinkResults({ - link, - results: [4], - done: resolve, - }); + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery }) + ); + + await expect(stream).toEmitValue({ data: 4 }); + await expect(stream).toComplete(); + } }); - itAsync("should split a link and a function", (resolve, reject) => { + it("should split a link and a function", async () => { const context = { add: 1 }; const returnOne = new SetContextLink(() => context); const link1 = returnOne.concat((operation, forward) => @@ -734,21 +677,28 @@ describe("ApolloClient", () => { link2 ); - testLinkResults({ - link, - results: [2], - }); + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery }) + ); + + await expect(stream).toEmitValue({ data: 2 }); + await expect(stream).toComplete(); + } context.add = 2; - testLinkResults({ - link, - results: [4], - done: resolve, - }); + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery }) + ); + + await expect(stream).toEmitValue({ data: 4 }); + await expect(stream).toComplete(); + } }); - itAsync("should allow concat after split to be join", (resolve, reject) => { + it("should allow concat after split to be join", async () => { const context = { test: true, add: 1 }; const start = new SetContextLink(() => ({ ...context })); const link = start @@ -771,92 +721,105 @@ describe("ApolloClient", () => { Observable.of({ data: operation.getContext().add }) ); - testLinkResults({ - link, - context, - results: [2], - }); + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery, context }) + ); + + await expect(stream).toEmitValue({ data: 2 }); + await expect(stream).toComplete(); + } context.test = false; - testLinkResults({ - link, - context, - results: [3], - done: resolve, - }); + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery, context }) + ); + + await expect(stream).toEmitValue({ data: 3 }); + await expect(stream).toComplete(); + } }); - itAsync( - "should allow default right to be empty or passthrough when forward available", - (resolve, reject) => { - let context = { test: true }; - const start = new SetContextLink(() => context); - const link = start.split( - (operation) => operation.getContext().test, - (operation) => - Observable.of({ - data: { - count: 1, - }, - }) - ); - const concat = link.concat((operation) => + it("should allow default right to be empty or passthrough when forward available", async () => { + let context = { test: true }; + const start = new SetContextLink(() => context); + const link = start.split( + (operation) => operation.getContext().test, + (operation) => Observable.of({ data: { - count: 2, + count: 1, }, }) + ); + const concat = link.concat((operation) => + Observable.of({ + data: { + count: 2, + }, + }) + ); + + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery }) ); - testLinkResults({ - link, - results: [{ count: 1 }], - }); + await expect(stream).toEmitValue({ data: { count: 1 } }); + await expect(stream).toComplete(); + } - context.test = false; + context.test = false; - testLinkResults({ - link, - results: [], - }); + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery }) + ); - testLinkResults({ - link: concat, - results: [{ count: 2 }], - done: resolve, - }); + await expect(stream).toComplete(); } - ); - itAsync( - "should create filter when single link passed in", - (resolve, reject) => { - const link = ApolloLink.split( - (operation) => operation.getContext().test, - (operation, forward) => Observable.of({ data: { count: 1 } }) + { + const stream = new ObservableStream( + execute(concat, { query: sampleQuery }) ); - let context = { test: true }; + await expect(stream).toEmitValue({ data: { count: 2 } }); + await expect(stream).toComplete(); + } + }); - testLinkResults({ - link, - results: [{ count: 1 }], - context, - }); + it("should create filter when single link passed in", async () => { + const link = ApolloLink.split( + (operation) => operation.getContext().test, + (operation, forward) => Observable.of({ data: { count: 1 } }) + ); - context.test = false; + let context = { test: true }; - testLinkResults({ - link, - results: [], - context, - done: resolve, - }); + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery, context }) + ); + + await expect(stream).toEmitValue({ data: { count: 1 } }); + await expect(stream).toComplete(); } - ); - itAsync("should split two functions", (resolve, reject) => { + context.test = false; + + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery, context }) + ); + + await expect(stream).toComplete(); + } + }); + + it("should split two functions", async () => { const link = ApolloLink.split( (operation) => operation.getContext().test, (operation, forward) => Observable.of({ data: { count: 1 } }), @@ -865,23 +828,28 @@ describe("ApolloClient", () => { let context = { test: true }; - testLinkResults({ - link, - results: [{ count: 1 }], - context, - }); + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery, context }) + ); + + await expect(stream).toEmitValue({ data: { count: 1 } }); + await expect(stream).toComplete(); + } context.test = false; - testLinkResults({ - link, - results: [{ count: 2 }], - context, - done: resolve, - }); + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery, context }) + ); + + await expect(stream).toEmitValue({ data: { count: 2 } }); + await expect(stream).toComplete(); + } }); - itAsync("should split two Links", (resolve, reject) => { + it("should split two Links", async () => { const link = ApolloLink.split( (operation) => operation.getContext().test, (operation, forward) => Observable.of({ data: { count: 1 } }), @@ -892,23 +860,28 @@ describe("ApolloClient", () => { let context = { test: true }; - testLinkResults({ - link, - results: [{ count: 1 }], - context, - }); + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery, context }) + ); + + await expect(stream).toEmitValue({ data: { count: 1 } }); + await expect(stream).toComplete(); + } context.test = false; - testLinkResults({ - link, - results: [{ count: 2 }], - context, - done: resolve, - }); + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery, context }) + ); + + await expect(stream).toEmitValue({ data: { count: 2 } }); + await expect(stream).toComplete(); + } }); - itAsync("should split a link and a function", (resolve, reject) => { + it("should split a link and a function", async () => { const link = ApolloLink.split( (operation) => operation.getContext().test, (operation, forward) => Observable.of({ data: { count: 1 } }), @@ -919,23 +892,28 @@ describe("ApolloClient", () => { let context = { test: true }; - testLinkResults({ - link, - results: [{ count: 1 }], - context, - }); + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery, context }) + ); + + await expect(stream).toEmitValue({ data: { count: 1 } }); + await expect(stream).toComplete(); + } context.test = false; - testLinkResults({ - link, - results: [{ count: 2 }], - context, - done: resolve, - }); + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery, context }) + ); + + await expect(stream).toEmitValue({ data: { count: 2 } }); + await expect(stream).toComplete(); + } }); - itAsync("should allow concat after split to be join", (resolve, reject) => { + it("should allow concat after split to be join", async () => { const context = { test: true }; const link = ApolloLink.split( (operation) => operation.getContext().test, @@ -945,47 +923,53 @@ describe("ApolloClient", () => { })) ).concat(() => Observable.of({ data: { count: 1 } })); - testLinkResults({ - link, - context, - results: [{ count: 2 }], - }); + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery, context }) + ); + + await expect(stream).toEmitValue({ data: { count: 2 } }); + await expect(stream).toComplete(); + } context.test = false; + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery, context }) + ); - testLinkResults({ - link, - context, - results: [{ count: 1 }], - done: resolve, - }); + await expect(stream).toEmitValue({ data: { count: 1 } }); + await expect(stream).toComplete(); + } }); - itAsync( - "should allow default right to be passthrough", - (resolve, reject) => { - const context = { test: true }; - const link = ApolloLink.split( - (operation) => operation.getContext().test, - (operation) => Observable.of({ data: { count: 2 } }) - ).concat((operation) => Observable.of({ data: { count: 1 } })); + it("should allow default right to be passthrough", async () => { + const context = { test: true }; + const link = ApolloLink.split( + (operation) => operation.getContext().test, + (operation) => Observable.of({ data: { count: 2 } }) + ).concat((operation) => Observable.of({ data: { count: 1 } })); - testLinkResults({ - link, - context, - results: [{ count: 2 }], - }); + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery, context }) + ); - context.test = false; + await expect(stream).toEmitValue({ data: { count: 2 } }); + await expect(stream).toComplete(); + } - testLinkResults({ - link, - context, - results: [{ count: 1 }], - done: resolve, - }); + context.test = false; + + { + const stream = new ObservableStream( + execute(link, { query: sampleQuery, context }) + ); + + await expect(stream).toEmitValue({ data: { count: 1 } }); + await expect(stream).toComplete(); } - ); + }); }); describe("Terminating links", () => { diff --git a/src/link/http/__tests__/parseAndCheckHttpResponse.ts b/src/link/http/__tests__/parseAndCheckHttpResponse.ts index 74e9b63018e..b667d95c35b 100644 --- a/src/link/http/__tests__/parseAndCheckHttpResponse.ts +++ b/src/link/http/__tests__/parseAndCheckHttpResponse.ts @@ -3,7 +3,6 @@ import fetchMock from "fetch-mock"; import { createOperation } from "../../utils/createOperation"; import { parseAndCheckHttpResponse } from "../parseAndCheckHttpResponse"; -import { itAsync } from "../../../testing"; const query = gql` query SampleQuery { @@ -20,98 +19,79 @@ describe("parseAndCheckResponse", () => { const operations = [createOperation({}, { query })]; - itAsync( - "throws a Server error when response is > 300 with unparsable json", - (resolve, reject) => { - const status = 400; - fetchMock.mock("begin:/error", status); - fetch("error") - .then(parseAndCheckHttpResponse(operations)) - .then(reject) - .catch((e) => { - expect(e.statusCode).toBe(status); - expect(e.name).toBe("ServerError"); - expect(e).toHaveProperty("response"); - expect(e.bodyText).toBe(undefined); - resolve(); - }) - .catch(reject); - } - ); - - itAsync( - "throws a ServerParse error when response is 200 with unparsable json", - (resolve, reject) => { - const status = 200; - fetchMock.mock("begin:/error", status); - fetch("error") - .then(parseAndCheckHttpResponse(operations)) - .then(reject) - .catch((e) => { - expect(e.statusCode).toBe(status); - expect(e.name).toBe("ServerParseError"); - expect(e).toHaveProperty("response"); - expect(e).toHaveProperty("bodyText"); - resolve(); - }) - .catch(reject); - } - ); - - itAsync( - "throws a network error with a status code and result", - (resolve, reject) => { - const status = 403; - const body = { data: "fail" }; //does not contain data or errors - fetchMock.mock("begin:/error", { - body, - status, - }); - fetch("error") - .then(parseAndCheckHttpResponse(operations)) - .then(reject) - .catch((e) => { - expect(e.statusCode).toBe(status); - expect(e.name).toBe("ServerError"); - expect(e).toHaveProperty("response"); - expect(e).toHaveProperty("result"); - resolve(); - }) - .catch(reject); - } - ); + it("throws a Server error when response is > 300 with unparsable json", async () => { + const status = 400; + fetchMock.mock("begin:/error", status); + + const error = await fetch("error") + .then(parseAndCheckHttpResponse(operations)) + .catch((error) => error); + + expect(error.statusCode).toBe(status); + expect(error.name).toBe("ServerError"); + expect(error).toHaveProperty("response"); + expect(error.bodyText).toBe(undefined); + }); + + it("throws a ServerParse error when response is 200 with unparsable json", async () => { + const status = 200; + fetchMock.mock("begin:/error", status); + const error = await fetch("error") + .then(parseAndCheckHttpResponse(operations)) + .catch((error) => error); + + expect(error.statusCode).toBe(status); + expect(error.name).toBe("ServerParseError"); + expect(error).toHaveProperty("response"); + expect(error).toHaveProperty("bodyText"); + }); - itAsync("throws a server error on incorrect data", (resolve, reject) => { + it("throws a network error with a status code and result", async () => { + const status = 403; + const body = { data: "fail" }; //does not contain data or errors + fetchMock.mock("begin:/error", { + body, + status, + }); + const error = await fetch("error") + .then(parseAndCheckHttpResponse(operations)) + .catch((error) => error); + + expect(error.statusCode).toBe(status); + expect(error.name).toBe("ServerError"); + expect(error).toHaveProperty("response"); + expect(error).toHaveProperty("result"); + }); + + it("throws a server error on incorrect data", async () => { const data = { hello: "world" }; //does not contain data or erros fetchMock.mock("begin:/incorrect", data); - fetch("incorrect") + const error = await fetch("incorrect") .then(parseAndCheckHttpResponse(operations)) - .then(reject) - .catch((e) => { - expect(e.statusCode).toBe(200); - expect(e.name).toBe("ServerError"); - expect(e).toHaveProperty("response"); - expect(e.result).toEqual(data); - resolve(); - }) - .catch(reject); + .catch((error) => error); + + expect(error.statusCode).toBe(200); + expect(error.name).toBe("ServerError"); + expect(error).toHaveProperty("response"); + expect(error.result).toEqual(data); }); - itAsync("is able to return a correct GraphQL result", (resolve, reject) => { + it("is able to return a correct GraphQL result", async () => { const errors = ["", "" + new Error("hi")]; const data = { data: { hello: "world" }, errors }; fetchMock.mock("begin:/data", { body: data, }); - fetch("data") - .then(parseAndCheckHttpResponse(operations)) - .then(({ data, errors: e }) => { - expect(data).toEqual({ hello: "world" }); - expect(e.length).toEqual(errors.length); - expect(e).toEqual(errors); - resolve(); - }) - .catch(reject); + + { + const { data, errors: e } = await fetch("data").then( + parseAndCheckHttpResponse(operations) + ); + + expect(data).toEqual({ hello: "world" }); + expect(e.length).toEqual(errors.length); + expect(e).toEqual(errors); + } }); }); diff --git a/src/link/schema/__tests__/schemaLink.ts b/src/link/schema/__tests__/schemaLink.ts index d4031b679ca..a536351002d 100644 --- a/src/link/schema/__tests__/schemaLink.ts +++ b/src/link/schema/__tests__/schemaLink.ts @@ -3,7 +3,7 @@ import gql from "graphql-tag"; import { execute } from "../../core/execute"; import { SchemaLink } from "../"; -import { itAsync } from "../../../testing"; +import { ObservableStream } from "../../../testing/internal"; const sampleQuery = gql` query SampleQuery { @@ -51,25 +51,18 @@ describe("SchemaLink", () => { expect(link.schema).toEqual(schema); }); - itAsync("calls next and then complete", (resolve, reject) => { - const next = jest.fn(); + it("calls next and then complete", async () => { const link = new SchemaLink({ schema }); const observable = execute(link, { query: sampleQuery, }); - observable.subscribe({ - next, - error: () => { - throw new Error("Received error"); - }, - complete: () => { - expect(next).toHaveBeenCalledTimes(1); - resolve(); - }, - }); + 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 = new SchemaLink({ validate: true, schema: makeExecutableSchema({ @@ -86,98 +79,67 @@ describe("SchemaLink", () => { const observable = execute(link, { query: sampleQuery, }); - observable.subscribe((result) => { - expect(result.errors).toBeTruthy(); - expect(result.errors!.length).toBe(1); - expect(result.errors![0].message).toMatch(/Unauthorized/); - resolve(); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitValue({ + data: { sampleQuery: null }, + errors: [{ message: "Unauthorized", path: ["sampleQuery"] }], }); }); - itAsync( - "supports query which is executed synchronously", - (resolve, reject) => { - const next = jest.fn(); - const link = new SchemaLink({ schema }); - const introspectionQuery = gql` - query IntrospectionQuery { - __schema { - types { - name - } + it("supports query which is executed synchronously", async () => { + const link = new SchemaLink({ schema }); + const introspectionQuery = gql` + query IntrospectionQuery { + __schema { + types { + name } } - `; - const observable = execute(link, { - query: introspectionQuery, - }); - observable.subscribe( - next, - () => { - throw new Error("Received error"); - }, - () => { - expect(next).toHaveBeenCalledTimes(1); - resolve(); - } - ); - } - ); - - itAsync( - "passes operation context into execute with context function", - (resolve, reject) => { - const next = jest.fn(); - const contextValue = { some: "value" }; - const contextProvider = jest.fn((operation) => operation.getContext()); - const resolvers = { - Query: { - sampleQuery: (root: any, args: any, context: any) => { - try { - expect(context).toEqual(contextValue); - } catch (error) { - reject("Should pass context into resolver"); - } - }, + } + `; + const observable = execute(link, { + query: introspectionQuery, + }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + await expect(stream).toComplete(); + }); + + it("passes operation context into execute with context function", async () => { + const contextValue = { some: "value" }; + const contextProvider = jest.fn((operation) => operation.getContext()); + const resolvers = { + Query: { + sampleQuery: (root: any, args: any, context: any) => { + expect(context).toEqual(contextValue); }, - }; - const schemaWithResolvers = makeExecutableSchema({ - typeDefs, - resolvers, - }); - const link = new SchemaLink({ - schema: schemaWithResolvers, - context: contextProvider, - }); - const observable = execute(link, { - query: sampleQuery, - context: contextValue, - }); - observable.subscribe( - next, - (error) => reject("Shouldn't call onError"), - () => { - try { - expect(next).toHaveBeenCalledTimes(1); - expect(contextProvider).toHaveBeenCalledTimes(1); - resolve(); - } catch (e) { - reject(e); - } - } - ); - } - ); + }, + }; + const schemaWithResolvers = makeExecutableSchema({ + typeDefs, + resolvers, + }); + const link = new SchemaLink({ + schema: schemaWithResolvers, + context: contextProvider, + }); + const observable = execute(link, { + query: sampleQuery, + context: contextValue, + }); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + await expect(stream).toComplete(); + expect(contextProvider).toHaveBeenCalledTimes(1); + }); - itAsync("passes static context into execute", (resolve, reject) => { - const next = jest.fn(); + it("passes static context into execute", async () => { const contextValue = { some: "value" }; const resolver = jest.fn((root, args, context) => { - try { - expect(context).toEqual(contextValue); - } catch (error) { - reject("Should pass context into resolver"); - } + expect(context).toEqual(contextValue); }); const resolvers = { @@ -196,22 +158,14 @@ describe("SchemaLink", () => { const observable = execute(link, { query: sampleQuery, }); - observable.subscribe( - next, - (error) => reject("Shouldn't call onError"), - () => { - try { - expect(next).toHaveBeenCalledTimes(1); - expect(resolver).toHaveBeenCalledTimes(1); - resolve(); - } catch (e) { - reject(e); - } - } - ); + const stream = new ObservableStream(observable); + + await expect(stream).toEmitNext(); + await expect(stream).toComplete(); + expect(resolver).toHaveBeenCalledTimes(1); }); - itAsync("reports errors for unknown queries", (resolve, reject) => { + it("reports errors for unknown queries", async () => { const link = new SchemaLink({ validate: true, schema: makeExecutableSchema({ @@ -225,11 +179,9 @@ describe("SchemaLink", () => { } `, }); - observable.subscribe((result) => { - expect(result.errors).toBeTruthy(); - expect(result.errors!.length).toBe(1); - expect(result.errors![0].message).toMatch(/Cannot query field "unknown"/); - resolve(); + const stream = new ObservableStream(observable); + await expect(stream).toEmitValue({ + errors: [{ message: 'Cannot query field "unknown" on type "Query".' }], }); }); }); diff --git a/src/link/utils/__tests__/toPromise.ts b/src/link/utils/__tests__/toPromise.ts index c81f3eb27b2..82bf7c04083 100644 --- a/src/link/utils/__tests__/toPromise.ts +++ b/src/link/utils/__tests__/toPromise.ts @@ -1,5 +1,4 @@ import { Observable } from "../../../utilities/observables/Observable"; -import { itAsync } from "../../../testing"; import { toPromise } from "../toPromise"; import { fromError } from "../fromError"; @@ -38,12 +37,11 @@ describe("toPromise", () => { console.warn = _warn; }); - itAsync("return error call as Promise rejection", (resolve, reject) => { - toPromise(Observable.of(data, data)).then((result) => { - expect(data).toEqual(result); - expect(spy).toHaveBeenCalled(); - resolve(); - }); + it("return error call as Promise rejection", async () => { + const result = await toPromise(Observable.of(data, data)); + + expect(data).toEqual(result); + expect(spy).toHaveBeenCalled(); }); }); }); diff --git a/src/masking/__benches__/types.bench.ts b/src/masking/__benches__/types.bench.ts index 0ae97b7edc4..97a67e69b4e 100644 --- a/src/masking/__benches__/types.bench.ts +++ b/src/masking/__benches__/types.bench.ts @@ -4,6 +4,8 @@ import { expectTypeOf } from "expect-type"; import type { DeepPartial } from "../../utilities/index.js"; import { setup } from "@ark/attest"; +import type { ContainsFragmentsRefs } from "../internal/types.js"; +import type { TypedDocumentNode } from "../../index.js"; setup({ updateSnapshots: !process.env.CI, @@ -566,7 +568,8 @@ test("detects `$fragmentRefs` on types with index signatures", (prefix) => { }).types([6, "instantiations"]); bench(prefix + "functionality", () => { - const x = {} as MaybeMasked; + const x = {} as Unmasked; + const y = {} as ContainsFragmentsRefs; expectTypeOf(x).branded.toEqualTypeOf<{ __typename: "Foo"; @@ -575,6 +578,7 @@ test("detects `$fragmentRefs` on types with index signatures", (prefix) => { foo: string; structuredMetadata: StructuredMetadata; }>(); + expectTypeOf(y).toEqualTypeOf(); }); }); @@ -592,3 +596,34 @@ test("recursive types: no error 'Type instantiation is excessively deep and poss expectTypeOf(x).branded.toEqualTypeOf(); }); }); + +test("MaybeMasked can be called with a generic if `mode` is not set to `unmask`", (prefix) => { + function withGenericResult( + arg: TypedDocumentNode + ) { + bench(prefix + "Result generic - instantiations", () => { + const maybeMasked: MaybeMasked = arg; + return maybeMasked; + }).types(); + + bench(prefix + "Result generic - functionality", () => { + const maybeMasked: MaybeMasked = arg; + expectTypeOf(maybeMasked).toEqualTypeOf(arg); + }); + } + function withGenericDocument(arg: T) { + bench(prefix + "Result generic - instantiations", () => { + const maybeMasked: MaybeMasked = arg; + return maybeMasked; + }).types(); + + bench(prefix + "Result generic - functionality", () => { + const maybeMasked: MaybeMasked = arg; + // cannot use unresolved generic with `expectTypeOf` here so we just try an assignment the other way round + const test: T = maybeMasked; + return test; + }); + } + withGenericResult({} as any); + withGenericDocument({} as any); +}); diff --git a/src/masking/types.ts b/src/masking/types.ts index b50617d6d64..f2c5f4cb4ca 100644 --- a/src/masking/types.ts +++ b/src/masking/types.ts @@ -39,15 +39,17 @@ export type FragmentType = * enabled. */ export type MaybeMasked = - // distribute TData - in case of a union, do the next steps for each member - TData extends any ? - // prevent "Type instantiation is excessively deep and possibly infinite." - true extends IsAny ? TData - : TData extends { __masked?: true } ? Prettify> - : DataMasking extends { enabled: true } ? TData - : true extends ContainsFragmentsRefs ? Unmasked - : TData - : never; + DataMasking extends { mode: "unmask" } ? + // distribute TData - in case of a union, do the next steps for each member + TData extends any ? + // prevent "Type instantiation is excessively deep and possibly infinite." + true extends IsAny ? TData + : TData extends { __masked?: true } ? Prettify> + : true extends ContainsFragmentsRefs ? Unmasked + : TData + : never + : DataMasking extends { mode: "preserveTypes" } ? TData + : TData; /** * Unmasks a type to provide its full result. diff --git a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx index 7e235ea7132..6ce0564cc63 100644 --- a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx +++ b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx @@ -7250,7 +7250,7 @@ describe.skip("type tests", () => { const [queryRef] = useBackgroundQuery(maskedQuery); const { data } = useReadQuery(queryRef); - expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).toEqualTypeOf>(); expectTypeOf(data).not.toEqualTypeOf(); } @@ -7261,8 +7261,8 @@ describe.skip("type tests", () => { >(maskedQuery); const { data } = useReadQuery(queryRef); - expectTypeOf(data).toEqualTypeOf(); - expectTypeOf(data).not.toEqualTypeOf(); + expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).not.toEqualTypeOf(); } { @@ -7272,7 +7272,7 @@ describe.skip("type tests", () => { >(maskedQuery); const { data } = useReadQuery(queryRef); - expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).toEqualTypeOf>(); expectTypeOf(data).not.toEqualTypeOf(); } }); @@ -7306,7 +7306,7 @@ describe.skip("type tests", () => { const [queryRef] = useBackgroundQuery(maskedQuery); const { data } = useReadQuery(queryRef); - expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).toEqualTypeOf>(); expectTypeOf(data).not.toEqualTypeOf(); } @@ -7317,8 +7317,8 @@ describe.skip("type tests", () => { >(maskedQuery); const { data } = useReadQuery(queryRef); - expectTypeOf(data).toEqualTypeOf(); - expectTypeOf(data).not.toEqualTypeOf(); + expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).not.toEqualTypeOf(); } { @@ -7328,7 +7328,7 @@ describe.skip("type tests", () => { >(maskedQuery); const { data } = useReadQuery(queryRef); - expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).toEqualTypeOf>(); expectTypeOf(data).not.toEqualTypeOf(); } }); @@ -7360,7 +7360,9 @@ describe.skip("type tests", () => { }); const { data } = useReadQuery(queryRef); - expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).toEqualTypeOf< + Masked | undefined + >(); expectTypeOf(data).not.toEqualTypeOf< UnmaskedVariablesCaseData | undefined >(); @@ -7373,9 +7375,9 @@ describe.skip("type tests", () => { >(maskedQuery, { errorPolicy: "all" }); const { data } = useReadQuery(queryRef); - expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).toEqualTypeOf(); expectTypeOf(data).not.toEqualTypeOf< - MaskedVariablesCaseData | undefined + UnmaskedVariablesCaseData | undefined >(); } @@ -7386,7 +7388,9 @@ describe.skip("type tests", () => { >(maskedQuery, { errorPolicy: "all" }); const { data } = useReadQuery(queryRef); - expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).toEqualTypeOf< + Masked | undefined + >(); expectTypeOf(data).not.toEqualTypeOf< UnmaskedVariablesCaseData | undefined >(); @@ -7420,7 +7424,7 @@ describe.skip("type tests", () => { }); const { data } = useReadQuery(queryRef); - expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).toEqualTypeOf>(); expectTypeOf(data).not.toEqualTypeOf(); } @@ -7431,8 +7435,8 @@ describe.skip("type tests", () => { >(maskedQuery, { errorPolicy: "none" }); const { data } = useReadQuery(queryRef); - expectTypeOf(data).toEqualTypeOf(); - expectTypeOf(data).not.toEqualTypeOf(); + expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).not.toEqualTypeOf(); } { @@ -7442,7 +7446,7 @@ describe.skip("type tests", () => { >(maskedQuery, { errorPolicy: "none" }); const { data } = useReadQuery(queryRef); - expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).toEqualTypeOf>(); expectTypeOf(data).not.toEqualTypeOf(); } }); @@ -7478,7 +7482,9 @@ describe.skip("type tests", () => { }); const { data } = useReadQuery(queryRef); - expectTypeOf(data).toEqualTypeOf>(); + expectTypeOf(data).toEqualTypeOf< + DeepPartial> + >(); expectTypeOf(data).not.toEqualTypeOf< DeepPartial >(); @@ -7491,11 +7497,9 @@ describe.skip("type tests", () => { >(maskedQuery, { returnPartialData: true }); const { data } = useReadQuery(queryRef); - expectTypeOf(data).toEqualTypeOf< - DeepPartial - >(); + expectTypeOf(data).toEqualTypeOf>(); expectTypeOf(data).not.toEqualTypeOf< - DeepPartial + DeepPartial >(); } @@ -7506,7 +7510,9 @@ describe.skip("type tests", () => { >(maskedQuery, { returnPartialData: true }); const { data } = useReadQuery(queryRef); - expectTypeOf(data).toEqualTypeOf>(); + expectTypeOf(data).toEqualTypeOf< + DeepPartial> + >(); expectTypeOf(data).not.toEqualTypeOf< DeepPartial >(); @@ -7544,7 +7550,7 @@ describe.skip("type tests", () => { }); const { data } = useReadQuery(queryRef); - expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).toEqualTypeOf>(); expectTypeOf(data).not.toEqualTypeOf(); } @@ -7555,8 +7561,8 @@ describe.skip("type tests", () => { >(maskedQuery, { returnPartialData: false }); const { data } = useReadQuery(queryRef); - expectTypeOf(data).toEqualTypeOf(); - expectTypeOf(data).not.toEqualTypeOf(); + expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).not.toEqualTypeOf(); } { @@ -7566,7 +7572,7 @@ describe.skip("type tests", () => { >(maskedQuery, { returnPartialData: false }); const { data } = useReadQuery(queryRef); - expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).toEqualTypeOf>(); expectTypeOf(data).not.toEqualTypeOf(); } }); @@ -7602,7 +7608,7 @@ describe.skip("type tests", () => { }); const { data } = useReadQuery(queryRef); - expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).toEqualTypeOf>(); expectTypeOf(data).not.toEqualTypeOf(); } @@ -7613,8 +7619,8 @@ describe.skip("type tests", () => { >(maskedQuery, { fetchPolicy: "no-cache" }); const { data } = useReadQuery(queryRef); - expectTypeOf(data).toEqualTypeOf(); - expectTypeOf(data).not.toEqualTypeOf(); + expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).not.toEqualTypeOf(); } { @@ -7624,7 +7630,7 @@ describe.skip("type tests", () => { >(maskedQuery, { fetchPolicy: "no-cache" }); const { data } = useReadQuery(queryRef); - expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).toEqualTypeOf>(); expectTypeOf(data).not.toEqualTypeOf(); } }); @@ -7675,7 +7681,7 @@ describe.skip("type tests", () => { const { data } = useReadQuery(queryRef); expectTypeOf(data).toEqualTypeOf< - DeepPartial | undefined + DeepPartial> | undefined >(); expectTypeOf(data).not.toEqualTypeOf< DeepPartial | undefined @@ -7690,10 +7696,10 @@ describe.skip("type tests", () => { const { data } = useReadQuery(queryRef); expectTypeOf(data).toEqualTypeOf< - DeepPartial | undefined + DeepPartial | undefined >(); expectTypeOf(data).not.toEqualTypeOf< - DeepPartial | undefined + DeepPartial | undefined >(); } @@ -7705,7 +7711,7 @@ describe.skip("type tests", () => { const { data } = useReadQuery(queryRef); expectTypeOf(data).toEqualTypeOf< - DeepPartial | undefined + DeepPartial> | undefined >(); expectTypeOf(data).not.toEqualTypeOf< DeepPartial | undefined @@ -7754,7 +7760,9 @@ describe.skip("type tests", () => { }); const { data } = useReadQuery(queryRef); - expectTypeOf(data).toEqualTypeOf>(); + expectTypeOf(data).toEqualTypeOf< + DeepPartial> + >(); expectTypeOf(data).not.toEqualTypeOf< DeepPartial >(); @@ -7767,11 +7775,9 @@ describe.skip("type tests", () => { >(maskedQuery, { returnPartialData: true, errorPolicy: "none" }); const { data } = useReadQuery(queryRef); - expectTypeOf(data).toEqualTypeOf< - DeepPartial - >(); + expectTypeOf(data).toEqualTypeOf>(); expectTypeOf(data).not.toEqualTypeOf< - DeepPartial + DeepPartial >(); } @@ -7782,7 +7788,9 @@ describe.skip("type tests", () => { >(maskedQuery, { returnPartialData: true, errorPolicy: "none" }); const { data } = useReadQuery(queryRef); - expectTypeOf(data).toEqualTypeOf>(); + expectTypeOf(data).toEqualTypeOf< + DeepPartial> + >(); expectTypeOf(data).not.toEqualTypeOf< DeepPartial >(); @@ -7826,7 +7834,9 @@ describe.skip("type tests", () => { }); const { data } = useReadQuery(queryRef); - expectTypeOf(data).toEqualTypeOf>(); + expectTypeOf(data).toEqualTypeOf< + DeepPartial> + >(); expectTypeOf(data).not.toEqualTypeOf< DeepPartial >(); @@ -7843,11 +7853,9 @@ describe.skip("type tests", () => { }); const { data } = useReadQuery(queryRef); - expectTypeOf(data).toEqualTypeOf< - DeepPartial - >(); + expectTypeOf(data).toEqualTypeOf>(); expectTypeOf(data).not.toEqualTypeOf< - DeepPartial + DeepPartial >(); } @@ -7862,7 +7870,9 @@ describe.skip("type tests", () => { }); const { data } = useReadQuery(queryRef); - expectTypeOf(data).toEqualTypeOf>(); + expectTypeOf(data).toEqualTypeOf< + DeepPartial> + >(); expectTypeOf(data).not.toEqualTypeOf< DeepPartial >(); @@ -8260,7 +8270,9 @@ describe.skip("type tests", () => { const result = await refetch(); - expectTypeOf(result.data).toEqualTypeOf(); + expectTypeOf(result.data).toEqualTypeOf< + Masked + >(); expectTypeOf(result.data).not.toEqualTypeOf(); } @@ -8269,8 +8281,8 @@ describe.skip("type tests", () => { const result = await refetch(); - expectTypeOf(result.data).toEqualTypeOf(); - expectTypeOf(result.data).not.toEqualTypeOf(); + expectTypeOf(result.data).toEqualTypeOf(); + expectTypeOf(result.data).not.toEqualTypeOf(); } }); @@ -8296,7 +8308,9 @@ describe.skip("type tests", () => { }, }); - expectTypeOf(result.data).toEqualTypeOf(); + expectTypeOf(result.data).toEqualTypeOf< + Masked + >(); expectTypeOf(result.data).not.toEqualTypeOf(); } @@ -8319,8 +8333,8 @@ describe.skip("type tests", () => { }, }); - expectTypeOf(result.data).toEqualTypeOf(); - expectTypeOf(result.data).not.toEqualTypeOf(); + expectTypeOf(result.data).toEqualTypeOf(); + expectTypeOf(result.data).not.toEqualTypeOf(); } }); diff --git a/src/react/hooks/__tests__/useLazyQuery.test.tsx b/src/react/hooks/__tests__/useLazyQuery.test.tsx index e678095d88b..ed99fdd8905 100644 --- a/src/react/hooks/__tests__/useLazyQuery.test.tsx +++ b/src/react/hooks/__tests__/useLazyQuery.test.tsx @@ -25,7 +25,7 @@ import { import { useLazyQuery } from "../useLazyQuery"; import { QueryResult } from "../../types/types"; import { InvariantError } from "../../../utilities/globals"; -import { MaskedDocumentNode } from "../../../masking"; +import { Masked, MaskedDocumentNode } from "../../../masking"; import { expectTypeOf } from "expect-type"; import { disableActEnvironment, @@ -2567,12 +2567,12 @@ describe.skip("Type Tests", () => { { data, previousData, subscribeToMore, fetchMore, refetch, updateQuery }, ] = useLazyQuery(query, { onCompleted(data) { - expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).toEqualTypeOf>(); }, }); - expectTypeOf(data).toEqualTypeOf(); - expectTypeOf(previousData).toEqualTypeOf(); + expectTypeOf(data).toEqualTypeOf | undefined>(); + expectTypeOf(previousData).toEqualTypeOf | undefined>(); subscribeToMore({ document: gql`` as TypedDocumentNode, @@ -2595,7 +2595,7 @@ describe.skip("Type Tests", () => { { const { data } = await execute(); - expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).toEqualTypeOf | undefined>(); } { @@ -2609,17 +2609,17 @@ describe.skip("Type Tests", () => { }, }); - expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).toEqualTypeOf>(); } { const { data } = await refetch(); - expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).toEqualTypeOf>(); } }); - test("uses unmasked types when using TypedDocumentNode", async () => { + test("uses unmodified types when using TypedDocumentNode", async () => { type UserFieldsFragment = { __typename: "User"; age: number; @@ -2666,12 +2666,12 @@ describe.skip("Type Tests", () => { { data, previousData, fetchMore, refetch, subscribeToMore, updateQuery }, ] = useLazyQuery(query, { onCompleted(data) { - expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).toEqualTypeOf(); }, }); - expectTypeOf(data).toEqualTypeOf(); - expectTypeOf(previousData).toEqualTypeOf(); + expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(previousData).toEqualTypeOf(); subscribeToMore({ document: gql`` as TypedDocumentNode, @@ -2694,7 +2694,7 @@ describe.skip("Type Tests", () => { { const { data } = await execute(); - expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).toEqualTypeOf(); } { @@ -2708,13 +2708,13 @@ describe.skip("Type Tests", () => { }, }); - expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).toEqualTypeOf(); } { const { data } = await refetch(); - expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).toEqualTypeOf(); } }); }); diff --git a/src/react/hooks/__tests__/useSubscription.test.tsx b/src/react/hooks/__tests__/useSubscription.test.tsx index f3aa2099bfd..2290401b550 100644 --- a/src/react/hooks/__tests__/useSubscription.test.tsx +++ b/src/react/hooks/__tests__/useSubscription.test.tsx @@ -20,7 +20,7 @@ import { ErrorBoundary } from "react-error-boundary"; import { MockedSubscriptionResult } from "../../../testing/core/mocking/mockSubscriptionLink"; import { GraphQLError } from "graphql"; import { InvariantError } from "ts-invariant"; -import { MaskedDocumentNode } from "../../../masking"; +import { Masked, MaskedDocumentNode } from "../../../masking"; import { expectTypeOf } from "expect-type"; import { disableActEnvironment, @@ -2433,19 +2433,21 @@ describe.skip("Type Tests", () => { const { data } = useSubscription(subscription, { onData: ({ data }) => { - expectTypeOf(data.data).toEqualTypeOf(); + expectTypeOf(data.data).toEqualTypeOf< + Masked | undefined + >(); }, onSubscriptionData: ({ subscriptionData }) => { expectTypeOf(subscriptionData.data).toEqualTypeOf< - Subscription | undefined + Masked | undefined >(); }, }); - expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).toEqualTypeOf | undefined>(); }); - test("uses unmasked types when using TypedDocumentNode", async () => { + test("uses unmodified type when using TypedDocumentNode", async () => { type UserFieldsFragment = { __typename: "User"; age: number; @@ -2459,30 +2461,19 @@ describe.skip("Type Tests", () => { } & { " $fragmentRefs"?: { UserFieldsFragment: UserFieldsFragment } }; } - interface UnmaskedSubscription { - userUpdated: { - __typename: "User"; - id: string; - name: string; - age: number; - }; - } - const subscription: TypedDocumentNode = gql``; const { data } = useSubscription(subscription, { onData: ({ data }) => { - expectTypeOf(data.data).toEqualTypeOf< - UnmaskedSubscription | undefined - >(); + expectTypeOf(data.data).toEqualTypeOf(); }, onSubscriptionData: ({ subscriptionData }) => { expectTypeOf(subscriptionData.data).toEqualTypeOf< - UnmaskedSubscription | undefined + Subscription | undefined >(); }, }); - expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).toEqualTypeOf(); }); }); diff --git a/src/react/hooks/__tests__/useSuspenseQuery.test.tsx b/src/react/hooks/__tests__/useSuspenseQuery.test.tsx index 72c2e4050e8..ca644a4ea2f 100644 --- a/src/react/hooks/__tests__/useSuspenseQuery.test.tsx +++ b/src/react/hooks/__tests__/useSuspenseQuery.test.tsx @@ -11816,7 +11816,7 @@ describe("useSuspenseQuery", () => { { const { data } = useSuspenseQuery(maskedQuery); - expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).toEqualTypeOf>(); expectTypeOf(data).not.toEqualTypeOf(); } @@ -11826,8 +11826,8 @@ describe("useSuspenseQuery", () => { VariablesCaseVariables >(maskedQuery); - expectTypeOf(data).toEqualTypeOf(); - expectTypeOf(data).not.toEqualTypeOf(); + expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).not.toEqualTypeOf(); } { @@ -11836,7 +11836,7 @@ describe("useSuspenseQuery", () => { VariablesCaseVariables >(maskedQuery); - expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).toEqualTypeOf>(); expectTypeOf(data).not.toEqualTypeOf(); } }); @@ -11868,7 +11868,9 @@ describe("useSuspenseQuery", () => { errorPolicy: "ignore", }); - expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).toEqualTypeOf< + Masked | undefined + >(); expectTypeOf(data).not.toEqualTypeOf< UnmaskedVariablesCaseData | undefined >(); @@ -11880,11 +11882,9 @@ describe("useSuspenseQuery", () => { VariablesCaseVariables >(maskedQuery, { errorPolicy: "ignore" }); - expectTypeOf(data).toEqualTypeOf< - UnmaskedVariablesCaseData | undefined - >(); + expectTypeOf(data).toEqualTypeOf(); expectTypeOf(data).not.toEqualTypeOf< - MaskedVariablesCaseData | undefined + UnmaskedVariablesCaseData | undefined >(); } @@ -11894,7 +11894,9 @@ describe("useSuspenseQuery", () => { VariablesCaseVariables >(maskedQuery, { errorPolicy: "ignore" }); - expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).toEqualTypeOf< + Masked | undefined + >(); expectTypeOf(data).not.toEqualTypeOf< UnmaskedVariablesCaseData | undefined >(); @@ -11926,7 +11928,9 @@ describe("useSuspenseQuery", () => { { const { data } = useSuspenseQuery(maskedQuery, { errorPolicy: "all" }); - expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).toEqualTypeOf< + Masked | undefined + >(); expectTypeOf(data).not.toEqualTypeOf< UnmaskedVariablesCaseData | undefined >(); @@ -11938,11 +11942,9 @@ describe("useSuspenseQuery", () => { VariablesCaseVariables >(maskedQuery, { errorPolicy: "all" }); - expectTypeOf(data).toEqualTypeOf< - UnmaskedVariablesCaseData | undefined - >(); + expectTypeOf(data).toEqualTypeOf(); expectTypeOf(data).not.toEqualTypeOf< - MaskedVariablesCaseData | undefined + UnmaskedVariablesCaseData | undefined >(); } @@ -11952,7 +11954,9 @@ describe("useSuspenseQuery", () => { VariablesCaseVariables >(maskedQuery, { errorPolicy: "all" }); - expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).toEqualTypeOf< + Masked | undefined + >(); expectTypeOf(data).not.toEqualTypeOf< UnmaskedVariablesCaseData | undefined >(); @@ -11984,7 +11988,7 @@ describe("useSuspenseQuery", () => { { const { data } = useSuspenseQuery(maskedQuery, { errorPolicy: "none" }); - expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).toEqualTypeOf>(); expectTypeOf(data).not.toEqualTypeOf(); } @@ -11994,8 +11998,8 @@ describe("useSuspenseQuery", () => { VariablesCaseVariables >(maskedQuery, { errorPolicy: "none" }); - expectTypeOf(data).toEqualTypeOf(); - expectTypeOf(data).not.toEqualTypeOf(); + expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).not.toEqualTypeOf(); } { @@ -12004,7 +12008,7 @@ describe("useSuspenseQuery", () => { VariablesCaseVariables >(maskedQuery, { errorPolicy: "none" }); - expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).toEqualTypeOf>(); expectTypeOf(data).not.toEqualTypeOf(); } }); @@ -12037,7 +12041,7 @@ describe("useSuspenseQuery", () => { }); expectTypeOf(data).toEqualTypeOf< - DeepPartial + DeepPartial> >(); expectTypeOf(data).not.toEqualTypeOf< DeepPartial @@ -12051,10 +12055,10 @@ describe("useSuspenseQuery", () => { >(maskedQuery, { returnPartialData: true }); expectTypeOf(data).toEqualTypeOf< - DeepPartial + DeepPartial >(); expectTypeOf(data).not.toEqualTypeOf< - DeepPartial + DeepPartial >(); } @@ -12065,7 +12069,7 @@ describe("useSuspenseQuery", () => { >(maskedQuery, { returnPartialData: true }); expectTypeOf(data).toEqualTypeOf< - DeepPartial + DeepPartial> >(); expectTypeOf(data).not.toEqualTypeOf< DeepPartial @@ -12104,7 +12108,7 @@ describe("useSuspenseQuery", () => { returnPartialData: false, }); - expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).toEqualTypeOf>(); expectTypeOf(data).not.toEqualTypeOf(); } @@ -12114,8 +12118,8 @@ describe("useSuspenseQuery", () => { VariablesCaseVariables >(maskedQuery, { returnPartialData: false }); - expectTypeOf(data).toEqualTypeOf(); - expectTypeOf(data).not.toEqualTypeOf(); + expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).not.toEqualTypeOf(); } { @@ -12124,7 +12128,7 @@ describe("useSuspenseQuery", () => { VariablesCaseVariables >(maskedQuery, { returnPartialData: false }); - expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).toEqualTypeOf>(); expectTypeOf(data).not.toEqualTypeOf(); } }); @@ -12168,7 +12172,9 @@ describe("useSuspenseQuery", () => { { const { data } = useSuspenseQuery(maskedQuery, { skip: true }); - expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).toEqualTypeOf< + Masked | undefined + >(); expectTypeOf(data).not.toEqualTypeOf< UnmaskedVariablesCaseData | undefined >(); @@ -12180,11 +12186,9 @@ describe("useSuspenseQuery", () => { VariablesCaseVariables >(maskedQuery, { skip: true }); - expectTypeOf(data).toEqualTypeOf< - UnmaskedVariablesCaseData | undefined - >(); + expectTypeOf(data).toEqualTypeOf(); expectTypeOf(data).not.toEqualTypeOf< - MaskedVariablesCaseData | undefined + UnmaskedVariablesCaseData | undefined >(); } @@ -12194,7 +12198,9 @@ describe("useSuspenseQuery", () => { VariablesCaseVariables >(maskedQuery, { skip: true }); - expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).toEqualTypeOf< + Masked | undefined + >(); expectTypeOf(data).not.toEqualTypeOf< UnmaskedVariablesCaseData | undefined >(); @@ -12210,7 +12216,9 @@ describe("useSuspenseQuery", () => { VariablesCaseVariables >(maskedQuery, { skip: options.skip }); - expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).toEqualTypeOf< + Masked | undefined + >(); expectTypeOf(data).not.toEqualTypeOf< UnmaskedVariablesCaseData | undefined >(); @@ -12247,7 +12255,9 @@ describe("useSuspenseQuery", () => { options.skip ? skipToken : { variables: { id: "1" } } ); - expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).toEqualTypeOf< + Masked | undefined + >(); expectTypeOf(data).not.toEqualTypeOf< UnmaskedVariablesCaseData | undefined >(); @@ -12259,11 +12269,9 @@ describe("useSuspenseQuery", () => { VariablesCaseVariables >(maskedQuery, options.skip ? skipToken : { variables: { id: "1" } }); - expectTypeOf(data).toEqualTypeOf< - UnmaskedVariablesCaseData | undefined - >(); + expectTypeOf(data).toEqualTypeOf(); expectTypeOf(data).not.toEqualTypeOf< - MaskedVariablesCaseData | undefined + UnmaskedVariablesCaseData | undefined >(); } @@ -12273,7 +12281,9 @@ describe("useSuspenseQuery", () => { VariablesCaseVariables >(maskedQuery, options.skip ? skipToken : { variables: { id: "1" } }); - expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).toEqualTypeOf< + Masked | undefined + >(); expectTypeOf(data).not.toEqualTypeOf< UnmaskedVariablesCaseData | undefined >(); @@ -12310,7 +12320,9 @@ describe("useSuspenseQuery", () => { options.skip ? skipToken : undefined ); - expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).toEqualTypeOf< + Masked | undefined + >(); expectTypeOf(data).not.toEqualTypeOf< UnmaskedVariablesCaseData | undefined >(); @@ -12322,11 +12334,9 @@ describe("useSuspenseQuery", () => { VariablesCaseVariables >(maskedQuery, options.skip ? skipToken : undefined); - expectTypeOf(data).toEqualTypeOf< - UnmaskedVariablesCaseData | undefined - >(); + expectTypeOf(data).toEqualTypeOf(); expectTypeOf(data).not.toEqualTypeOf< - MaskedVariablesCaseData | undefined + UnmaskedVariablesCaseData | undefined >(); } @@ -12336,7 +12346,9 @@ describe("useSuspenseQuery", () => { VariablesCaseVariables >(maskedQuery, options.skip ? skipToken : undefined); - expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).toEqualTypeOf< + Masked | undefined + >(); expectTypeOf(data).not.toEqualTypeOf< UnmaskedVariablesCaseData | undefined >(); @@ -12378,7 +12390,7 @@ describe("useSuspenseQuery", () => { ); expectTypeOf(data).toEqualTypeOf< - DeepPartial | undefined + DeepPartial> | undefined >(); expectTypeOf(data).not.toEqualTypeOf< DeepPartial | undefined @@ -12392,10 +12404,10 @@ describe("useSuspenseQuery", () => { >(maskedQuery, options.skip ? skipToken : { returnPartialData: true }); expectTypeOf(data).toEqualTypeOf< - DeepPartial | undefined + DeepPartial | undefined >(); expectTypeOf(data).not.toEqualTypeOf< - DeepPartial | undefined + DeepPartial | undefined >(); } @@ -12406,7 +12418,7 @@ describe("useSuspenseQuery", () => { >(maskedQuery, options.skip ? skipToken : { returnPartialData: true }); expectTypeOf(data).toEqualTypeOf< - DeepPartial | undefined + DeepPartial> | undefined >(); expectTypeOf(data).not.toEqualTypeOf< DeepPartial | undefined @@ -12445,7 +12457,7 @@ describe("useSuspenseQuery", () => { fetchPolicy: "no-cache", }); - expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).toEqualTypeOf>(); expectTypeOf(data).not.toEqualTypeOf(); } @@ -12455,8 +12467,8 @@ describe("useSuspenseQuery", () => { VariablesCaseVariables >(maskedQuery, { fetchPolicy: "no-cache" }); - expectTypeOf(data).toEqualTypeOf(); - expectTypeOf(data).not.toEqualTypeOf(); + expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).not.toEqualTypeOf(); } { @@ -12465,7 +12477,7 @@ describe("useSuspenseQuery", () => { VariablesCaseVariables >(maskedQuery, { fetchPolicy: "no-cache" }); - expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).toEqualTypeOf>(); expectTypeOf(data).not.toEqualTypeOf(); } }); @@ -12516,7 +12528,7 @@ describe("useSuspenseQuery", () => { }); expectTypeOf(data).toEqualTypeOf< - DeepPartial | undefined + DeepPartial> | undefined >(); expectTypeOf(data).not.toEqualTypeOf< DeepPartial | undefined @@ -12557,7 +12569,7 @@ describe("useSuspenseQuery", () => { }); expectTypeOf(data).toEqualTypeOf< - DeepPartial | undefined + DeepPartial> | undefined >(); expectTypeOf(data).not.toEqualTypeOf< DeepPartial | undefined @@ -12595,7 +12607,9 @@ describe("useSuspenseQuery", () => { errorPolicy: "ignore", }); - expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).toEqualTypeOf< + Masked | undefined + >(); expectTypeOf(data).not.toEqualTypeOf< UnmaskedVariablesCaseData | undefined >(); @@ -12630,7 +12644,9 @@ describe("useSuspenseQuery", () => { errorPolicy: "none", }); - expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(data).toEqualTypeOf< + Masked | undefined + >(); expectTypeOf(data).not.toEqualTypeOf< UnmaskedVariablesCaseData | undefined >(); @@ -12673,7 +12689,7 @@ describe("useSuspenseQuery", () => { }); expectTypeOf(data).toEqualTypeOf< - DeepPartial | undefined + DeepPartial> | undefined >(); expectTypeOf(data).not.toEqualTypeOf< DeepPartial | undefined @@ -12715,7 +12731,7 @@ describe("useSuspenseQuery", () => { }); expectTypeOf(data).toEqualTypeOf< - DeepPartial + DeepPartial> >(); expectTypeOf(data).not.toEqualTypeOf< DeepPartial @@ -12733,10 +12749,10 @@ describe("useSuspenseQuery", () => { }); expectTypeOf(data).toEqualTypeOf< - DeepPartial + DeepPartial >(); expectTypeOf(data).not.toEqualTypeOf< - DeepPartial + DeepPartial >(); } @@ -12751,7 +12767,7 @@ describe("useSuspenseQuery", () => { }); expectTypeOf(data).toEqualTypeOf< - DeepPartial + DeepPartial> >(); expectTypeOf(data).not.toEqualTypeOf< DeepPartial @@ -12767,7 +12783,9 @@ describe("useSuspenseQuery", () => { const result = await refetch(); - expectTypeOf(result.data).toEqualTypeOf(); + expectTypeOf(result.data).toEqualTypeOf< + Masked + >(); expectTypeOf( result.data ).not.toEqualTypeOf(); @@ -12778,8 +12796,10 @@ describe("useSuspenseQuery", () => { const result = await refetch(); - expectTypeOf(result.data).toEqualTypeOf(); - expectTypeOf(result.data).not.toEqualTypeOf(); + expectTypeOf(result.data).toEqualTypeOf(); + expectTypeOf( + result.data + ).not.toEqualTypeOf(); } }); @@ -12807,7 +12827,9 @@ describe("useSuspenseQuery", () => { }, }); - expectTypeOf(result.data).toEqualTypeOf(); + expectTypeOf(result.data).toEqualTypeOf< + Masked + >(); expectTypeOf( result.data ).not.toEqualTypeOf(); @@ -12834,8 +12856,10 @@ describe("useSuspenseQuery", () => { }, }); - expectTypeOf(result.data).toEqualTypeOf(); - expectTypeOf(result.data).not.toEqualTypeOf(); + expectTypeOf(result.data).toEqualTypeOf(); + expectTypeOf( + result.data + ).not.toEqualTypeOf(); } }); diff --git a/src/testing/core/observableToPromise.ts b/src/testing/core/observableToPromise.ts deleted file mode 100644 index 428517e1aff..00000000000 --- a/src/testing/core/observableToPromise.ts +++ /dev/null @@ -1,103 +0,0 @@ -import type { ObservableQuery, ApolloQueryResult } from "../../core/index.js"; -import type { ObservableSubscription } from "../../utilities/index.js"; - -export interface Options { - /** - * The ObservableQuery to subscribe to. - */ - observable: ObservableQuery; - /** - * Should we resolve after seeing all our callbacks? [default: true] - * (use this if you are racing the promise against another) - */ - shouldResolve?: boolean; - /** - * How long to wait after seeing desired callbacks before resolving? - * [default: -1 => don't wait] - */ - wait?: number; - /** - * An expected set of errors. - */ - errorCallbacks?: ((error: Error) => any)[]; -} - -export type ResultCallback = (result: ApolloQueryResult) => any; - -// Take an observable and N callbacks, and observe the observable, -// ensuring it is called exactly N times, resolving once it has done so. -// Optionally takes a timeout, which it will wait X ms after the Nth callback -// to ensure it is not called again. -export function observableToPromiseAndSubscription( - { observable, shouldResolve = true, wait = -1, errorCallbacks = [] }: Options, - ...cbs: ResultCallback[] -): { promise: Promise; subscription: ObservableSubscription } { - let subscription: ObservableSubscription = null as never; - const promise = new Promise((resolve, reject) => { - let errorIndex = 0; - let cbIndex = 0; - const results: any[] = []; - - const tryToResolve = () => { - if (!shouldResolve) { - return; - } - - const done = () => { - subscription.unsubscribe(); - // XXX: we could pass a few other things out here? - resolve(results); - }; - - if (cbIndex === cbs.length && errorIndex === errorCallbacks.length) { - if (wait === -1) { - done(); - } else { - setTimeout(done, wait); - } - } - }; - - let queue = Promise.resolve(); - - subscription = observable.subscribe({ - next(result: ApolloQueryResult) { - queue = queue - .then(() => { - const cb = cbs[cbIndex++]; - if (cb) return cb(result); - reject( - new Error( - `Observable 'next' method called more than ${cbs.length} times` - ) - ); - }) - .then((res) => { - results.push(res); - tryToResolve(); - }, reject); - }, - error(error: Error) { - queue = queue - .then(() => { - const errorCb = errorCallbacks[errorIndex++]; - if (errorCb) return errorCb(error); - reject(error); - }) - .then(tryToResolve, reject); - }, - }); - }); - - return { - promise, - subscription, - }; -} - -export default function ( - options: Options, - ...cbs: ResultCallback[] -): Promise { - return observableToPromiseAndSubscription(options, ...cbs).promise; -} diff --git a/src/testing/internal/ObservableStream.ts b/src/testing/internal/ObservableStream.ts index f6c53169b87..ad5d7c05175 100644 --- a/src/testing/internal/ObservableStream.ts +++ b/src/testing/internal/ObservableStream.ts @@ -1,3 +1,7 @@ +import type { Tester } from "@jest/expect-utils"; +import { equals, iterableEquality } from "@jest/expect-utils"; +import { expect } from "@jest/globals"; +import * as matcherUtils from "jest-matcher-utils"; import type { Observable, ObservableSubscription, @@ -31,7 +35,7 @@ export class ObservableStream { take({ timeout = 100 }: TakeOptions = {}) { return Promise.race([ this.reader.read().then((result) => result.value!), - new Promise((_, reject) => { + new Promise>((_, reject) => { setTimeout( reject, timeout, @@ -47,18 +51,57 @@ export class ObservableStream { async takeNext(options?: TakeOptions): Promise { const event = await this.take(options); - expect(event).toEqual({ type: "next", value: expect.anything() }); + validateEquals(event, { type: "next", value: expect.anything() }); return (event as ObservableEvent & { type: "next" }).value; } async takeError(options?: TakeOptions): Promise { const event = await this.take(options); - expect(event).toEqual({ type: "error", error: expect.anything() }); + validateEquals(event, { type: "error", error: expect.anything() }); return (event as ObservableEvent & { type: "error" }).error; } async takeComplete(options?: TakeOptions): Promise { const event = await this.take(options); - expect(event).toEqual({ type: "complete" }); + validateEquals(event, { type: "complete" }); } } + +// Lightweight expect(...).toEqual(...) check that avoids using `expect` so that +// `expect.assertions(num)` does not double count assertions when using the take* +// functions inside of expect(stream).toEmit* matchers. +function validateEquals( + actualEvent: ObservableEvent, + expectedEvent: ObservableEvent +) { + // Uses the same matchers as expect(...).toEqual(...) + // https://github.com/jestjs/jest/blob/611d1a4ba0008d67b5dcda485177f0813b2b573e/packages/expect/src/matchers.ts#L626-L629 + const isEqual = equals(actualEvent, expectedEvent, [ + ...getCustomMatchers(), + iterableEquality, + ]); + + if (isEqual) { + return; + } + + const hint = matcherUtils.matcherHint("toEqual", "stream", "expected"); + + throw new Error( + hint + + "\n\n" + + matcherUtils.printDiffOrStringify( + expectedEvent, + actualEvent, + "Expected", + "Received", + true + ) + ); +} + +function getCustomMatchers(): Array { + // https://github.com/jestjs/jest/blob/611d1a4ba0008d67b5dcda485177f0813b2b573e/packages/expect/src/jestMatchersObject.ts#L141-L143 + const JEST_MATCHERS_OBJECT = Symbol.for("$$jest-matchers-object"); + return (globalThis as any)[JEST_MATCHERS_OBJECT].customEqualityTesters; +} diff --git a/src/testing/react/__tests__/MockedProvider.test.tsx b/src/testing/react/__tests__/MockedProvider.test.tsx index f4a5caed150..8751429cce9 100644 --- a/src/testing/react/__tests__/MockedProvider.test.tsx +++ b/src/testing/react/__tests__/MockedProvider.test.tsx @@ -3,7 +3,7 @@ import { DocumentNode } from "graphql"; import { act, render, screen, waitFor } from "@testing-library/react"; import gql from "graphql-tag"; -import { itAsync, MockedResponse, MockLink } from "../../core"; +import { MockedResponse, MockLink } from "../../core"; import { MockedProvider } from "../MockedProvider"; import { useQuery } from "../../../react/hooks"; import { InMemoryCache } from "../../../cache"; @@ -82,7 +82,7 @@ describe("General use", () => { errorThrown = false; }); - itAsync("should mock the data", (resolve, reject) => { + it("should mock the data", async () => { let finished = false; function Component({ username }: Variables) { const { loading, data } = useQuery(query, { variables }); @@ -99,106 +99,97 @@ describe("General use", () => { ); - waitFor(() => { + await waitFor(() => { expect(finished).toBe(true); - }).then(resolve, reject); + }); }); - itAsync( - "should pass the variables to the result function", - async (resolve, reject) => { - function Component({ ...variables }: Variables) { - useQuery(query, { variables }); - return null; - } + it("should pass the variables to the result function", async () => { + function Component({ ...variables }: Variables) { + useQuery(query, { variables }); + return null; + } - const mock2: MockedResponse = { - request: { - query, - variables, - }, - result: jest.fn().mockResolvedValue({ data: { user } }), - }; + const mock2: MockedResponse = { + request: { + query, + variables, + }, + result: jest.fn().mockResolvedValue({ data: { user } }), + }; - render( - - - - ); + render( + + + + ); - waitFor(() => { - expect(mock2.result as jest.Mock).toHaveBeenCalledWith(variables); - }).then(resolve, reject); + await waitFor(() => { + expect(mock2.result as jest.Mock).toHaveBeenCalledWith(variables); + }); + }); + + it("should pass the variables to the variableMatcher", async () => { + function Component({ ...variables }: Variables) { + useQuery(query, { variables }); + return null; } - ); - - itAsync( - "should pass the variables to the variableMatcher", - async (resolve, reject) => { - function Component({ ...variables }: Variables) { - useQuery(query, { variables }); - return null; - } - const mock2: MockedResponse = { - request: { - query, - }, - variableMatcher: jest.fn().mockReturnValue(true), - result: { data: { user } }, - }; + const mock2: MockedResponse = { + request: { + query, + }, + variableMatcher: jest.fn().mockReturnValue(true), + result: { data: { user } }, + }; - render( - - - - ); + render( + + + + ); - waitFor(() => { - expect(mock2.variableMatcher as jest.Mock).toHaveBeenCalledWith( - variables - ); - }).then(resolve, reject); - } - ); + await waitFor(() => { + expect(mock2.variableMatcher as jest.Mock).toHaveBeenCalledWith( + variables + ); + }); + }); - itAsync( - "should use a mock if the variableMatcher returns true", - async (resolve, reject) => { - let finished = false; + it("should use a mock if the variableMatcher returns true", async () => { + let finished = false; - function Component({ username }: Variables) { - const { loading, data } = useQuery(query, { - variables, - }); - if (!loading) { - expect(data!.user).toMatchSnapshot(); - finished = true; - } - return null; + function Component({ username }: Variables) { + const { loading, data } = useQuery(query, { + variables, + }); + if (!loading) { + expect(data!.user).toMatchSnapshot(); + finished = true; } + return null; + } - const mock2: MockedResponse = { - request: { - query, - }, - variableMatcher: (v) => v.username === variables.username, - result: { data: { user } }, - }; + const mock2: MockedResponse = { + request: { + query, + }, + variableMatcher: (v) => v.username === variables.username, + result: { data: { user } }, + }; - render( - - - - ); + render( + + + + ); - waitFor(() => { - expect(finished).toBe(true); - }).then(resolve, reject); - } - ); + await waitFor(() => { + expect(finished).toBe(true); + }); + }); - itAsync("should allow querying with the typename", (resolve, reject) => { + it("should allow querying with the typename", async () => { let finished = false; function Component({ username }: Variables) { const { loading, data } = useQuery(query, { variables }); @@ -225,12 +216,12 @@ describe("General use", () => { ); - waitFor(() => { + await waitFor(() => { expect(finished).toBe(true); - }).then(resolve, reject); + }); }); - itAsync("should allow using a custom cache", (resolve, reject) => { + it("should allow using a custom cache", async () => { let finished = false; const cache = new InMemoryCache(); cache.writeQuery({ @@ -254,169 +245,157 @@ describe("General use", () => { ); - waitFor(() => { + await waitFor(() => { expect(finished).toBe(true); - }).then(resolve, reject); + }); }); - itAsync( - "should error if the variables in the mock and component do not match", - (resolve, reject) => { - let finished = false; - function Component({ ...variables }: Variables) { - const { loading, error } = useQuery(query, { - variables, - }); - if (!loading) { - expect(error).toMatchSnapshot(); - finished = true; - } - return null; + it("should error if the variables in the mock and component do not match", async () => { + let finished = false; + function Component({ ...variables }: Variables) { + const { loading, error } = useQuery(query, { + variables, + }); + if (!loading) { + expect(error).toMatchSnapshot(); + finished = true; } + return null; + } - const variables2 = { - username: "other_user", - age: undefined, - }; + const variables2 = { + username: "other_user", + age: undefined, + }; - render( - - - - ); + render( + + + + ); - waitFor(() => { - expect(finished).toBe(true); - }).then(resolve, reject); - } - ); - - itAsync( - "should error if the variableMatcher returns false", - async (resolve, reject) => { - let finished = false; - function Component({ ...variables }: Variables) { - const { loading, error } = useQuery(query, { - variables, - }); - if (!loading) { - expect(error).toMatchSnapshot(); - finished = true; - } - return null; + await waitFor(() => { + expect(finished).toBe(true); + }); + }); + + it("should error if the variableMatcher returns false", async () => { + let finished = false; + function Component({ ...variables }: Variables) { + const { loading, error } = useQuery(query, { + variables, + }); + if (!loading) { + expect(error).toMatchSnapshot(); + finished = true; } + return null; + } - const mock2: MockedResponse = { - request: { - query, - }, - variableMatcher: () => false, - result: { data: { user } }, - }; + const mock2: MockedResponse = { + request: { + query, + }, + variableMatcher: () => false, + result: { data: { user } }, + }; - render( - - - - ); + render( + + + + ); - waitFor(() => { - expect(finished).toBe(true); - }).then(resolve, reject); - } - ); - - itAsync( - "should error if the variables do not deep equal", - (resolve, reject) => { - let finished = false; - function Component({ ...variables }: Variables) { - const { loading, error } = useQuery(query, { - variables, - }); - if (!loading) { - expect(error).toMatchSnapshot(); - finished = true; - } - return null; + await waitFor(() => { + expect(finished).toBe(true); + }); + }); + + it("should error if the variables do not deep equal", async () => { + let finished = false; + function Component({ ...variables }: Variables) { + const { loading, error } = useQuery(query, { + variables, + }); + if (!loading) { + expect(error).toMatchSnapshot(); + finished = true; } + return null; + } - const mocks2 = [ - { - request: { - query, - variables: { - age: 13, - username: "some_user", - }, + const mocks2 = [ + { + request: { + query, + variables: { + age: 13, + username: "some_user", }, - result: { data: { user } }, }, - ]; + result: { data: { user } }, + }, + ]; - const variables2 = { - username: "some_user", - age: 42, - }; + const variables2 = { + username: "some_user", + age: 42, + }; - render( - - - - ); + render( + + + + ); - waitFor(() => { - expect(finished).toBe(true); - }).then(resolve, reject); - } - ); - - itAsync( - "should not error if the variables match but have different order", - (resolve, reject) => { - let finished = false; - function Component({ ...variables }: Variables) { - const { loading, data } = useQuery(query, { - variables, - }); - if (!loading) { - expect(data).toMatchSnapshot(); - finished = true; - } - return null; + await waitFor(() => { + expect(finished).toBe(true); + }); + }); + + it("should not error if the variables match but have different order", async () => { + let finished = false; + function Component({ ...variables }: Variables) { + const { loading, data } = useQuery(query, { + variables, + }); + if (!loading) { + expect(data).toMatchSnapshot(); + finished = true; } + return null; + } - const mocks2 = [ - { - request: { - query, - variables: { - age: 13, - username: "some_user", - }, + const mocks2 = [ + { + request: { + query, + variables: { + age: 13, + username: "some_user", }, - result: { data: { user } }, }, - ]; + result: { data: { user } }, + }, + ]; - const variables2 = { - username: "some_user", - age: 13, - }; + const variables2 = { + username: "some_user", + age: 13, + }; - render( - - - - ); + render( + + + + ); - waitFor(() => { - expect(finished).toBe(true); - }).then(resolve, reject); - } - ); + await waitFor(() => { + expect(finished).toBe(true); + }); + }); - itAsync("should support mocking a network error", (resolve, reject) => { + it("should support mocking a network error", async () => { let finished = false; function Component({ ...variables }: Variables) { const { loading, error } = useQuery(query, { @@ -447,53 +426,50 @@ describe("General use", () => { ); - waitFor(() => { + await waitFor(() => { expect(finished).toBe(true); - }).then(resolve, reject); + }); }); - itAsync( - "should error if the query in the mock and component do not match", - (resolve, reject) => { - let finished = false; - function Component({ ...variables }: Variables) { - const { loading, error } = useQuery(query, { - variables, - }); - if (!loading) { - expect(error).toMatchSnapshot(); - finished = true; - } - return null; + it("should error if the query in the mock and component do not match", async () => { + let finished = false; + function Component({ ...variables }: Variables) { + const { loading, error } = useQuery(query, { + variables, + }); + if (!loading) { + expect(error).toMatchSnapshot(); + finished = true; } + return null; + } - const mocksDifferentQuery = [ - { - request: { - query: gql` - query OtherQuery { - otherQuery { - id - } + const mocksDifferentQuery = [ + { + request: { + query: gql` + query OtherQuery { + otherQuery { + id } - `, - variables, - }, - result: { data: { user } }, + } + `, + variables, }, - ]; + result: { data: { user } }, + }, + ]; - render( - - - - ); + render( + + + + ); - waitFor(() => { - expect(finished).toBe(true); - }).then(resolve, reject); - } - ); + await waitFor(() => { + expect(finished).toBe(true); + }); + }); it("should pass down props prop in mock as props for the component", () => { function Component({ ...variables }) { @@ -523,71 +499,68 @@ describe("General use", () => { unmount(); }); - itAsync( - "should support returning mocked results from a function", - (resolve, reject) => { - let finished = false; - let resultReturned = false; + it("should support returning mocked results from a function", async () => { + let finished = false; + let resultReturned = false; - const testUser = { - __typename: "User", - id: 12345, - }; + const testUser = { + __typename: "User", + id: 12345, + }; - function Component({ ...variables }: Variables) { - const { loading, data } = useQuery(query, { - variables, - }); - if (!loading) { - expect(data!.user).toEqual(testUser); - expect(resultReturned).toBe(true); - finished = true; - } - return null; + function Component({ ...variables }: Variables) { + const { loading, data } = useQuery(query, { + variables, + }); + if (!loading) { + expect(data!.user).toEqual(testUser); + expect(resultReturned).toBe(true); + finished = true; } + return null; + } - const testQuery: DocumentNode = gql` - query GetUser($username: String!) { - user(username: $username) { - id - } + const testQuery: DocumentNode = gql` + query GetUser($username: String!) { + user(username: $username) { + id } - `; + } + `; - const testVariables = { - username: "jsmith", - }; - const testMocks = [ - { - request: { - query: testQuery, - variables: testVariables, - }, - result() { - resultReturned = true; - return { - data: { - user: { - __typename: "User", - id: 12345, - }, + const testVariables = { + username: "jsmith", + }; + const testMocks = [ + { + request: { + query: testQuery, + variables: testVariables, + }, + result() { + resultReturned = true; + return { + data: { + user: { + __typename: "User", + id: 12345, }, - }; - }, + }, + }; }, - ]; + }, + ]; - render( - - - - ); + render( + + + + ); - waitFor(() => { - expect(finished).toBe(true); - }).then(resolve, reject); - } - ); + await waitFor(() => { + expect(finished).toBe(true); + }); + }); it('should return "No more mocked responses" errors in response', async () => { let finished = false; @@ -1028,66 +1001,60 @@ describe("General use", () => { consoleSpy.mockRestore(); }); - itAsync( - "should support custom error handling using setOnError", - (resolve, reject) => { - let finished = false; - function Component({ ...variables }: Variables) { - useQuery(query, { variables }); - return null; - } + it("should support custom error handling using setOnError", async () => { + let finished = false; + function Component({ ...variables }: Variables) { + useQuery(query, { variables }); + return null; + } - const mockLink = new MockLink([], true, { showWarnings: false }); - mockLink.setOnError((error) => { - expect(error).toMatchSnapshot(); - finished = true; - }); - const link = ApolloLink.from([errorLink, mockLink]); + const mockLink = new MockLink([], true, { showWarnings: false }); + mockLink.setOnError((error) => { + expect(error).toMatchSnapshot(); + finished = true; + }); + const link = ApolloLink.from([errorLink, mockLink]); - render( - - - - ); + render( + + + + ); - waitFor(() => { - expect(finished).toBe(true); - }).then(resolve, reject); - } - ); - - itAsync( - "should pipe exceptions thrown in custom onError functions through the link chain", - (resolve, reject) => { - let finished = false; - function Component({ ...variables }: Variables) { - const { loading, error } = useQuery(query, { - variables, - }); - if (!loading) { - expect(error).toMatchSnapshot(); - finished = true; - } - return null; - } + await waitFor(() => { + expect(finished).toBe(true); + }); + }); - const mockLink = new MockLink([], true, { showWarnings: false }); - mockLink.setOnError(() => { - throw new Error("oh no!"); + it("should pipe exceptions thrown in custom onError functions through the link chain", async () => { + let finished = false; + function Component({ ...variables }: Variables) { + const { loading, error } = useQuery(query, { + variables, }); - const link = ApolloLink.from([errorLink, mockLink]); + if (!loading) { + expect(error).toMatchSnapshot(); + finished = true; + } + return null; + } - render( - - - - ); + const mockLink = new MockLink([], true, { showWarnings: false }); + mockLink.setOnError(() => { + throw new Error("oh no!"); + }); + const link = ApolloLink.from([errorLink, mockLink]); - waitFor(() => { - expect(finished).toBe(true); - }).then(resolve, reject); - } - ); + render( + + + + ); + + await waitFor(() => { + expect(finished).toBe(true); + }); + }); it("should support loading state testing with delay", async () => { jest.useFakeTimers(); @@ -1224,100 +1191,94 @@ describe("General use", () => { }); describe("@client testing", () => { - itAsync( - "should support @client fields with a custom cache", - (resolve, reject) => { - let finished = false; - const cache = new InMemoryCache(); - - cache.writeQuery({ - query: gql` - { - networkStatus { - isOnline - } + it("should support @client fields with a custom cache", async () => { + let finished = false; + const cache = new InMemoryCache(); + + cache.writeQuery({ + query: gql` + { + networkStatus { + isOnline } - `, - data: { - networkStatus: { - __typename: "NetworkStatus", - isOnline: true, - }, + } + `, + data: { + networkStatus: { + __typename: "NetworkStatus", + isOnline: true, }, - }); + }, + }); - function Component() { - const { loading, data } = useQuery(gql` - { - networkStatus @client { - isOnline - } + function Component() { + const { loading, data } = useQuery(gql` + { + networkStatus @client { + isOnline } - `); - if (!loading) { - expect(data!.networkStatus.__typename).toEqual("NetworkStatus"); - expect(data!.networkStatus.isOnline).toEqual(true); - finished = true; } - return null; + `); + if (!loading) { + expect(data!.networkStatus.__typename).toEqual("NetworkStatus"); + expect(data!.networkStatus.isOnline).toEqual(true); + finished = true; } + return null; + } - render( - - - - ); + render( + + + + ); - waitFor(() => { - expect(finished).toBe(true); - }).then(resolve, reject); - } - ); - - itAsync( - "should support @client fields with field policies", - (resolve, reject) => { - let finished = false; - const cache = new InMemoryCache({ - typePolicies: { - Query: { - fields: { - networkStatus() { - return { - __typename: "NetworkStatus", - isOnline: true, - }; - }, + await waitFor(() => { + expect(finished).toBe(true); + }); + }); + + it("should support @client fields with field policies", async () => { + let finished = false; + const cache = new InMemoryCache({ + typePolicies: { + Query: { + fields: { + networkStatus() { + return { + __typename: "NetworkStatus", + isOnline: true, + }; }, }, }, - }); + }, + }); - function Component() { - const { loading, data } = useQuery(gql` - { - networkStatus @client { - isOnline - } + function Component() { + const { loading, data } = useQuery(gql` + { + networkStatus @client { + isOnline } - `); - if (!loading) { - expect(data!.networkStatus.__typename).toEqual("NetworkStatus"); - expect(data!.networkStatus.isOnline).toEqual(true); - finished = true; } - return null; + `); + if (!loading) { + expect(data!.networkStatus.__typename).toEqual("NetworkStatus"); + expect(data!.networkStatus.isOnline).toEqual(true); + finished = true; } + return null; + } - render( - - - - ); + render( + + + + ); - waitFor(() => { - expect(finished).toBe(true); - }).then(resolve, reject); - } - ); + await waitFor(() => { + expect(finished).toBe(true); + }); + }); }); diff --git a/src/utilities/observables/__tests__/Concast.ts b/src/utilities/observables/__tests__/Concast.ts index b590cde2fb8..256f6651929 100644 --- a/src/utilities/observables/__tests__/Concast.ts +++ b/src/utilities/observables/__tests__/Concast.ts @@ -1,9 +1,9 @@ -import { itAsync } from "../../../testing/core"; import { Observable, Observer } from "../Observable"; import { Concast, ConcastSourcesIterable } from "../Concast"; +import { ObservableStream } from "../../../testing/internal"; describe("Concast Observable (similar to Behavior Subject in RxJS)", () => { - itAsync("can concatenate other observables", (resolve, reject) => { + it("can concatenate other observables", async () => { const concast = new Concast([ Observable.of(1, 2, 3), Promise.resolve(Observable.of(4, 5)), @@ -12,114 +12,94 @@ describe("Concast Observable (similar to Behavior Subject in RxJS)", () => { Observable.of(11), ]); - const results: number[] = []; - concast.subscribe({ - next(num) { - results.push(num); - }, + const stream = new ObservableStream(concast); + + await expect(stream).toEmitValue(1); + await expect(stream).toEmitValue(2); + await expect(stream).toEmitValue(3); + await expect(stream).toEmitValue(4); + await expect(stream).toEmitValue(5); + await expect(stream).toEmitValue(6); + await expect(stream).toEmitValue(7); + await expect(stream).toEmitValue(8); + await expect(stream).toEmitValue(9); + await expect(stream).toEmitValue(10); + await expect(stream).toEmitValue(11); + await expect(stream).toComplete(); + + const finalResult = await concast.promise; - error: reject, + expect(finalResult).toBe(11); + }); + it("Can tolerate being completed before input Promise resolves", async () => { + let resolvePromise: (sources: ConcastSourcesIterable) => void; + const delayPromise = new Promise>( + (resolve) => { + resolvePromise = resolve; + } + ); + + const concast = new Concast(delayPromise); + const observer = { + next() { + throw new Error("should not have called observer.next"); + }, + error() { + throw new Error("Should not have called observer.error"); + }, complete() { - concast.promise - .then((finalResult) => { - expect(results).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]); - expect(finalResult).toBe(11); - resolve(); - }) - .catch(reject); + throw new Error("should not have called observer.complete"); }, - }); + }; + + concast.addObserver(observer); + concast.removeObserver(observer); + + const finalResult = await concast.promise; + expect(finalResult).toBeUndefined(); + + resolvePromise!([]); + const delayedPromiseResult = await delayPromise; + + expect(delayedPromiseResult).toEqual([]); }); - itAsync( - "Can tolerate being completed before input Promise resolves", - (resolve, reject) => { - let resolvePromise: (sources: ConcastSourcesIterable) => void; - const delayPromise = new Promise>( - (resolve) => { - resolvePromise = resolve; - } - ); - - const concast = new Concast(delayPromise); - const observer = { - next() { - reject(new Error("should not have called observer.next")); - }, - error: reject, - complete() { - reject(new Error("should not have called observer.complete")); - }, - }; - - concast.addObserver(observer); - concast.removeObserver(observer); - - return concast.promise - .then((finalResult) => { - expect(finalResult).toBeUndefined(); - resolvePromise([]); - return delayPromise; - }) - .then((delayedPromiseResult) => { - expect(delayedPromiseResult).toEqual([]); - resolve(); - }) - .catch(reject); - } - ); - - itAsync( - "behaves appropriately if unsubscribed before first result", - (resolve, reject) => { - const concast = new Concast([ - new Promise((resolve) => setTimeout(resolve, 100)).then(() => - Observable.of(1, 2, 3) - ), - ]); + it("behaves appropriately if unsubscribed before first result", async () => { + const concast = new Concast([ + new Promise((resolve) => setTimeout(resolve, 100)).then(() => + Observable.of(1, 2, 3) + ), + ]); - const cleanupCounts = { - first: 0, - second: 0, - }; + const cleanupCounts = { + first: 0, + second: 0, + }; - concast.beforeNext(() => { - ++cleanupCounts.first; - }); + concast.beforeNext(() => { + ++cleanupCounts.first; + }); + const stream = new ObservableStream(concast); - const unsubscribe = concast.subscribe({ - next() { - reject("should not have called observer.next"); - }, - error() { - reject("should not have called observer.error"); - }, - complete() { - reject("should not have called observer.complete"); - }, - }); + concast.beforeNext(() => { + ++cleanupCounts.second; + }); - concast.beforeNext(() => { - ++cleanupCounts.second; - }); + // Immediately unsubscribe the observer we just added, triggering + // completion. + stream.unsubscribe(); - // Immediately unsubscribe the observer we just added, triggering - // completion. - unsubscribe.unsubscribe(); - - return concast.promise - .then((finalResult) => { - expect(finalResult).toBeUndefined(); - expect(cleanupCounts).toEqual({ - first: 1, - second: 1, - }); - resolve(); - }) - .catch(reject); - } - ); + const finalResult = await concast.promise; + + expect(finalResult).toBeUndefined(); + expect(cleanupCounts).toEqual({ + first: 1, + second: 1, + }); + + await expect(stream).not.toEmitAnything(); + }); it("concast.beforeNext listeners run before next result/error", () => { const log: Array = [];