From 861639d284098bbd737f1aa73c364c4be47e04f6 Mon Sep 17 00:00:00 2001 From: alessia Date: Thu, 18 May 2023 14:09:38 -0400 Subject: [PATCH 01/17] chore: improve type signature of useBackgroundQuery refetch --- src/react/hooks/__tests__/useBackgroundQuery.test.tsx | 6 ++---- src/react/hooks/useBackgroundQuery.ts | 7 ++----- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx index 8f841823cf9..bac52940cab 100644 --- a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx +++ b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx @@ -38,7 +38,7 @@ import { useBackgroundQuery, useReadQuery } from '../useBackgroundQuery'; import { ApolloProvider } from '../../context'; import { SuspenseCache } from '../../cache'; import { InMemoryCache } from '../../../cache'; -import { FetchMoreFunction } from '../../../react'; +import { FetchMoreFunction, RefetchFunction } from '../../../react'; import { QueryReference } from '../../cache/QueryReference'; function renderIntegrationTest({ @@ -1875,9 +1875,7 @@ describe('useBackgroundQuery', () => { queryRef, refetch, }: { - refetch: ( - variables?: Partial | undefined - ) => Promise>; + refetch: RefetchFunction; queryRef: QueryReference; onChange: (id: string) => void; }) { diff --git a/src/react/hooks/useBackgroundQuery.ts b/src/react/hooks/useBackgroundQuery.ts index db43c4a1c13..881199b1464 100644 --- a/src/react/hooks/useBackgroundQuery.ts +++ b/src/react/hooks/useBackgroundQuery.ts @@ -6,10 +6,7 @@ import type { } from '../../core'; import { useApolloClient } from './useApolloClient'; import type { QueryReference } from '../cache/QueryReference'; -import type { - SuspenseQueryHookOptions, - ObservableQueryFields, -} from '../types/types'; +import type { SuspenseQueryHookOptions } from '../types/types'; import { __use } from './internal'; import { useSuspenseCache } from './useSuspenseCache'; import { @@ -28,7 +25,7 @@ export type UseBackgroundQueryResult< QueryReference, { fetchMore: FetchMoreFunction; - refetch: ObservableQueryFields['refetch']; + refetch: RefetchFunction; } ]; From 243aa179c6272023c5193aa6091e105556324adb Mon Sep 17 00:00:00 2001 From: alessia Date: Mon, 5 Jun 2023 16:46:38 -0400 Subject: [PATCH 02/17] chore: improve useBackgroundQuery/useReadQuery types and type tests --- .../__tests__/useBackgroundQuery.test.tsx | 355 +++++++++++++++--- .../hooks/__tests__/useSuspenseQuery.test.tsx | 58 +-- src/react/hooks/useBackgroundQuery.ts | 84 ++++- src/react/hooks/useSuspenseQuery.ts | 2 - src/react/types/types.ts | 4 + 5 files changed, 427 insertions(+), 76 deletions(-) diff --git a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx index bac52940cab..ccb361498b1 100644 --- a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx +++ b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx @@ -9,6 +9,7 @@ import { } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { ErrorBoundary, ErrorBoundaryProps } from 'react-error-boundary'; +import { expectTypeOf } from 'expect-type'; import { GraphQLError } from 'graphql'; import { gql, @@ -38,8 +39,11 @@ import { useBackgroundQuery, useReadQuery } from '../useBackgroundQuery'; import { ApolloProvider } from '../../context'; import { SuspenseCache } from '../../cache'; import { InMemoryCache } from '../../../cache'; -import { FetchMoreFunction, RefetchFunction } from '../../../react'; -import { QueryReference } from '../../cache/QueryReference'; +import { + FetchMoreFunction, + RefetchFunction, + QueryReference, +} from '../../../react'; function renderIntegrationTest({ client, @@ -131,6 +135,37 @@ function renderIntegrationTest({ return { ...rest, query, client: _client, renders }; } +interface VariablesCaseData { + character: { + id: string; + name: string; + }; +} + +interface VariablesCaseVariables { + id: string; +} + +function useVariablesIntegrationTestCase() { + const query: TypedDocumentNode< + VariablesCaseData, + VariablesCaseVariables + > = gql` + query CharacterQuery($id: ID!) { + character(id: $id) { + id + name + } + } + `; + const CHARACTERS = ['Spider-Man', 'Black Widow', 'Iron Man', 'Hulk']; + let mocks = [...CHARACTERS].map((name, index) => ({ + request: { query, variables: { id: String(index + 1) } }, + result: { data: { character: { id: String(index + 1), name } } }, + })); + return { mocks, query }; +} + function renderVariablesIntegrationTest({ variables, mocks, @@ -150,32 +185,8 @@ function renderVariablesIntegrationTest({ variables: { id: string }; errorPolicy?: ErrorPolicy; }) { - const CHARACTERS = ['Spider-Man', 'Black Widow', 'Iron Man', 'Hulk']; - - interface QueryData { - character: { - id: string; - name: string; - }; - } + let { mocks: _mocks, query } = useVariablesIntegrationTestCase(); - interface QueryVariables { - id: string; - } - - const query: TypedDocumentNode = gql` - query CharacterQuery($id: ID!) { - character(id: $id) { - id - name - } - } - `; - - let _mocks = [...CHARACTERS].map((name, index) => ({ - request: { query, variables: { id: String(index + 1) } }, - result: { data: { character: { id: String(index + 1), name } } }, - })); // duplicate mocks with (updated) in the name for refetches _mocks = [..._mocks, ..._mocks, ..._mocks].map( ({ request, result }, index) => { @@ -208,7 +219,7 @@ function renderVariablesIntegrationTest({ suspenseCount: number; count: number; frames: { - data: QueryData; + data: VariablesCaseData; networkStatus: NetworkStatus; error: ApolloError | undefined; }[]; @@ -239,11 +250,11 @@ function renderVariablesIntegrationTest({ variables: _variables, queryRef, }: { - variables: QueryVariables; + variables: VariablesCaseVariables; refetch: ( variables?: Partial | undefined - ) => Promise>; - queryRef: QueryReference; + ) => Promise>; + queryRef: QueryReference; }) { const { data, error, networkStatus } = useReadQuery(queryRef); const [variables, setVariables] = React.useState(_variables); @@ -276,7 +287,7 @@ function renderVariablesIntegrationTest({ variables, errorPolicy = 'none', }: { - variables: QueryVariables; + variables: VariablesCaseVariables; errorPolicy?: ErrorPolicy; }) { const [queryRef, { refetch }] = useBackgroundQuery(query, { @@ -294,7 +305,7 @@ function renderVariablesIntegrationTest({ variables, errorPolicy, }: { - variables: QueryVariables; + variables: VariablesCaseVariables; errorPolicy?: ErrorPolicy; }) { return ( @@ -314,7 +325,7 @@ function renderVariablesIntegrationTest({ const { ...rest } = render( ); - const rerender = ({ variables }: { variables: QueryVariables }) => { + const rerender = ({ variables }: { variables: VariablesCaseVariables }) => { return rest.rerender(); }; return { ...rest, query, rerender, client, renders }; @@ -1243,7 +1254,15 @@ describe('useBackgroundQuery', () => { }); it('does not suspend deferred queries with data in the cache and using a "cache-and-network" fetch policy', async () => { - const query = gql` + interface Data { + greeting: { + __typename: string; + message: string; + recipient: { name: string; __typename: string }; + }; + } + + const query: TypedDocumentNode = gql` query { greeting { message @@ -1256,13 +1275,6 @@ describe('useBackgroundQuery', () => { } `; - interface Data { - greeting: { - message: string; - recipient: { name: string }; - }; - } - const link = new MockSubscriptionLink(); const cache = new InMemoryCache(); cache.writeQuery({ @@ -2197,11 +2209,270 @@ describe('useBackgroundQuery', () => { // @ts-expect-error should not allow returnPartialData in options useBackgroundQuery(query, { returnPartialData: true }); }); + it('disallows refetchWritePolicy in BackgroundQueryHookOptions', () => { const { query } = renderIntegrationTest(); // @ts-expect-error should not allow refetchWritePolicy in options useBackgroundQuery(query, { refetchWritePolicy: 'overwrite' }); }); + + it('returns unknown when TData cannot be inferred', () => { + const query = gql` + query { + hello + } + `; + + const [queryRef] = useBackgroundQuery(query); + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf(); + }); + + it('disallows wider variables type than specified', () => { + const { query } = useVariablesIntegrationTestCase(); + + // @ts-expect-error should not allow wider TVariables type + useBackgroundQuery(query, { variables: { id: '1', foo: 'bar' } }); + }); + + it('returns TData in default case', () => { + const { query } = useVariablesIntegrationTestCase(); + + const [inferredQueryRef] = useBackgroundQuery(query); + const { data: inferred } = useReadQuery(inferredQueryRef); + + expectTypeOf(inferred).toEqualTypeOf(); + expectTypeOf(inferred).not.toEqualTypeOf(); + + const [explicitQueryRef] = useBackgroundQuery< + VariablesCaseData, + VariablesCaseVariables + >(query); + + const { data: explicit } = useReadQuery(explicitQueryRef); + + expectTypeOf(explicit).toEqualTypeOf(); + expectTypeOf(explicit).not.toEqualTypeOf(); + }); + + it('returns TData | undefined with errorPolicy: "ignore"', () => { + const { query } = useVariablesIntegrationTestCase(); + + const [inferredQueryRef] = useBackgroundQuery(query, { + errorPolicy: 'ignore', + }); + const { data: inferred } = useReadQuery(inferredQueryRef); + + expectTypeOf(inferred).toEqualTypeOf(); + expectTypeOf(inferred).not.toEqualTypeOf(); + + const [explicitQueryRef] = useBackgroundQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { + errorPolicy: 'ignore', + }); + + const { data: explicit } = useReadQuery(explicitQueryRef); + + expectTypeOf(explicit).toEqualTypeOf(); + expectTypeOf(explicit).not.toEqualTypeOf(); + }); + + it('returns TData | undefined with errorPolicy: "all"', () => { + const { query } = useVariablesIntegrationTestCase(); + + const [inferredQueryRef] = useBackgroundQuery(query, { + errorPolicy: 'all', + }); + const { data: inferred } = useReadQuery(inferredQueryRef); + + expectTypeOf(inferred).toEqualTypeOf(); + expectTypeOf(inferred).not.toEqualTypeOf(); + + const [explicitQueryRef] = useBackgroundQuery(query, { + errorPolicy: 'all', + }); + const { data: explicit } = useReadQuery(explicitQueryRef); + + expectTypeOf(explicit).toEqualTypeOf(); + expectTypeOf(explicit).not.toEqualTypeOf(); + }); + + it('returns TData with errorPolicy: "none"', () => { + const { query } = useVariablesIntegrationTestCase(); + + const [inferredQueryRef] = useBackgroundQuery(query, { + errorPolicy: 'none', + }); + const { data: inferred } = useReadQuery(inferredQueryRef); + + expectTypeOf(inferred).toEqualTypeOf(); + expectTypeOf(inferred).not.toEqualTypeOf(); + + const [explicitQueryRef] = useBackgroundQuery(query, { + errorPolicy: 'none', + }); + const { data: explicit } = useReadQuery(explicitQueryRef); + + expectTypeOf(explicit).toEqualTypeOf(); + expectTypeOf(explicit).not.toEqualTypeOf(); + }); + + // it('returns DeepPartial with returnPartialData: true', () => { + // const { query } = useVariablesQueryCase(); + + // const { data: inferred } = useSuspenseQuery< + // VariablesCaseData, + // VariablesCaseVariables + // >(query, { + // returnPartialData: true, + // }); + + // expectTypeOf(inferred).toEqualTypeOf>(); + // expectTypeOf(inferred).not.toEqualTypeOf(); + + // const { data: explicit } = useSuspenseQuery(query, { + // returnPartialData: true, + // }); + + // expectTypeOf(explicit).toEqualTypeOf>(); + // expectTypeOf(explicit).not.toEqualTypeOf(); + // }); + + // it('returns TData with returnPartialData: false', () => { + // const { query } = useVariablesQueryCase(); + + // const { data: inferred } = useSuspenseQuery(query, { + // returnPartialData: false, + // }); + + // expectTypeOf(inferred).toEqualTypeOf(); + // expectTypeOf(inferred).not.toEqualTypeOf< + // DeepPartial + // >(); + + // const { data: explicit } = useSuspenseQuery< + // VariablesCaseData, + // VariablesCaseVariables + // >(query, { + // returnPartialData: false, + // }); + + // expectTypeOf(explicit).toEqualTypeOf(); + // expectTypeOf(explicit).not.toEqualTypeOf< + // DeepPartial + // >(); + // }); + + // it('returns TData when passing an option that does not affect TData', () => { + // const { query } = useVariablesQueryCase(); + + // const { data: inferred } = useSuspenseQuery< + // VariablesCaseData, + // VariablesCaseVariables + // >(query, { + // fetchPolicy: 'no-cache', + // }); + + // expectTypeOf(inferred).toEqualTypeOf(); + // expectTypeOf(inferred).not.toEqualTypeOf< + // DeepPartial + // >(); + + // const { data: explicit } = useSuspenseQuery(query, { + // fetchPolicy: 'no-cache', + // }); + + // expectTypeOf(explicit).toEqualTypeOf(); + // expectTypeOf(explicit).not.toEqualTypeOf< + // DeepPartial + // >(); + // }); + + // it('handles combinations of options', () => { + // const { query } = useVariablesQueryCase(); + + // const { data: inferredPartialDataIgnore } = useSuspenseQuery< + // VariablesCaseData, + // VariablesCaseVariables + // >(query, { + // returnPartialData: true, + // errorPolicy: 'ignore', + // }); + + // expectTypeOf(inferredPartialDataIgnore).toEqualTypeOf< + // DeepPartial | undefined + // >(); + // expectTypeOf( + // inferredPartialDataIgnore + // ).not.toEqualTypeOf(); + + // const { data: explicitPartialDataIgnore } = useSuspenseQuery(query, { + // returnPartialData: true, + // errorPolicy: 'ignore', + // }); + + // expectTypeOf(explicitPartialDataIgnore).toEqualTypeOf< + // DeepPartial | undefined + // >(); + // expectTypeOf( + // explicitPartialDataIgnore + // ).not.toEqualTypeOf(); + + // const { data: inferredPartialDataNone } = useSuspenseQuery< + // VariablesCaseData, + // VariablesCaseVariables + // >(query, { + // returnPartialData: true, + // errorPolicy: 'none', + // }); + + // expectTypeOf(inferredPartialDataNone).toEqualTypeOf< + // DeepPartial + // >(); + // expectTypeOf( + // inferredPartialDataNone + // ).not.toEqualTypeOf(); + + // const { data: explicitPartialDataNone } = useSuspenseQuery(query, { + // returnPartialData: true, + // errorPolicy: 'none', + // }); + + // expectTypeOf(explicitPartialDataNone).toEqualTypeOf< + // DeepPartial + // >(); + // expectTypeOf( + // explicitPartialDataNone + // ).not.toEqualTypeOf(); + // }); + + // it('returns correct TData type when combined options that do not affect TData', () => { + // const { query } = useVariablesQueryCase(); + + // const { data: inferred } = useSuspenseQuery(query, { + // fetchPolicy: 'no-cache', + // returnPartialData: true, + // errorPolicy: 'none', + // }); + + // expectTypeOf(inferred).toEqualTypeOf>(); + // expectTypeOf(inferred).not.toEqualTypeOf(); + + // const { data: explicit } = useSuspenseQuery< + // VariablesCaseData, + // VariablesCaseVariables + // >(query, { + // fetchPolicy: 'no-cache', + // returnPartialData: true, + // errorPolicy: 'none', + // }); + + // expectTypeOf(explicit).toEqualTypeOf>(); + // expectTypeOf(explicit).not.toEqualTypeOf(); + // }); }); }); diff --git a/src/react/hooks/__tests__/useSuspenseQuery.test.tsx b/src/react/hooks/__tests__/useSuspenseQuery.test.tsx index 404f313b0c6..3c5af6afdb2 100644 --- a/src/react/hooks/__tests__/useSuspenseQuery.test.tsx +++ b/src/react/hooks/__tests__/useSuspenseQuery.test.tsx @@ -6928,17 +6928,17 @@ describe('useSuspenseQuery', () => { it('returns TData | undefined with errorPolicy: "all"', () => { const { query } = useVariablesQueryCase(); - const { data: inferred } = useSuspenseQuery< - VariablesCaseData, - VariablesCaseVariables - >(query, { + const { data: inferred } = useSuspenseQuery(query, { errorPolicy: 'all', }); expectTypeOf(inferred).toEqualTypeOf(); expectTypeOf(inferred).not.toEqualTypeOf(); - const { data: explicit } = useSuspenseQuery(query, { + const { data: explicit } = useSuspenseQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { errorPolicy: 'all', }); @@ -6949,17 +6949,17 @@ describe('useSuspenseQuery', () => { it('returns TData with errorPolicy: "none"', () => { const { query } = useVariablesQueryCase(); - const { data: inferred } = useSuspenseQuery< - VariablesCaseData, - VariablesCaseVariables - >(query, { + const { data: inferred } = useSuspenseQuery(query, { errorPolicy: 'none', }); expectTypeOf(inferred).toEqualTypeOf(); expectTypeOf(inferred).not.toEqualTypeOf(); - const { data: explicit } = useSuspenseQuery(query, { + const { data: explicit } = useSuspenseQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { errorPolicy: 'none', }); @@ -6970,17 +6970,17 @@ describe('useSuspenseQuery', () => { it('returns DeepPartial with returnPartialData: true', () => { const { query } = useVariablesQueryCase(); - const { data: inferred } = useSuspenseQuery< - VariablesCaseData, - VariablesCaseVariables - >(query, { + const { data: inferred } = useSuspenseQuery(query, { returnPartialData: true, }); expectTypeOf(inferred).toEqualTypeOf>(); expectTypeOf(inferred).not.toEqualTypeOf(); - const { data: explicit } = useSuspenseQuery(query, { + const { data: explicit } = useSuspenseQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { returnPartialData: true, }); @@ -7016,10 +7016,7 @@ describe('useSuspenseQuery', () => { it('returns TData when passing an option that does not affect TData', () => { const { query } = useVariablesQueryCase(); - const { data: inferred } = useSuspenseQuery< - VariablesCaseData, - VariablesCaseVariables - >(query, { + const { data: inferred } = useSuspenseQuery(query, { fetchPolicy: 'no-cache', }); @@ -7028,7 +7025,10 @@ describe('useSuspenseQuery', () => { DeepPartial >(); - const { data: explicit } = useSuspenseQuery(query, { + const { data: explicit } = useSuspenseQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { fetchPolicy: 'no-cache', }); @@ -7041,31 +7041,31 @@ describe('useSuspenseQuery', () => { it('handles combinations of options', () => { const { query } = useVariablesQueryCase(); - const { data: inferredPartialDataIgnore } = useSuspenseQuery< - VariablesCaseData, - VariablesCaseVariables - >(query, { + const { data: explicitPartialDataIgnore } = useSuspenseQuery(query, { returnPartialData: true, errorPolicy: 'ignore', }); - expectTypeOf(inferredPartialDataIgnore).toEqualTypeOf< + expectTypeOf(explicitPartialDataIgnore).toEqualTypeOf< DeepPartial | undefined >(); expectTypeOf( - inferredPartialDataIgnore + explicitPartialDataIgnore ).not.toEqualTypeOf(); - const { data: explicitPartialDataIgnore } = useSuspenseQuery(query, { + const { data: inferredPartialDataIgnore } = useSuspenseQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { returnPartialData: true, errorPolicy: 'ignore', }); - expectTypeOf(explicitPartialDataIgnore).toEqualTypeOf< + expectTypeOf(inferredPartialDataIgnore).toEqualTypeOf< DeepPartial | undefined >(); expectTypeOf( - explicitPartialDataIgnore + inferredPartialDataIgnore ).not.toEqualTypeOf(); const { data: inferredPartialDataNone } = useSuspenseQuery< diff --git a/src/react/hooks/useBackgroundQuery.ts b/src/react/hooks/useBackgroundQuery.ts index 881199b1464..2979863a9f8 100644 --- a/src/react/hooks/useBackgroundQuery.ts +++ b/src/react/hooks/useBackgroundQuery.ts @@ -6,7 +6,7 @@ import type { } from '../../core'; import { useApolloClient } from './useApolloClient'; import type { QueryReference } from '../cache/QueryReference'; -import type { SuspenseQueryHookOptions } from '../types/types'; +import type { SuspenseQueryHookOptions, NoInfer } from '../types/types'; import { __use } from './internal'; import { useSuspenseCache } from './useSuspenseCache'; import { @@ -16,10 +16,11 @@ import { } from './useSuspenseQuery'; import type { FetchMoreFunction, RefetchFunction } from './useSuspenseQuery'; import { canonicalStringify } from '../../cache'; +import type { DeepPartial } from '../../utilities'; import { invariant } from '../../utilities/globals'; export type UseBackgroundQueryResult< - TData = any, + TData = unknown, TVariables extends OperationVariables = OperationVariables > = [ QueryReference, @@ -30,7 +31,84 @@ export type UseBackgroundQueryResult< ]; export function useBackgroundQuery< - TData = any, + TData, + TVariables extends OperationVariables, + TOptions extends Omit< + SuspenseQueryHookOptions, + 'variables' | 'returnPartialData' | 'refetchWritePolicy' + > +>( + query: DocumentNode | TypedDocumentNode, + options?: Omit< + SuspenseQueryHookOptions, NoInfer>, + 'returnPartialData' | 'refetchWritePolicy' + > & + TOptions +): UseBackgroundQueryResult< + TOptions['errorPolicy'] extends 'ignore' | 'all' + ? // TOptions['returnPartialData'] extends true + // ? DeepPartial | undefined + // : TData | undefined + TData | undefined + : // : TOptions['returnPartialData'] extends true + // ? DeepPartial + TData, + TVariables +>; + +export function useBackgroundQuery< + TData = unknown, + TVariables extends OperationVariables = OperationVariables +>( + query: DocumentNode | TypedDocumentNode, + options: Omit< + SuspenseQueryHookOptions, NoInfer>, + 'returnPartialData' | 'refetchWritePolicy' + > & { + returnPartialData: true; + errorPolicy: 'ignore' | 'all'; + } +): UseBackgroundQueryResult | undefined, TVariables>; + +export function useBackgroundQuery< + TData = unknown, + TVariables extends OperationVariables = OperationVariables +>( + query: DocumentNode | TypedDocumentNode, + options: Omit< + SuspenseQueryHookOptions, NoInfer>, + 'returnPartialData' | 'refetchWritePolicy' + > & { + errorPolicy: 'ignore' | 'all'; + } +): UseBackgroundQueryResult; + +// export function useBackgroundQuery< +// TData = unknown, +// TVariables extends OperationVariables = OperationVariables +// >( +// query: DocumentNode | TypedDocumentNode, +// options: Omit< +// SuspenseQueryHookOptions, NoInfer>, +// 'returnPartialData' | 'refetchWritePolicy' +// > & { +// returnPartialData: true; +// } +// ): UseBackgroundQueryResult, TVariables>; + +export function useBackgroundQuery< + TData = unknown, + TVariables extends OperationVariables = OperationVariables +>( + query: DocumentNode | TypedDocumentNode, + options?: Omit< + SuspenseQueryHookOptions, NoInfer>, + 'returnPartialData' | 'refetchWritePolicy' + > +): UseBackgroundQueryResult; + +export function useBackgroundQuery< + TData = unknown, TVariables extends OperationVariables = OperationVariables >( query: DocumentNode | TypedDocumentNode, diff --git a/src/react/hooks/useSuspenseQuery.ts b/src/react/hooks/useSuspenseQuery.ts index e381e590102..58144811c83 100644 --- a/src/react/hooks/useSuspenseQuery.ts +++ b/src/react/hooks/useSuspenseQuery.ts @@ -61,8 +61,6 @@ export type SubscribeToMoreFunction< TVariables extends OperationVariables > = ObservableQueryFields['subscribeToMore']; -export type Version = 'main' | 'network'; - export function useSuspenseQuery< TData, TVariables extends OperationVariables, diff --git a/src/react/types/types.ts b/src/react/types/types.ts index 64d0a6b9b7d..863cf27552b 100644 --- a/src/react/types/types.ts +++ b/src/react/types/types.ts @@ -20,6 +20,10 @@ import type { } from '../../core'; import type { SuspenseCache } from '../cache'; +/* QueryReference type */ + +export type { QueryReference } from '../cache/QueryReference'; + /* Common types */ export type { DefaultContext as Context } from "../../core"; From c29a5a40c104b65a692cddf293ba76e86897b864 Mon Sep 17 00:00:00 2001 From: alessia Date: Mon, 5 Jun 2023 16:54:58 -0400 Subject: [PATCH 03/17] chore: update code comments --- .../__tests__/useBackgroundQuery.test.tsx | 144 +----------------- src/react/hooks/useBackgroundQuery.ts | 7 +- 2 files changed, 11 insertions(+), 140 deletions(-) diff --git a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx index ccb361498b1..a2a29a9db69 100644 --- a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx +++ b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx @@ -2321,158 +2321,24 @@ describe('useBackgroundQuery', () => { expectTypeOf(explicit).not.toEqualTypeOf(); }); + // TODO: https://github.com/apollographql/apollo-client/issues/10893 // it('returns DeepPartial with returnPartialData: true', () => { - // const { query } = useVariablesQueryCase(); - - // const { data: inferred } = useSuspenseQuery< - // VariablesCaseData, - // VariablesCaseVariables - // >(query, { - // returnPartialData: true, - // }); - - // expectTypeOf(inferred).toEqualTypeOf>(); - // expectTypeOf(inferred).not.toEqualTypeOf(); - - // const { data: explicit } = useSuspenseQuery(query, { - // returnPartialData: true, - // }); - - // expectTypeOf(explicit).toEqualTypeOf>(); - // expectTypeOf(explicit).not.toEqualTypeOf(); // }); + // TODO: https://github.com/apollographql/apollo-client/issues/10893 // it('returns TData with returnPartialData: false', () => { - // const { query } = useVariablesQueryCase(); - - // const { data: inferred } = useSuspenseQuery(query, { - // returnPartialData: false, - // }); - - // expectTypeOf(inferred).toEqualTypeOf(); - // expectTypeOf(inferred).not.toEqualTypeOf< - // DeepPartial - // >(); - - // const { data: explicit } = useSuspenseQuery< - // VariablesCaseData, - // VariablesCaseVariables - // >(query, { - // returnPartialData: false, - // }); - - // expectTypeOf(explicit).toEqualTypeOf(); - // expectTypeOf(explicit).not.toEqualTypeOf< - // DeepPartial - // >(); // }); + // TODO: https://github.com/apollographql/apollo-client/issues/10893 // it('returns TData when passing an option that does not affect TData', () => { - // const { query } = useVariablesQueryCase(); - - // const { data: inferred } = useSuspenseQuery< - // VariablesCaseData, - // VariablesCaseVariables - // >(query, { - // fetchPolicy: 'no-cache', - // }); - - // expectTypeOf(inferred).toEqualTypeOf(); - // expectTypeOf(inferred).not.toEqualTypeOf< - // DeepPartial - // >(); - - // const { data: explicit } = useSuspenseQuery(query, { - // fetchPolicy: 'no-cache', - // }); - - // expectTypeOf(explicit).toEqualTypeOf(); - // expectTypeOf(explicit).not.toEqualTypeOf< - // DeepPartial - // >(); // }); + // TODO: https://github.com/apollographql/apollo-client/issues/10893 // it('handles combinations of options', () => { - // const { query } = useVariablesQueryCase(); - - // const { data: inferredPartialDataIgnore } = useSuspenseQuery< - // VariablesCaseData, - // VariablesCaseVariables - // >(query, { - // returnPartialData: true, - // errorPolicy: 'ignore', - // }); - - // expectTypeOf(inferredPartialDataIgnore).toEqualTypeOf< - // DeepPartial | undefined - // >(); - // expectTypeOf( - // inferredPartialDataIgnore - // ).not.toEqualTypeOf(); - - // const { data: explicitPartialDataIgnore } = useSuspenseQuery(query, { - // returnPartialData: true, - // errorPolicy: 'ignore', - // }); - - // expectTypeOf(explicitPartialDataIgnore).toEqualTypeOf< - // DeepPartial | undefined - // >(); - // expectTypeOf( - // explicitPartialDataIgnore - // ).not.toEqualTypeOf(); - - // const { data: inferredPartialDataNone } = useSuspenseQuery< - // VariablesCaseData, - // VariablesCaseVariables - // >(query, { - // returnPartialData: true, - // errorPolicy: 'none', - // }); - - // expectTypeOf(inferredPartialDataNone).toEqualTypeOf< - // DeepPartial - // >(); - // expectTypeOf( - // inferredPartialDataNone - // ).not.toEqualTypeOf(); - - // const { data: explicitPartialDataNone } = useSuspenseQuery(query, { - // returnPartialData: true, - // errorPolicy: 'none', - // }); - - // expectTypeOf(explicitPartialDataNone).toEqualTypeOf< - // DeepPartial - // >(); - // expectTypeOf( - // explicitPartialDataNone - // ).not.toEqualTypeOf(); // }); + // TODO: https://github.com/apollographql/apollo-client/issues/10893 // it('returns correct TData type when combined options that do not affect TData', () => { - // const { query } = useVariablesQueryCase(); - - // const { data: inferred } = useSuspenseQuery(query, { - // fetchPolicy: 'no-cache', - // returnPartialData: true, - // errorPolicy: 'none', - // }); - - // expectTypeOf(inferred).toEqualTypeOf>(); - // expectTypeOf(inferred).not.toEqualTypeOf(); - - // const { data: explicit } = useSuspenseQuery< - // VariablesCaseData, - // VariablesCaseVariables - // >(query, { - // fetchPolicy: 'no-cache', - // returnPartialData: true, - // errorPolicy: 'none', - // }); - - // expectTypeOf(explicit).toEqualTypeOf>(); - // expectTypeOf(explicit).not.toEqualTypeOf(); // }); }); }); diff --git a/src/react/hooks/useBackgroundQuery.ts b/src/react/hooks/useBackgroundQuery.ts index 2979863a9f8..79e91cf93be 100644 --- a/src/react/hooks/useBackgroundQuery.ts +++ b/src/react/hooks/useBackgroundQuery.ts @@ -46,7 +46,9 @@ export function useBackgroundQuery< TOptions ): UseBackgroundQueryResult< TOptions['errorPolicy'] extends 'ignore' | 'all' - ? // TOptions['returnPartialData'] extends true + ? // TODO: support `returnPartialData` | `refetchWritePolicy` + // see https://github.com/apollographql/apollo-client/issues/10893 + // TOptions['returnPartialData'] extends true // ? DeepPartial | undefined // : TData | undefined TData | undefined @@ -83,6 +85,9 @@ export function useBackgroundQuery< } ): UseBackgroundQueryResult; +// TODO: support `returnPartialData` | `refetchWritePolicy` +// see https://github.com/apollographql/apollo-client/issues/10893 + // export function useBackgroundQuery< // TData = unknown, // TVariables extends OperationVariables = OperationVariables From 0e0e7e1b0b7c0af0ddaa2afc5182cbf8759cee0e Mon Sep 17 00:00:00 2001 From: alessia Date: Mon, 5 Jun 2023 16:55:30 -0400 Subject: [PATCH 04/17] chore: add changeset --- .changeset/friendly-mugs-repeat.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/friendly-mugs-repeat.md diff --git a/.changeset/friendly-mugs-repeat.md b/.changeset/friendly-mugs-repeat.md new file mode 100644 index 00000000000..a4050061576 --- /dev/null +++ b/.changeset/friendly-mugs-repeat.md @@ -0,0 +1,5 @@ +--- +'@apollo/client': patch +--- + +Improve `useBackgroundQuery` type interface From 35550c4946cbc206332b70b8bf2440e7e236b2db Mon Sep 17 00:00:00 2001 From: alessia Date: Mon, 5 Jun 2023 17:02:32 -0400 Subject: [PATCH 05/17] chore: fix explicit/inferred references in uSQ test --- .../hooks/__tests__/useSuspenseQuery.test.tsx | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/react/hooks/__tests__/useSuspenseQuery.test.tsx b/src/react/hooks/__tests__/useSuspenseQuery.test.tsx index 3c5af6afdb2..e278cfeaf47 100644 --- a/src/react/hooks/__tests__/useSuspenseQuery.test.tsx +++ b/src/react/hooks/__tests__/useSuspenseQuery.test.tsx @@ -7041,19 +7041,19 @@ describe('useSuspenseQuery', () => { it('handles combinations of options', () => { const { query } = useVariablesQueryCase(); - const { data: explicitPartialDataIgnore } = useSuspenseQuery(query, { + const { data: inferredPartialDataIgnore } = useSuspenseQuery(query, { returnPartialData: true, errorPolicy: 'ignore', }); - expectTypeOf(explicitPartialDataIgnore).toEqualTypeOf< + expectTypeOf(inferredPartialDataIgnore).toEqualTypeOf< DeepPartial | undefined >(); expectTypeOf( - explicitPartialDataIgnore + inferredPartialDataIgnore ).not.toEqualTypeOf(); - const { data: inferredPartialDataIgnore } = useSuspenseQuery< + const { data: explicitPartialDataIgnore } = useSuspenseQuery< VariablesCaseData, VariablesCaseVariables >(query, { @@ -7061,17 +7061,14 @@ describe('useSuspenseQuery', () => { errorPolicy: 'ignore', }); - expectTypeOf(inferredPartialDataIgnore).toEqualTypeOf< + expectTypeOf(explicitPartialDataIgnore).toEqualTypeOf< DeepPartial | undefined >(); expectTypeOf( - inferredPartialDataIgnore + explicitPartialDataIgnore ).not.toEqualTypeOf(); - const { data: inferredPartialDataNone } = useSuspenseQuery< - VariablesCaseData, - VariablesCaseVariables - >(query, { + const { data: inferredPartialDataNone } = useSuspenseQuery(query, { returnPartialData: true, errorPolicy: 'none', }); @@ -7083,7 +7080,10 @@ describe('useSuspenseQuery', () => { inferredPartialDataNone ).not.toEqualTypeOf(); - const { data: explicitPartialDataNone } = useSuspenseQuery(query, { + const { data: explicitPartialDataNone } = useSuspenseQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { returnPartialData: true, errorPolicy: 'none', }); From 2234a0a28451c5bf82a31f4054189b51081fa83b Mon Sep 17 00:00:00 2001 From: alessia Date: Wed, 7 Jun 2023 15:00:37 -0400 Subject: [PATCH 06/17] chore: updates types to accept returnPartialData | refetchWritePolicy --- .../__tests__/useBackgroundQuery.test.tsx | 211 +++++++++++++++--- src/react/hooks/useBackgroundQuery.ts | 76 +++---- 2 files changed, 209 insertions(+), 78 deletions(-) diff --git a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx index a2a29a9db69..46128d60c14 100644 --- a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx +++ b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx @@ -34,7 +34,11 @@ import { MockSubscriptionLink, mockSingleLink, } from '../../../testing'; -import { concatPagination, offsetLimitPagination } from '../../../utilities'; +import { + concatPagination, + offsetLimitPagination, + DeepPartial, +} from '../../../utilities'; import { useBackgroundQuery, useReadQuery } from '../useBackgroundQuery'; import { ApolloProvider } from '../../context'; import { SuspenseCache } from '../../cache'; @@ -2203,20 +2207,6 @@ describe('useBackgroundQuery', () => { }); }); describe.skip('type tests', () => { - it('disallows returnPartialData in BackgroundQueryHookOptions', () => { - const { query } = renderIntegrationTest(); - - // @ts-expect-error should not allow returnPartialData in options - useBackgroundQuery(query, { returnPartialData: true }); - }); - - it('disallows refetchWritePolicy in BackgroundQueryHookOptions', () => { - const { query } = renderIntegrationTest(); - - // @ts-expect-error should not allow refetchWritePolicy in options - useBackgroundQuery(query, { refetchWritePolicy: 'overwrite' }); - }); - it('returns unknown when TData cannot be inferred', () => { const query = gql` query { @@ -2321,24 +2311,185 @@ describe('useBackgroundQuery', () => { expectTypeOf(explicit).not.toEqualTypeOf(); }); - // TODO: https://github.com/apollographql/apollo-client/issues/10893 - // it('returns DeepPartial with returnPartialData: true', () => { - // }); + it('returns DeepPartial with returnPartialData: true', () => { + const { query } = useVariablesIntegrationTestCase(); + + const [inferredQueryRef] = useBackgroundQuery(query, { + returnPartialData: true, + }); + const { data: inferred } = useReadQuery(inferredQueryRef); + + expectTypeOf(inferred).toEqualTypeOf>(); + expectTypeOf(inferred).not.toEqualTypeOf(); + + const [explicitQueryRef] = useBackgroundQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { + returnPartialData: true, + }); + + const { data: explicit } = useReadQuery(explicitQueryRef); + + expectTypeOf(explicit).toEqualTypeOf>(); + expectTypeOf(explicit).not.toEqualTypeOf(); + }); + + it('returns TData with returnPartialData: false', () => { + const { query } = useVariablesIntegrationTestCase(); + + const [inferredQueryRef] = useBackgroundQuery(query, { + returnPartialData: false, + }); + const { data: inferred } = useReadQuery(inferredQueryRef); + + expectTypeOf(inferred).toEqualTypeOf(); + expectTypeOf(inferred).not.toEqualTypeOf< + DeepPartial + >(); + + const [explicitQueryRef] = useBackgroundQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { + returnPartialData: false, + }); + + const { data: explicit } = useReadQuery(explicitQueryRef); + + expectTypeOf(explicit).toEqualTypeOf(); + expectTypeOf(explicit).not.toEqualTypeOf< + DeepPartial + >(); + }); + + it('returns TData when passing an option that does not affect TData', () => { + const { query } = useVariablesIntegrationTestCase(); + + const [inferredQueryRef] = useBackgroundQuery(query, { + fetchPolicy: 'no-cache', + }); + const { data: inferred } = useReadQuery(inferredQueryRef); + + expectTypeOf(inferred).toEqualTypeOf(); + expectTypeOf(inferred).not.toEqualTypeOf< + DeepPartial + >(); + + const [explicitQueryRef] = useBackgroundQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { + fetchPolicy: 'no-cache', + }); + + const { data: explicit } = useReadQuery(explicitQueryRef); + + expectTypeOf(explicit).toEqualTypeOf(); + expectTypeOf(explicit).not.toEqualTypeOf< + DeepPartial + >(); + }); + + it('handles combinations of options', () => { + const { query } = useVariablesIntegrationTestCase(); + + const [inferredPartialDataIgnoreQueryRef] = useBackgroundQuery(query, { + returnPartialData: true, + errorPolicy: 'ignore', + }); + const { data: inferredPartialDataIgnore } = useReadQuery( + inferredPartialDataIgnoreQueryRef + ); + + expectTypeOf(inferredPartialDataIgnore).toEqualTypeOf< + DeepPartial | undefined + >(); + expectTypeOf( + inferredPartialDataIgnore + ).not.toEqualTypeOf(); + + const [explicitPartialDataIgnoreQueryRef] = useBackgroundQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { + returnPartialData: true, + errorPolicy: 'ignore', + }); + + const { data: explicitPartialDataIgnore } = useReadQuery( + explicitPartialDataIgnoreQueryRef + ); + + expectTypeOf(explicitPartialDataIgnore).toEqualTypeOf< + DeepPartial | undefined + >(); + expectTypeOf( + explicitPartialDataIgnore + ).not.toEqualTypeOf(); + + const [inferredPartialDataNoneQueryRef] = useBackgroundQuery(query, { + returnPartialData: true, + errorPolicy: 'none', + }); + + const { data: inferredPartialDataNone } = useReadQuery( + inferredPartialDataNoneQueryRef + ); + + expectTypeOf(inferredPartialDataNone).toEqualTypeOf< + DeepPartial + >(); + expectTypeOf( + inferredPartialDataNone + ).not.toEqualTypeOf(); + + const [explicitPartialDataNoneQueryRef] = useBackgroundQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { + returnPartialData: true, + errorPolicy: 'none', + }); + + const { data: explicitPartialDataNone } = useReadQuery( + explicitPartialDataNoneQueryRef + ); + + expectTypeOf(explicitPartialDataNone).toEqualTypeOf< + DeepPartial + >(); + expectTypeOf( + explicitPartialDataNone + ).not.toEqualTypeOf(); + }); + + it('returns correct TData type when combined options that do not affect TData', () => { + const { query } = useVariablesIntegrationTestCase(); + + const [inferredQueryRef] = useBackgroundQuery(query, { + fetchPolicy: 'no-cache', + returnPartialData: true, + errorPolicy: 'none', + }); + const { data: inferred } = useReadQuery(inferredQueryRef); - // TODO: https://github.com/apollographql/apollo-client/issues/10893 - // it('returns TData with returnPartialData: false', () => { - // }); + expectTypeOf(inferred).toEqualTypeOf>(); + expectTypeOf(inferred).not.toEqualTypeOf(); - // TODO: https://github.com/apollographql/apollo-client/issues/10893 - // it('returns TData when passing an option that does not affect TData', () => { - // }); + const [explicitQueryRef] = useBackgroundQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { + fetchPolicy: 'no-cache', + returnPartialData: true, + errorPolicy: 'none', + }); - // TODO: https://github.com/apollographql/apollo-client/issues/10893 - // it('handles combinations of options', () => { - // }); + const { data: explicit } = useReadQuery(explicitQueryRef); - // TODO: https://github.com/apollographql/apollo-client/issues/10893 - // it('returns correct TData type when combined options that do not affect TData', () => { - // }); + expectTypeOf(explicit).toEqualTypeOf>(); + expectTypeOf(explicit).not.toEqualTypeOf(); + }); }); }); diff --git a/src/react/hooks/useBackgroundQuery.ts b/src/react/hooks/useBackgroundQuery.ts index 79e91cf93be..9dc91d1e06b 100644 --- a/src/react/hooks/useBackgroundQuery.ts +++ b/src/react/hooks/useBackgroundQuery.ts @@ -30,31 +30,27 @@ export type UseBackgroundQueryResult< } ]; +type SuspenseQueryHookOptionsNoInfer< + TData, + TVariables extends OperationVariables +> = SuspenseQueryHookOptions, NoInfer>; + export function useBackgroundQuery< TData, TVariables extends OperationVariables, - TOptions extends Omit< - SuspenseQueryHookOptions, - 'variables' | 'returnPartialData' | 'refetchWritePolicy' - > + TOptions extends Omit, 'variables'> >( query: DocumentNode | TypedDocumentNode, - options?: Omit< - SuspenseQueryHookOptions, NoInfer>, - 'returnPartialData' | 'refetchWritePolicy' - > & + options?: SuspenseQueryHookOptions, NoInfer> & TOptions ): UseBackgroundQueryResult< TOptions['errorPolicy'] extends 'ignore' | 'all' - ? // TODO: support `returnPartialData` | `refetchWritePolicy` - // see https://github.com/apollographql/apollo-client/issues/10893 - // TOptions['returnPartialData'] extends true - // ? DeepPartial | undefined - // : TData | undefined - TData | undefined - : // : TOptions['returnPartialData'] extends true - // ? DeepPartial - TData, + ? TOptions['returnPartialData'] extends true + ? DeepPartial | undefined + : TData | undefined + : TOptions['returnPartialData'] extends true + ? DeepPartial + : TData, TVariables >; @@ -63,10 +59,7 @@ export function useBackgroundQuery< TVariables extends OperationVariables = OperationVariables >( query: DocumentNode | TypedDocumentNode, - options: Omit< - SuspenseQueryHookOptions, NoInfer>, - 'returnPartialData' | 'refetchWritePolicy' - > & { + options: SuspenseQueryHookOptionsNoInfer & { returnPartialData: true; errorPolicy: 'ignore' | 'all'; } @@ -77,39 +70,27 @@ export function useBackgroundQuery< TVariables extends OperationVariables = OperationVariables >( query: DocumentNode | TypedDocumentNode, - options: Omit< - SuspenseQueryHookOptions, NoInfer>, - 'returnPartialData' | 'refetchWritePolicy' - > & { + options: SuspenseQueryHookOptionsNoInfer & { errorPolicy: 'ignore' | 'all'; } ): UseBackgroundQueryResult; -// TODO: support `returnPartialData` | `refetchWritePolicy` -// see https://github.com/apollographql/apollo-client/issues/10893 - -// export function useBackgroundQuery< -// TData = unknown, -// TVariables extends OperationVariables = OperationVariables -// >( -// query: DocumentNode | TypedDocumentNode, -// options: Omit< -// SuspenseQueryHookOptions, NoInfer>, -// 'returnPartialData' | 'refetchWritePolicy' -// > & { -// returnPartialData: true; -// } -// ): UseBackgroundQueryResult, TVariables>; +export function useBackgroundQuery< + TData = unknown, + TVariables extends OperationVariables = OperationVariables +>( + query: DocumentNode | TypedDocumentNode, + options: SuspenseQueryHookOptionsNoInfer & { + returnPartialData: true; + } +): UseBackgroundQueryResult, TVariables>; export function useBackgroundQuery< TData = unknown, TVariables extends OperationVariables = OperationVariables >( query: DocumentNode | TypedDocumentNode, - options?: Omit< - SuspenseQueryHookOptions, NoInfer>, - 'returnPartialData' | 'refetchWritePolicy' - > + options?: SuspenseQueryHookOptionsNoInfer ): UseBackgroundQueryResult; export function useBackgroundQuery< @@ -117,10 +98,9 @@ export function useBackgroundQuery< TVariables extends OperationVariables = OperationVariables >( query: DocumentNode | TypedDocumentNode, - options: Omit< - SuspenseQueryHookOptions, - 'returnPartialData' | 'refetchWritePolicy' - > = Object.create(null) + options: SuspenseQueryHookOptionsNoInfer = Object.create( + null + ) ): UseBackgroundQueryResult { const suspenseCache = useSuspenseCache(options.suspenseCache); const client = useApolloClient(options.client); From dad112893e7523ed00f225219beb7479e4058469 Mon Sep 17 00:00:00 2001 From: alessia Date: Wed, 7 Jun 2023 16:53:33 -0400 Subject: [PATCH 07/17] chore: wip tests --- .../__tests__/useBackgroundQuery.test.tsx | 162 ++++++++++++++++++ src/utilities/index.ts | 6 +- src/utilities/types/DeepPartial.ts | 2 +- 3 files changed, 166 insertions(+), 4 deletions(-) diff --git a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx index 46128d60c14..ea5119ca979 100644 --- a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx +++ b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx @@ -38,6 +38,7 @@ import { concatPagination, offsetLimitPagination, DeepPartial, + DeepPartialObject, } from '../../../utilities'; import { useBackgroundQuery, useReadQuery } from '../useBackgroundQuery'; import { ApolloProvider } from '../../context'; @@ -2205,7 +2206,168 @@ describe('useBackgroundQuery', () => { expect(todo1).toHaveTextContent('Clean room'); }); }); + + // refetchWritePolicy + // it('honors refetchWritePolicy set to "merge"', async () => { + + // }); + + // it('defaults refetchWritePolicy to "overwrite"', async () => { + + // }); + + // returnPartialData + it.only('does not suspend when partial data is in the cache and using a "cache-first" fetch policy with returnPartialData', async () => { + // const user = userEvent.setup(); + + interface Data { + character: { + id: string; + name: string; + }; + } + + const fullQuery: TypedDocumentNode = gql` + query { + character { + id + name + } + } + `; + + const partialQuery = gql` + query { + character { + id + } + } + `; + const mocks = [ + { + request: { query: fullQuery }, + result: { data: { character: { id: '1', name: 'Doctor Strange' } } }, + }, + ]; + + const cache = new InMemoryCache(); + + cache.writeQuery({ + query: partialQuery, + data: { character: { id: '1' } }, + }); + + const client = new ApolloClient({ + link: new MockLink(mocks), + cache, + }); + + const suspenseCache = new SuspenseCache(); + + function App() { + return ( + + }> + + + + ); + } + + function SuspenseFallback() { + return

Loading

; + } + + function Parent() { + const [queryRef] = useBackgroundQuery(fullQuery, { + fetchPolicy: 'cache-first', + returnPartialData: true, + }); + return ; + } + + function Todo({ + queryRef, + }: { + queryRef: QueryReference>; + }) { + const { data, networkStatus } = useReadQuery(queryRef); + + return ( +
+ {networkStatus} - {data.character?.id} - {data.character?.name} +
+ ); + } + + render(); + + // expect(screen.getByText('Loading')).toBeInTheDocument(); + + // expect(await screen.findByTestId('todos')).toBeInTheDocument(); + + // const todos = screen.getByTestId('todos'); + // const todo1 = screen.getByTestId('todo:1'); + // const button = screen.getByText('Load more'); + + // expect(todo1).toBeInTheDocument(); + + // await act(() => user.click(button)); + + // // startTransition will avoid rendering the suspense fallback for already + // // revealed content if the state update inside the transition causes the + // // component to suspend. + // // + // // Here we should not see the suspense fallback while the component suspends + // // until the todo is finished loading. Seeing the suspense fallback is an + // // indication that we are suspending the component too late in the process. + // expect(screen.queryByText('Loading')).not.toBeInTheDocument(); + + // // We can ensure this works with isPending from useTransition in the process + // expect(todos).toHaveAttribute('aria-busy', 'true'); + + // // Ensure we are showing the stale UI until the new todo has loaded + // expect(todo1).toHaveTextContent('Clean room'); + + // // Eventually we should see the updated todos content once its done + // // suspending. + // await waitFor(() => { + // expect(screen.getByTestId('todo:2')).toHaveTextContent( + // 'Take out trash (completed)' + // ); + // expect(todo1).toHaveTextContent('Clean room'); + // }); + }); + + // it('suspends and does not use partial data when changing variables and using a "cache-first" fetch policy with returnPartialData', async () => { + + // }); + + // it('suspends when partial data is in the cache and using a "network-only" fetch policy with returnPartialData', async () => { + + // }); + + // it('suspends when partial data is in the cache and using a "no-cache" fetch policy with returnPartialData', async () => { + + // }); + + // it('warns when using returnPartialData with a "no-cache" fetch policy', async () => { + + // }); + + // it('does not suspend when partial data is in the cache and using a "cache-and-network" fetch policy with returnPartialData', async () => { + + // }); + + // it('suspends and does not use partial data when changing variables and using a "cache-and-network" fetch policy with returnPartialData', async () => { + + // }); + + // it('does not suspend deferred queries with partial data in the cache and using a "cache-first" fetch policy with `returnPartialData`', async () => { + + // }); }); + describe.skip('type tests', () => { it('returns unknown when TData cannot be inferred', () => { const query = gql` diff --git a/src/utilities/index.ts b/src/utilities/index.ts index 048f1dcbeb1..b1e91147ec9 100644 --- a/src/utilities/index.ts +++ b/src/utilities/index.ts @@ -12,7 +12,7 @@ export { getInclusionDirectives, } from './graphql/directives'; -export { +export { DocumentTransform, DocumentTransformCacheKey } from './graphql/DocumentTransform'; @@ -92,7 +92,7 @@ export { ObservableSubscription } from './observables/Observable'; -export { +export { isStatefulPromise, createFulfilledPromise, createRejectedPromise, @@ -122,4 +122,4 @@ export { stripTypename } from './common/stripTypename'; export * from './types/IsStrictlyAny'; export { DeepOmit } from './types/DeepOmit'; -export { DeepPartial } from './types/DeepPartial'; +export { DeepPartial, DeepPartialObject } from './types/DeepPartial'; diff --git a/src/utilities/types/DeepPartial.ts b/src/utilities/types/DeepPartial.ts index e9d1848e9e2..219a2627806 100644 --- a/src/utilities/types/DeepPartial.ts +++ b/src/utilities/types/DeepPartial.ts @@ -53,6 +53,6 @@ type DeepPartialReadonlyMap = {} & ReadonlyMap< type DeepPartialSet = {} & Set>; type DeepPartialReadonlySet = {} & ReadonlySet>; -type DeepPartialObject = { +export type DeepPartialObject = { [K in keyof T]?: DeepPartial; }; From 396c80af3de584bb0ac4dff7d0df8c780fd5cefd Mon Sep 17 00:00:00 2001 From: alessia Date: Mon, 26 Jun 2023 15:11:25 -0400 Subject: [PATCH 08/17] fix: address review comments --- .../__tests__/useBackgroundQuery.test.tsx | 3 +- src/react/hooks/useBackgroundQuery.ts | 63 +++---------------- src/utilities/index.ts | 2 +- src/utilities/types/DeepPartial.ts | 2 +- 4 files changed, 11 insertions(+), 59 deletions(-) diff --git a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx index 0ad002d008f..e1dc00142d6 100644 --- a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx +++ b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx @@ -38,7 +38,6 @@ import { concatPagination, offsetLimitPagination, DeepPartial, - DeepPartialObject, } from '../../../utilities'; import { useBackgroundQuery, useReadQuery } from '../useBackgroundQuery'; import { ApolloProvider } from '../../context'; @@ -2810,7 +2809,7 @@ describe('useBackgroundQuery', () => { function Todo({ queryRef, }: { - queryRef: QueryReference>; + queryRef: QueryReference>; }) { const { data, networkStatus } = useReadQuery(queryRef); diff --git a/src/react/hooks/useBackgroundQuery.ts b/src/react/hooks/useBackgroundQuery.ts index b88d8b23718..3df324da577 100644 --- a/src/react/hooks/useBackgroundQuery.ts +++ b/src/react/hooks/useBackgroundQuery.ts @@ -45,8 +45,7 @@ export function useBackgroundQuery< TOptions extends Omit, 'variables'> >( query: DocumentNode | TypedDocumentNode, - options?: SuspenseQueryHookOptions, NoInfer> & - TOptions + options?: SuspenseQueryHookOptionsNoInfer & TOptions ): UseBackgroundQueryResult< TOptions['errorPolicy'] extends 'ignore' | 'all' ? TOptions['returnPartialData'] extends true @@ -67,10 +66,7 @@ export function useBackgroundQuery< TVariables extends OperationVariables = OperationVariables >( query: DocumentNode | TypedDocumentNode, - options: Omit< - SuspenseQueryHookOptions, NoInfer>, - 'returnPartialData' | 'refetchWritePolicy' - > & { + options: SuspenseQueryHookOptionsNoInfer & { returnPartialData: true; errorPolicy: 'ignore' | 'all'; } @@ -81,62 +77,19 @@ export function useBackgroundQuery< TVariables extends OperationVariables = OperationVariables >( query: DocumentNode | TypedDocumentNode, - options: Omit< - SuspenseQueryHookOptions, NoInfer>, - 'returnPartialData' | 'refetchWritePolicy' - > & { + options: SuspenseQueryHookOptionsNoInfer & { errorPolicy: 'ignore' | 'all'; } ): UseBackgroundQueryResult; -export function useBackgroundQuery< - TData = unknown, - TVariables extends OperationVariables = OperationVariables ->( - query: DocumentNode | TypedDocumentNode, - options: Omit< - SuspenseQueryHookOptions, NoInfer>, - 'returnPartialData' | 'refetchWritePolicy' - > & { - skip: boolean; - } -): UseBackgroundQueryResult; - -// TODO: support `returnPartialData` | `refetchWritePolicy` -// see https://github.com/apollographql/apollo-client/issues/10893 - -// export function useBackgroundQuery< -// TData = unknown, -// TVariables extends OperationVariables = OperationVariables -// >( -// query: DocumentNode | TypedDocumentNode, -// options: Omit< -// SuspenseQueryHookOptions, NoInfer>, -// 'returnPartialData' | 'refetchWritePolicy' -// > & { -// returnPartialData: true; -// } -// ): UseBackgroundQueryResult, TVariables>; - -export function useBackgroundQuery< - TData = unknown, - TVariables extends OperationVariables = OperationVariables ->( - query: DocumentNode | TypedDocumentNode, - options?: Omit< - SuspenseQueryHookOptions, NoInfer>, - 'returnPartialData' | 'refetchWritePolicy' - > -): UseBackgroundQueryResult; - export function useBackgroundQuery< TData = unknown, TVariables extends OperationVariables = OperationVariables >( query: DocumentNode | TypedDocumentNode, options: SuspenseQueryHookOptionsNoInfer & { + skip: boolean; returnPartialData: true; - errorPolicy: 'ignore' | 'all'; } ): UseBackgroundQueryResult | undefined, TVariables>; @@ -146,9 +99,9 @@ export function useBackgroundQuery< >( query: DocumentNode | TypedDocumentNode, options: SuspenseQueryHookOptionsNoInfer & { - errorPolicy: 'ignore' | 'all'; + returnPartialData: true; } -): UseBackgroundQueryResult; +): UseBackgroundQueryResult, TVariables>; export function useBackgroundQuery< TData = unknown, @@ -156,9 +109,9 @@ export function useBackgroundQuery< >( query: DocumentNode | TypedDocumentNode, options: SuspenseQueryHookOptionsNoInfer & { - returnPartialData: true; + skip: boolean; } -): UseBackgroundQueryResult, TVariables>; +): UseBackgroundQueryResult; export function useBackgroundQuery< TData = unknown, diff --git a/src/utilities/index.ts b/src/utilities/index.ts index 88681d110e0..b0dc4a17873 100644 --- a/src/utilities/index.ts +++ b/src/utilities/index.ts @@ -126,4 +126,4 @@ export { stripTypename } from './common/stripTypename'; export * from './types/IsStrictlyAny'; export { DeepOmit } from './types/DeepOmit'; -export { DeepPartial, DeepPartialObject } from './types/DeepPartial'; +export { DeepPartial } from './types/DeepPartial'; diff --git a/src/utilities/types/DeepPartial.ts b/src/utilities/types/DeepPartial.ts index 219a2627806..e9d1848e9e2 100644 --- a/src/utilities/types/DeepPartial.ts +++ b/src/utilities/types/DeepPartial.ts @@ -53,6 +53,6 @@ type DeepPartialReadonlyMap = {} & ReadonlyMap< type DeepPartialSet = {} & Set>; type DeepPartialReadonlySet = {} & ReadonlySet>; -export type DeepPartialObject = { +type DeepPartialObject = { [K in keyof T]?: DeepPartial; }; From 1062ca63f4b4b69eeae5083e4e8114a2d9054dd3 Mon Sep 17 00:00:00 2001 From: alessia Date: Mon, 26 Jun 2023 15:18:11 -0400 Subject: [PATCH 09/17] fix: remove redundant test cases already in place --- .../__tests__/useBackgroundQuery.test.tsx | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx index e1dc00142d6..def23050096 100644 --- a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx +++ b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx @@ -3315,25 +3315,5 @@ describe('useBackgroundQuery', () => { expectTypeOf(dynamic).toEqualTypeOf(); expectTypeOf(dynamic).not.toEqualTypeOf(); }); - - // TODO: https://github.com/apollographql/apollo-client/issues/10893 - // it('returns DeepPartial with returnPartialData: true', () => { - // }); - - // TODO: https://github.com/apollographql/apollo-client/issues/10893 - // it('returns TData with returnPartialData: false', () => { - // }); - - // TODO: https://github.com/apollographql/apollo-client/issues/10893 - // it('returns TData when passing an option that does not affect TData', () => { - // }); - - // TODO: https://github.com/apollographql/apollo-client/issues/10893 - // it('handles combinations of options', () => { - // }); - - // TODO: https://github.com/apollographql/apollo-client/issues/10893 - // it('returns correct TData type when combined options that do not affect TData', () => { - // }); }); }); From 6d0d3a4f19b8de57321927c31c41ad81675bdca8 Mon Sep 17 00:00:00 2001 From: alessia Date: Tue, 27 Jun 2023 16:45:14 -0400 Subject: [PATCH 10/17] tests: adds refetchWritePolicy tests --- .../__tests__/useBackgroundQuery.test.tsx | 307 ++++++++++++++++-- 1 file changed, 281 insertions(+), 26 deletions(-) diff --git a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx index def23050096..f5139cc9e47 100644 --- a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx +++ b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx @@ -2727,14 +2727,269 @@ describe('useBackgroundQuery', () => { }); }); - // refetchWritePolicy - // it('honors refetchWritePolicy set to "merge"', async () => { + it('honors refetchWritePolicy set to "merge"', async () => { + const user = userEvent.setup(); + + const query: TypedDocumentNode< + { primes: number[] }, + { min: number; max: number } + > = gql` + query GetPrimes($min: number, $max: number) { + primes(min: $min, max: $max) + } + `; + + interface QueryData { + primes: number[]; + } + + const mocks = [ + { + request: { query, variables: { min: 0, max: 12 } }, + result: { data: { primes: [2, 3, 5, 7, 11] } }, + }, + { + request: { query, variables: { min: 12, max: 30 } }, + result: { data: { primes: [13, 17, 19, 23, 29] } }, + delay: 10, + }, + ]; + + const mergeParams: [number[] | undefined, number[]][] = []; + const cache = new InMemoryCache({ + typePolicies: { + Query: { + fields: { + primes: { + keyArgs: false, + merge(existing: number[] | undefined, incoming: number[]) { + mergeParams.push([existing, incoming]); + return existing ? existing.concat(incoming) : incoming; + }, + }, + }, + }, + }, + }); + + function SuspenseFallback() { + return
loading
; + } + + const client = new ApolloClient({ + link: new MockLink(mocks), + cache, + }); + + const suspenseCache = new SuspenseCache(); + + function Child({ + refetch, + queryRef, + }: { + refetch: ( + variables?: Partial | undefined + ) => Promise>; + queryRef: QueryReference; + }) { + const { data, error, networkStatus } = useReadQuery(queryRef); + + return ( +
+ +
{data?.primes.join(', ')}
+
{networkStatus}
+
{error?.message || 'undefined'}
+
+ ); + } + + function Parent() { + const [queryRef, { refetch }] = useBackgroundQuery(query, { + variables: { min: 0, max: 12 }, + refetchWritePolicy: 'merge', + }); + return ; + } + + function App() { + return ( + + }> + + + + ); + } + + render(); + + await waitFor(() => { + expect(screen.getByTestId('primes')).toHaveTextContent( + '2, 3, 5, 7, 11' + ); + }); + expect(screen.getByTestId('network-status')).toHaveTextContent( + '7' // ready + ); + expect(screen.getByTestId('error')).toHaveTextContent('undefined'); + expect(mergeParams).toEqual([[undefined, [2, 3, 5, 7, 11]]]); + + await act(() => user.click(screen.getByText('Refetch'))); + + await waitFor(() => { + expect(screen.getByTestId('primes')).toHaveTextContent( + '2, 3, 5, 7, 11, 13, 17, 19, 23, 29' + ); + }); + expect(screen.getByTestId('network-status')).toHaveTextContent( + '7' // ready + ); + expect(screen.getByTestId('error')).toHaveTextContent('undefined'); + expect(mergeParams).toEqual([ + [undefined, [2, 3, 5, 7, 11]], + [ + [2, 3, 5, 7, 11], + [13, 17, 19, 23, 29], + ], + ]); + }); + + it('defaults refetchWritePolicy to "overwrite"', async () => { + const user = userEvent.setup(); + + const query: TypedDocumentNode< + { primes: number[] }, + { min: number; max: number } + > = gql` + query GetPrimes($min: number, $max: number) { + primes(min: $min, max: $max) + } + `; + + interface QueryData { + primes: number[]; + } + + const mocks = [ + { + request: { query, variables: { min: 0, max: 12 } }, + result: { data: { primes: [2, 3, 5, 7, 11] } }, + }, + { + request: { query, variables: { min: 12, max: 30 } }, + result: { data: { primes: [13, 17, 19, 23, 29] } }, + delay: 10, + }, + ]; + + const mergeParams: [number[] | undefined, number[]][] = []; + const cache = new InMemoryCache({ + typePolicies: { + Query: { + fields: { + primes: { + keyArgs: false, + merge(existing: number[] | undefined, incoming: number[]) { + mergeParams.push([existing, incoming]); + return existing ? existing.concat(incoming) : incoming; + }, + }, + }, + }, + }, + }); + + function SuspenseFallback() { + return
loading
; + } + + const client = new ApolloClient({ + link: new MockLink(mocks), + cache, + }); - // }); + const suspenseCache = new SuspenseCache(); - // it('defaults refetchWritePolicy to "overwrite"', async () => { + function Child({ + refetch, + queryRef, + }: { + refetch: ( + variables?: Partial | undefined + ) => Promise>; + queryRef: QueryReference; + }) { + const { data, error, networkStatus } = useReadQuery(queryRef); - // }); + return ( +
+ +
{data?.primes.join(', ')}
+
{networkStatus}
+
{error?.message || 'undefined'}
+
+ ); + } + + function Parent() { + const [queryRef, { refetch }] = useBackgroundQuery(query, { + variables: { min: 0, max: 12 }, + }); + return ; + } + + function App() { + return ( + + }> + + + + ); + } + + render(); + + await waitFor(() => { + expect(screen.getByTestId('primes')).toHaveTextContent( + '2, 3, 5, 7, 11' + ); + }); + expect(screen.getByTestId('network-status')).toHaveTextContent( + '7' // ready + ); + expect(screen.getByTestId('error')).toHaveTextContent('undefined'); + expect(mergeParams).toEqual([[undefined, [2, 3, 5, 7, 11]]]); + + await act(() => user.click(screen.getByText('Refetch'))); + + await waitFor(() => { + expect(screen.getByTestId('primes')).toHaveTextContent( + '13, 17, 19, 23, 29' + ); + }); + expect(screen.getByTestId('network-status')).toHaveTextContent( + '7' // ready + ); + expect(screen.getByTestId('error')).toHaveTextContent('undefined'); + expect(mergeParams).toEqual([ + [undefined, [2, 3, 5, 7, 11]], + [undefined, [13, 17, 19, 23, 29]], + ]); + }); // returnPartialData it('does not suspend when partial data is in the cache and using a "cache-first" fetch policy with returnPartialData', async () => { @@ -2859,33 +3114,33 @@ describe('useBackgroundQuery', () => { // }); }); - // it('suspends and does not use partial data when changing variables and using a "cache-first" fetch policy with returnPartialData', async () => { - - // }); - - // it('suspends when partial data is in the cache and using a "network-only" fetch policy with returnPartialData', async () => { - - // }); - - // it('suspends when partial data is in the cache and using a "no-cache" fetch policy with returnPartialData', async () => { - - // }); - - // it('warns when using returnPartialData with a "no-cache" fetch policy', async () => { - - // }); + it.todo( + 'suspends and does not use partial data when changing variables and using a "cache-first" fetch policy with returnPartialData' + ); - // it('does not suspend when partial data is in the cache and using a "cache-and-network" fetch policy with returnPartialData', async () => { + it.todo( + 'suspends when partial data is in the cache and using a "network-only" fetch policy with returnPartialData' + ); - // }); + it.todo( + 'suspends when partial data is in the cache and using a "no-cache" fetch policy with returnPartialData' + ); - // it('suspends and does not use partial data when changing variables and using a "cache-and-network" fetch policy with returnPartialData', async () => { + it.todo( + 'warns when using returnPartialData with a "no-cache" fetch policy' + ); - // }); + it.todo( + 'does not suspend when partial data is in the cache and using a "cache-and-network" fetch policy with returnPartialData' + ); - // it('does not suspend deferred queries with partial data in the cache and using a "cache-first" fetch policy with `returnPartialData`', async () => { + it.todo( + 'suspends and does not use partial data when changing variables and using a "cache-and-network" fetch policy with returnPartialData' + ); - // }); + it.todo( + 'does not suspend deferred queries with partial data in the cache and using a "cache-first" fetch policy with `returnPartialData`' + ); }); describe.skip('type tests', () => { From d94180da8674d4f3ad5636d3731eee3082e0af5c Mon Sep 17 00:00:00 2001 From: alessia Date: Tue, 27 Jun 2023 16:56:36 -0400 Subject: [PATCH 11/17] tests: adds returnPartialData test case --- .../__tests__/useBackgroundQuery.test.tsx | 77 +++++++++---------- 1 file changed, 37 insertions(+), 40 deletions(-) diff --git a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx index f5139cc9e47..f21446625b3 100644 --- a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx +++ b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx @@ -2991,10 +2991,7 @@ describe('useBackgroundQuery', () => { ]); }); - // returnPartialData it('does not suspend when partial data is in the cache and using a "cache-first" fetch policy with returnPartialData', async () => { - // const user = userEvent.setup(); - interface Data { character: { id: string; @@ -3025,6 +3022,19 @@ describe('useBackgroundQuery', () => { }, ]; + interface Renders { + errors: Error[]; + errorCount: number; + suspenseCount: number; + count: number; + } + const renders: Renders = { + errors: [], + errorCount: 0, + suspenseCount: 0, + count: 0, + }; + const cache = new InMemoryCache(); cache.writeQuery({ @@ -3050,6 +3060,7 @@ describe('useBackgroundQuery', () => { } function SuspenseFallback() { + renders.suspenseCount++; return

Loading

; } @@ -3066,52 +3077,38 @@ describe('useBackgroundQuery', () => { }: { queryRef: QueryReference>; }) { - const { data, networkStatus } = useReadQuery(queryRef); + const { data, networkStatus, error } = useReadQuery(queryRef); + renders.count++; return ( -
- {networkStatus} - {data.character?.id} - {data.character?.name} -
+ <> +
{data.character?.id}
+
{data.character?.name}
+
{networkStatus}
+
{error?.message || 'undefined'}
+ ); } render(); - // expect(screen.getByText('Loading')).toBeInTheDocument(); - - // expect(await screen.findByTestId('todos')).toBeInTheDocument(); - - // const todos = screen.getByTestId('todos'); - // const todo1 = screen.getByTestId('todo:1'); - // const button = screen.getByText('Load more'); - - // expect(todo1).toBeInTheDocument(); - - // await act(() => user.click(button)); - - // // startTransition will avoid rendering the suspense fallback for already - // // revealed content if the state update inside the transition causes the - // // component to suspend. - // // - // // Here we should not see the suspense fallback while the component suspends - // // until the todo is finished loading. Seeing the suspense fallback is an - // // indication that we are suspending the component too late in the process. - // expect(screen.queryByText('Loading')).not.toBeInTheDocument(); - - // // We can ensure this works with isPending from useTransition in the process - // expect(todos).toHaveAttribute('aria-busy', 'true'); + expect(renders.suspenseCount).toBe(0); + expect(screen.getByTestId('character-id')).toHaveTextContent('1'); + expect(screen.getByTestId('character-name')).toHaveTextContent(''); + expect(screen.getByTestId('network-status')).toHaveTextContent('1'); // loading + expect(screen.getByTestId('error')).toHaveTextContent('undefined'); - // // Ensure we are showing the stale UI until the new todo has loaded - // expect(todo1).toHaveTextContent('Clean room'); + await waitFor(() => { + expect(screen.getByTestId('character-name')).toHaveTextContent( + 'Doctor Strange' + ); + }); + expect(screen.getByTestId('character-id')).toHaveTextContent('1'); + expect(screen.getByTestId('network-status')).toHaveTextContent('7'); // ready + expect(screen.getByTestId('error')).toHaveTextContent('undefined'); - // // Eventually we should see the updated todos content once its done - // // suspending. - // await waitFor(() => { - // expect(screen.getByTestId('todo:2')).toHaveTextContent( - // 'Take out trash (completed)' - // ); - // expect(todo1).toHaveTextContent('Clean room'); - // }); + expect(renders.count).toBe(2); + expect(renders.suspenseCount).toBe(0); }); it.todo( From 4206168ccbfb2b9a188030e6116b7e3c28b95289 Mon Sep 17 00:00:00 2001 From: alessia Date: Tue, 27 Jun 2023 19:57:19 -0400 Subject: [PATCH 12/17] chore: removes accidentally duplicated type tests --- .../__tests__/useBackgroundQuery.test.tsx | 104 ------------------ 1 file changed, 104 deletions(-) diff --git a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx index f21446625b3..78ba4e406a3 100644 --- a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx +++ b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx @@ -3426,110 +3426,6 @@ describe('useBackgroundQuery', () => { expectTypeOf(explicit).not.toEqualTypeOf(); }); - it('returns unknown when TData cannot be inferred', () => { - const query = gql` - query { - hello - } - `; - - const [queryRef] = useBackgroundQuery(query); - const { data } = useReadQuery(queryRef); - - expectTypeOf(data).toEqualTypeOf(); - }); - - it('disallows wider variables type than specified', () => { - const { query } = useVariablesIntegrationTestCase(); - - // @ts-expect-error should not allow wider TVariables type - useBackgroundQuery(query, { variables: { id: '1', foo: 'bar' } }); - }); - - it('returns TData in default case', () => { - const { query } = useVariablesIntegrationTestCase(); - - const [inferredQueryRef] = useBackgroundQuery(query); - const { data: inferred } = useReadQuery(inferredQueryRef); - - expectTypeOf(inferred).toEqualTypeOf(); - expectTypeOf(inferred).not.toEqualTypeOf(); - - const [explicitQueryRef] = useBackgroundQuery< - VariablesCaseData, - VariablesCaseVariables - >(query); - - const { data: explicit } = useReadQuery(explicitQueryRef); - - expectTypeOf(explicit).toEqualTypeOf(); - expectTypeOf(explicit).not.toEqualTypeOf(); - }); - - it('returns TData | undefined with errorPolicy: "ignore"', () => { - const { query } = useVariablesIntegrationTestCase(); - - const [inferredQueryRef] = useBackgroundQuery(query, { - errorPolicy: 'ignore', - }); - const { data: inferred } = useReadQuery(inferredQueryRef); - - expectTypeOf(inferred).toEqualTypeOf(); - expectTypeOf(inferred).not.toEqualTypeOf(); - - const [explicitQueryRef] = useBackgroundQuery< - VariablesCaseData, - VariablesCaseVariables - >(query, { - errorPolicy: 'ignore', - }); - - const { data: explicit } = useReadQuery(explicitQueryRef); - - expectTypeOf(explicit).toEqualTypeOf(); - expectTypeOf(explicit).not.toEqualTypeOf(); - }); - - it('returns TData | undefined with errorPolicy: "all"', () => { - const { query } = useVariablesIntegrationTestCase(); - - const [inferredQueryRef] = useBackgroundQuery(query, { - errorPolicy: 'all', - }); - const { data: inferred } = useReadQuery(inferredQueryRef); - - expectTypeOf(inferred).toEqualTypeOf(); - expectTypeOf(inferred).not.toEqualTypeOf(); - - const [explicitQueryRef] = useBackgroundQuery(query, { - errorPolicy: 'all', - }); - const { data: explicit } = useReadQuery(explicitQueryRef); - - expectTypeOf(explicit).toEqualTypeOf(); - expectTypeOf(explicit).not.toEqualTypeOf(); - }); - - it('returns TData with errorPolicy: "none"', () => { - const { query } = useVariablesIntegrationTestCase(); - - const [inferredQueryRef] = useBackgroundQuery(query, { - errorPolicy: 'none', - }); - const { data: inferred } = useReadQuery(inferredQueryRef); - - expectTypeOf(inferred).toEqualTypeOf(); - expectTypeOf(inferred).not.toEqualTypeOf(); - - const [explicitQueryRef] = useBackgroundQuery(query, { - errorPolicy: 'none', - }); - const { data: explicit } = useReadQuery(explicitQueryRef); - - expectTypeOf(explicit).toEqualTypeOf(); - expectTypeOf(explicit).not.toEqualTypeOf(); - }); - it('returns TData | undefined when `skip` is present', () => { const { query } = useVariablesIntegrationTestCase(); From f2347fdf6f71859838926cb21fe00690fa08b868 Mon Sep 17 00:00:00 2001 From: alessia Date: Tue, 27 Jun 2023 20:54:36 -0400 Subject: [PATCH 13/17] tests: adds returnPartialData tests and counts renders in child components --- .../__tests__/useBackgroundQuery.test.tsx | 240 ++++++++++++++++-- 1 file changed, 213 insertions(+), 27 deletions(-) diff --git a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx index 78ba4e406a3..25aabce6e1a 100644 --- a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx +++ b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx @@ -49,6 +49,7 @@ import { RefetchFunction, QueryReference, } from '../../../react'; +import { SuspenseQueryHookOptions } from '../../types/types'; import equal from '@wry/equality'; function renderIntegrationTest({ @@ -108,20 +109,18 @@ function renderIntegrationTest({ function Child({ queryRef }: { queryRef: QueryReference }) { const { data } = useReadQuery(queryRef); + // count renders in the child component + renders.count++; return
{data.foo.bar}
; } function Parent() { const [queryRef] = useBackgroundQuery(query); - // count renders in the parent component - renders.count++; return ; } function ParentWithVariables() { const [queryRef] = useBackgroundQuery(query); - // count renders in the parent component - renders.count++; return ; } @@ -176,6 +175,8 @@ function renderVariablesIntegrationTest({ variables, mocks, errorPolicy, + options, + cache, }: { mocks?: { request: { query: DocumentNode; variables: { id: string } }; @@ -189,6 +190,8 @@ function renderVariablesIntegrationTest({ }; }[]; variables: { id: string }; + options?: SuspenseQueryHookOptions; + cache?: InMemoryCache; errorPolicy?: ErrorPolicy; }) { let { mocks: _mocks, query } = useVariablesIntegrationTestCase(); @@ -216,7 +219,7 @@ function renderVariablesIntegrationTest({ ); const suspenseCache = new SuspenseCache(); const client = new ApolloClient({ - cache: new InMemoryCache(), + cache: cache || new InMemoryCache(), link: new MockLink(mocks || _mocks), }); interface Renders { @@ -264,7 +267,8 @@ function renderVariablesIntegrationTest({ }) { const { data, error, networkStatus } = useReadQuery(queryRef); const [variables, setVariables] = React.useState(_variables); - + // count renders in the child component + renders.count++; renders.frames.push({ data, networkStatus, error }); return ( @@ -297,11 +301,10 @@ function renderVariablesIntegrationTest({ errorPolicy?: ErrorPolicy; }) { const [queryRef, { refetch }] = useBackgroundQuery(query, { + ...options, variables, errorPolicy, }); - // count renders in the parent component - renders.count++; return ( ); @@ -334,7 +337,7 @@ function renderVariablesIntegrationTest({ const rerender = ({ variables }: { variables: VariablesCaseVariables }) => { return rest.rerender(); }; - return { ...rest, query, rerender, client, renders }; + return { ...rest, query, rerender, client, renders, mocks: mocks || _mocks }; } function renderPaginatedIntegrationTest({ @@ -443,7 +446,8 @@ function renderPaginatedIntegrationTest({ queryRef: QueryReference; }) { const { data, error } = useReadQuery(queryRef); - + // count renders in the child component + renders.count++; return (
{error ?
{error.message}
: null} @@ -485,8 +489,6 @@ function renderPaginatedIntegrationTest({ const [queryRef, { fetchMore }] = useBackgroundQuery(query, { variables: { limit: 2, offset: 0 }, }); - // count renders in the parent component - renders.count++; return ; } @@ -1122,7 +1124,7 @@ describe('useBackgroundQuery', () => { // the parent component re-renders when promise fulfilled expect(await screen.findByText('hello')).toBeInTheDocument(); - expect(renders.count).toBe(2); + expect(renders.count).toBe(1); }); it('works with startTransition to change variables', async () => { @@ -1414,7 +1416,7 @@ describe('useBackgroundQuery', () => { // the parent component re-renders when promise fulfilled expect(await screen.findByText('hello')).toBeInTheDocument(); - expect(renders.count).toBe(2); + expect(renders.count).toBe(1); client.writeQuery({ query, @@ -1424,6 +1426,7 @@ describe('useBackgroundQuery', () => { // the parent component re-renders when promise fulfilled expect(await screen.findByText('baz')).toBeInTheDocument(); + expect(renders.count).toBe(2); expect(renders.suspenseCount).toBe(1); client.writeQuery({ @@ -1987,7 +1990,7 @@ describe('useBackgroundQuery', () => { // parent component re-suspends expect(renders.suspenseCount).toBe(2); - expect(renders.count).toBe(4); + expect(renders.count).toBe(2); expect( await screen.findByText('1 - Spider-Man (updated)') @@ -2052,7 +2055,7 @@ describe('useBackgroundQuery', () => { // parent component re-suspends expect(renders.suspenseCount).toBe(2); - expect(renders.count).toBe(4); + expect(renders.count).toBe(3); // extra render puts an additional frame into the array expect(renders.frames).toMatchObject([ @@ -2089,7 +2092,7 @@ describe('useBackgroundQuery', () => { // parent component re-suspends expect(renders.suspenseCount).toBe(2); - expect(renders.count).toBe(4); + expect(renders.count).toBe(2); expect( await screen.findByText('1 - Spider-Man (updated)') @@ -2099,7 +2102,7 @@ describe('useBackgroundQuery', () => { // parent component re-suspends expect(renders.suspenseCount).toBe(3); - expect(renders.count).toBe(6); + expect(renders.count).toBe(3); expect( await screen.findByText('1 - Spider-Man (updated again)') @@ -2499,7 +2502,7 @@ describe('useBackgroundQuery', () => { // parent component re-suspends expect(renders.suspenseCount).toBe(2); await waitFor(() => { - expect(renders.count).toBe(4); + expect(renders.count).toBe(2); }); expect(getItemTexts()).toStrictEqual(['C', 'D']); @@ -2524,7 +2527,7 @@ describe('useBackgroundQuery', () => { // parent component re-suspends expect(renders.suspenseCount).toBe(2); await waitFor(() => { - expect(renders.count).toBe(4); + expect(renders.count).toBe(2); }); const moreItems = await screen.findAllByTestId(/letter/i); @@ -2550,7 +2553,7 @@ describe('useBackgroundQuery', () => { // parent component re-suspends expect(renders.suspenseCount).toBe(2); await waitFor(() => { - expect(renders.count).toBe(4); + expect(renders.count).toBe(2); }); const moreItems = await screen.findAllByTestId(/letter/i); @@ -3111,13 +3114,196 @@ describe('useBackgroundQuery', () => { expect(renders.suspenseCount).toBe(0); }); - it.todo( - 'suspends and does not use partial data when changing variables and using a "cache-first" fetch policy with returnPartialData' - ); + it('suspends and does not use partial data when changing variables and using a "cache-first" fetch policy with returnPartialData', async () => { + const partialQuery = gql` + query ($id: ID!) { + character(id: $id) { + id + } + } + `; - it.todo( - 'suspends when partial data is in the cache and using a "network-only" fetch policy with returnPartialData' - ); + const cache = new InMemoryCache(); + + cache.writeQuery({ + query: partialQuery, + data: { character: { id: '1' } }, + variables: { id: '1' }, + }); + + const { renders, mocks, rerender } = renderVariablesIntegrationTest({ + variables: { id: '1' }, + cache, + options: { + fetchPolicy: 'cache-first', + returnPartialData: true, + }, + }); + expect(renders.suspenseCount).toBe(0); + + expect(await screen.findByText('1 - Spider-Man')).toBeInTheDocument(); + + rerender({ variables: { id: '2' } }); + + expect(await screen.findByText('2 - Black Widow')).toBeInTheDocument(); + + expect(renders.frames[2]).toMatchObject({ + ...mocks[1].result, + networkStatus: NetworkStatus.ready, + error: undefined, + }); + + expect(renders.count).toBe(3); + expect(renders.suspenseCount).toBe(1); + expect(renders.frames).toMatchObject([ + { + data: { character: { id: '1' } }, + networkStatus: NetworkStatus.loading, + error: undefined, + }, + { + ...mocks[0].result, + networkStatus: NetworkStatus.ready, + error: undefined, + }, + { + ...mocks[1].result, + networkStatus: NetworkStatus.ready, + error: undefined, + }, + ]); + }); + + it('suspends when partial data is in the cache and using a "network-only" fetch policy with returnPartialData', async () => { + interface Data { + character: { + id: string; + name: string; + }; + } + + const fullQuery: TypedDocumentNode = gql` + query { + character { + id + name + } + } + `; + + const partialQuery = gql` + query { + character { + id + } + } + `; + const mocks = [ + { + request: { query: fullQuery }, + result: { data: { character: { id: '1', name: 'Doctor Strange' } } }, + }, + ]; + + interface Renders { + errors: Error[]; + errorCount: number; + suspenseCount: number; + count: number; + frames: { + data: DeepPartial; + networkStatus: NetworkStatus; + error: ApolloError | undefined; + }[]; + } + const renders: Renders = { + errors: [], + errorCount: 0, + suspenseCount: 0, + count: 0, + frames: [], + }; + + const cache = new InMemoryCache(); + + cache.writeQuery({ + query: partialQuery, + data: { character: { id: '1' } }, + }); + + const client = new ApolloClient({ + link: new MockLink(mocks), + cache, + }); + + const suspenseCache = new SuspenseCache(); + + function App() { + return ( + + }> + + + + ); + } + + function SuspenseFallback() { + renders.suspenseCount++; + return

Loading

; + } + + function Parent() { + const [queryRef] = useBackgroundQuery(fullQuery, { + fetchPolicy: 'network-only', + returnPartialData: true, + }); + + return ; + } + + function Todo({ + queryRef, + }: { + queryRef: QueryReference>; + }) { + const { data, networkStatus, error } = useReadQuery(queryRef); + renders.frames.push({ data, networkStatus, error }); + renders.count++; + return ( + <> +
{data.character?.id}
+
{data.character?.name}
+
{networkStatus}
+
{error?.message || 'undefined'}
+ + ); + } + + render(); + + expect(renders.suspenseCount).toBe(1); + + await waitFor(() => { + expect(screen.getByTestId('character-name')).toHaveTextContent( + 'Doctor Strange' + ); + }); + expect(screen.getByTestId('character-id')).toHaveTextContent('1'); + expect(screen.getByTestId('network-status')).toHaveTextContent('7'); // ready + expect(screen.getByTestId('error')).toHaveTextContent('undefined'); + + expect(renders.count).toBe(1); + expect(renders.suspenseCount).toBe(1); + + expect(renders.frames).toMatchObject([ + { + ...mocks[0].result, + networkStatus: NetworkStatus.ready, + error: undefined, + }, + ]); + }); it.todo( 'suspends when partial data is in the cache and using a "no-cache" fetch policy with returnPartialData' From 1419c34d61679f7cb06f0f9c4b44cef7641d9ca5 Mon Sep 17 00:00:00 2001 From: alessia Date: Tue, 27 Jun 2023 21:02:43 -0400 Subject: [PATCH 14/17] tests: adds two more returnPartialData tests --- .../__tests__/useBackgroundQuery.test.tsx | 170 +++++++++++++++++- 1 file changed, 164 insertions(+), 6 deletions(-) diff --git a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx index 25aabce6e1a..c27775dc462 100644 --- a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx +++ b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx @@ -3305,13 +3305,171 @@ describe('useBackgroundQuery', () => { ]); }); - it.todo( - 'suspends when partial data is in the cache and using a "no-cache" fetch policy with returnPartialData' - ); + it('suspends when partial data is in the cache and using a "no-cache" fetch policy with returnPartialData', async () => { + const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(); + interface Data { + character: { + id: string; + name: string; + }; + } - it.todo( - 'warns when using returnPartialData with a "no-cache" fetch policy' - ); + const fullQuery: TypedDocumentNode = gql` + query { + character { + id + name + } + } + `; + + const partialQuery = gql` + query { + character { + id + } + } + `; + const mocks = [ + { + request: { query: fullQuery }, + result: { data: { character: { id: '1', name: 'Doctor Strange' } } }, + }, + ]; + + interface Renders { + errors: Error[]; + errorCount: number; + suspenseCount: number; + count: number; + frames: { + data: DeepPartial; + networkStatus: NetworkStatus; + error: ApolloError | undefined; + }[]; + } + const renders: Renders = { + errors: [], + errorCount: 0, + suspenseCount: 0, + count: 0, + frames: [], + }; + + const cache = new InMemoryCache(); + + cache.writeQuery({ + query: partialQuery, + data: { character: { id: '1' } }, + }); + + const client = new ApolloClient({ + link: new MockLink(mocks), + cache, + }); + + const suspenseCache = new SuspenseCache(); + + function App() { + return ( + + }> + + + + ); + } + + function SuspenseFallback() { + renders.suspenseCount++; + return

Loading

; + } + + function Parent() { + const [queryRef] = useBackgroundQuery(fullQuery, { + fetchPolicy: 'no-cache', + returnPartialData: true, + }); + + return ; + } + + function Todo({ + queryRef, + }: { + queryRef: QueryReference>; + }) { + const { data, networkStatus, error } = useReadQuery(queryRef); + renders.frames.push({ data, networkStatus, error }); + renders.count++; + return ( + <> +
{data.character?.id}
+
{data.character?.name}
+
{networkStatus}
+
{error?.message || 'undefined'}
+ + ); + } + + render(); + + expect(renders.suspenseCount).toBe(1); + + await waitFor(() => { + expect(screen.getByTestId('character-name')).toHaveTextContent( + 'Doctor Strange' + ); + }); + expect(screen.getByTestId('character-id')).toHaveTextContent('1'); + expect(screen.getByTestId('network-status')).toHaveTextContent('7'); // ready + expect(screen.getByTestId('error')).toHaveTextContent('undefined'); + + expect(renders.count).toBe(1); + expect(renders.suspenseCount).toBe(1); + + expect(renders.frames).toMatchObject([ + { + ...mocks[0].result, + networkStatus: NetworkStatus.ready, + error: undefined, + }, + ]); + + consoleSpy.mockRestore(); + }); + + it('warns when using returnPartialData with a "no-cache" fetch policy', async () => { + const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(); + + const query: TypedDocumentNode = gql` + query UserQuery { + greeting + } + `; + const mocks = [ + { + request: { query }, + result: { data: { greeting: 'Hello' } }, + }, + ]; + + renderSuspenseHook( + () => + useBackgroundQuery(query, { + fetchPolicy: 'no-cache', + returnPartialData: true, + }), + { mocks } + ); + + expect(console.warn).toHaveBeenCalledTimes(1); + expect(console.warn).toHaveBeenCalledWith( + 'Using `returnPartialData` with a `no-cache` fetch policy has no effect. To read partial data from the cache, consider using an alternate fetch policy.' + ); + + consoleSpy.mockRestore(); + }); it.todo( 'does not suspend when partial data is in the cache and using a "cache-and-network" fetch policy with returnPartialData' From c7b1a6e5e71f8433ac789e69172a7537b70c05ba Mon Sep 17 00:00:00 2001 From: alessia Date: Tue, 27 Jun 2023 21:17:27 -0400 Subject: [PATCH 15/17] tests: adds cache-and-network + returnPartialData test case --- .../__tests__/useBackgroundQuery.test.tsx | 200 +++++++++++++++++- 1 file changed, 194 insertions(+), 6 deletions(-) diff --git a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx index c27775dc462..d5ea5e4edeb 100644 --- a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx +++ b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx @@ -3471,13 +3471,201 @@ describe('useBackgroundQuery', () => { consoleSpy.mockRestore(); }); - it.todo( - 'does not suspend when partial data is in the cache and using a "cache-and-network" fetch policy with returnPartialData' - ); + it('does not suspend when partial data is in the cache and using a "cache-and-network" fetch policy with returnPartialData', async () => { + interface Data { + character: { + id: string; + name: string; + }; + } - it.todo( - 'suspends and does not use partial data when changing variables and using a "cache-and-network" fetch policy with returnPartialData' - ); + const fullQuery: TypedDocumentNode = gql` + query { + character { + id + name + } + } + `; + + const partialQuery = gql` + query { + character { + id + } + } + `; + const mocks = [ + { + request: { query: fullQuery }, + result: { data: { character: { id: '1', name: 'Doctor Strange' } } }, + }, + ]; + + interface Renders { + errors: Error[]; + errorCount: number; + suspenseCount: number; + count: number; + frames: { + data: DeepPartial; + networkStatus: NetworkStatus; + error: ApolloError | undefined; + }[]; + } + const renders: Renders = { + errors: [], + errorCount: 0, + suspenseCount: 0, + count: 0, + frames: [], + }; + + const cache = new InMemoryCache(); + + cache.writeQuery({ + query: partialQuery, + data: { character: { id: '1' } }, + }); + + const client = new ApolloClient({ + link: new MockLink(mocks), + cache, + }); + + const suspenseCache = new SuspenseCache(); + + function App() { + return ( + + }> + + + + ); + } + + function SuspenseFallback() { + renders.suspenseCount++; + return

Loading

; + } + + function Parent() { + const [queryRef] = useBackgroundQuery(fullQuery, { + fetchPolicy: 'cache-and-network', + returnPartialData: true, + }); + + return ; + } + + function Todo({ + queryRef, + }: { + queryRef: QueryReference>; + }) { + const { data, networkStatus, error } = useReadQuery(queryRef); + renders.frames.push({ data, networkStatus, error }); + renders.count++; + return ( + <> +
{data.character?.id}
+
{data.character?.name}
+
{networkStatus}
+
{error?.message || 'undefined'}
+ + ); + } + + render(); + + expect(renders.suspenseCount).toBe(0); + expect(screen.getByTestId('character-id')).toHaveTextContent('1'); + // name is not present yet, since it's missing in partial data + expect(screen.getByTestId('character-name')).toHaveTextContent(''); + expect(screen.getByTestId('network-status')).toHaveTextContent('1'); // loading + expect(screen.getByTestId('error')).toHaveTextContent('undefined'); + + await waitFor(() => { + expect(screen.getByTestId('character-name')).toHaveTextContent( + 'Doctor Strange' + ); + }); + expect(screen.getByTestId('character-id')).toHaveTextContent('1'); + expect(screen.getByTestId('network-status')).toHaveTextContent('7'); // ready + expect(screen.getByTestId('error')).toHaveTextContent('undefined'); + + expect(renders.count).toBe(2); + expect(renders.suspenseCount).toBe(0); + + expect(renders.frames).toMatchObject([ + { + data: { character: { id: '1' } }, + networkStatus: NetworkStatus.loading, + error: undefined, + }, + { + ...mocks[0].result, + networkStatus: NetworkStatus.ready, + error: undefined, + }, + ]); + }); + + it('suspends and does not use partial data when changing variables and using a "cache-and-network" fetch policy with returnPartialData', async () => { + const partialQuery = gql` + query ($id: ID!) { + character(id: $id) { + id + } + } + `; + + const cache = new InMemoryCache(); + + cache.writeQuery({ + query: partialQuery, + data: { character: { id: '1' } }, + variables: { id: '1' }, + }); + + const { renders, mocks, rerender } = renderVariablesIntegrationTest({ + variables: { id: '1' }, + cache, + options: { + fetchPolicy: 'cache-and-network', + returnPartialData: true, + }, + }); + + expect(renders.suspenseCount).toBe(0); + + expect(await screen.findByText('1 - Spider-Man')).toBeInTheDocument(); + + rerender({ variables: { id: '2' } }); + + expect(await screen.findByText('2 - Black Widow')).toBeInTheDocument(); + + expect(renders.count).toBe(3); + expect(renders.suspenseCount).toBe(1); + expect(renders.frames).toMatchObject([ + { + data: { character: { id: '1' } }, + networkStatus: NetworkStatus.loading, + error: undefined, + }, + { + ...mocks[0].result, + networkStatus: NetworkStatus.ready, + error: undefined, + }, + { + ...mocks[1].result, + networkStatus: NetworkStatus.ready, + error: undefined, + }, + ]); + }); it.todo( 'does not suspend deferred queries with partial data in the cache and using a "cache-first" fetch policy with `returnPartialData`' From 3085b6310a7a3c4640c15654530a5290add794a9 Mon Sep 17 00:00:00 2001 From: alessia Date: Tue, 27 Jun 2023 21:34:48 -0400 Subject: [PATCH 16/17] tests: adds defer test case --- .../__tests__/useBackgroundQuery.test.tsx | 197 +++++++++++++++++- 1 file changed, 194 insertions(+), 3 deletions(-) diff --git a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx index d5ea5e4edeb..08a8b97b5be 100644 --- a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx +++ b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx @@ -3667,9 +3667,200 @@ describe('useBackgroundQuery', () => { ]); }); - it.todo( - 'does not suspend deferred queries with partial data in the cache and using a "cache-first" fetch policy with `returnPartialData`' - ); + it('does not suspend deferred queries with partial data in the cache and using a "cache-first" fetch policy with `returnPartialData`', async () => { + interface QueryData { + greeting: { + __typename: string; + message?: string; + recipient?: { + __typename: string; + name: string; + }; + }; + } + + const query: TypedDocumentNode = gql` + query { + greeting { + message + ... on Greeting @defer { + recipient { + name + } + } + } + } + `; + + const link = new MockSubscriptionLink(); + const cache = new InMemoryCache(); + + // We are intentionally writing partial data to the cache. Supress console + // warnings to avoid unnecessary noise in the test. + const consoleSpy = jest.spyOn(console, 'error').mockImplementation(); + cache.writeQuery({ + query, + data: { + greeting: { + __typename: 'Greeting', + recipient: { __typename: 'Person', name: 'Cached Alice' }, + }, + }, + }); + consoleSpy.mockRestore(); + + interface Renders { + errors: Error[]; + errorCount: number; + suspenseCount: number; + count: number; + frames: { + data: DeepPartial; + networkStatus: NetworkStatus; + error: ApolloError | undefined; + }[]; + } + const renders: Renders = { + errors: [], + errorCount: 0, + suspenseCount: 0, + count: 0, + frames: [], + }; + + const client = new ApolloClient({ + link, + cache, + }); + + const suspenseCache = new SuspenseCache(); + + function App() { + return ( + + }> + + + + ); + } + + function SuspenseFallback() { + renders.suspenseCount++; + return

Loading

; + } + + function Parent() { + const [queryRef] = useBackgroundQuery(query, { + fetchPolicy: 'cache-first', + returnPartialData: true, + }); + + return ; + } + + function Todo({ + queryRef, + }: { + queryRef: QueryReference>; + }) { + const { data, networkStatus, error } = useReadQuery(queryRef); + renders.frames.push({ data, networkStatus, error }); + renders.count++; + return ( + <> +
{data.greeting?.message}
+
{data.greeting?.recipient?.name}
+
{networkStatus}
+
{error?.message || 'undefined'}
+ + ); + } + + render(); + + expect(renders.suspenseCount).toBe(0); + expect(screen.getByTestId('recipient')).toHaveTextContent('Cached Alice'); + // message is not present yet, since it's missing in partial data + expect(screen.getByTestId('message')).toHaveTextContent(''); + expect(screen.getByTestId('network-status')).toHaveTextContent('1'); // loading + expect(screen.getByTestId('error')).toHaveTextContent('undefined'); + + link.simulateResult({ + result: { + data: { + greeting: { message: 'Hello world', __typename: 'Greeting' }, + }, + hasNext: true, + }, + }); + + await waitFor(() => { + expect(screen.getByTestId('message')).toHaveTextContent('Hello world'); + }); + expect(screen.getByTestId('recipient')).toHaveTextContent('Cached Alice'); + expect(screen.getByTestId('network-status')).toHaveTextContent('7'); // ready + expect(screen.getByTestId('error')).toHaveTextContent('undefined'); + + link.simulateResult({ + result: { + incremental: [ + { + data: { + __typename: 'Greeting', + recipient: { name: 'Alice', __typename: 'Person' }, + }, + path: ['greeting'], + }, + ], + hasNext: false, + }, + }); + + await waitFor(() => { + expect(screen.getByTestId('recipient').textContent).toEqual('Alice'); + }); + expect(screen.getByTestId('message')).toHaveTextContent('Hello world'); + expect(screen.getByTestId('network-status')).toHaveTextContent('7'); // ready + expect(screen.getByTestId('error')).toHaveTextContent('undefined'); + + expect(renders.count).toBe(3); + expect(renders.suspenseCount).toBe(0); + expect(renders.frames).toMatchObject([ + { + data: { + greeting: { + __typename: 'Greeting', + recipient: { __typename: 'Person', name: 'Cached Alice' }, + }, + }, + networkStatus: NetworkStatus.loading, + error: undefined, + }, + { + data: { + greeting: { + __typename: 'Greeting', + message: 'Hello world', + recipient: { __typename: 'Person', name: 'Cached Alice' }, + }, + }, + networkStatus: NetworkStatus.ready, + error: undefined, + }, + { + data: { + greeting: { + __typename: 'Greeting', + message: 'Hello world', + recipient: { __typename: 'Person', name: 'Alice' }, + }, + }, + networkStatus: NetworkStatus.ready, + error: undefined, + }, + ]); + }); }); describe.skip('type tests', () => { From e20e0f7156f42bea7fcfade36ac1b3616871be1a Mon Sep 17 00:00:00 2001 From: alessia Date: Tue, 27 Jun 2023 21:41:37 -0400 Subject: [PATCH 17/17] chore: adds changeset --- .changeset/spotty-news-stare.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/spotty-news-stare.md diff --git a/.changeset/spotty-news-stare.md b/.changeset/spotty-news-stare.md new file mode 100644 index 00000000000..7dd57152d5c --- /dev/null +++ b/.changeset/spotty-news-stare.md @@ -0,0 +1,5 @@ +--- +'@apollo/client': patch +--- + +Adds support for `returnPartialData` and `refetchWritePolicy` options in `useBackgroundQuery` hook.