From c735af0b08547c3ceeea3d9f5c3122ac1d345205 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Fri, 25 Aug 2023 16:39:14 -0600 Subject: [PATCH 001/199] Get bones of useInteractiveQuery in place --- .../__tests__/useInteractiveQuery.test.tsx | 4411 +++++++++++++++++ src/react/hooks/useInteractiveQuery.ts | 193 + src/react/types/types.ts | 21 + 3 files changed, 4625 insertions(+) create mode 100644 src/react/hooks/__tests__/useInteractiveQuery.test.tsx create mode 100644 src/react/hooks/useInteractiveQuery.ts diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx new file mode 100644 index 00000000000..60844bd76cf --- /dev/null +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -0,0 +1,4411 @@ +import React, { Fragment, StrictMode, Suspense } from "react"; +import { + act, + render, + screen, + renderHook, + RenderHookOptions, + waitFor, +} 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, + ApolloError, + DocumentNode, + ApolloClient, + ErrorPolicy, + NormalizedCacheObject, + NetworkStatus, + ApolloCache, + TypedDocumentNode, + ApolloLink, + Observable, + FetchMoreQueryOptions, + OperationVariables, + ApolloQueryResult, +} from "../../../core"; +import { + MockedResponse, + MockLink, + MockSubscriptionLink, +} from "../../../testing"; +import { + concatPagination, + offsetLimitPagination, + DeepPartial, +} from "../../../utilities"; +import { useInteractiveQuery } from "../useInteractiveQuery"; +import { useReadQuery } from "../useReadQuery"; +import { ApolloProvider } from "../../context"; +import { InMemoryCache } from "../../../cache"; +import { + FetchMoreFunction, + RefetchFunction, + QueryReference, +} from "../../../react"; +import { + InteractiveQueryHookOptions, + InteractiveQueryHookFetchPolicy, +} from "../../types/types"; +import { RefetchWritePolicy } from "../../../core/watchQueryOptions"; +import invariant from "ts-invariant"; + +function renderIntegrationTest({ + client, +}: { + client?: ApolloClient; +} = {}) { + const query: TypedDocumentNode = gql` + query SimpleQuery { + foo { + bar + } + } + `; + + const user = userEvent.setup(); + + const mocks = [ + { + request: { query }, + result: { data: { foo: { bar: "hello" } } }, + delay: 10, + }, + ]; + const _client = + client || + new ApolloClient({ + cache: new InMemoryCache(), + link: new MockLink(mocks), + }); + interface Renders { + errors: Error[]; + errorCount: number; + suspenseCount: number; + count: number; + } + const renders: Renders = { + errors: [], + errorCount: 0, + suspenseCount: 0, + count: 0, + }; + const errorBoundaryProps: ErrorBoundaryProps = { + fallback:
Error
, + onError: (error) => { + renders.errorCount++; + renders.errors.push(error); + }, + }; + + interface QueryData { + foo: { bar: string }; + } + + function SuspenseFallback() { + renders.suspenseCount++; + return
loading
; + } + + 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, loadQuery] = useInteractiveQuery(query); + return ( +
+ + {queryRef && } +
+ ); + } + + function App() { + return ( + + + }> + + + + + ); + } + + const utils = render(); + + return { + ...utils, + query, + client: _client, + renders, + user, + loadQueryButton: screen.getByText("Load query"), + }; +} + +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 } } }, + delay: 20, + })); + return { mocks, query }; +} + +function renderVariablesIntegrationTest({ + variables, + mocks, + errorPolicy, + options, + cache, +}: { + mocks?: { + request: { query: DocumentNode; variables: { id: string } }; + result: { + data?: { + character: { + id: string; + name: string | null; + }; + }; + }; + }[]; + variables: { id: string }; + options?: InteractiveQueryHookOptions; + cache?: InMemoryCache; + errorPolicy?: ErrorPolicy; +}) { + const user = userEvent.setup(); + let { mocks: _mocks, query } = useVariablesIntegrationTestCase(); + + // duplicate mocks with (updated) in the name for refetches + _mocks = [..._mocks, ..._mocks, ..._mocks].map((mock, index) => { + return { + ...mock, + request: mock.request, + result: { + data: { + character: { + ...mock.result.data.character, + name: + index > 3 + ? index > 7 + ? `${mock.result.data.character.name} (updated again)` + : `${mock.result.data.character.name} (updated)` + : mock.result.data.character.name, + }, + }, + }, + }; + }); + const client = new ApolloClient({ + cache: cache || new InMemoryCache(), + link: new MockLink(mocks || _mocks), + }); + interface Renders { + errors: Error[]; + errorCount: number; + suspenseCount: number; + count: number; + frames: { + data: VariablesCaseData; + networkStatus: NetworkStatus; + error: ApolloError | undefined; + }[]; + } + const renders: Renders = { + errors: [], + errorCount: 0, + suspenseCount: 0, + count: 0, + frames: [], + }; + + const errorBoundaryProps: ErrorBoundaryProps = { + fallback:
Error
, + onError: (error) => { + renders.errorCount++; + renders.errors.push(error); + }, + }; + + function SuspenseFallback() { + renders.suspenseCount++; + return
loading
; + } + + function Child({ + onChange, + queryRef, + }: { + onChange: (variables: VariablesCaseVariables) => void; + queryRef: QueryReference; + }) { + const { data, error, networkStatus } = useReadQuery(queryRef); + // count renders in the child component + renders.count++; + renders.frames.push({ data, networkStatus, error }); + + return ( +
+ {error ?
{error.message}
: null} + + {data?.character.id} - {data?.character.name} +
+ ); + } + + function ParentWithVariables({ + variables, + errorPolicy = "none", + }: { + variables: VariablesCaseVariables; + errorPolicy?: ErrorPolicy; + }) { + const [queryRef, loadQuery] = useInteractiveQuery(query, { + ...options, + variables, + errorPolicy, + }); + return ( +
+ + }> + {queryRef && } + +
+ ); + } + + function App({ + variables, + errorPolicy, + }: { + variables: VariablesCaseVariables; + errorPolicy?: ErrorPolicy; + }) { + return ( + + + + + + ); + } + + const { ...rest } = render( + + ); + const rerender = ({ variables }: { variables: VariablesCaseVariables }) => { + return rest.rerender(); + }; + return { + ...rest, + query, + rerender, + client, + renders, + mocks: mocks || _mocks, + user, + loadQueryButton: screen.getByText("Load query"), + }; +} + +function renderPaginatedIntegrationTest({ + updateQuery, + fieldPolicies, +}: { + fieldPolicies?: boolean; + updateQuery?: boolean; + mocks?: { + request: { + query: DocumentNode; + variables: { offset: number; limit: number }; + }; + result: { + data: { + letters: { + letter: string; + position: number; + }[]; + }; + }; + }[]; +} = {}) { + interface QueryData { + letters: { + letter: string; + position: number; + }[]; + } + + interface Variables { + limit?: number; + offset?: number; + } + + const query: TypedDocumentNode = gql` + query letters($limit: Int, $offset: Int) { + letters(limit: $limit) { + letter + position + } + } + `; + + const data = "ABCDEFG" + .split("") + .map((letter, index) => ({ letter, position: index + 1 })); + + const link = new ApolloLink((operation) => { + const { offset = 0, limit = 2 } = operation.variables; + const letters = data.slice(offset, offset + limit); + + return new Observable((observer) => { + setTimeout(() => { + observer.next({ data: { letters } }); + observer.complete(); + }, 10); + }); + }); + + const cacheWithTypePolicies = new InMemoryCache({ + typePolicies: { + Query: { + fields: { + letters: concatPagination(), + }, + }, + }, + }); + const client = new ApolloClient({ + cache: fieldPolicies ? cacheWithTypePolicies : new InMemoryCache(), + link, + }); + interface Renders { + errors: Error[]; + errorCount: number; + suspenseCount: number; + count: number; + } + const renders: Renders = { + errors: [], + errorCount: 0, + suspenseCount: 0, + count: 0, + }; + + const errorBoundaryProps: ErrorBoundaryProps = { + fallback:
Error
, + onError: (error) => { + renders.errorCount++; + renders.errors.push(error); + }, + }; + + function SuspenseFallback() { + renders.suspenseCount++; + return
loading
; + } + + function Child({ + queryRef, + fetchMore, + }: { + fetchMore: FetchMoreFunction; + queryRef: QueryReference; + }) { + const { data, error } = useReadQuery(queryRef); + // count renders in the child component + renders.count++; + return ( +
+ {error ?
{error.message}
: null} + +
    + {data.letters.map(({ letter, position }) => ( +
  • + {letter} +
  • + ))} +
+
+ ); + } + + function ParentWithVariables() { + const [queryRef, { fetchMore }] = useInteractiveQuery(query, { + variables: { limit: 2, offset: 0 }, + }); + return ; + } + + function App() { + return ( + + + }> + + + + + ); + } + + const { ...rest } = render(); + return { ...rest, data, query, client, renders }; +} + +type RenderSuspenseHookOptions = Omit< + RenderHookOptions, + "wrapper" +> & { + client?: ApolloClient; + link?: ApolloLink; + cache?: ApolloCache; + mocks?: MockedResponse[]; + strictMode?: boolean; +}; + +interface Renders { + errors: Error[]; + errorCount: number; + suspenseCount: number; + count: number; + frames: Result[]; +} + +interface SimpleQueryData { + greeting: string; +} + +function renderSuspenseHook( + render: (initialProps: Props) => Result, + options: RenderSuspenseHookOptions = Object.create(null) +) { + function SuspenseFallback() { + renders.suspenseCount++; + + return
loading
; + } + + const renders: Renders = { + errors: [], + errorCount: 0, + suspenseCount: 0, + count: 0, + frames: [], + }; + + const { mocks = [], strictMode, ...renderHookOptions } = options; + + const client = + options.client || + new ApolloClient({ + cache: options.cache || new InMemoryCache(), + link: options.link || new MockLink(mocks), + }); + + const view = renderHook( + (props) => { + renders.count++; + + const view = render(props); + + renders.frames.push(view); + + return view; + }, + { + ...renderHookOptions, + wrapper: ({ children }) => { + const Wrapper = strictMode ? StrictMode : Fragment; + + return ( + + }> + Error} + onError={(error) => { + renders.errorCount++; + renders.errors.push(error); + }} + > + {children} + + + + ); + }, + } + ); + + return { ...view, renders }; +} + +describe("useInteractiveQuery", () => { + // it('fetches a simple query with minimal config', async () => { + // const query = gql` + // query { + // hello + // } + // `; + // const mocks = [ + // { + // request: { query }, + // result: { data: { hello: 'world 1' } }, + // }, + // ]; + // const { result } = renderHook(() => useInteractiveQuery(query), { + // wrapper: ({ children }) => ( + // {children} + // ), + // }); + + // const [queryRef, loadQuery] = result.current; + + // const _result = await queryRef[QUERY_REFERENCE_SYMBOL].promise; + + // expect(_result).toEqual({ + // data: { hello: 'world 1' }, + // loading: false, + // networkStatus: 7, + // }); + // }); + + // it('allows the client to be overridden', async () => { + // const query: TypedDocumentNode = gql` + // query UserQuery { + // greeting + // } + // `; + + // const globalClient = new ApolloClient({ + // link: new ApolloLink(() => + // Observable.of({ data: { greeting: 'global hello' } }) + // ), + // cache: new InMemoryCache(), + // }); + + // const localClient = new ApolloClient({ + // link: new ApolloLink(() => + // Observable.of({ data: { greeting: 'local hello' } }) + // ), + // cache: new InMemoryCache(), + // }); + + // const { result } = renderSuspenseHook( + // () => useInteractiveQuery(query, { client: localClient }), + // { client: globalClient } + // ); + + // const [queryRef] = result.current; + + // const _result = await queryRef[QUERY_REFERENCE_SYMBOL].promise; + + // await waitFor(() => { + // expect(_result).toEqual({ + // data: { greeting: 'local hello' }, + // loading: false, + // networkStatus: NetworkStatus.ready, + // }); + // }); + // }); + + // it('passes context to the link', async () => { + // const query = gql` + // query ContextQuery { + // context + // } + // `; + + // const link = new ApolloLink((operation) => { + // return new Observable((observer) => { + // const { valueA, valueB } = operation.getContext(); + + // observer.next({ data: { context: { valueA, valueB } } }); + // observer.complete(); + // }); + // }); + + // const { result } = renderHook( + // () => + // useInteractiveQuery(query, { + // context: { valueA: 'A', valueB: 'B' }, + // }), + // { + // wrapper: ({ children }) => ( + // {children} + // ), + // } + // ); + + // const [queryRef] = result.current; + + // const _result = await queryRef[QUERY_REFERENCE_SYMBOL].promise; + + // await waitFor(() => { + // expect(_result).toMatchObject({ + // data: { context: { valueA: 'A', valueB: 'B' } }, + // networkStatus: NetworkStatus.ready, + // }); + // }); + // }); + + // it('enables canonical results when canonizeResults is "true"', async () => { + // interface Result { + // __typename: string; + // value: number; + // } + + // const cache = new InMemoryCache({ + // typePolicies: { + // Result: { + // keyFields: false, + // }, + // }, + // }); + + // const query: TypedDocumentNode<{ results: Result[] }> = gql` + // query { + // results { + // value + // } + // } + // `; + + // const results: Result[] = [ + // { __typename: 'Result', value: 0 }, + // { __typename: 'Result', value: 1 }, + // { __typename: 'Result', value: 1 }, + // { __typename: 'Result', value: 2 }, + // { __typename: 'Result', value: 3 }, + // { __typename: 'Result', value: 5 }, + // ]; + + // cache.writeQuery({ + // query, + // data: { results }, + // }); + + // const { result } = renderHook( + // () => + // useInteractiveQuery(query, { + // canonizeResults: true, + // }), + // { + // wrapper: ({ children }) => ( + // {children} + // ), + // } + // ); + + // const [queryRef] = result.current; + + // const _result = await queryRef[QUERY_REFERENCE_SYMBOL].promise; + // const resultSet = new Set(_result.data.results); + // const values = Array.from(resultSet).map((item) => item.value); + + // expect(_result.data).toEqual({ results }); + // expect(_result.data.results.length).toBe(6); + // expect(resultSet.size).toBe(5); + // expect(values).toEqual([0, 1, 2, 3, 5]); + // }); + + // it("can disable canonical results when the cache's canonizeResults setting is true", async () => { + // interface Result { + // __typename: string; + // value: number; + // } + + // const cache = new InMemoryCache({ + // canonizeResults: true, + // typePolicies: { + // Result: { + // keyFields: false, + // }, + // }, + // }); + + // const query: TypedDocumentNode<{ results: Result[] }> = gql` + // query { + // results { + // value + // } + // } + // `; + + // const results: Result[] = [ + // { __typename: 'Result', value: 0 }, + // { __typename: 'Result', value: 1 }, + // { __typename: 'Result', value: 1 }, + // { __typename: 'Result', value: 2 }, + // { __typename: 'Result', value: 3 }, + // { __typename: 'Result', value: 5 }, + // ]; + + // cache.writeQuery({ + // query, + // data: { results }, + // }); + + // const { result } = renderHook( + // () => + // useInteractiveQuery(query, { + // canonizeResults: false, + // }), + // { + // wrapper: ({ children }) => ( + // {children} + // ), + // } + // ); + + // const [queryRef] = result.current; + + // const _result = await queryRef[QUERY_REFERENCE_SYMBOL].promise; + // const resultSet = new Set(_result.data.results); + // const values = Array.from(resultSet).map((item) => item.value); + + // expect(_result.data).toEqual({ results }); + // expect(_result.data.results.length).toBe(6); + // expect(resultSet.size).toBe(6); + // expect(values).toEqual([0, 1, 1, 2, 3, 5]); + // }); + + // // TODO(FIXME): test fails, should return cache data first if it exists + // it.skip('returns initial cache data followed by network data when the fetch policy is `cache-and-network`', async () => { + // const query = gql` + // { + // hello + // } + // `; + // const cache = new InMemoryCache(); + // const link = mockSingleLink({ + // request: { query }, + // result: { data: { hello: 'from link' } }, + // delay: 20, + // }); + + // const client = new ApolloClient({ + // link, + // cache, + // }); + + // cache.writeQuery({ query, data: { hello: 'from cache' } }); + + // const { result } = renderHook( + // () => useInteractiveQuery(query, { fetchPolicy: 'cache-and-network' }), + // { + // wrapper: ({ children }) => ( + // {children} + // ), + // } + // ); + + // const [queryRef] = result.current; + + // const _result = await queryRef[QUERY_REFERENCE_SYMBOL].promise; + + // expect(_result).toEqual({ + // data: { hello: 'from link' }, + // loading: false, + // networkStatus: 7, + // }); + // }); + + // it('all data is present in the cache, no network request is made', async () => { + // const query = gql` + // { + // hello + // } + // `; + // const cache = new InMemoryCache(); + // const link = mockSingleLink({ + // request: { query }, + // result: { data: { hello: 'from link' } }, + // delay: 20, + // }); + + // const client = new ApolloClient({ + // link, + // cache, + // }); + + // cache.writeQuery({ query, data: { hello: 'from cache' } }); + + // const { result } = renderHook( + // () => useInteractiveQuery(query, { fetchPolicy: 'cache-first' }), + // { + // wrapper: ({ children }) => ( + // {children} + // ), + // } + // ); + + // const [queryRef] = result.current; + + // const _result = await queryRef[QUERY_REFERENCE_SYMBOL].promise; + + // expect(_result).toEqual({ + // data: { hello: 'from cache' }, + // loading: false, + // networkStatus: 7, + // }); + // }); + // it('partial data is present in the cache so it is ignored and network request is made', async () => { + // const query = gql` + // { + // hello + // foo + // } + // `; + // const cache = new InMemoryCache(); + // const link = mockSingleLink({ + // request: { query }, + // result: { data: { hello: 'from link', foo: 'bar' } }, + // delay: 20, + // }); + + // const client = new ApolloClient({ + // link, + // cache, + // }); + + // // we expect a "Missing field 'foo' while writing result..." error + // // when writing hello to the cache, so we'll silence the console.error + // const originalConsoleError = console.error; + // console.error = () => { + // /* noop */ + // }; + // cache.writeQuery({ query, data: { hello: 'from cache' } }); + // console.error = originalConsoleError; + + // const { result } = renderHook( + // () => useInteractiveQuery(query, { fetchPolicy: 'cache-first' }), + // { + // wrapper: ({ children }) => ( + // {children} + // ), + // } + // ); + + // const [queryRef] = result.current; + + // const _result = await queryRef[QUERY_REFERENCE_SYMBOL].promise; + + // expect(_result).toEqual({ + // data: { foo: 'bar', hello: 'from link' }, + // loading: false, + // networkStatus: 7, + // }); + // }); + + // it('existing data in the cache is ignored', async () => { + // const query = gql` + // { + // hello + // } + // `; + // const cache = new InMemoryCache(); + // const link = mockSingleLink({ + // request: { query }, + // result: { data: { hello: 'from link' } }, + // delay: 20, + // }); + + // const client = new ApolloClient({ + // link, + // cache, + // }); + + // cache.writeQuery({ query, data: { hello: 'from cache' } }); + + // const { result } = renderHook( + // () => useInteractiveQuery(query, { fetchPolicy: 'network-only' }), + // { + // wrapper: ({ children }) => ( + // {children} + // ), + // } + // ); + + // const [queryRef] = result.current; + + // const _result = await queryRef[QUERY_REFERENCE_SYMBOL].promise; + + // expect(_result).toEqual({ + // data: { hello: 'from link' }, + // loading: false, + // networkStatus: 7, + // }); + // expect(client.cache.extract()).toEqual({ + // ROOT_QUERY: { __typename: 'Query', hello: 'from link' }, + // }); + // }); + + // it('fetches data from the network but does not update the cache', async () => { + // const query = gql` + // { + // hello + // } + // `; + // const cache = new InMemoryCache(); + // const link = mockSingleLink({ + // request: { query }, + // result: { data: { hello: 'from link' } }, + // delay: 20, + // }); + + // const client = new ApolloClient({ + // link, + // cache, + // }); + + // cache.writeQuery({ query, data: { hello: 'from cache' } }); + + // const { result } = renderHook( + // () => useInteractiveQuery(query, { fetchPolicy: 'no-cache' }), + // { + // wrapper: ({ children }) => ( + // {children} + // ), + // } + // ); + + // const [queryRef] = result.current; + + // const _result = await queryRef[QUERY_REFERENCE_SYMBOL].promise; + + // expect(_result).toEqual({ + // data: { hello: 'from link' }, + // loading: false, + // networkStatus: 7, + // }); + // // ...but not updated in the cache + // expect(client.cache.extract()).toEqual({ + // ROOT_QUERY: { __typename: 'Query', hello: 'from cache' }, + // }); + // }); + + describe("integration tests with useReadQuery", () => { + it("suspends and renders hello", async () => { + const user = userEvent.setup(); + const { renders, loadQueryButton } = renderIntegrationTest(); + + expect(renders.suspenseCount).toBe(0); + + await act(() => user.click(loadQueryButton)); + + expect(await screen.findByText("loading")).toBeInTheDocument(); + + // the parent component re-renders when promise fulfilled + expect(await screen.findByText("hello")).toBeInTheDocument(); + expect(renders.suspenseCount).toBe(1); + expect(renders.count).toBe(1); + }); + + it("works with startTransition to change variables", async () => { + type Variables = { + id: string; + }; + + interface Data { + todo: { + id: string; + name: string; + completed: boolean; + }; + } + const user = userEvent.setup(); + + const query: TypedDocumentNode = gql` + query TodoItemQuery($id: ID!) { + todo(id: $id) { + id + name + completed + } + } + `; + + const mocks: MockedResponse[] = [ + { + request: { query, variables: { id: "1" } }, + result: { + data: { todo: { id: "1", name: "Clean room", completed: false } }, + }, + delay: 10, + }, + { + request: { query, variables: { id: "2" } }, + result: { + data: { + todo: { id: "2", name: "Take out trash", completed: true }, + }, + }, + delay: 10, + }, + ]; + + const client = new ApolloClient({ + link: new MockLink(mocks), + cache: new InMemoryCache(), + }); + + function App() { + return ( + + }> + + + + ); + } + + function SuspenseFallback() { + return

Loading

; + } + + function Parent() { + const [queryRef, loadQuery] = useInteractiveQuery(query); + + return ( +
+ + {queryRef && ( + loadQuery({ id })} /> + )} +
+ ); + } + + function Todo({ + queryRef, + onChange, + }: { + queryRef: QueryReference; + onChange: (id: string) => void; + }) { + const { data } = useReadQuery(queryRef); + const [isPending, startTransition] = React.useTransition(); + const { todo } = data; + + return ( + <> + +
+ {todo.name} + {todo.completed && " (completed)"} +
+ + ); + } + + render(); + + await act(() => user.click(screen.getByText("Load first todo"))); + + expect(screen.getByText("Loading")).toBeInTheDocument(); + expect(await screen.findByTestId("todo")).toBeInTheDocument(); + + const todo = screen.getByTestId("todo"); + const button = screen.getByText("Refresh"); + + expect(todo).toHaveTextContent("Clean room"); + + 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(todo).toHaveAttribute("aria-busy", "true"); + + // Ensure we are showing the stale UI until the new todo has loaded + expect(todo).toHaveTextContent("Clean room"); + + // Eventually we should see the updated todo content once its done + // suspending. + await waitFor(() => { + expect(todo).toHaveTextContent("Take out trash (completed)"); + }); + }); + + it('does not suspend deferred queries with data in the cache and using a "cache-and-network" fetch policy', async () => { + interface Data { + greeting: { + __typename: string; + message: string; + recipient: { name: string; __typename: string }; + }; + } + + const user = userEvent.setup(); + + const query: TypedDocumentNode = gql` + query { + greeting { + message + ... on Greeting @defer { + recipient { + name + } + } + } + } + `; + + const link = new MockSubscriptionLink(); + const cache = new InMemoryCache(); + cache.writeQuery({ + query, + data: { + greeting: { + __typename: "Greeting", + message: "Hello cached", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + }); + const client = new ApolloClient({ cache, link }); + let renders = 0; + let suspenseCount = 0; + + function App() { + return ( + + }> + + + + ); + } + + function SuspenseFallback() { + suspenseCount++; + return

Loading

; + } + + function Parent() { + const [queryRef, loadQuery] = useInteractiveQuery(query, { + fetchPolicy: "cache-and-network", + }); + return ( +
+ + {queryRef && } +
+ ); + } + + function Todo({ queryRef }: { queryRef: QueryReference }) { + const { data, networkStatus, error } = useReadQuery(queryRef); + const { greeting } = data; + renders++; + + return ( + <> +
Message: {greeting.message}
+
Recipient: {greeting.recipient.name}
+
Network status: {networkStatus}
+
Error: {error ? error.message : "none"}
+ + ); + } + + render(); + + await act(() => user.click(screen.getByText("Load todo"))); + + expect(screen.getByText(/Message/i)).toHaveTextContent( + "Message: Hello cached" + ); + expect(screen.getByText(/Recipient/i)).toHaveTextContent( + "Recipient: Cached Alice" + ); + expect(screen.getByText(/Network status/i)).toHaveTextContent( + "Network status: 1" // loading + ); + expect(screen.getByText(/Error/i)).toHaveTextContent("none"); + + link.simulateResult({ + result: { + data: { + greeting: { __typename: "Greeting", message: "Hello world" }, + }, + hasNext: true, + }, + }); + + await waitFor(() => { + expect(screen.getByText(/Message/i)).toHaveTextContent( + "Message: Hello world" + ); + }); + expect(screen.getByText(/Recipient/i)).toHaveTextContent( + "Recipient: Cached Alice" + ); + expect(screen.getByText(/Network status/i)).toHaveTextContent( + "Network status: 7" // ready + ); + expect(screen.getByText(/Error/i)).toHaveTextContent("none"); + + link.simulateResult({ + result: { + incremental: [ + { + data: { + recipient: { name: "Alice", __typename: "Person" }, + __typename: "Greeting", + }, + path: ["greeting"], + }, + ], + hasNext: false, + }, + }); + + await waitFor(() => { + expect(screen.getByText(/Recipient/i)).toHaveTextContent( + "Recipient: Alice" + ); + }); + expect(screen.getByText(/Message/i)).toHaveTextContent( + "Message: Hello world" + ); + expect(screen.getByText(/Network status/i)).toHaveTextContent( + "Network status: 7" // ready + ); + expect(screen.getByText(/Error/i)).toHaveTextContent("none"); + + expect(renders).toBe(3); + expect(suspenseCount).toBe(0); + }); + }); + + it("reacts to cache updates", async () => { + const { renders, client, query, loadQueryButton, user } = + renderIntegrationTest(); + + await act(() => user.click(loadQueryButton)); + + expect(renders.suspenseCount).toBe(1); + expect(screen.getByText("loading")).toBeInTheDocument(); + + // the parent component re-renders when promise fulfilled + expect(await screen.findByText("hello")).toBeInTheDocument(); + expect(renders.count).toBe(1); + + client.writeQuery({ + query, + data: { foo: { bar: "baz" } }, + }); + + // 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({ + query, + data: { foo: { bar: "bat" } }, + }); + + expect(await screen.findByText("bat")).toBeInTheDocument(); + + expect(renders.suspenseCount).toBe(1); + }); + + it("reacts to variables updates", async () => { + const { renders, user, loadQueryButton } = renderVariablesIntegrationTest({ + variables: { id: "1" }, + }); + + await act(() => user.click(loadQueryButton)); + + expect(renders.suspenseCount).toBe(1); + expect(screen.getByText("loading")).toBeInTheDocument(); + + expect(await screen.findByText("1 - Spider-Man")).toBeInTheDocument(); + + await act(() => user.click(screen.getByText("Change variables"))); + + expect(renders.suspenseCount).toBe(2); + expect(screen.getByText("loading")).toBeInTheDocument(); + + expect(await screen.findByText("2 - Black Widow")).toBeInTheDocument(); + }); + + it("applies `errorPolicy` on next fetch when it changes between renders", async () => { + interface Data { + greeting: string; + } + + const user = userEvent.setup(); + + const query: TypedDocumentNode = gql` + query { + greeting + } + `; + + const mocks = [ + { + request: { query }, + result: { data: { greeting: "Hello" } }, + }, + { + request: { query }, + result: { + errors: [new GraphQLError("oops")], + }, + }, + ]; + + const client = new ApolloClient({ + link: new MockLink(mocks), + cache: new InMemoryCache(), + }); + + function SuspenseFallback() { + return
Loading...
; + } + + function Parent() { + const [errorPolicy, setErrorPolicy] = React.useState("none"); + const [queryRef, { refetch }] = useInteractiveQuery(query, { + errorPolicy, + }); + + return ( + <> + + + }> + + + + ); + } + + function Greeting({ queryRef }: { queryRef: QueryReference }) { + const { data, error } = useReadQuery(queryRef); + + return error ? ( +
{error.message}
+ ) : ( +
{data.greeting}
+ ); + } + + function App() { + return ( + + Error boundary} + > + + + + ); + } + + render(); + + expect(await screen.findByTestId("greeting")).toHaveTextContent("Hello"); + + await act(() => user.click(screen.getByText("Change error policy"))); + await act(() => user.click(screen.getByText("Refetch greeting"))); + + // Ensure we aren't rendering the error boundary and instead rendering the + // error message in the Greeting component. + expect(await screen.findByTestId("error")).toHaveTextContent("oops"); + }); + + it("applies `context` on next fetch when it changes between renders", async () => { + interface Data { + context: Record; + } + + const user = userEvent.setup(); + + const query: TypedDocumentNode = gql` + query { + context + } + `; + + const link = new ApolloLink((operation) => { + return Observable.of({ + data: { + context: operation.getContext(), + }, + }); + }); + + const client = new ApolloClient({ + link, + cache: new InMemoryCache(), + }); + + function SuspenseFallback() { + return
Loading...
; + } + + function Parent() { + const [phase, setPhase] = React.useState("initial"); + const [queryRef, { refetch }] = useInteractiveQuery(query, { + context: { phase }, + }); + + return ( + <> + + + }> + + + + ); + } + + function Context({ queryRef }: { queryRef: QueryReference }) { + const { data } = useReadQuery(queryRef); + + return
{data.context.phase}
; + } + + function App() { + return ( + + + + ); + } + + render(); + + expect(await screen.findByTestId("context")).toHaveTextContent("initial"); + + await act(() => user.click(screen.getByText("Update context"))); + await act(() => user.click(screen.getByText("Refetch"))); + + expect(await screen.findByTestId("context")).toHaveTextContent("rerender"); + }); + + // NOTE: We only test the `false` -> `true` path here. If the option changes + // from `true` -> `false`, the data has already been canonized, so it has no + // effect on the output. + it("returns canonical results immediately when `canonizeResults` changes from `false` to `true` between renders", async () => { + interface Result { + __typename: string; + value: number; + } + + interface Data { + results: Result[]; + } + + const cache = new InMemoryCache({ + typePolicies: { + Result: { + keyFields: false, + }, + }, + }); + + const query: TypedDocumentNode = gql` + query { + results { + value + } + } + `; + + const results: Result[] = [ + { __typename: "Result", value: 0 }, + { __typename: "Result", value: 1 }, + { __typename: "Result", value: 1 }, + { __typename: "Result", value: 2 }, + { __typename: "Result", value: 3 }, + { __typename: "Result", value: 5 }, + ]; + + const user = userEvent.setup(); + + cache.writeQuery({ + query, + data: { results }, + }); + + const client = new ApolloClient({ + link: new MockLink([]), + cache, + }); + + const result: { current: Data | null } = { + current: null, + }; + + function SuspenseFallback() { + return
Loading...
; + } + + function Parent() { + const [canonizeResults, setCanonizeResults] = React.useState(false); + const [queryRef] = useInteractiveQuery(query, { + canonizeResults, + }); + + return ( + <> + + }> + + + + ); + } + + function Results({ queryRef }: { queryRef: QueryReference }) { + const { data } = useReadQuery(queryRef); + + result.current = data; + + return null; + } + + function App() { + return ( + + + + ); + } + + render(); + + function verifyCanonicalResults(data: Data, canonized: boolean) { + const resultSet = new Set(data.results); + const values = Array.from(resultSet).map((item) => item.value); + + expect(data).toEqual({ results }); + + if (canonized) { + expect(data.results.length).toBe(6); + expect(resultSet.size).toBe(5); + expect(values).toEqual([0, 1, 2, 3, 5]); + } else { + expect(data.results.length).toBe(6); + expect(resultSet.size).toBe(6); + expect(values).toEqual([0, 1, 1, 2, 3, 5]); + } + } + + verifyCanonicalResults(result.current!, false); + + await act(() => user.click(screen.getByText("Canonize results"))); + + verifyCanonicalResults(result.current!, true); + }); + + it("applies changed `refetchWritePolicy` to next fetch when changing between renders", async () => { + interface Data { + primes: number[]; + } + + const user = userEvent.setup(); + + const query: TypedDocumentNode = gql` + query GetPrimes($min: number, $max: number) { + primes(min: $min, max: $max) + } + `; + + 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, + }, + { + request: { query, variables: { min: 30, max: 50 } }, + result: { data: { primes: [31, 37, 41, 43, 47] } }, + 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; + }, + }, + }, + }, + }, + }); + + const client = new ApolloClient({ + link: new MockLink(mocks), + cache, + }); + + function SuspenseFallback() { + return
Loading...
; + } + + function Parent() { + const [refetchWritePolicy, setRefetchWritePolicy] = + React.useState("merge"); + + const [queryRef, { refetch }] = useInteractiveQuery(query, { + refetchWritePolicy, + variables: { min: 0, max: 12 }, + }); + + return ( + <> + + + + }> + + + + ); + } + + function Primes({ queryRef }: { queryRef: QueryReference }) { + const { data } = useReadQuery(queryRef); + + return {data.primes.join(", ")}; + } + + function App() { + return ( + + + + ); + } + + render(); + + const primes = await screen.findByTestId("primes"); + + expect(primes).toHaveTextContent("2, 3, 5, 7, 11"); + expect(mergeParams).toEqual([[undefined, [2, 3, 5, 7, 11]]]); + + await act(() => user.click(screen.getByText("Refetch next"))); + + await waitFor(() => { + expect(primes).toHaveTextContent("2, 3, 5, 7, 11, 13, 17, 19, 23, 29"); + }); + + expect(mergeParams).toEqual([ + [undefined, [2, 3, 5, 7, 11]], + [ + [2, 3, 5, 7, 11], + [13, 17, 19, 23, 29], + ], + ]); + + await act(() => + user.click(screen.getByText("Change refetch write policy")) + ); + + await act(() => user.click(screen.getByText("Refetch last"))); + + await waitFor(() => { + expect(primes).toHaveTextContent("31, 37, 41, 43, 47"); + }); + + expect(mergeParams).toEqual([ + [undefined, [2, 3, 5, 7, 11]], + [ + [2, 3, 5, 7, 11], + [13, 17, 19, 23, 29], + ], + [undefined, [31, 37, 41, 43, 47]], + ]); + }); + + it("applies `returnPartialData` on next fetch when it changes between renders", async () => { + interface Data { + character: { + __typename: "Character"; + id: string; + name: string; + }; + } + + interface PartialData { + character: { + __typename: "Character"; + id: string; + }; + } + + const user = userEvent.setup(); + + const fullQuery: TypedDocumentNode = gql` + query { + character { + __typename + id + name + } + } + `; + + const partialQuery: TypedDocumentNode = gql` + query { + character { + __typename + id + } + } + `; + + const mocks = [ + { + request: { query: fullQuery }, + result: { + data: { + character: { + __typename: "Character", + id: "1", + name: "Doctor Strange", + }, + }, + }, + }, + { + request: { query: fullQuery }, + result: { + data: { + character: { + __typename: "Character", + id: "1", + name: "Doctor Strange (refetched)", + }, + }, + }, + delay: 100, + }, + ]; + + const cache = new InMemoryCache(); + + cache.writeQuery({ + query: partialQuery, + data: { character: { __typename: "Character", id: "1" } }, + }); + + const client = new ApolloClient({ + link: new MockLink(mocks), + cache, + }); + + function SuspenseFallback() { + return
Loading...
; + } + + function Parent() { + const [returnPartialData, setReturnPartialData] = React.useState(false); + + const [queryRef] = useInteractiveQuery(fullQuery, { + returnPartialData, + }); + + return ( + <> + + }> + + + + ); + } + + function Character({ queryRef }: { queryRef: QueryReference }) { + const { data } = useReadQuery(queryRef); + + return ( + {data.character.name ?? "unknown"} + ); + } + + function App() { + return ( + + + + ); + } + + render(); + + const character = await screen.findByTestId("character"); + + expect(character).toHaveTextContent("Doctor Strange"); + + await act(() => user.click(screen.getByText("Update partial data"))); + + cache.modify({ + id: cache.identify({ __typename: "Character", id: "1" }), + fields: { + name: (_, { DELETE }) => DELETE, + }, + }); + + await waitFor(() => { + expect(character).toHaveTextContent("unknown"); + }); + + await waitFor(() => { + expect(character).toHaveTextContent("Doctor Strange (refetched)"); + }); + }); + + it("applies updated `fetchPolicy` on next fetch when it changes between renders", async () => { + interface Data { + character: { + __typename: "Character"; + id: string; + name: string; + }; + } + + const user = userEvent.setup(); + + const query: TypedDocumentNode = gql` + query { + character { + __typename + id + name + } + } + `; + + const mocks = [ + { + request: { query }, + result: { + data: { + character: { + __typename: "Character", + id: "1", + name: "Doctor Strange", + }, + }, + }, + delay: 10, + }, + ]; + + const cache = new InMemoryCache(); + + cache.writeQuery({ + query, + data: { + character: { + __typename: "Character", + id: "1", + name: "Doctor Strangecache", + }, + }, + }); + + const client = new ApolloClient({ + link: new MockLink(mocks), + cache, + }); + + function SuspenseFallback() { + return
Loading...
; + } + + function Parent() { + const [fetchPolicy, setFetchPolicy] = + React.useState("cache-first"); + + const [queryRef, { refetch }] = useInteractiveQuery(query, { + fetchPolicy, + }); + + return ( + <> + + + }> + + + + ); + } + + function Character({ queryRef }: { queryRef: QueryReference }) { + const { data } = useReadQuery(queryRef); + + return {data.character.name}; + } + + function App() { + return ( + + + + ); + } + + render(); + + const character = await screen.findByTestId("character"); + + expect(character).toHaveTextContent("Doctor Strangecache"); + + await act(() => user.click(screen.getByText("Change fetch policy"))); + await act(() => user.click(screen.getByText("Refetch"))); + await waitFor(() => { + expect(character).toHaveTextContent("Doctor Strange"); + }); + + // Because we switched to a `no-cache` fetch policy, we should not see the + // newly fetched data in the cache after the fetch occured. + expect(cache.readQuery({ query })).toEqual({ + character: { + __typename: "Character", + id: "1", + name: "Doctor Strangecache", + }, + }); + }); + + it("properly handles changing options along with changing `variables`", async () => { + interface Data { + character: { + __typename: "Character"; + id: string; + name: string; + }; + } + + const user = userEvent.setup(); + const query: TypedDocumentNode = gql` + query ($id: ID!) { + character(id: $id) { + __typename + id + name + } + } + `; + + const mocks = [ + { + request: { query, variables: { id: "1" } }, + result: { + errors: [new GraphQLError("oops")], + }, + delay: 10, + }, + { + request: { query, variables: { id: "2" } }, + result: { + data: { + character: { + __typename: "Character", + id: "2", + name: "Hulk", + }, + }, + }, + delay: 10, + }, + ]; + + const cache = new InMemoryCache(); + + cache.writeQuery({ + query, + variables: { + id: "1", + }, + data: { + character: { + __typename: "Character", + id: "1", + name: "Doctor Strangecache", + }, + }, + }); + + const client = new ApolloClient({ + link: new MockLink(mocks), + cache, + }); + + function SuspenseFallback() { + return
Loading...
; + } + + function Parent() { + const [id, setId] = React.useState("1"); + + const [queryRef, { refetch }] = useInteractiveQuery(query, { + errorPolicy: id === "1" ? "all" : "none", + variables: { id }, + }); + + return ( + <> + + + + Error boundary} + > + }> + + + + + ); + } + + function Character({ queryRef }: { queryRef: QueryReference }) { + const { data, error } = useReadQuery(queryRef); + + return error ? ( +
{error.message}
+ ) : ( + {data.character.name} + ); + } + + function App() { + return ( + + + + ); + } + + render(); + + const character = await screen.findByTestId("character"); + + expect(character).toHaveTextContent("Doctor Strangecache"); + + await act(() => user.click(screen.getByText("Get second character"))); + + await waitFor(() => { + expect(character).toHaveTextContent("Hulk"); + }); + + await act(() => user.click(screen.getByText("Get first character"))); + + await waitFor(() => { + expect(character).toHaveTextContent("Doctor Strangecache"); + }); + + await act(() => user.click(screen.getByText("Refetch"))); + + // Ensure we render the inline error instead of the error boundary, which + // tells us the error policy was properly applied. + expect(await screen.findByTestId("error")).toHaveTextContent("oops"); + }); + + describe("refetch", () => { + it("re-suspends when calling `refetch`", async () => { + const { renders } = renderVariablesIntegrationTest({ + variables: { id: "1" }, + }); + + expect(renders.suspenseCount).toBe(1); + expect(screen.getByText("loading")).toBeInTheDocument(); + + expect(await screen.findByText("1 - Spider-Man")).toBeInTheDocument(); + + const button = screen.getByText("Refetch"); + const user = userEvent.setup(); + await act(() => user.click(button)); + + // parent component re-suspends + expect(renders.suspenseCount).toBe(2); + expect(renders.count).toBe(2); + + expect( + await screen.findByText("1 - Spider-Man (updated)") + ).toBeInTheDocument(); + }); + it("re-suspends when calling `refetch` with new variables", async () => { + interface QueryData { + character: { + id: string; + name: string; + }; + } + + interface QueryVariables { + id: string; + } + const query: TypedDocumentNode = gql` + query CharacterQuery($id: ID!) { + character(id: $id) { + id + name + } + } + `; + + const mocks = [ + { + request: { query, variables: { id: "1" } }, + result: { + data: { character: { id: "1", name: "Captain Marvel" } }, + }, + }, + { + request: { query, variables: { id: "2" } }, + result: { + data: { character: { id: "2", name: "Captain America" } }, + }, + }, + ]; + + const { renders } = renderVariablesIntegrationTest({ + variables: { id: "1" }, + mocks, + }); + + expect(renders.suspenseCount).toBe(1); + expect(screen.getByText("loading")).toBeInTheDocument(); + + expect(await screen.findByText("1 - Captain Marvel")).toBeInTheDocument(); + + const newVariablesRefetchButton = screen.getByText( + "Set variables to id: 2" + ); + const refetchButton = screen.getByText("Refetch"); + const user = userEvent.setup(); + await act(() => user.click(newVariablesRefetchButton)); + await act(() => user.click(refetchButton)); + + expect( + await screen.findByText("2 - Captain America") + ).toBeInTheDocument(); + + // parent component re-suspends + expect(renders.suspenseCount).toBe(2); + expect(renders.count).toBe(3); + + // extra render puts an additional frame into the array + expect(renders.frames).toMatchObject([ + { + ...mocks[0].result, + networkStatus: NetworkStatus.ready, + error: undefined, + }, + { + ...mocks[0].result, + networkStatus: NetworkStatus.ready, + error: undefined, + }, + { + ...mocks[1].result, + networkStatus: NetworkStatus.ready, + error: undefined, + }, + ]); + }); + it("re-suspends multiple times when calling `refetch` multiple times", async () => { + const { renders } = renderVariablesIntegrationTest({ + variables: { id: "1" }, + }); + + expect(renders.suspenseCount).toBe(1); + expect(screen.getByText("loading")).toBeInTheDocument(); + + expect(await screen.findByText("1 - Spider-Man")).toBeInTheDocument(); + + const button = screen.getByText("Refetch"); + const user = userEvent.setup(); + await act(() => user.click(button)); + + // parent component re-suspends + expect(renders.suspenseCount).toBe(2); + expect(renders.count).toBe(2); + + expect( + await screen.findByText("1 - Spider-Man (updated)") + ).toBeInTheDocument(); + + await act(() => user.click(button)); + + // parent component re-suspends + expect(renders.suspenseCount).toBe(3); + expect(renders.count).toBe(3); + + expect( + await screen.findByText("1 - Spider-Man (updated again)") + ).toBeInTheDocument(); + }); + it("throws errors when errors are returned after calling `refetch`", async () => { + const consoleSpy = jest.spyOn(console, "error").mockImplementation(); + interface QueryData { + character: { + id: string; + name: string; + }; + } + + interface QueryVariables { + id: string; + } + const query: TypedDocumentNode = gql` + query CharacterQuery($id: ID!) { + character(id: $id) { + id + name + } + } + `; + const mocks = [ + { + request: { query, variables: { id: "1" } }, + result: { + data: { character: { id: "1", name: "Captain Marvel" } }, + }, + }, + { + request: { query, variables: { id: "1" } }, + result: { + errors: [new GraphQLError("Something went wrong")], + }, + }, + ]; + const { renders } = renderVariablesIntegrationTest({ + variables: { id: "1" }, + mocks, + }); + + expect(renders.suspenseCount).toBe(1); + expect(screen.getByText("loading")).toBeInTheDocument(); + + expect(await screen.findByText("1 - Captain Marvel")).toBeInTheDocument(); + + const button = screen.getByText("Refetch"); + const user = userEvent.setup(); + await act(() => user.click(button)); + + await waitFor(() => { + expect(renders.errorCount).toBe(1); + }); + + expect(renders.errors).toEqual([ + new ApolloError({ + graphQLErrors: [new GraphQLError("Something went wrong")], + }), + ]); + + consoleSpy.mockRestore(); + }); + it('ignores errors returned after calling `refetch` when errorPolicy is set to "ignore"', async () => { + interface QueryData { + character: { + id: string; + name: string; + }; + } + + interface QueryVariables { + id: string; + } + const query: TypedDocumentNode = gql` + query CharacterQuery($id: ID!) { + character(id: $id) { + id + name + } + } + `; + const mocks = [ + { + request: { query, variables: { id: "1" } }, + result: { + data: { character: { id: "1", name: "Captain Marvel" } }, + }, + }, + { + request: { query, variables: { id: "1" } }, + result: { + errors: [new GraphQLError("Something went wrong")], + }, + }, + ]; + + const { renders } = renderVariablesIntegrationTest({ + variables: { id: "1" }, + errorPolicy: "ignore", + mocks, + }); + + expect(await screen.findByText("1 - Captain Marvel")).toBeInTheDocument(); + + const button = screen.getByText("Refetch"); + const user = userEvent.setup(); + await act(() => user.click(button)); + + expect(renders.errorCount).toBe(0); + expect(renders.errors).toEqual([]); + }); + it('returns errors after calling `refetch` when errorPolicy is set to "all"', async () => { + interface QueryData { + character: { + id: string; + name: string; + }; + } + + interface QueryVariables { + id: string; + } + const query: TypedDocumentNode = gql` + query CharacterQuery($id: ID!) { + character(id: $id) { + id + name + } + } + `; + const mocks = [ + { + request: { query, variables: { id: "1" } }, + result: { + data: { character: { id: "1", name: "Captain Marvel" } }, + }, + }, + { + request: { query, variables: { id: "1" } }, + result: { + errors: [new GraphQLError("Something went wrong")], + }, + }, + ]; + + const { renders } = renderVariablesIntegrationTest({ + variables: { id: "1" }, + errorPolicy: "all", + mocks, + }); + + expect(await screen.findByText("1 - Captain Marvel")).toBeInTheDocument(); + + const button = screen.getByText("Refetch"); + const user = userEvent.setup(); + await act(() => user.click(button)); + + expect(renders.errorCount).toBe(0); + expect(renders.errors).toEqual([]); + + expect( + await screen.findByText("Something went wrong") + ).toBeInTheDocument(); + }); + it('handles partial data results after calling `refetch` when errorPolicy is set to "all"', async () => { + interface QueryData { + character: { + id: string; + name: string; + }; + } + + interface QueryVariables { + id: string; + } + const query: TypedDocumentNode = gql` + query CharacterQuery($id: ID!) { + character(id: $id) { + id + name + } + } + `; + const mocks = [ + { + request: { query, variables: { id: "1" } }, + result: { + data: { character: { id: "1", name: "Captain Marvel" } }, + }, + }, + { + request: { query, variables: { id: "1" } }, + result: { + data: { character: { id: "1", name: null } }, + errors: [new GraphQLError("Something went wrong")], + }, + }, + ]; + + const { renders } = renderVariablesIntegrationTest({ + variables: { id: "1" }, + errorPolicy: "all", + mocks, + }); + + expect(await screen.findByText("1 - Captain Marvel")).toBeInTheDocument(); + + const button = screen.getByText("Refetch"); + const user = userEvent.setup(); + await act(() => user.click(button)); + + expect(renders.errorCount).toBe(0); + expect(renders.errors).toEqual([]); + + expect( + await screen.findByText("Something went wrong") + ).toBeInTheDocument(); + + const expectedError = new ApolloError({ + graphQLErrors: [new GraphQLError("Something went wrong")], + }); + + expect(renders.frames).toMatchObject([ + { + ...mocks[0].result, + networkStatus: NetworkStatus.ready, + error: undefined, + }, + { + data: mocks[1].result.data, + networkStatus: NetworkStatus.error, + error: expectedError, + }, + ]); + }); + it("`refetch` works with startTransition to allow React to show stale UI until finished suspending", async () => { + type Variables = { + id: string; + }; + + interface Data { + todo: { + id: string; + name: string; + completed: boolean; + }; + } + const user = userEvent.setup(); + + const query: TypedDocumentNode = gql` + query TodoItemQuery($id: ID!) { + todo(id: $id) { + id + name + completed + } + } + `; + + const mocks: MockedResponse[] = [ + { + request: { query, variables: { id: "1" } }, + result: { + data: { todo: { id: "1", name: "Clean room", completed: false } }, + }, + delay: 10, + }, + { + request: { query, variables: { id: "1" } }, + result: { + data: { todo: { id: "1", name: "Clean room", completed: true } }, + }, + delay: 10, + }, + ]; + + const client = new ApolloClient({ + link: new MockLink(mocks), + cache: new InMemoryCache(), + }); + + function App() { + return ( + + }> + + + + ); + } + + function SuspenseFallback() { + return

Loading

; + } + + function Parent() { + const [id, setId] = React.useState("1"); + const [queryRef, { refetch }] = useInteractiveQuery(query, { + variables: { id }, + }); + return ; + } + + function Todo({ + queryRef, + refetch, + }: { + refetch: RefetchFunction; + queryRef: QueryReference; + onChange: (id: string) => void; + }) { + const { data } = useReadQuery(queryRef); + const [isPending, startTransition] = React.useTransition(); + const { todo } = data; + + return ( + <> + +
+ {todo.name} + {todo.completed && " (completed)"} +
+ + ); + } + + render(); + + expect(screen.getByText("Loading")).toBeInTheDocument(); + + expect(await screen.findByTestId("todo")).toBeInTheDocument(); + + const todo = screen.getByTestId("todo"); + const button = screen.getByText("Refresh"); + + expect(todo).toHaveTextContent("Clean room"); + + 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(todo).toHaveAttribute("aria-busy", "true"); + + // Ensure we are showing the stale UI until the new todo has loaded + expect(todo).toHaveTextContent("Clean room"); + + // Eventually we should see the updated todo content once its done + // suspending. + await waitFor(() => { + expect(todo).toHaveTextContent("Clean room (completed)"); + }); + }); + }); + + describe("fetchMore", () => { + function getItemTexts() { + return screen.getAllByTestId(/letter/).map( + // eslint-disable-next-line testing-library/no-node-access + (li) => li.firstChild!.textContent + ); + } + it("re-suspends when calling `fetchMore` with different variables", async () => { + const { renders } = renderPaginatedIntegrationTest(); + + expect(renders.suspenseCount).toBe(1); + expect(screen.getByText("loading")).toBeInTheDocument(); + + const items = await screen.findAllByTestId(/letter/i); + expect(items).toHaveLength(2); + expect(getItemTexts()).toStrictEqual(["A", "B"]); + + const button = screen.getByText("Fetch more"); + const user = userEvent.setup(); + await act(() => user.click(button)); + + // parent component re-suspends + expect(renders.suspenseCount).toBe(2); + await waitFor(() => { + expect(renders.count).toBe(2); + }); + + expect(getItemTexts()).toStrictEqual(["C", "D"]); + }); + it("properly uses `updateQuery` when calling `fetchMore`", async () => { + const { renders } = renderPaginatedIntegrationTest({ + updateQuery: true, + }); + + expect(renders.suspenseCount).toBe(1); + expect(screen.getByText("loading")).toBeInTheDocument(); + + const items = await screen.findAllByTestId(/letter/i); + + expect(items).toHaveLength(2); + expect(getItemTexts()).toStrictEqual(["A", "B"]); + + const button = screen.getByText("Fetch more"); + const user = userEvent.setup(); + await act(() => user.click(button)); + + // parent component re-suspends + expect(renders.suspenseCount).toBe(2); + await waitFor(() => { + expect(renders.count).toBe(2); + }); + + const moreItems = await screen.findAllByTestId(/letter/i); + expect(moreItems).toHaveLength(4); + expect(getItemTexts()).toStrictEqual(["A", "B", "C", "D"]); + }); + it("properly uses cache field policies when calling `fetchMore` without `updateQuery`", async () => { + const { renders } = renderPaginatedIntegrationTest({ + fieldPolicies: true, + }); + expect(renders.suspenseCount).toBe(1); + expect(screen.getByText("loading")).toBeInTheDocument(); + + const items = await screen.findAllByTestId(/letter/i); + + expect(items).toHaveLength(2); + expect(getItemTexts()).toStrictEqual(["A", "B"]); + + const button = screen.getByText("Fetch more"); + const user = userEvent.setup(); + await act(() => user.click(button)); + + // parent component re-suspends + expect(renders.suspenseCount).toBe(2); + await waitFor(() => { + expect(renders.count).toBe(2); + }); + + const moreItems = await screen.findAllByTestId(/letter/i); + expect(moreItems).toHaveLength(4); + expect(getItemTexts()).toStrictEqual(["A", "B", "C", "D"]); + }); + it("`fetchMore` works with startTransition to allow React to show stale UI until finished suspending", async () => { + type Variables = { + offset: number; + }; + + interface Todo { + __typename: "Todo"; + id: string; + name: string; + completed: boolean; + } + interface Data { + todos: Todo[]; + } + const user = userEvent.setup(); + + const query: TypedDocumentNode = gql` + query TodosQuery($offset: Int!) { + todos(offset: $offset) { + id + name + completed + } + } + `; + + const mocks: MockedResponse[] = [ + { + request: { query, variables: { offset: 0 } }, + result: { + data: { + todos: [ + { + __typename: "Todo", + id: "1", + name: "Clean room", + completed: false, + }, + ], + }, + }, + delay: 10, + }, + { + request: { query, variables: { offset: 1 } }, + result: { + data: { + todos: [ + { + __typename: "Todo", + id: "2", + name: "Take out trash", + completed: true, + }, + ], + }, + }, + delay: 10, + }, + ]; + + const client = new ApolloClient({ + link: new MockLink(mocks), + cache: new InMemoryCache({ + typePolicies: { + Query: { + fields: { + todos: offsetLimitPagination(), + }, + }, + }, + }), + }); + + function App() { + return ( + + }> + + + + ); + } + + function SuspenseFallback() { + return

Loading

; + } + + function Parent() { + const [queryRef, { fetchMore }] = useInteractiveQuery(query, { + variables: { offset: 0 }, + }); + return ; + } + + function Todo({ + queryRef, + fetchMore, + }: { + fetchMore: FetchMoreFunction; + queryRef: QueryReference; + }) { + const { data } = useReadQuery(queryRef); + const [isPending, startTransition] = React.useTransition(); + const { todos } = data; + + return ( + <> + +
+ {todos.map((todo) => ( +
+ {todo.name} + {todo.completed && " (completed)"} +
+ ))} +
+ + ); + } + + 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('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, + }); + + 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 }] = useInteractiveQuery(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, + }); + + 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 }] = useInteractiveQuery(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]], + ]); + }); + + it('does not suspend when partial data is in the cache and using a "cache-first" 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; + } + const renders: Renders = { + errors: [], + errorCount: 0, + suspenseCount: 0, + count: 0, + }; + + const cache = new InMemoryCache(); + + cache.writeQuery({ + query: partialQuery, + data: { character: { id: "1" } }, + }); + + const client = new ApolloClient({ + link: new MockLink(mocks), + cache, + }); + + function App() { + return ( + + }> + + + + ); + } + + function SuspenseFallback() { + renders.suspenseCount++; + return

Loading

; + } + + function Parent() { + const [queryRef] = useInteractiveQuery(fullQuery, { + fetchPolicy: "cache-first", + returnPartialData: true, + }); + return ; + } + + function Todo({ + queryRef, + }: { + queryRef: QueryReference>; + }) { + const { data, networkStatus, error } = useReadQuery(queryRef); + 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"); + 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); + }); + + 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 + } + } + `; + + 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, + }); + + function App() { + return ( + + }> + + + + ); + } + + function SuspenseFallback() { + renders.suspenseCount++; + return

Loading

; + } + + function Parent() { + const [queryRef] = useInteractiveQuery(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('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; + }; + } + + 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, + }); + + function App() { + return ( + + }> + + + + ); + } + + function SuspenseFallback() { + renders.suspenseCount++; + return

Loading

; + } + + function Parent() { + const [queryRef] = useInteractiveQuery(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( + () => + useInteractiveQuery(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('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; + }; + } + + 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, + }); + + function App() { + return ( + + }> + + + + ); + } + + function SuspenseFallback() { + renders.suspenseCount++; + return

Loading

; + } + + function Parent() { + const [queryRef] = useInteractiveQuery(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('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 user = userEvent.setup(); + + 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, + }); + + function App() { + return ( + + }> + + + + ); + } + + function SuspenseFallback() { + renders.suspenseCount++; + return

Loading

; + } + + function Parent() { + const [queryRef, loadTodo] = useInteractiveQuery(query, { + fetchPolicy: "cache-first", + returnPartialData: true, + }); + + return ( +
+ + {queryRef && } +
+ ); + } + + 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(); + + await act(() => user.click(screen.getByText("Load todo"))); + + 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", () => { + it("returns unknown when TData cannot be inferred", () => { + const query = gql` + query { + hello + } + `; + + const [queryRef] = useInteractiveQuery(query); + invariant(queryRef); + + 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 + useInteractiveQuery(query, { variables: { id: "1", foo: "bar" } }); + }); + + it("returns TData in default case", () => { + const { query } = useVariablesIntegrationTestCase(); + + const [inferredQueryRef] = useInteractiveQuery(query); + invariant(inferredQueryRef); + const { data: inferred } = useReadQuery(inferredQueryRef); + + expectTypeOf(inferred).toEqualTypeOf(); + expectTypeOf(inferred).not.toEqualTypeOf(); + + const [explicitQueryRef] = useInteractiveQuery< + VariablesCaseData, + VariablesCaseVariables + >(query); + + invariant(explicitQueryRef); + + const { data: explicit } = useReadQuery(explicitQueryRef); + + expectTypeOf(explicit).toEqualTypeOf(); + expectTypeOf(explicit).not.toEqualTypeOf(); + }); + + it('returns TData | undefined with errorPolicy: "ignore"', () => { + const { query } = useVariablesIntegrationTestCase(); + + const [inferredQueryRef] = useInteractiveQuery(query, { + errorPolicy: "ignore", + }); + invariant(inferredQueryRef); + const { data: inferred } = useReadQuery(inferredQueryRef); + + expectTypeOf(inferred).toEqualTypeOf(); + expectTypeOf(inferred).not.toEqualTypeOf(); + + const [explicitQueryRef] = useInteractiveQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { + errorPolicy: "ignore", + }); + invariant(explicitQueryRef); + + const { data: explicit } = useReadQuery(explicitQueryRef); + + expectTypeOf(explicit).toEqualTypeOf(); + expectTypeOf(explicit).not.toEqualTypeOf(); + }); + + it('returns TData | undefined with errorPolicy: "all"', () => { + const { query } = useVariablesIntegrationTestCase(); + + const [inferredQueryRef] = useInteractiveQuery(query, { + errorPolicy: "all", + }); + invariant(inferredQueryRef); + const { data: inferred } = useReadQuery(inferredQueryRef); + + expectTypeOf(inferred).toEqualTypeOf(); + expectTypeOf(inferred).not.toEqualTypeOf(); + + const [explicitQueryRef] = useInteractiveQuery(query, { + errorPolicy: "all", + }); + invariant(explicitQueryRef); + const { data: explicit } = useReadQuery(explicitQueryRef); + + expectTypeOf(explicit).toEqualTypeOf(); + expectTypeOf(explicit).not.toEqualTypeOf(); + }); + + it('returns TData with errorPolicy: "none"', () => { + const { query } = useVariablesIntegrationTestCase(); + + const [inferredQueryRef] = useInteractiveQuery(query, { + errorPolicy: "none", + }); + invariant(inferredQueryRef); + const { data: inferred } = useReadQuery(inferredQueryRef); + + expectTypeOf(inferred).toEqualTypeOf(); + expectTypeOf(inferred).not.toEqualTypeOf(); + + const [explicitQueryRef] = useInteractiveQuery(query, { + errorPolicy: "none", + }); + invariant(explicitQueryRef); + const { data: explicit } = useReadQuery(explicitQueryRef); + + expectTypeOf(explicit).toEqualTypeOf(); + expectTypeOf(explicit).not.toEqualTypeOf(); + }); + + it("returns DeepPartial with returnPartialData: true", () => { + const { query } = useVariablesIntegrationTestCase(); + + const [inferredQueryRef] = useInteractiveQuery(query, { + returnPartialData: true, + }); + invariant(inferredQueryRef); + const { data: inferred } = useReadQuery(inferredQueryRef); + + expectTypeOf(inferred).toEqualTypeOf>(); + expectTypeOf(inferred).not.toEqualTypeOf(); + + const [explicitQueryRef] = useInteractiveQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { + returnPartialData: true, + }); + invariant(explicitQueryRef); + + const { data: explicit } = useReadQuery(explicitQueryRef); + + expectTypeOf(explicit).toEqualTypeOf>(); + expectTypeOf(explicit).not.toEqualTypeOf(); + }); + + it("returns TData with returnPartialData: false", () => { + const { query } = useVariablesIntegrationTestCase(); + + const [inferredQueryRef] = useInteractiveQuery(query, { + returnPartialData: false, + }); + invariant(inferredQueryRef); + const { data: inferred } = useReadQuery(inferredQueryRef); + + expectTypeOf(inferred).toEqualTypeOf(); + expectTypeOf(inferred).not.toEqualTypeOf< + DeepPartial + >(); + + const [explicitQueryRef] = useInteractiveQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { + returnPartialData: false, + }); + invariant(explicitQueryRef); + + 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] = useInteractiveQuery(query, { + fetchPolicy: "no-cache", + }); + invariant(inferredQueryRef); + const { data: inferred } = useReadQuery(inferredQueryRef); + + expectTypeOf(inferred).toEqualTypeOf(); + expectTypeOf(inferred).not.toEqualTypeOf< + DeepPartial + >(); + + const [explicitQueryRef] = useInteractiveQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { + fetchPolicy: "no-cache", + }); + invariant(explicitQueryRef); + + const { data: explicit } = useReadQuery(explicitQueryRef); + + expectTypeOf(explicit).toEqualTypeOf(); + expectTypeOf(explicit).not.toEqualTypeOf< + DeepPartial + >(); + }); + + it("handles combinations of options", () => { + const { query } = useVariablesIntegrationTestCase(); + + const [inferredPartialDataIgnoreQueryRef] = useInteractiveQuery(query, { + returnPartialData: true, + errorPolicy: "ignore", + }); + invariant(inferredPartialDataIgnoreQueryRef); + const { data: inferredPartialDataIgnore } = useReadQuery( + inferredPartialDataIgnoreQueryRef + ); + + expectTypeOf(inferredPartialDataIgnore).toEqualTypeOf< + DeepPartial | undefined + >(); + expectTypeOf( + inferredPartialDataIgnore + ).not.toEqualTypeOf(); + + const [explicitPartialDataIgnoreQueryRef] = useInteractiveQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { + returnPartialData: true, + errorPolicy: "ignore", + }); + invariant(explicitPartialDataIgnoreQueryRef); + + const { data: explicitPartialDataIgnore } = useReadQuery( + explicitPartialDataIgnoreQueryRef + ); + + expectTypeOf(explicitPartialDataIgnore).toEqualTypeOf< + DeepPartial | undefined + >(); + expectTypeOf( + explicitPartialDataIgnore + ).not.toEqualTypeOf(); + + const [inferredPartialDataNoneQueryRef] = useInteractiveQuery(query, { + returnPartialData: true, + errorPolicy: "none", + }); + invariant(inferredPartialDataNoneQueryRef); + + const { data: inferredPartialDataNone } = useReadQuery( + inferredPartialDataNoneQueryRef + ); + + expectTypeOf(inferredPartialDataNone).toEqualTypeOf< + DeepPartial + >(); + expectTypeOf( + inferredPartialDataNone + ).not.toEqualTypeOf(); + + const [explicitPartialDataNoneQueryRef] = useInteractiveQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { + returnPartialData: true, + errorPolicy: "none", + }); + invariant(explicitPartialDataNoneQueryRef); + + 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] = useInteractiveQuery(query, { + fetchPolicy: "no-cache", + returnPartialData: true, + errorPolicy: "none", + }); + invariant(inferredQueryRef); + const { data: inferred } = useReadQuery(inferredQueryRef); + + expectTypeOf(inferred).toEqualTypeOf>(); + expectTypeOf(inferred).not.toEqualTypeOf(); + + const [explicitQueryRef] = useInteractiveQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { + fetchPolicy: "no-cache", + returnPartialData: true, + errorPolicy: "none", + }); + invariant(explicitQueryRef); + + const { data: explicit } = useReadQuery(explicitQueryRef); + + expectTypeOf(explicit).toEqualTypeOf>(); + expectTypeOf(explicit).not.toEqualTypeOf(); + }); + }); +}); diff --git a/src/react/hooks/useInteractiveQuery.ts b/src/react/hooks/useInteractiveQuery.ts new file mode 100644 index 00000000000..a3240fa29a4 --- /dev/null +++ b/src/react/hooks/useInteractiveQuery.ts @@ -0,0 +1,193 @@ +import * as React from "react"; +import type { + DocumentNode, + OperationVariables, + TypedDocumentNode, +} from "../../core/index.js"; +import { useApolloClient } from "./useApolloClient.js"; +import { + type QueryReference, + type InternalQueryReference, + wrapQueryRef, +} from "../cache/QueryReference.js"; +import type { InteractiveQueryHookOptions, NoInfer } from "../types/types.js"; +import { __use } from "./internal/index.js"; +import { getSuspenseCache } from "../cache/index.js"; +import { useWatchQueryOptions } from "./useSuspenseQuery.js"; +import type { FetchMoreFunction, RefetchFunction } from "./useSuspenseQuery.js"; +import { canonicalStringify } from "../../cache/index.js"; +import type { DeepPartial } from "../../utilities/index.js"; +import type { CacheKey } from "../cache/types.js"; + +type LoadQuery = ( + ...args: [TVariables] extends [never] ? [] : [TVariables] +) => void; + +export type UseInteractiveQueryResult< + TData = unknown, + TVariables extends OperationVariables = OperationVariables, +> = [ + QueryReference | null, + LoadQuery, + { + fetchMore: FetchMoreFunction; + refetch: RefetchFunction; + }, + ]; + +type InteractiveQueryHookOptionsNoInfer< + TData, + TVariables extends OperationVariables, +> = InteractiveQueryHookOptions, NoInfer>; + +export function useInteractiveQuery< + TData, + TVariables extends OperationVariables, + TOptions extends Omit, "variables">, +>( + query: DocumentNode | TypedDocumentNode, + options?: InteractiveQueryHookOptionsNoInfer & TOptions +): UseInteractiveQueryResult< + TOptions["errorPolicy"] extends "ignore" | "all" + ? TOptions["returnPartialData"] extends true + ? DeepPartial | undefined + : TData | undefined + : TOptions["returnPartialData"] extends true + ? DeepPartial + : TData, + TVariables +>; + +export function useInteractiveQuery< + TData = unknown, + TVariables extends OperationVariables = OperationVariables, +>( + query: DocumentNode | TypedDocumentNode, + options: InteractiveQueryHookOptionsNoInfer & { + returnPartialData: true; + errorPolicy: "ignore" | "all"; + } +): UseInteractiveQueryResult | undefined, TVariables>; + +export function useInteractiveQuery< + TData = unknown, + TVariables extends OperationVariables = OperationVariables, +>( + query: DocumentNode | TypedDocumentNode, + options: InteractiveQueryHookOptionsNoInfer & { + errorPolicy: "ignore" | "all"; + } +): UseInteractiveQueryResult; + +export function useInteractiveQuery< + TData = unknown, + TVariables extends OperationVariables = OperationVariables, +>( + query: DocumentNode | TypedDocumentNode, + options: InteractiveQueryHookOptionsNoInfer & { + returnPartialData: true; + } +): UseInteractiveQueryResult, TVariables>; + +export function useInteractiveQuery< + TData = unknown, + TVariables extends OperationVariables = OperationVariables, +>( + query: DocumentNode | TypedDocumentNode, + options?: InteractiveQueryHookOptionsNoInfer +): UseInteractiveQueryResult; + +export function useInteractiveQuery< + TData = unknown, + TVariables extends OperationVariables = OperationVariables, +>( + query: DocumentNode | TypedDocumentNode, + options: InteractiveQueryHookOptionsNoInfer< + TData, + TVariables + > = Object.create(null) +): UseInteractiveQueryResult { + const client = useApolloClient(options.client); + const suspenseCache = getSuspenseCache(client); + const watchQueryOptions = useWatchQueryOptions({ client, query, options }); + const { queryKey = [] } = options; + + const [queryRef, setQueryRef] = + React.useState | null>(null); + + const [promiseCache, setPromiseCache] = React.useState(() => + queryRef ? new Map([[queryRef.key, queryRef.promise]]) : new Map() + ); + + if (queryRef) { + queryRef.promiseCache = promiseCache; + } + + React.useEffect(() => queryRef?.retain(), [queryRef]); + + const fetchMore: FetchMoreFunction = React.useCallback( + (options) => { + if (!queryRef) { + throw new Error( + "The query has not been loaded. Please load the query." + ); + } + + const promise = queryRef.fetchMore(options); + + setPromiseCache((promiseCache) => + new Map(promiseCache).set(queryRef.key, queryRef.promise) + ); + + return promise; + }, + [queryRef] + ); + + const refetch: RefetchFunction = React.useCallback( + (options) => { + if (!queryRef) { + throw new Error( + "The query has not been loaded. Please load the query." + ); + } + + const promise = queryRef.refetch(options); + + setPromiseCache((promiseCache) => + new Map(promiseCache).set(queryRef.key, queryRef.promise) + ); + + return promise; + }, + [queryRef] + ); + + const loadQuery: LoadQuery = React.useCallback( + (...args) => { + const [variables] = args; + + const cacheKey: CacheKey = [ + query, + canonicalStringify(variables), + ...([] as any[]).concat(queryKey), + ]; + + const queryRef = suspenseCache.getQueryRef(cacheKey, () => + client.watchQuery({ ...watchQueryOptions, variables }) + ); + + promiseCache.set(queryRef.key, queryRef.promise); + setQueryRef(queryRef); + }, + [query, queryKey, suspenseCache, watchQueryOptions, promiseCache] + ); + + return React.useMemo(() => { + return [ + queryRef && wrapQueryRef(queryRef), + loadQuery, + { fetchMore, refetch }, + ]; + }, [queryRef, loadQuery, fetchMore, refetch]); +} diff --git a/src/react/types/types.ts b/src/react/types/types.ts index 70df3b03458..ea4731f879a 100644 --- a/src/react/types/types.ts +++ b/src/react/types/types.ts @@ -188,6 +188,27 @@ export interface BackgroundQueryHookOptions< skip?: boolean; } +export type InteractiveQueryHookFetchPolicy = Extract< + WatchQueryFetchPolicy, + "cache-first" | "network-only" | "no-cache" | "cache-and-network" +>; + +export interface InteractiveQueryHookOptions< + TData = unknown, + TVariables extends OperationVariables = OperationVariables, +> extends Pick< + QueryHookOptions, + | "client" + | "errorPolicy" + | "context" + | "canonizeResults" + | "returnPartialData" + | "refetchWritePolicy" + > { + fetchPolicy?: InteractiveQueryHookFetchPolicy; + queryKey?: string | number | any[]; +} + /** * @deprecated TODO Delete this unused interface. */ From 3034600707e1a7fca14a0efd35f2d25236ac4e13 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 5 Sep 2023 16:31:38 -0600 Subject: [PATCH 002/199] Disable useInteractiveQuery tests with React 17 --- config/jest.config.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/config/jest.config.js b/config/jest.config.js index 3dcd6e6de56..529bafcde1b 100644 --- a/config/jest.config.js +++ b/config/jest.config.js @@ -31,10 +31,11 @@ const ignoreTSXFiles = ".tsx$"; const react17TestFileIgnoreList = [ ignoreTSFiles, - // For now, we only support useSuspenseQuery with React 18, so no need to test - // it with React 17 + // We only support Suspense with React 18, so don't test suspense hooks with + // React 17 "src/react/hooks/__tests__/useSuspenseQuery.test.tsx", "src/react/hooks/__tests__/useBackgroundQuery.test.tsx", + "src/react/hooks/__tests__/useInteractiveQuery.test.tsx", ]; const tsStandardConfig = { From bf591e016361e99d3967dcc9e9f9ad73ee8ad74b Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 5 Sep 2023 17:00:23 -0600 Subject: [PATCH 003/199] Use separate type imports for useInteractiveQuery --- src/react/hooks/useInteractiveQuery.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/react/hooks/useInteractiveQuery.ts b/src/react/hooks/useInteractiveQuery.ts index a3240fa29a4..890e1189a43 100644 --- a/src/react/hooks/useInteractiveQuery.ts +++ b/src/react/hooks/useInteractiveQuery.ts @@ -5,10 +5,10 @@ import type { TypedDocumentNode, } from "../../core/index.js"; import { useApolloClient } from "./useApolloClient.js"; -import { - type QueryReference, - type InternalQueryReference, - wrapQueryRef, +import { wrapQueryRef } from "../cache/QueryReference.js"; +import type { + QueryReference, + InternalQueryReference, } from "../cache/QueryReference.js"; import type { InteractiveQueryHookOptions, NoInfer } from "../types/types.js"; import { __use } from "./internal/index.js"; From 2a9a14f322ce886d800a60ae8308858a8107bebd Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 16 Oct 2023 14:54:31 -0600 Subject: [PATCH 004/199] Get first test passing with profiler --- .../__tests__/useInteractiveQuery.test.tsx | 166 ++++++++++++++---- 1 file changed, 136 insertions(+), 30 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index 60844bd76cf..89d202c6c09 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -28,6 +28,7 @@ import { ApolloQueryResult, } from "../../../core"; import { + MockedProvider, MockedResponse, MockLink, MockSubscriptionLink, @@ -41,17 +42,39 @@ import { useInteractiveQuery } from "../useInteractiveQuery"; import { useReadQuery } from "../useReadQuery"; import { ApolloProvider } from "../../context"; import { InMemoryCache } from "../../../cache"; -import { - FetchMoreFunction, - RefetchFunction, - QueryReference, -} from "../../../react"; +import { QueryReference } from "../../../react"; import { InteractiveQueryHookOptions, InteractiveQueryHookFetchPolicy, } from "../../types/types"; +import { FetchMoreFunction, RefetchFunction } from "../useSuspenseQuery"; import { RefetchWritePolicy } from "../../../core/watchQueryOptions"; import invariant from "ts-invariant"; +import { profile } from "../../../testing/internal"; + +interface SimpleQueryData { + greeting: string; +} + +function useSimpleQueryCase( + mockOverrides?: MockedResponse[] +) { + const query: TypedDocumentNode = gql` + query GreetingQuery { + greeting + } + `; + + const mocks: MockedResponse[] = mockOverrides || [ + { + request: { query }, + result: { data: { greeting: "Hello" } }, + delay: 10, + }, + ]; + + return { query, mocks }; +} function renderIntegrationTest({ client, @@ -600,34 +623,117 @@ function renderSuspenseHook( } describe("useInteractiveQuery", () => { - // it('fetches a simple query with minimal config', async () => { - // const query = gql` - // query { - // hello - // } - // `; - // const mocks = [ - // { - // request: { query }, - // result: { data: { hello: 'world 1' } }, - // }, - // ]; - // const { result } = renderHook(() => useInteractiveQuery(query), { - // wrapper: ({ children }) => ( - // {children} - // ), - // }); + it("loads a query when the load query function is called", async () => { + const user = userEvent.setup(); + const { query, mocks } = useSimpleQueryCase(); - // const [queryRef, loadQuery] = result.current; + function SuspenseFallback() { + ProfiledApp.updateSnapshot((snapshot) => ({ + ...snapshot, + suspenseCount: snapshot.suspenseCount + 1, + })); - // const _result = await queryRef[QUERY_REFERENCE_SYMBOL].promise; + return

Loading

; + } - // expect(_result).toEqual({ - // data: { hello: 'world 1' }, - // loading: false, - // networkStatus: 7, - // }); - // }); + function Parent() { + const [queryRef, loadQuery] = useInteractiveQuery(query); + + ProfiledApp.updateSnapshot((snapshot) => ({ + ...snapshot, + parentRenderCount: snapshot.parentRenderCount + 1, + })); + + return ( + <> + + }> + {queryRef && } + + + ); + } + + function Greeting({ + queryRef, + }: { + queryRef: QueryReference; + }) { + const result = useReadQuery(queryRef); + + ProfiledApp.updateSnapshot((snapshot) => ({ + ...snapshot, + result, + childRenderCount: snapshot.childRenderCount + 1, + })); + + return
{result.data.greeting}
; + } + + const ProfiledApp = profile<{ + result: ReturnType | null; + suspenseCount: number; + parentRenderCount: number; + childRenderCount: number; + }>({ + Component: () => ( + + + + ), + snapshotDOM: true, + initialSnapshot: { + result: null, + suspenseCount: 0, + parentRenderCount: 0, + childRenderCount: 0, + }, + }); + + render(); + + { + const { snapshot } = await ProfiledApp.takeRender(); + expect(snapshot).toEqual({ + result: null, + suspenseCount: 0, + parentRenderCount: 1, + childRenderCount: 0, + }); + } + + await act(() => user.click(screen.getByText("Load query"))); + + { + const { snapshot, withinDOM } = await ProfiledApp.takeRender(); + + expect(withinDOM().getByText("Loading")).toBeInTheDocument(); + expect(snapshot).toEqual({ + result: null, + suspenseCount: 1, + parentRenderCount: 2, + childRenderCount: 0, + }); + } + + { + const { snapshot, withinDOM } = await ProfiledApp.takeRender(); + + expect(withinDOM().getByText("Hello")).toBeInTheDocument(); + expect(snapshot).toEqual({ + result: { + data: { greeting: "Hello" }, + networkStatus: NetworkStatus.ready, + error: undefined, + }, + suspenseCount: 1, + parentRenderCount: 2, + childRenderCount: 1, + }); + } + + expect(ProfiledApp).not.toRerender(); + }); // it('allows the client to be overridden', async () => { // const query: TypedDocumentNode = gql` From 8d354e4cb3f2c2f0dc3dcf6e7ff1681418ef9b50 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 16 Oct 2023 15:26:53 -0600 Subject: [PATCH 005/199] Export RefetchWritePolicy type from core/index --- src/core/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core/index.ts b/src/core/index.ts index 5757cdb2071..fd7d53aec12 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -19,6 +19,7 @@ export type { ErrorPolicy, FetchMoreQueryOptions, SubscribeToMoreOptions, + RefetchWritePolicy, } from "./watchQueryOptions.js"; export { NetworkStatus, isNetworkRequestSettled } from "./networkStatus.js"; export * from "./types.js"; From 7e50bf8e93a129f837404597001bfbd11002902d Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 16 Oct 2023 15:27:08 -0600 Subject: [PATCH 006/199] Simplify InteractiveQueryOptions type by flattening the type and only specifying used options --- src/react/hooks/useInteractiveQuery.ts | 50 ++++++++----------- src/react/types/types.ts | 66 +++++++++++++++++++++----- 2 files changed, 75 insertions(+), 41 deletions(-) diff --git a/src/react/hooks/useInteractiveQuery.ts b/src/react/hooks/useInteractiveQuery.ts index 890e1189a43..0c6779c0ed1 100644 --- a/src/react/hooks/useInteractiveQuery.ts +++ b/src/react/hooks/useInteractiveQuery.ts @@ -10,7 +10,7 @@ import type { QueryReference, InternalQueryReference, } from "../cache/QueryReference.js"; -import type { InteractiveQueryHookOptions, NoInfer } from "../types/types.js"; +import type { InteractiveQueryHookOptions } from "../types/types.js"; import { __use } from "./internal/index.js"; import { getSuspenseCache } from "../cache/index.js"; import { useWatchQueryOptions } from "./useSuspenseQuery.js"; @@ -27,34 +27,29 @@ export type UseInteractiveQueryResult< TData = unknown, TVariables extends OperationVariables = OperationVariables, > = [ - QueryReference | null, - LoadQuery, - { - fetchMore: FetchMoreFunction; - refetch: RefetchFunction; - }, - ]; - -type InteractiveQueryHookOptionsNoInfer< - TData, - TVariables extends OperationVariables, -> = InteractiveQueryHookOptions, NoInfer>; + QueryReference | null, + LoadQuery, + { + fetchMore: FetchMoreFunction; + refetch: RefetchFunction; + }, +]; export function useInteractiveQuery< TData, TVariables extends OperationVariables, - TOptions extends Omit, "variables">, + TOptions extends InteractiveQueryHookOptions, >( query: DocumentNode | TypedDocumentNode, - options?: InteractiveQueryHookOptionsNoInfer & TOptions + options?: InteractiveQueryHookOptions & TOptions ): UseInteractiveQueryResult< TOptions["errorPolicy"] extends "ignore" | "all" - ? TOptions["returnPartialData"] extends true - ? DeepPartial | 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,7 +58,7 @@ export function useInteractiveQuery< TVariables extends OperationVariables = OperationVariables, >( query: DocumentNode | TypedDocumentNode, - options: InteractiveQueryHookOptionsNoInfer & { + options: InteractiveQueryHookOptions & { returnPartialData: true; errorPolicy: "ignore" | "all"; } @@ -74,7 +69,7 @@ export function useInteractiveQuery< TVariables extends OperationVariables = OperationVariables, >( query: DocumentNode | TypedDocumentNode, - options: InteractiveQueryHookOptionsNoInfer & { + options: InteractiveQueryHookOptions & { errorPolicy: "ignore" | "all"; } ): UseInteractiveQueryResult; @@ -84,7 +79,7 @@ export function useInteractiveQuery< TVariables extends OperationVariables = OperationVariables, >( query: DocumentNode | TypedDocumentNode, - options: InteractiveQueryHookOptionsNoInfer & { + options: InteractiveQueryHookOptions & { returnPartialData: true; } ): UseInteractiveQueryResult, TVariables>; @@ -94,7 +89,7 @@ export function useInteractiveQuery< TVariables extends OperationVariables = OperationVariables, >( query: DocumentNode | TypedDocumentNode, - options?: InteractiveQueryHookOptionsNoInfer + options?: InteractiveQueryHookOptions ): UseInteractiveQueryResult; export function useInteractiveQuery< @@ -102,10 +97,7 @@ export function useInteractiveQuery< TVariables extends OperationVariables = OperationVariables, >( query: DocumentNode | TypedDocumentNode, - options: InteractiveQueryHookOptionsNoInfer< - TData, - TVariables - > = Object.create(null) + options: InteractiveQueryHookOptions = Object.create(null) ): UseInteractiveQueryResult { const client = useApolloClient(options.client); const suspenseCache = getSuspenseCache(client); diff --git a/src/react/types/types.ts b/src/react/types/types.ts index ea4731f879a..e3a1b6ae06f 100644 --- a/src/react/types/types.ts +++ b/src/react/types/types.ts @@ -20,6 +20,8 @@ import type { InternalRefetchQueriesInclude, WatchQueryOptions, WatchQueryFetchPolicy, + ErrorPolicy, + RefetchWritePolicy, } from "../../core/index.js"; /* QueryReference type */ @@ -193,20 +195,60 @@ export type InteractiveQueryHookFetchPolicy = Extract< "cache-first" | "network-only" | "no-cache" | "cache-and-network" >; -export interface InteractiveQueryHookOptions< - TData = unknown, - TVariables extends OperationVariables = OperationVariables, -> extends Pick< - QueryHookOptions, - | "client" - | "errorPolicy" - | "context" - | "canonizeResults" - | "returnPartialData" - | "refetchWritePolicy" - > { +export interface InteractiveQueryHookOptions { + /** + * Whether to canonize cache results before returning them. Canonization + * takes some extra time, but it speeds up future deep equality comparisons. + * Defaults to false. + */ + canonizeResults?: boolean; + /** + * The instance of {@link ApolloClient} to use to execute the query. + * + * By default, the instance that's passed down via context is used, but you + * can provide a different instance here. + */ + client?: ApolloClient; + /** + * Context to be passed to link execution chain + */ + context?: DefaultContext; + /** + * Specifies the {@link ErrorPolicy} to be used for this query + */ + errorPolicy?: ErrorPolicy; + /** + * + * Specifies how the query interacts with the Apollo Client cache during + * execution (for example, whether it checks the cache for results before + * sending a request to the server). + * + * For details, see {@link https://www.apollographql.com/docs/react/data/queries/#setting-a-fetch-policy | Setting a fetch policy}. + * + * The default value is `cache-first`. + */ fetchPolicy?: InteractiveQueryHookFetchPolicy; + /** + * A unique identifier for the query. Each item in the array must be a stable + * identifier to prevent infinite fetches. + * + * This is useful when using the same query and variables combination in more + * than one component, otherwise the components may clobber each other. This + * can also be used to force the query to re-evaluate fresh. + */ queryKey?: string | number | any[]; + /** + * Specifies whether a {@link NetworkStatus.refetch} operation should merge + * incoming field data with existing data, or overwrite the existing data. + * Overwriting is probably preferable, but merging is currently the default + * behavior, for backwards compatibility with Apollo Client 3.x. + */ + refetchWritePolicy?: RefetchWritePolicy; + /** + * Allow returning incomplete data from the cache when a larger query cannot + * be fully satisfied by the cache, instead of returning nothing. + */ + returnPartialData?: boolean; } /** From 48c4a2fa78d7c6b76c777266a0388c0eadae97e7 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 16 Oct 2023 15:54:12 -0600 Subject: [PATCH 007/199] Add comment to loadQuery and add label to variadic arg --- src/react/hooks/useInteractiveQuery.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/react/hooks/useInteractiveQuery.ts b/src/react/hooks/useInteractiveQuery.ts index 0c6779c0ed1..23f97c52de6 100644 --- a/src/react/hooks/useInteractiveQuery.ts +++ b/src/react/hooks/useInteractiveQuery.ts @@ -20,7 +20,11 @@ import type { DeepPartial } from "../../utilities/index.js"; import type { CacheKey } from "../cache/types.js"; type LoadQuery = ( - ...args: [TVariables] extends [never] ? [] : [TVariables] + // Use variadic args to handle cases where TVariables is type `never`, in + // which case we don't want to allow a variables argument. In other + // words, we don't want to allow variables to be passed as an argument to this + // function if the query does not expect variables in the document. + ...args: [TVariables] extends [never] ? [] : [variables: TVariables] ) => void; export type UseInteractiveQueryResult< From d1fbedf4f5c24ca8a49158e7ff301285be135a98 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 16 Oct 2023 16:30:13 -0600 Subject: [PATCH 008/199] Add proper type tests for useInteractiveQuery --- .../__tests__/useInteractiveQuery.test.tsx | 461 ++++++++++-------- 1 file changed, 253 insertions(+), 208 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index 89d202c6c09..51e96b10ebb 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -4213,305 +4213,350 @@ describe("useInteractiveQuery", () => { } `; - const [queryRef] = useInteractiveQuery(query); + const [queryRef, loadQuery] = useInteractiveQuery(query); + invariant(queryRef); const { data } = useReadQuery(queryRef); expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(loadQuery).toEqualTypeOf< + (variables: OperationVariables) => void + >(); }); - it("disallows wider variables type than specified", () => { + it("enforces variables argument to loadQuery function when TVariables is specified", () => { const { query } = useVariablesIntegrationTestCase(); - // @ts-expect-error should not allow wider TVariables type - useInteractiveQuery(query, { variables: { id: "1", foo: "bar" } }); + const [, loadQuery] = useInteractiveQuery(query); + + expectTypeOf(loadQuery).toEqualTypeOf< + (variables: VariablesCaseVariables) => void + >(); + // @ts-expect-error enforces variables argument when type is specified + loadQuery(); + }); + + it("disallows wider variables type", () => { + const { query } = useVariablesIntegrationTestCase(); + + const [, loadQuery] = useInteractiveQuery(query); + + expectTypeOf(loadQuery).toEqualTypeOf< + (variables: VariablesCaseVariables) => void + >(); + // @ts-expect-error does not allow wider TVariables type + loadQuery({ id: "1", foo: "bar" }); + }); + + it("does not allow variables argument to loadQuery when TVariables is `never`", () => { + const query: TypedDocumentNode = gql` + query { + greeting + } + `; + + const [, loadQuery] = useInteractiveQuery(query); + + expectTypeOf(loadQuery).toEqualTypeOf<() => void>(); + // @ts-expect-error does not allow variables argument when TVariables is `never` + loadQuery({}); }); it("returns TData in default case", () => { const { query } = useVariablesIntegrationTestCase(); - const [inferredQueryRef] = useInteractiveQuery(query); - invariant(inferredQueryRef); - const { data: inferred } = useReadQuery(inferredQueryRef); + { + const [queryRef] = useInteractiveQuery(query); + + invariant(queryRef); - expectTypeOf(inferred).toEqualTypeOf(); - expectTypeOf(inferred).not.toEqualTypeOf(); + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf(); + } - const [explicitQueryRef] = useInteractiveQuery< - VariablesCaseData, - VariablesCaseVariables - >(query); + { + const [queryRef] = useInteractiveQuery< + VariablesCaseData, + VariablesCaseVariables + >(query); - invariant(explicitQueryRef); + invariant(queryRef); - const { data: explicit } = useReadQuery(explicitQueryRef); + const { data } = useReadQuery(queryRef); - expectTypeOf(explicit).toEqualTypeOf(); - expectTypeOf(explicit).not.toEqualTypeOf(); + expectTypeOf(data).toEqualTypeOf(); + } }); it('returns TData | undefined with errorPolicy: "ignore"', () => { const { query } = useVariablesIntegrationTestCase(); - const [inferredQueryRef] = useInteractiveQuery(query, { - errorPolicy: "ignore", - }); - invariant(inferredQueryRef); - const { data: inferred } = useReadQuery(inferredQueryRef); + { + const [queryRef] = useInteractiveQuery(query, { + errorPolicy: "ignore", + }); - expectTypeOf(inferred).toEqualTypeOf(); - expectTypeOf(inferred).not.toEqualTypeOf(); + invariant(queryRef); - const [explicitQueryRef] = useInteractiveQuery< - VariablesCaseData, - VariablesCaseVariables - >(query, { - errorPolicy: "ignore", - }); - invariant(explicitQueryRef); + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf(); + } + + { + const [queryRef] = useInteractiveQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { errorPolicy: "ignore" }); + + invariant(queryRef); - const { data: explicit } = useReadQuery(explicitQueryRef); + const { data } = useReadQuery(queryRef); - expectTypeOf(explicit).toEqualTypeOf(); - expectTypeOf(explicit).not.toEqualTypeOf(); + expectTypeOf(data).toEqualTypeOf(); + } }); it('returns TData | undefined with errorPolicy: "all"', () => { const { query } = useVariablesIntegrationTestCase(); - const [inferredQueryRef] = useInteractiveQuery(query, { - errorPolicy: "all", - }); - invariant(inferredQueryRef); - const { data: inferred } = useReadQuery(inferredQueryRef); + { + const [queryRef] = useInteractiveQuery(query, { + errorPolicy: "all", + }); - expectTypeOf(inferred).toEqualTypeOf(); - expectTypeOf(inferred).not.toEqualTypeOf(); + invariant(queryRef); - const [explicitQueryRef] = useInteractiveQuery(query, { - errorPolicy: "all", - }); - invariant(explicitQueryRef); - const { data: explicit } = useReadQuery(explicitQueryRef); + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf(); + } + + { + const [queryRef] = useInteractiveQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { errorPolicy: "all" }); + + invariant(queryRef); - expectTypeOf(explicit).toEqualTypeOf(); - expectTypeOf(explicit).not.toEqualTypeOf(); + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf(); + } }); it('returns TData with errorPolicy: "none"', () => { const { query } = useVariablesIntegrationTestCase(); - const [inferredQueryRef] = useInteractiveQuery(query, { - errorPolicy: "none", - }); - invariant(inferredQueryRef); - const { data: inferred } = useReadQuery(inferredQueryRef); + { + const [queryRef] = useInteractiveQuery(query, { + errorPolicy: "none", + }); - expectTypeOf(inferred).toEqualTypeOf(); - expectTypeOf(inferred).not.toEqualTypeOf(); + invariant(queryRef); - const [explicitQueryRef] = useInteractiveQuery(query, { - errorPolicy: "none", - }); - invariant(explicitQueryRef); - const { data: explicit } = useReadQuery(explicitQueryRef); + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf(); + } - expectTypeOf(explicit).toEqualTypeOf(); - expectTypeOf(explicit).not.toEqualTypeOf(); + { + const [queryRef] = useInteractiveQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { errorPolicy: "none" }); + + invariant(queryRef); + + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf(); + } }); it("returns DeepPartial with returnPartialData: true", () => { const { query } = useVariablesIntegrationTestCase(); - const [inferredQueryRef] = useInteractiveQuery(query, { - returnPartialData: true, - }); - invariant(inferredQueryRef); - const { data: inferred } = useReadQuery(inferredQueryRef); + { + const [queryRef] = useInteractiveQuery(query, { + returnPartialData: true, + }); - expectTypeOf(inferred).toEqualTypeOf>(); - expectTypeOf(inferred).not.toEqualTypeOf(); + invariant(queryRef); - const [explicitQueryRef] = useInteractiveQuery< - VariablesCaseData, - VariablesCaseVariables - >(query, { - returnPartialData: true, - }); - invariant(explicitQueryRef); + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf>(); + } + + { + const [queryRef] = useInteractiveQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { returnPartialData: true }); - const { data: explicit } = useReadQuery(explicitQueryRef); + invariant(queryRef); - expectTypeOf(explicit).toEqualTypeOf>(); - expectTypeOf(explicit).not.toEqualTypeOf(); + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf>(); + } }); it("returns TData with returnPartialData: false", () => { const { query } = useVariablesIntegrationTestCase(); - const [inferredQueryRef] = useInteractiveQuery(query, { - returnPartialData: false, - }); - invariant(inferredQueryRef); - const { data: inferred } = useReadQuery(inferredQueryRef); + { + const [queryRef] = useInteractiveQuery(query, { + returnPartialData: false, + }); - expectTypeOf(inferred).toEqualTypeOf(); - expectTypeOf(inferred).not.toEqualTypeOf< - DeepPartial - >(); + invariant(queryRef); - const [explicitQueryRef] = useInteractiveQuery< - VariablesCaseData, - VariablesCaseVariables - >(query, { - returnPartialData: false, - }); - invariant(explicitQueryRef); + const { data } = useReadQuery(queryRef); - const { data: explicit } = useReadQuery(explicitQueryRef); + expectTypeOf(data).toEqualTypeOf(); + } - expectTypeOf(explicit).toEqualTypeOf(); - expectTypeOf(explicit).not.toEqualTypeOf< - DeepPartial - >(); + { + const [queryRef] = useInteractiveQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { returnPartialData: false }); + + invariant(queryRef); + + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf(); + } }); it("returns TData when passing an option that does not affect TData", () => { const { query } = useVariablesIntegrationTestCase(); - const [inferredQueryRef] = useInteractiveQuery(query, { - fetchPolicy: "no-cache", - }); - invariant(inferredQueryRef); - const { data: inferred } = useReadQuery(inferredQueryRef); + { + const [queryRef] = useInteractiveQuery(query, { + fetchPolicy: "no-cache", + }); - expectTypeOf(inferred).toEqualTypeOf(); - expectTypeOf(inferred).not.toEqualTypeOf< - DeepPartial - >(); + invariant(queryRef); - const [explicitQueryRef] = useInteractiveQuery< - VariablesCaseData, - VariablesCaseVariables - >(query, { - fetchPolicy: "no-cache", - }); - invariant(explicitQueryRef); + const { data } = useReadQuery(queryRef); - const { data: explicit } = useReadQuery(explicitQueryRef); + expectTypeOf(data).toEqualTypeOf(); + } - expectTypeOf(explicit).toEqualTypeOf(); - expectTypeOf(explicit).not.toEqualTypeOf< - DeepPartial - >(); + { + const [queryRef] = useInteractiveQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { fetchPolicy: "no-cache" }); + + invariant(queryRef); + + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf(); + } }); it("handles combinations of options", () => { const { query } = useVariablesIntegrationTestCase(); - const [inferredPartialDataIgnoreQueryRef] = useInteractiveQuery(query, { - returnPartialData: true, - errorPolicy: "ignore", - }); - invariant(inferredPartialDataIgnoreQueryRef); - const { data: inferredPartialDataIgnore } = useReadQuery( - inferredPartialDataIgnoreQueryRef - ); + { + const [queryRef] = useInteractiveQuery(query, { + returnPartialData: true, + errorPolicy: "ignore", + }); - expectTypeOf(inferredPartialDataIgnore).toEqualTypeOf< - DeepPartial | undefined - >(); - expectTypeOf( - inferredPartialDataIgnore - ).not.toEqualTypeOf(); - - const [explicitPartialDataIgnoreQueryRef] = useInteractiveQuery< - VariablesCaseData, - VariablesCaseVariables - >(query, { - returnPartialData: true, - errorPolicy: "ignore", - }); - invariant(explicitPartialDataIgnoreQueryRef); + invariant(queryRef); - const { data: explicitPartialDataIgnore } = useReadQuery( - explicitPartialDataIgnoreQueryRef - ); + const { data } = useReadQuery(queryRef); - expectTypeOf(explicitPartialDataIgnore).toEqualTypeOf< - DeepPartial | undefined - >(); - expectTypeOf( - explicitPartialDataIgnore - ).not.toEqualTypeOf(); + expectTypeOf(data).toEqualTypeOf< + DeepPartial | undefined + >(); + } - const [inferredPartialDataNoneQueryRef] = useInteractiveQuery(query, { - returnPartialData: true, - errorPolicy: "none", - }); - invariant(inferredPartialDataNoneQueryRef); + { + const [queryRef] = useInteractiveQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { returnPartialData: true, errorPolicy: "ignore" }); - const { data: inferredPartialDataNone } = useReadQuery( - inferredPartialDataNoneQueryRef - ); + invariant(queryRef); - expectTypeOf(inferredPartialDataNone).toEqualTypeOf< - DeepPartial - >(); - expectTypeOf( - inferredPartialDataNone - ).not.toEqualTypeOf(); - - const [explicitPartialDataNoneQueryRef] = useInteractiveQuery< - VariablesCaseData, - VariablesCaseVariables - >(query, { - returnPartialData: true, - errorPolicy: "none", - }); - invariant(explicitPartialDataNoneQueryRef); + const { data } = useReadQuery(queryRef); - const { data: explicitPartialDataNone } = useReadQuery( - explicitPartialDataNoneQueryRef - ); + expectTypeOf(data).toEqualTypeOf< + DeepPartial | undefined + >(); + } - expectTypeOf(explicitPartialDataNone).toEqualTypeOf< - DeepPartial - >(); - expectTypeOf( - explicitPartialDataNone - ).not.toEqualTypeOf(); + { + const [queryRef] = useInteractiveQuery(query, { + returnPartialData: true, + errorPolicy: "none", + }); + + invariant(queryRef); + + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf>(); + } + + { + const [queryRef] = useInteractiveQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { returnPartialData: true, errorPolicy: "none" }); + + invariant(queryRef); + + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf>(); + } }); it("returns correct TData type when combined options that do not affect TData", () => { const { query } = useVariablesIntegrationTestCase(); - const [inferredQueryRef] = useInteractiveQuery(query, { - fetchPolicy: "no-cache", - returnPartialData: true, - errorPolicy: "none", - }); - invariant(inferredQueryRef); - const { data: inferred } = useReadQuery(inferredQueryRef); - - expectTypeOf(inferred).toEqualTypeOf>(); - expectTypeOf(inferred).not.toEqualTypeOf(); - - const [explicitQueryRef] = useInteractiveQuery< - VariablesCaseData, - VariablesCaseVariables - >(query, { - fetchPolicy: "no-cache", - returnPartialData: true, - errorPolicy: "none", - }); - invariant(explicitQueryRef); + { + const [queryRef] = useInteractiveQuery(query, { + fetchPolicy: "no-cache", + returnPartialData: true, + errorPolicy: "none", + }); + + invariant(queryRef); + + const { data } = useReadQuery(queryRef); - const { data: explicit } = useReadQuery(explicitQueryRef); + expectTypeOf(data).toEqualTypeOf>(); + } + + { + const [queryRef] = useInteractiveQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { + fetchPolicy: "no-cache", + returnPartialData: true, + errorPolicy: "none", + }); + + invariant(queryRef); - expectTypeOf(explicit).toEqualTypeOf>(); - expectTypeOf(explicit).not.toEqualTypeOf(); + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf>(); + } }); }); }); From 88ba71ea6aead4207dfc5c352ddd075085cbd81a Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 16 Oct 2023 16:34:19 -0600 Subject: [PATCH 009/199] Pull tests out to top-level --- .../__tests__/useInteractiveQuery.test.tsx | 6552 ++++++++--------- 1 file changed, 3267 insertions(+), 3285 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index 51e96b10ebb..8ba5523a1d5 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -622,1587 +622,1738 @@ function renderSuspenseHook( return { ...view, renders }; } -describe("useInteractiveQuery", () => { - it("loads a query when the load query function is called", async () => { - const user = userEvent.setup(); - const { query, mocks } = useSimpleQueryCase(); +it("loads a query when the load query function is called", async () => { + const user = userEvent.setup(); + const { query, mocks } = useSimpleQueryCase(); - function SuspenseFallback() { - ProfiledApp.updateSnapshot((snapshot) => ({ - ...snapshot, - suspenseCount: snapshot.suspenseCount + 1, - })); + function SuspenseFallback() { + ProfiledApp.updateSnapshot((snapshot) => ({ + ...snapshot, + suspenseCount: snapshot.suspenseCount + 1, + })); - return

Loading

; - } + return

Loading

; + } - function Parent() { - const [queryRef, loadQuery] = useInteractiveQuery(query); + function Parent() { + const [queryRef, loadQuery] = useInteractiveQuery(query); - ProfiledApp.updateSnapshot((snapshot) => ({ - ...snapshot, - parentRenderCount: snapshot.parentRenderCount + 1, - })); + ProfiledApp.updateSnapshot((snapshot) => ({ + ...snapshot, + parentRenderCount: snapshot.parentRenderCount + 1, + })); - return ( - <> - - }> - {queryRef && } - - - ); - } + return ( + <> + + }> + {queryRef && } + + + ); + } - function Greeting({ - queryRef, - }: { - queryRef: QueryReference; - }) { - const result = useReadQuery(queryRef); + function Greeting({ + queryRef, + }: { + queryRef: QueryReference; + }) { + const result = useReadQuery(queryRef); - ProfiledApp.updateSnapshot((snapshot) => ({ - ...snapshot, - result, - childRenderCount: snapshot.childRenderCount + 1, - })); + ProfiledApp.updateSnapshot((snapshot) => ({ + ...snapshot, + result, + childRenderCount: snapshot.childRenderCount + 1, + })); - return
{result.data.greeting}
; - } + return
{result.data.greeting}
; + } - const ProfiledApp = profile<{ - result: ReturnType | null; - suspenseCount: number; - parentRenderCount: number; - childRenderCount: number; - }>({ - Component: () => ( - - - - ), - snapshotDOM: true, - initialSnapshot: { - result: null, - suspenseCount: 0, - parentRenderCount: 0, - childRenderCount: 0, - }, + const ProfiledApp = profile<{ + result: ReturnType | null; + suspenseCount: number; + parentRenderCount: number; + childRenderCount: number; + }>({ + Component: () => ( + + + + ), + snapshotDOM: true, + initialSnapshot: { + result: null, + suspenseCount: 0, + parentRenderCount: 0, + childRenderCount: 0, + }, + }); + + render(); + + { + const { snapshot } = await ProfiledApp.takeRender(); + expect(snapshot).toEqual({ + result: null, + suspenseCount: 0, + parentRenderCount: 1, + childRenderCount: 0, }); + } - render(); + await act(() => user.click(screen.getByText("Load query"))); - { - const { snapshot } = await ProfiledApp.takeRender(); - expect(snapshot).toEqual({ - result: null, - suspenseCount: 0, - parentRenderCount: 1, - childRenderCount: 0, - }); - } + { + const { snapshot, withinDOM } = await ProfiledApp.takeRender(); - await act(() => user.click(screen.getByText("Load query"))); + expect(withinDOM().getByText("Loading")).toBeInTheDocument(); + expect(snapshot).toEqual({ + result: null, + suspenseCount: 1, + parentRenderCount: 2, + childRenderCount: 0, + }); + } - { - const { snapshot, withinDOM } = await ProfiledApp.takeRender(); - - expect(withinDOM().getByText("Loading")).toBeInTheDocument(); - expect(snapshot).toEqual({ - result: null, - suspenseCount: 1, - parentRenderCount: 2, - childRenderCount: 0, - }); - } + { + const { snapshot, withinDOM } = await ProfiledApp.takeRender(); - { - const { snapshot, withinDOM } = await ProfiledApp.takeRender(); + expect(withinDOM().getByText("Hello")).toBeInTheDocument(); + expect(snapshot).toEqual({ + result: { + data: { greeting: "Hello" }, + networkStatus: NetworkStatus.ready, + error: undefined, + }, + suspenseCount: 1, + parentRenderCount: 2, + childRenderCount: 1, + }); + } - expect(withinDOM().getByText("Hello")).toBeInTheDocument(); - expect(snapshot).toEqual({ - result: { - data: { greeting: "Hello" }, - networkStatus: NetworkStatus.ready, - error: undefined, - }, - suspenseCount: 1, - parentRenderCount: 2, - childRenderCount: 1, - }); - } + expect(ProfiledApp).not.toRerender(); +}); + +// it('allows the client to be overridden', async () => { +// const query: TypedDocumentNode = gql` +// query UserQuery { +// greeting +// } +// `; + +// const globalClient = new ApolloClient({ +// link: new ApolloLink(() => +// Observable.of({ data: { greeting: 'global hello' } }) +// ), +// cache: new InMemoryCache(), +// }); + +// const localClient = new ApolloClient({ +// link: new ApolloLink(() => +// Observable.of({ data: { greeting: 'local hello' } }) +// ), +// cache: new InMemoryCache(), +// }); + +// const { result } = renderSuspenseHook( +// () => useInteractiveQuery(query, { client: localClient }), +// { client: globalClient } +// ); + +// const [queryRef] = result.current; + +// const _result = await queryRef[QUERY_REFERENCE_SYMBOL].promise; + +// await waitFor(() => { +// expect(_result).toEqual({ +// data: { greeting: 'local hello' }, +// loading: false, +// networkStatus: NetworkStatus.ready, +// }); +// }); +// }); + +// it('passes context to the link', async () => { +// const query = gql` +// query ContextQuery { +// context +// } +// `; + +// const link = new ApolloLink((operation) => { +// return new Observable((observer) => { +// const { valueA, valueB } = operation.getContext(); + +// observer.next({ data: { context: { valueA, valueB } } }); +// observer.complete(); +// }); +// }); + +// const { result } = renderHook( +// () => +// useInteractiveQuery(query, { +// context: { valueA: 'A', valueB: 'B' }, +// }), +// { +// wrapper: ({ children }) => ( +// {children} +// ), +// } +// ); + +// const [queryRef] = result.current; + +// const _result = await queryRef[QUERY_REFERENCE_SYMBOL].promise; + +// await waitFor(() => { +// expect(_result).toMatchObject({ +// data: { context: { valueA: 'A', valueB: 'B' } }, +// networkStatus: NetworkStatus.ready, +// }); +// }); +// }); + +// it('enables canonical results when canonizeResults is "true"', async () => { +// interface Result { +// __typename: string; +// value: number; +// } + +// const cache = new InMemoryCache({ +// typePolicies: { +// Result: { +// keyFields: false, +// }, +// }, +// }); + +// const query: TypedDocumentNode<{ results: Result[] }> = gql` +// query { +// results { +// value +// } +// } +// `; + +// const results: Result[] = [ +// { __typename: 'Result', value: 0 }, +// { __typename: 'Result', value: 1 }, +// { __typename: 'Result', value: 1 }, +// { __typename: 'Result', value: 2 }, +// { __typename: 'Result', value: 3 }, +// { __typename: 'Result', value: 5 }, +// ]; + +// cache.writeQuery({ +// query, +// data: { results }, +// }); + +// const { result } = renderHook( +// () => +// useInteractiveQuery(query, { +// canonizeResults: true, +// }), +// { +// wrapper: ({ children }) => ( +// {children} +// ), +// } +// ); + +// const [queryRef] = result.current; + +// const _result = await queryRef[QUERY_REFERENCE_SYMBOL].promise; +// const resultSet = new Set(_result.data.results); +// const values = Array.from(resultSet).map((item) => item.value); + +// expect(_result.data).toEqual({ results }); +// expect(_result.data.results.length).toBe(6); +// expect(resultSet.size).toBe(5); +// expect(values).toEqual([0, 1, 2, 3, 5]); +// }); + +// it("can disable canonical results when the cache's canonizeResults setting is true", async () => { +// interface Result { +// __typename: string; +// value: number; +// } + +// const cache = new InMemoryCache({ +// canonizeResults: true, +// typePolicies: { +// Result: { +// keyFields: false, +// }, +// }, +// }); + +// const query: TypedDocumentNode<{ results: Result[] }> = gql` +// query { +// results { +// value +// } +// } +// `; + +// const results: Result[] = [ +// { __typename: 'Result', value: 0 }, +// { __typename: 'Result', value: 1 }, +// { __typename: 'Result', value: 1 }, +// { __typename: 'Result', value: 2 }, +// { __typename: 'Result', value: 3 }, +// { __typename: 'Result', value: 5 }, +// ]; + +// cache.writeQuery({ +// query, +// data: { results }, +// }); + +// const { result } = renderHook( +// () => +// useInteractiveQuery(query, { +// canonizeResults: false, +// }), +// { +// wrapper: ({ children }) => ( +// {children} +// ), +// } +// ); + +// const [queryRef] = result.current; + +// const _result = await queryRef[QUERY_REFERENCE_SYMBOL].promise; +// const resultSet = new Set(_result.data.results); +// const values = Array.from(resultSet).map((item) => item.value); + +// expect(_result.data).toEqual({ results }); +// expect(_result.data.results.length).toBe(6); +// expect(resultSet.size).toBe(6); +// expect(values).toEqual([0, 1, 1, 2, 3, 5]); +// }); + +// // TODO(FIXME): test fails, should return cache data first if it exists +// it.skip('returns initial cache data followed by network data when the fetch policy is `cache-and-network`', async () => { +// const query = gql` +// { +// hello +// } +// `; +// const cache = new InMemoryCache(); +// const link = mockSingleLink({ +// request: { query }, +// result: { data: { hello: 'from link' } }, +// delay: 20, +// }); + +// const client = new ApolloClient({ +// link, +// cache, +// }); + +// cache.writeQuery({ query, data: { hello: 'from cache' } }); + +// const { result } = renderHook( +// () => useInteractiveQuery(query, { fetchPolicy: 'cache-and-network' }), +// { +// wrapper: ({ children }) => ( +// {children} +// ), +// } +// ); + +// const [queryRef] = result.current; + +// const _result = await queryRef[QUERY_REFERENCE_SYMBOL].promise; + +// expect(_result).toEqual({ +// data: { hello: 'from link' }, +// loading: false, +// networkStatus: 7, +// }); +// }); + +// it('all data is present in the cache, no network request is made', async () => { +// const query = gql` +// { +// hello +// } +// `; +// const cache = new InMemoryCache(); +// const link = mockSingleLink({ +// request: { query }, +// result: { data: { hello: 'from link' } }, +// delay: 20, +// }); + +// const client = new ApolloClient({ +// link, +// cache, +// }); + +// cache.writeQuery({ query, data: { hello: 'from cache' } }); + +// const { result } = renderHook( +// () => useInteractiveQuery(query, { fetchPolicy: 'cache-first' }), +// { +// wrapper: ({ children }) => ( +// {children} +// ), +// } +// ); + +// const [queryRef] = result.current; + +// const _result = await queryRef[QUERY_REFERENCE_SYMBOL].promise; + +// expect(_result).toEqual({ +// data: { hello: 'from cache' }, +// loading: false, +// networkStatus: 7, +// }); +// }); +// it('partial data is present in the cache so it is ignored and network request is made', async () => { +// const query = gql` +// { +// hello +// foo +// } +// `; +// const cache = new InMemoryCache(); +// const link = mockSingleLink({ +// request: { query }, +// result: { data: { hello: 'from link', foo: 'bar' } }, +// delay: 20, +// }); + +// const client = new ApolloClient({ +// link, +// cache, +// }); + +// // we expect a "Missing field 'foo' while writing result..." error +// // when writing hello to the cache, so we'll silence the console.error +// const originalConsoleError = console.error; +// console.error = () => { +// /* noop */ +// }; +// cache.writeQuery({ query, data: { hello: 'from cache' } }); +// console.error = originalConsoleError; + +// const { result } = renderHook( +// () => useInteractiveQuery(query, { fetchPolicy: 'cache-first' }), +// { +// wrapper: ({ children }) => ( +// {children} +// ), +// } +// ); + +// const [queryRef] = result.current; + +// const _result = await queryRef[QUERY_REFERENCE_SYMBOL].promise; + +// expect(_result).toEqual({ +// data: { foo: 'bar', hello: 'from link' }, +// loading: false, +// networkStatus: 7, +// }); +// }); + +// it('existing data in the cache is ignored', async () => { +// const query = gql` +// { +// hello +// } +// `; +// const cache = new InMemoryCache(); +// const link = mockSingleLink({ +// request: { query }, +// result: { data: { hello: 'from link' } }, +// delay: 20, +// }); + +// const client = new ApolloClient({ +// link, +// cache, +// }); + +// cache.writeQuery({ query, data: { hello: 'from cache' } }); + +// const { result } = renderHook( +// () => useInteractiveQuery(query, { fetchPolicy: 'network-only' }), +// { +// wrapper: ({ children }) => ( +// {children} +// ), +// } +// ); + +// const [queryRef] = result.current; + +// const _result = await queryRef[QUERY_REFERENCE_SYMBOL].promise; + +// expect(_result).toEqual({ +// data: { hello: 'from link' }, +// loading: false, +// networkStatus: 7, +// }); +// expect(client.cache.extract()).toEqual({ +// ROOT_QUERY: { __typename: 'Query', hello: 'from link' }, +// }); +// }); + +// it('fetches data from the network but does not update the cache', async () => { +// const query = gql` +// { +// hello +// } +// `; +// const cache = new InMemoryCache(); +// const link = mockSingleLink({ +// request: { query }, +// result: { data: { hello: 'from link' } }, +// delay: 20, +// }); + +// const client = new ApolloClient({ +// link, +// cache, +// }); + +// cache.writeQuery({ query, data: { hello: 'from cache' } }); + +// const { result } = renderHook( +// () => useInteractiveQuery(query, { fetchPolicy: 'no-cache' }), +// { +// wrapper: ({ children }) => ( +// {children} +// ), +// } +// ); + +// const [queryRef] = result.current; + +// const _result = await queryRef[QUERY_REFERENCE_SYMBOL].promise; + +// expect(_result).toEqual({ +// data: { hello: 'from link' }, +// loading: false, +// networkStatus: 7, +// }); +// // ...but not updated in the cache +// expect(client.cache.extract()).toEqual({ +// ROOT_QUERY: { __typename: 'Query', hello: 'from cache' }, +// }); +// }); + +describe("integration tests with useReadQuery", () => { + it("suspends and renders hello", async () => { + const user = userEvent.setup(); + const { renders, loadQueryButton } = renderIntegrationTest(); + + expect(renders.suspenseCount).toBe(0); - expect(ProfiledApp).not.toRerender(); + await act(() => user.click(loadQueryButton)); + + expect(await screen.findByText("loading")).toBeInTheDocument(); + + // the parent component re-renders when promise fulfilled + expect(await screen.findByText("hello")).toBeInTheDocument(); + expect(renders.suspenseCount).toBe(1); + expect(renders.count).toBe(1); }); - // it('allows the client to be overridden', async () => { - // const query: TypedDocumentNode = gql` - // query UserQuery { - // greeting - // } - // `; - - // const globalClient = new ApolloClient({ - // link: new ApolloLink(() => - // Observable.of({ data: { greeting: 'global hello' } }) - // ), - // cache: new InMemoryCache(), - // }); - - // const localClient = new ApolloClient({ - // link: new ApolloLink(() => - // Observable.of({ data: { greeting: 'local hello' } }) - // ), - // cache: new InMemoryCache(), - // }); - - // const { result } = renderSuspenseHook( - // () => useInteractiveQuery(query, { client: localClient }), - // { client: globalClient } - // ); - - // const [queryRef] = result.current; - - // const _result = await queryRef[QUERY_REFERENCE_SYMBOL].promise; - - // await waitFor(() => { - // expect(_result).toEqual({ - // data: { greeting: 'local hello' }, - // loading: false, - // networkStatus: NetworkStatus.ready, - // }); - // }); - // }); - - // it('passes context to the link', async () => { - // const query = gql` - // query ContextQuery { - // context - // } - // `; - - // const link = new ApolloLink((operation) => { - // return new Observable((observer) => { - // const { valueA, valueB } = operation.getContext(); - - // observer.next({ data: { context: { valueA, valueB } } }); - // observer.complete(); - // }); - // }); - - // const { result } = renderHook( - // () => - // useInteractiveQuery(query, { - // context: { valueA: 'A', valueB: 'B' }, - // }), - // { - // wrapper: ({ children }) => ( - // {children} - // ), - // } - // ); - - // const [queryRef] = result.current; - - // const _result = await queryRef[QUERY_REFERENCE_SYMBOL].promise; - - // await waitFor(() => { - // expect(_result).toMatchObject({ - // data: { context: { valueA: 'A', valueB: 'B' } }, - // networkStatus: NetworkStatus.ready, - // }); - // }); - // }); - - // it('enables canonical results when canonizeResults is "true"', async () => { - // interface Result { - // __typename: string; - // value: number; - // } - - // const cache = new InMemoryCache({ - // typePolicies: { - // Result: { - // keyFields: false, - // }, - // }, - // }); - - // const query: TypedDocumentNode<{ results: Result[] }> = gql` - // query { - // results { - // value - // } - // } - // `; - - // const results: Result[] = [ - // { __typename: 'Result', value: 0 }, - // { __typename: 'Result', value: 1 }, - // { __typename: 'Result', value: 1 }, - // { __typename: 'Result', value: 2 }, - // { __typename: 'Result', value: 3 }, - // { __typename: 'Result', value: 5 }, - // ]; - - // cache.writeQuery({ - // query, - // data: { results }, - // }); - - // const { result } = renderHook( - // () => - // useInteractiveQuery(query, { - // canonizeResults: true, - // }), - // { - // wrapper: ({ children }) => ( - // {children} - // ), - // } - // ); - - // const [queryRef] = result.current; - - // const _result = await queryRef[QUERY_REFERENCE_SYMBOL].promise; - // const resultSet = new Set(_result.data.results); - // const values = Array.from(resultSet).map((item) => item.value); - - // expect(_result.data).toEqual({ results }); - // expect(_result.data.results.length).toBe(6); - // expect(resultSet.size).toBe(5); - // expect(values).toEqual([0, 1, 2, 3, 5]); - // }); - - // it("can disable canonical results when the cache's canonizeResults setting is true", async () => { - // interface Result { - // __typename: string; - // value: number; - // } - - // const cache = new InMemoryCache({ - // canonizeResults: true, - // typePolicies: { - // Result: { - // keyFields: false, - // }, - // }, - // }); - - // const query: TypedDocumentNode<{ results: Result[] }> = gql` - // query { - // results { - // value - // } - // } - // `; - - // const results: Result[] = [ - // { __typename: 'Result', value: 0 }, - // { __typename: 'Result', value: 1 }, - // { __typename: 'Result', value: 1 }, - // { __typename: 'Result', value: 2 }, - // { __typename: 'Result', value: 3 }, - // { __typename: 'Result', value: 5 }, - // ]; - - // cache.writeQuery({ - // query, - // data: { results }, - // }); - - // const { result } = renderHook( - // () => - // useInteractiveQuery(query, { - // canonizeResults: false, - // }), - // { - // wrapper: ({ children }) => ( - // {children} - // ), - // } - // ); - - // const [queryRef] = result.current; - - // const _result = await queryRef[QUERY_REFERENCE_SYMBOL].promise; - // const resultSet = new Set(_result.data.results); - // const values = Array.from(resultSet).map((item) => item.value); - - // expect(_result.data).toEqual({ results }); - // expect(_result.data.results.length).toBe(6); - // expect(resultSet.size).toBe(6); - // expect(values).toEqual([0, 1, 1, 2, 3, 5]); - // }); - - // // TODO(FIXME): test fails, should return cache data first if it exists - // it.skip('returns initial cache data followed by network data when the fetch policy is `cache-and-network`', async () => { - // const query = gql` - // { - // hello - // } - // `; - // const cache = new InMemoryCache(); - // const link = mockSingleLink({ - // request: { query }, - // result: { data: { hello: 'from link' } }, - // delay: 20, - // }); - - // const client = new ApolloClient({ - // link, - // cache, - // }); - - // cache.writeQuery({ query, data: { hello: 'from cache' } }); - - // const { result } = renderHook( - // () => useInteractiveQuery(query, { fetchPolicy: 'cache-and-network' }), - // { - // wrapper: ({ children }) => ( - // {children} - // ), - // } - // ); - - // const [queryRef] = result.current; - - // const _result = await queryRef[QUERY_REFERENCE_SYMBOL].promise; - - // expect(_result).toEqual({ - // data: { hello: 'from link' }, - // loading: false, - // networkStatus: 7, - // }); - // }); - - // it('all data is present in the cache, no network request is made', async () => { - // const query = gql` - // { - // hello - // } - // `; - // const cache = new InMemoryCache(); - // const link = mockSingleLink({ - // request: { query }, - // result: { data: { hello: 'from link' } }, - // delay: 20, - // }); - - // const client = new ApolloClient({ - // link, - // cache, - // }); - - // cache.writeQuery({ query, data: { hello: 'from cache' } }); - - // const { result } = renderHook( - // () => useInteractiveQuery(query, { fetchPolicy: 'cache-first' }), - // { - // wrapper: ({ children }) => ( - // {children} - // ), - // } - // ); - - // const [queryRef] = result.current; - - // const _result = await queryRef[QUERY_REFERENCE_SYMBOL].promise; - - // expect(_result).toEqual({ - // data: { hello: 'from cache' }, - // loading: false, - // networkStatus: 7, - // }); - // }); - // it('partial data is present in the cache so it is ignored and network request is made', async () => { - // const query = gql` - // { - // hello - // foo - // } - // `; - // const cache = new InMemoryCache(); - // const link = mockSingleLink({ - // request: { query }, - // result: { data: { hello: 'from link', foo: 'bar' } }, - // delay: 20, - // }); - - // const client = new ApolloClient({ - // link, - // cache, - // }); - - // // we expect a "Missing field 'foo' while writing result..." error - // // when writing hello to the cache, so we'll silence the console.error - // const originalConsoleError = console.error; - // console.error = () => { - // /* noop */ - // }; - // cache.writeQuery({ query, data: { hello: 'from cache' } }); - // console.error = originalConsoleError; - - // const { result } = renderHook( - // () => useInteractiveQuery(query, { fetchPolicy: 'cache-first' }), - // { - // wrapper: ({ children }) => ( - // {children} - // ), - // } - // ); - - // const [queryRef] = result.current; - - // const _result = await queryRef[QUERY_REFERENCE_SYMBOL].promise; - - // expect(_result).toEqual({ - // data: { foo: 'bar', hello: 'from link' }, - // loading: false, - // networkStatus: 7, - // }); - // }); - - // it('existing data in the cache is ignored', async () => { - // const query = gql` - // { - // hello - // } - // `; - // const cache = new InMemoryCache(); - // const link = mockSingleLink({ - // request: { query }, - // result: { data: { hello: 'from link' } }, - // delay: 20, - // }); - - // const client = new ApolloClient({ - // link, - // cache, - // }); - - // cache.writeQuery({ query, data: { hello: 'from cache' } }); - - // const { result } = renderHook( - // () => useInteractiveQuery(query, { fetchPolicy: 'network-only' }), - // { - // wrapper: ({ children }) => ( - // {children} - // ), - // } - // ); - - // const [queryRef] = result.current; - - // const _result = await queryRef[QUERY_REFERENCE_SYMBOL].promise; - - // expect(_result).toEqual({ - // data: { hello: 'from link' }, - // loading: false, - // networkStatus: 7, - // }); - // expect(client.cache.extract()).toEqual({ - // ROOT_QUERY: { __typename: 'Query', hello: 'from link' }, - // }); - // }); - - // it('fetches data from the network but does not update the cache', async () => { - // const query = gql` - // { - // hello - // } - // `; - // const cache = new InMemoryCache(); - // const link = mockSingleLink({ - // request: { query }, - // result: { data: { hello: 'from link' } }, - // delay: 20, - // }); - - // const client = new ApolloClient({ - // link, - // cache, - // }); - - // cache.writeQuery({ query, data: { hello: 'from cache' } }); - - // const { result } = renderHook( - // () => useInteractiveQuery(query, { fetchPolicy: 'no-cache' }), - // { - // wrapper: ({ children }) => ( - // {children} - // ), - // } - // ); - - // const [queryRef] = result.current; - - // const _result = await queryRef[QUERY_REFERENCE_SYMBOL].promise; - - // expect(_result).toEqual({ - // data: { hello: 'from link' }, - // loading: false, - // networkStatus: 7, - // }); - // // ...but not updated in the cache - // expect(client.cache.extract()).toEqual({ - // ROOT_QUERY: { __typename: 'Query', hello: 'from cache' }, - // }); - // }); - - describe("integration tests with useReadQuery", () => { - it("suspends and renders hello", async () => { - const user = userEvent.setup(); - const { renders, loadQueryButton } = renderIntegrationTest(); - - expect(renders.suspenseCount).toBe(0); - - await act(() => user.click(loadQueryButton)); - - expect(await screen.findByText("loading")).toBeInTheDocument(); - - // the parent component re-renders when promise fulfilled - expect(await screen.findByText("hello")).toBeInTheDocument(); - expect(renders.suspenseCount).toBe(1); - expect(renders.count).toBe(1); - }); + it("works with startTransition to change variables", async () => { + type Variables = { + id: string; + }; - it("works with startTransition to change variables", async () => { - type Variables = { + interface Data { + todo: { id: string; + name: string; + completed: boolean; }; + } + const user = userEvent.setup(); - interface Data { - todo: { - id: string; - name: string; - completed: boolean; - }; - } - const user = userEvent.setup(); - - const query: TypedDocumentNode = gql` - query TodoItemQuery($id: ID!) { - todo(id: $id) { - id - name - completed - } + const query: TypedDocumentNode = gql` + query TodoItemQuery($id: ID!) { + todo(id: $id) { + id + name + completed } - `; + } + `; - const mocks: MockedResponse[] = [ - { - request: { query, variables: { id: "1" } }, - result: { - data: { todo: { id: "1", name: "Clean room", completed: false } }, - }, - delay: 10, + const mocks: MockedResponse[] = [ + { + request: { query, variables: { id: "1" } }, + result: { + data: { todo: { id: "1", name: "Clean room", completed: false } }, }, - { - request: { query, variables: { id: "2" } }, - result: { - data: { - todo: { id: "2", name: "Take out trash", completed: true }, - }, + delay: 10, + }, + { + request: { query, variables: { id: "2" } }, + result: { + data: { + todo: { id: "2", name: "Take out trash", completed: true }, }, - delay: 10, }, - ]; + delay: 10, + }, + ]; - const client = new ApolloClient({ - link: new MockLink(mocks), - cache: new InMemoryCache(), - }); + const client = new ApolloClient({ + link: new MockLink(mocks), + cache: new InMemoryCache(), + }); - function App() { - return ( - - }> - - - - ); - } + function App() { + return ( + + }> + + + + ); + } - function SuspenseFallback() { - return

Loading

; - } + function SuspenseFallback() { + return

Loading

; + } - function Parent() { - const [queryRef, loadQuery] = useInteractiveQuery(query); + function Parent() { + const [queryRef, loadQuery] = useInteractiveQuery(query); - return ( -
- - {queryRef && ( - loadQuery({ id })} /> - )} -
- ); - } + return ( +
+ + {queryRef && ( + loadQuery({ id })} /> + )} +
+ ); + } - function Todo({ - queryRef, - onChange, - }: { - queryRef: QueryReference; - onChange: (id: string) => void; - }) { - const { data } = useReadQuery(queryRef); - const [isPending, startTransition] = React.useTransition(); - const { todo } = data; + function Todo({ + queryRef, + onChange, + }: { + queryRef: QueryReference; + onChange: (id: string) => void; + }) { + const { data } = useReadQuery(queryRef); + const [isPending, startTransition] = React.useTransition(); + const { todo } = data; - return ( - <> - -
- {todo.name} - {todo.completed && " (completed)"} -
- - ); - } + return ( + <> + +
+ {todo.name} + {todo.completed && " (completed)"} +
+ + ); + } - render(); + render(); - await act(() => user.click(screen.getByText("Load first todo"))); + await act(() => user.click(screen.getByText("Load first todo"))); - expect(screen.getByText("Loading")).toBeInTheDocument(); - expect(await screen.findByTestId("todo")).toBeInTheDocument(); + expect(screen.getByText("Loading")).toBeInTheDocument(); + expect(await screen.findByTestId("todo")).toBeInTheDocument(); - const todo = screen.getByTestId("todo"); - const button = screen.getByText("Refresh"); + const todo = screen.getByTestId("todo"); + const button = screen.getByText("Refresh"); - expect(todo).toHaveTextContent("Clean room"); + expect(todo).toHaveTextContent("Clean room"); - await act(() => user.click(button)); + 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(); + // 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(todo).toHaveAttribute("aria-busy", "true"); + // We can ensure this works with isPending from useTransition in the process + expect(todo).toHaveAttribute("aria-busy", "true"); - // Ensure we are showing the stale UI until the new todo has loaded - expect(todo).toHaveTextContent("Clean room"); + // Ensure we are showing the stale UI until the new todo has loaded + expect(todo).toHaveTextContent("Clean room"); - // Eventually we should see the updated todo content once its done - // suspending. - await waitFor(() => { - expect(todo).toHaveTextContent("Take out trash (completed)"); - }); + // Eventually we should see the updated todo content once its done + // suspending. + await waitFor(() => { + expect(todo).toHaveTextContent("Take out trash (completed)"); }); + }); - it('does not suspend deferred queries with data in the cache and using a "cache-and-network" fetch policy', async () => { - interface Data { - greeting: { - __typename: string; - message: string; - recipient: { name: string; __typename: string }; - }; - } + it('does not suspend deferred queries with data in the cache and using a "cache-and-network" fetch policy', async () => { + interface Data { + greeting: { + __typename: string; + message: string; + recipient: { name: string; __typename: string }; + }; + } - const user = userEvent.setup(); + const user = userEvent.setup(); - const query: TypedDocumentNode = gql` - query { - greeting { - message - ... on Greeting @defer { - recipient { - name - } + const query: TypedDocumentNode = gql` + query { + greeting { + message + ... on Greeting @defer { + recipient { + name } } } - `; - - const link = new MockSubscriptionLink(); - const cache = new InMemoryCache(); - cache.writeQuery({ - query, - data: { - greeting: { - __typename: "Greeting", - message: "Hello cached", - recipient: { __typename: "Person", name: "Cached Alice" }, - }, - }, - }); - const client = new ApolloClient({ cache, link }); - let renders = 0; - let suspenseCount = 0; - - function App() { - return ( - - }> - - - - ); - } - - function SuspenseFallback() { - suspenseCount++; - return

Loading

; } + `; - function Parent() { - const [queryRef, loadQuery] = useInteractiveQuery(query, { - fetchPolicy: "cache-and-network", - }); - return ( -
- - {queryRef && } -
- ); - } + const link = new MockSubscriptionLink(); + const cache = new InMemoryCache(); + cache.writeQuery({ + query, + data: { + greeting: { + __typename: "Greeting", + message: "Hello cached", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + }); + const client = new ApolloClient({ cache, link }); + let renders = 0; + let suspenseCount = 0; - function Todo({ queryRef }: { queryRef: QueryReference }) { - const { data, networkStatus, error } = useReadQuery(queryRef); - const { greeting } = data; - renders++; + function App() { + return ( + + }> + + + + ); + } - return ( - <> -
Message: {greeting.message}
-
Recipient: {greeting.recipient.name}
-
Network status: {networkStatus}
-
Error: {error ? error.message : "none"}
- - ); - } + function SuspenseFallback() { + suspenseCount++; + return

Loading

; + } - render(); + function Parent() { + const [queryRef, loadQuery] = useInteractiveQuery(query, { + fetchPolicy: "cache-and-network", + }); + return ( +
+ + {queryRef && } +
+ ); + } - await act(() => user.click(screen.getByText("Load todo"))); + function Todo({ queryRef }: { queryRef: QueryReference }) { + const { data, networkStatus, error } = useReadQuery(queryRef); + const { greeting } = data; + renders++; - expect(screen.getByText(/Message/i)).toHaveTextContent( - "Message: Hello cached" - ); - expect(screen.getByText(/Recipient/i)).toHaveTextContent( - "Recipient: Cached Alice" - ); - expect(screen.getByText(/Network status/i)).toHaveTextContent( - "Network status: 1" // loading + return ( + <> +
Message: {greeting.message}
+
Recipient: {greeting.recipient.name}
+
Network status: {networkStatus}
+
Error: {error ? error.message : "none"}
+ ); - expect(screen.getByText(/Error/i)).toHaveTextContent("none"); + } - link.simulateResult({ - result: { - data: { - greeting: { __typename: "Greeting", message: "Hello world" }, - }, - hasNext: true, - }, - }); + render(); - await waitFor(() => { - expect(screen.getByText(/Message/i)).toHaveTextContent( - "Message: Hello world" - ); - }); - expect(screen.getByText(/Recipient/i)).toHaveTextContent( - "Recipient: Cached Alice" - ); - expect(screen.getByText(/Network status/i)).toHaveTextContent( - "Network status: 7" // ready - ); - expect(screen.getByText(/Error/i)).toHaveTextContent("none"); + await act(() => user.click(screen.getByText("Load todo"))); - link.simulateResult({ - result: { - incremental: [ - { - data: { - recipient: { name: "Alice", __typename: "Person" }, - __typename: "Greeting", - }, - path: ["greeting"], - }, - ], - hasNext: false, + expect(screen.getByText(/Message/i)).toHaveTextContent( + "Message: Hello cached" + ); + expect(screen.getByText(/Recipient/i)).toHaveTextContent( + "Recipient: Cached Alice" + ); + expect(screen.getByText(/Network status/i)).toHaveTextContent( + "Network status: 1" // loading + ); + expect(screen.getByText(/Error/i)).toHaveTextContent("none"); + + link.simulateResult({ + result: { + data: { + greeting: { __typename: "Greeting", message: "Hello world" }, }, - }); + hasNext: true, + }, + }); - await waitFor(() => { - expect(screen.getByText(/Recipient/i)).toHaveTextContent( - "Recipient: Alice" - ); - }); + await waitFor(() => { expect(screen.getByText(/Message/i)).toHaveTextContent( "Message: Hello world" ); - expect(screen.getByText(/Network status/i)).toHaveTextContent( - "Network status: 7" // ready - ); - expect(screen.getByText(/Error/i)).toHaveTextContent("none"); - - expect(renders).toBe(3); - expect(suspenseCount).toBe(0); }); - }); - - it("reacts to cache updates", async () => { - const { renders, client, query, loadQueryButton, user } = - renderIntegrationTest(); - - await act(() => user.click(loadQueryButton)); + expect(screen.getByText(/Recipient/i)).toHaveTextContent( + "Recipient: Cached Alice" + ); + expect(screen.getByText(/Network status/i)).toHaveTextContent( + "Network status: 7" // ready + ); + expect(screen.getByText(/Error/i)).toHaveTextContent("none"); - expect(renders.suspenseCount).toBe(1); - expect(screen.getByText("loading")).toBeInTheDocument(); - - // the parent component re-renders when promise fulfilled - expect(await screen.findByText("hello")).toBeInTheDocument(); - expect(renders.count).toBe(1); - - client.writeQuery({ - query, - data: { foo: { bar: "baz" } }, + link.simulateResult({ + result: { + incremental: [ + { + data: { + recipient: { name: "Alice", __typename: "Person" }, + __typename: "Greeting", + }, + path: ["greeting"], + }, + ], + hasNext: false, + }, }); - // 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({ - query, - data: { foo: { bar: "bat" } }, + await waitFor(() => { + expect(screen.getByText(/Recipient/i)).toHaveTextContent( + "Recipient: Alice" + ); }); + expect(screen.getByText(/Message/i)).toHaveTextContent( + "Message: Hello world" + ); + expect(screen.getByText(/Network status/i)).toHaveTextContent( + "Network status: 7" // ready + ); + expect(screen.getByText(/Error/i)).toHaveTextContent("none"); - expect(await screen.findByText("bat")).toBeInTheDocument(); - - expect(renders.suspenseCount).toBe(1); + expect(renders).toBe(3); + expect(suspenseCount).toBe(0); }); +}); - it("reacts to variables updates", async () => { - const { renders, user, loadQueryButton } = renderVariablesIntegrationTest({ - variables: { id: "1" }, - }); - - await act(() => user.click(loadQueryButton)); - - expect(renders.suspenseCount).toBe(1); - expect(screen.getByText("loading")).toBeInTheDocument(); +it("reacts to cache updates", async () => { + const { renders, client, query, loadQueryButton, user } = + renderIntegrationTest(); - expect(await screen.findByText("1 - Spider-Man")).toBeInTheDocument(); + await act(() => user.click(loadQueryButton)); - await act(() => user.click(screen.getByText("Change variables"))); + expect(renders.suspenseCount).toBe(1); + expect(screen.getByText("loading")).toBeInTheDocument(); - expect(renders.suspenseCount).toBe(2); - expect(screen.getByText("loading")).toBeInTheDocument(); + // the parent component re-renders when promise fulfilled + expect(await screen.findByText("hello")).toBeInTheDocument(); + expect(renders.count).toBe(1); - expect(await screen.findByText("2 - Black Widow")).toBeInTheDocument(); + client.writeQuery({ + query, + data: { foo: { bar: "baz" } }, }); - it("applies `errorPolicy` on next fetch when it changes between renders", async () => { - interface Data { - greeting: string; - } - - const user = userEvent.setup(); + // the parent component re-renders when promise fulfilled + expect(await screen.findByText("baz")).toBeInTheDocument(); - const query: TypedDocumentNode = gql` - query { - greeting - } - `; + expect(renders.count).toBe(2); + expect(renders.suspenseCount).toBe(1); - const mocks = [ - { - request: { query }, - result: { data: { greeting: "Hello" } }, - }, - { - request: { query }, - result: { - errors: [new GraphQLError("oops")], - }, - }, - ]; + client.writeQuery({ + query, + data: { foo: { bar: "bat" } }, + }); - const client = new ApolloClient({ - link: new MockLink(mocks), - cache: new InMemoryCache(), - }); + expect(await screen.findByText("bat")).toBeInTheDocument(); - function SuspenseFallback() { - return
Loading...
; - } + expect(renders.suspenseCount).toBe(1); +}); - function Parent() { - const [errorPolicy, setErrorPolicy] = React.useState("none"); - const [queryRef, { refetch }] = useInteractiveQuery(query, { - errorPolicy, - }); +it("reacts to variables updates", async () => { + const { renders, user, loadQueryButton } = renderVariablesIntegrationTest({ + variables: { id: "1" }, + }); - return ( - <> - - - }> - - - - ); - } + await act(() => user.click(loadQueryButton)); - function Greeting({ queryRef }: { queryRef: QueryReference }) { - const { data, error } = useReadQuery(queryRef); + expect(renders.suspenseCount).toBe(1); + expect(screen.getByText("loading")).toBeInTheDocument(); - return error ? ( -
{error.message}
- ) : ( -
{data.greeting}
- ); - } + expect(await screen.findByText("1 - Spider-Man")).toBeInTheDocument(); - function App() { - return ( - - Error boundary} - > - - - - ); - } + await act(() => user.click(screen.getByText("Change variables"))); - render(); + expect(renders.suspenseCount).toBe(2); + expect(screen.getByText("loading")).toBeInTheDocument(); - expect(await screen.findByTestId("greeting")).toHaveTextContent("Hello"); + expect(await screen.findByText("2 - Black Widow")).toBeInTheDocument(); +}); - await act(() => user.click(screen.getByText("Change error policy"))); - await act(() => user.click(screen.getByText("Refetch greeting"))); +it("applies `errorPolicy` on next fetch when it changes between renders", async () => { + interface Data { + greeting: string; + } - // Ensure we aren't rendering the error boundary and instead rendering the - // error message in the Greeting component. - expect(await screen.findByTestId("error")).toHaveTextContent("oops"); - }); + const user = userEvent.setup(); - it("applies `context` on next fetch when it changes between renders", async () => { - interface Data { - context: Record; + const query: TypedDocumentNode = gql` + query { + greeting } + `; - const user = userEvent.setup(); + const mocks = [ + { + request: { query }, + result: { data: { greeting: "Hello" } }, + }, + { + request: { query }, + result: { + errors: [new GraphQLError("oops")], + }, + }, + ]; - const query: TypedDocumentNode = gql` - query { - context - } - `; + const client = new ApolloClient({ + link: new MockLink(mocks), + cache: new InMemoryCache(), + }); - const link = new ApolloLink((operation) => { - return Observable.of({ - data: { - context: operation.getContext(), - }, - }); - }); + function SuspenseFallback() { + return
Loading...
; + } - const client = new ApolloClient({ - link, - cache: new InMemoryCache(), + function Parent() { + const [errorPolicy, setErrorPolicy] = React.useState("none"); + const [queryRef, { refetch }] = useInteractiveQuery(query, { + errorPolicy, }); - function SuspenseFallback() { - return
Loading...
; - } - - function Parent() { - const [phase, setPhase] = React.useState("initial"); - const [queryRef, { refetch }] = useInteractiveQuery(query, { - context: { phase }, - }); - - return ( - <> - - - }> - - - - ); - } + return ( + <> + + + }> + + + + ); + } - function Context({ queryRef }: { queryRef: QueryReference }) { - const { data } = useReadQuery(queryRef); + function Greeting({ queryRef }: { queryRef: QueryReference }) { + const { data, error } = useReadQuery(queryRef); - return
{data.context.phase}
; - } + return error ? ( +
{error.message}
+ ) : ( +
{data.greeting}
+ ); + } - function App() { - return ( - + function App() { + return ( + + Error boundary}> - - ); - } + + + ); + } - render(); + render(); - expect(await screen.findByTestId("context")).toHaveTextContent("initial"); + expect(await screen.findByTestId("greeting")).toHaveTextContent("Hello"); - await act(() => user.click(screen.getByText("Update context"))); - await act(() => user.click(screen.getByText("Refetch"))); + await act(() => user.click(screen.getByText("Change error policy"))); + await act(() => user.click(screen.getByText("Refetch greeting"))); - expect(await screen.findByTestId("context")).toHaveTextContent("rerender"); - }); + // Ensure we aren't rendering the error boundary and instead rendering the + // error message in the Greeting component. + expect(await screen.findByTestId("error")).toHaveTextContent("oops"); +}); - // NOTE: We only test the `false` -> `true` path here. If the option changes - // from `true` -> `false`, the data has already been canonized, so it has no - // effect on the output. - it("returns canonical results immediately when `canonizeResults` changes from `false` to `true` between renders", async () => { - interface Result { - __typename: string; - value: number; - } +it("applies `context` on next fetch when it changes between renders", async () => { + interface Data { + context: Record; + } - interface Data { - results: Result[]; + const user = userEvent.setup(); + + const query: TypedDocumentNode = gql` + query { + context } + `; - const cache = new InMemoryCache({ - typePolicies: { - Result: { - keyFields: false, - }, + const link = new ApolloLink((operation) => { + return Observable.of({ + data: { + context: operation.getContext(), }, }); + }); - const query: TypedDocumentNode = gql` - query { - results { - value - } - } - `; - - const results: Result[] = [ - { __typename: "Result", value: 0 }, - { __typename: "Result", value: 1 }, - { __typename: "Result", value: 1 }, - { __typename: "Result", value: 2 }, - { __typename: "Result", value: 3 }, - { __typename: "Result", value: 5 }, - ]; - - const user = userEvent.setup(); + const client = new ApolloClient({ + link, + cache: new InMemoryCache(), + }); - cache.writeQuery({ - query, - data: { results }, - }); + function SuspenseFallback() { + return
Loading...
; + } - const client = new ApolloClient({ - link: new MockLink([]), - cache, + function Parent() { + const [phase, setPhase] = React.useState("initial"); + const [queryRef, { refetch }] = useInteractiveQuery(query, { + context: { phase }, }); - const result: { current: Data | null } = { - current: null, - }; + return ( + <> + + + }> + + + + ); + } - function SuspenseFallback() { - return
Loading...
; - } + function Context({ queryRef }: { queryRef: QueryReference }) { + const { data } = useReadQuery(queryRef); - function Parent() { - const [canonizeResults, setCanonizeResults] = React.useState(false); - const [queryRef] = useInteractiveQuery(query, { - canonizeResults, - }); + return
{data.context.phase}
; + } - return ( - <> - - }> - - - - ); - } + function App() { + return ( + + + + ); + } - function Results({ queryRef }: { queryRef: QueryReference }) { - const { data } = useReadQuery(queryRef); + render(); - result.current = data; + expect(await screen.findByTestId("context")).toHaveTextContent("initial"); - return null; - } + await act(() => user.click(screen.getByText("Update context"))); + await act(() => user.click(screen.getByText("Refetch"))); - function App() { - return ( - - - - ); - } + expect(await screen.findByTestId("context")).toHaveTextContent("rerender"); +}); - render(); +// NOTE: We only test the `false` -> `true` path here. If the option changes +// from `true` -> `false`, the data has already been canonized, so it has no +// effect on the output. +it("returns canonical results immediately when `canonizeResults` changes from `false` to `true` between renders", async () => { + interface Result { + __typename: string; + value: number; + } - function verifyCanonicalResults(data: Data, canonized: boolean) { - const resultSet = new Set(data.results); - const values = Array.from(resultSet).map((item) => item.value); + interface Data { + results: Result[]; + } - expect(data).toEqual({ results }); + const cache = new InMemoryCache({ + typePolicies: { + Result: { + keyFields: false, + }, + }, + }); - if (canonized) { - expect(data.results.length).toBe(6); - expect(resultSet.size).toBe(5); - expect(values).toEqual([0, 1, 2, 3, 5]); - } else { - expect(data.results.length).toBe(6); - expect(resultSet.size).toBe(6); - expect(values).toEqual([0, 1, 1, 2, 3, 5]); + const query: TypedDocumentNode = gql` + query { + results { + value } } + `; - verifyCanonicalResults(result.current!, false); + const results: Result[] = [ + { __typename: "Result", value: 0 }, + { __typename: "Result", value: 1 }, + { __typename: "Result", value: 1 }, + { __typename: "Result", value: 2 }, + { __typename: "Result", value: 3 }, + { __typename: "Result", value: 5 }, + ]; - await act(() => user.click(screen.getByText("Canonize results"))); + const user = userEvent.setup(); - verifyCanonicalResults(result.current!, true); + cache.writeQuery({ + query, + data: { results }, }); - it("applies changed `refetchWritePolicy` to next fetch when changing between renders", async () => { - interface Data { - primes: number[]; - } + const client = new ApolloClient({ + link: new MockLink([]), + cache, + }); - const user = userEvent.setup(); + const result: { current: Data | null } = { + current: null, + }; - const query: TypedDocumentNode = gql` - query GetPrimes($min: number, $max: number) { - primes(min: $min, max: $max) - } - `; + function SuspenseFallback() { + return
Loading...
; + } - 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, - }, - { - request: { query, variables: { min: 30, max: 50 } }, - result: { data: { primes: [31, 37, 41, 43, 47] } }, - delay: 10, - }, - ]; + function Parent() { + const [canonizeResults, setCanonizeResults] = React.useState(false); + const [queryRef] = useInteractiveQuery(query, { + canonizeResults, + }); - const mergeParams: [number[] | undefined, number[]][] = []; + return ( + <> + + }> + + + + ); + } - 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 Results({ queryRef }: { queryRef: QueryReference }) { + const { data } = useReadQuery(queryRef); - const client = new ApolloClient({ - link: new MockLink(mocks), - cache, - }); + result.current = data; - function SuspenseFallback() { - return
Loading...
; - } + return null; + } - function Parent() { - const [refetchWritePolicy, setRefetchWritePolicy] = - React.useState("merge"); + function App() { + return ( + + + + ); + } - const [queryRef, { refetch }] = useInteractiveQuery(query, { - refetchWritePolicy, - variables: { min: 0, max: 12 }, - }); + render(); - return ( - <> - - - - }> - - - - ); - } + function verifyCanonicalResults(data: Data, canonized: boolean) { + const resultSet = new Set(data.results); + const values = Array.from(resultSet).map((item) => item.value); - function Primes({ queryRef }: { queryRef: QueryReference }) { - const { data } = useReadQuery(queryRef); + expect(data).toEqual({ results }); - return {data.primes.join(", ")}; + if (canonized) { + expect(data.results.length).toBe(6); + expect(resultSet.size).toBe(5); + expect(values).toEqual([0, 1, 2, 3, 5]); + } else { + expect(data.results.length).toBe(6); + expect(resultSet.size).toBe(6); + expect(values).toEqual([0, 1, 1, 2, 3, 5]); } + } - function App() { - return ( - - - - ); + verifyCanonicalResults(result.current!, false); + + await act(() => user.click(screen.getByText("Canonize results"))); + + verifyCanonicalResults(result.current!, true); +}); + +it("applies changed `refetchWritePolicy` to next fetch when changing between renders", async () => { + interface Data { + primes: number[]; + } + + const user = userEvent.setup(); + + const query: TypedDocumentNode = gql` + query GetPrimes($min: number, $max: number) { + primes(min: $min, max: $max) } + `; - render(); + 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, + }, + { + request: { query, variables: { min: 30, max: 50 } }, + result: { data: { primes: [31, 37, 41, 43, 47] } }, + delay: 10, + }, + ]; - const primes = await screen.findByTestId("primes"); + const mergeParams: [number[] | undefined, number[]][] = []; - expect(primes).toHaveTextContent("2, 3, 5, 7, 11"); - expect(mergeParams).toEqual([[undefined, [2, 3, 5, 7, 11]]]); + 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; + }, + }, + }, + }, + }, + }); - await act(() => user.click(screen.getByText("Refetch next"))); + const client = new ApolloClient({ + link: new MockLink(mocks), + cache, + }); - await waitFor(() => { - expect(primes).toHaveTextContent("2, 3, 5, 7, 11, 13, 17, 19, 23, 29"); + function SuspenseFallback() { + return
Loading...
; + } + + function Parent() { + const [refetchWritePolicy, setRefetchWritePolicy] = + React.useState("merge"); + + const [queryRef, { refetch }] = useInteractiveQuery(query, { + refetchWritePolicy, + variables: { min: 0, max: 12 }, }); - expect(mergeParams).toEqual([ - [undefined, [2, 3, 5, 7, 11]], - [ - [2, 3, 5, 7, 11], - [13, 17, 19, 23, 29], - ], - ]); + return ( + <> + + + + }> + + + + ); + } + + function Primes({ queryRef }: { queryRef: QueryReference }) { + const { data } = useReadQuery(queryRef); - await act(() => - user.click(screen.getByText("Change refetch write policy")) + return {data.primes.join(", ")}; + } + + function App() { + return ( + + + ); + } - await act(() => user.click(screen.getByText("Refetch last"))); + render(); - await waitFor(() => { - expect(primes).toHaveTextContent("31, 37, 41, 43, 47"); - }); + const primes = await screen.findByTestId("primes"); - expect(mergeParams).toEqual([ - [undefined, [2, 3, 5, 7, 11]], - [ - [2, 3, 5, 7, 11], - [13, 17, 19, 23, 29], - ], - [undefined, [31, 37, 41, 43, 47]], - ]); + expect(primes).toHaveTextContent("2, 3, 5, 7, 11"); + expect(mergeParams).toEqual([[undefined, [2, 3, 5, 7, 11]]]); + + await act(() => user.click(screen.getByText("Refetch next"))); + + await waitFor(() => { + expect(primes).toHaveTextContent("2, 3, 5, 7, 11, 13, 17, 19, 23, 29"); }); - it("applies `returnPartialData` on next fetch when it changes between renders", async () => { - interface Data { - character: { - __typename: "Character"; - id: string; - name: string; - }; - } + expect(mergeParams).toEqual([ + [undefined, [2, 3, 5, 7, 11]], + [ + [2, 3, 5, 7, 11], + [13, 17, 19, 23, 29], + ], + ]); - interface PartialData { - character: { - __typename: "Character"; - id: string; - }; - } + await act(() => user.click(screen.getByText("Change refetch write policy"))); - const user = userEvent.setup(); + await act(() => user.click(screen.getByText("Refetch last"))); - const fullQuery: TypedDocumentNode = gql` - query { - character { - __typename - id - name - } + await waitFor(() => { + expect(primes).toHaveTextContent("31, 37, 41, 43, 47"); + }); + + expect(mergeParams).toEqual([ + [undefined, [2, 3, 5, 7, 11]], + [ + [2, 3, 5, 7, 11], + [13, 17, 19, 23, 29], + ], + [undefined, [31, 37, 41, 43, 47]], + ]); +}); + +it("applies `returnPartialData` on next fetch when it changes between renders", async () => { + interface Data { + character: { + __typename: "Character"; + id: string; + name: string; + }; + } + + interface PartialData { + character: { + __typename: "Character"; + id: string; + }; + } + + const user = userEvent.setup(); + + const fullQuery: TypedDocumentNode = gql` + query { + character { + __typename + id + name } - `; + } + `; - const partialQuery: TypedDocumentNode = gql` - query { - character { - __typename - id - } + const partialQuery: TypedDocumentNode = gql` + query { + character { + __typename + id } - `; + } + `; - const mocks = [ - { - request: { query: fullQuery }, - result: { - data: { - character: { - __typename: "Character", - id: "1", - name: "Doctor Strange", - }, + const mocks = [ + { + request: { query: fullQuery }, + result: { + data: { + character: { + __typename: "Character", + id: "1", + name: "Doctor Strange", }, }, }, - { - request: { query: fullQuery }, - result: { - data: { - character: { - __typename: "Character", - id: "1", - name: "Doctor Strange (refetched)", - }, + }, + { + request: { query: fullQuery }, + result: { + data: { + character: { + __typename: "Character", + id: "1", + name: "Doctor Strange (refetched)", }, }, - delay: 100, }, - ]; + delay: 100, + }, + ]; - const cache = new InMemoryCache(); + const cache = new InMemoryCache(); - cache.writeQuery({ - query: partialQuery, - data: { character: { __typename: "Character", id: "1" } }, - }); + cache.writeQuery({ + query: partialQuery, + data: { character: { __typename: "Character", id: "1" } }, + }); - const client = new ApolloClient({ - link: new MockLink(mocks), - cache, - }); + const client = new ApolloClient({ + link: new MockLink(mocks), + cache, + }); - function SuspenseFallback() { - return
Loading...
; - } + function SuspenseFallback() { + return
Loading...
; + } - function Parent() { - const [returnPartialData, setReturnPartialData] = React.useState(false); + function Parent() { + const [returnPartialData, setReturnPartialData] = React.useState(false); - const [queryRef] = useInteractiveQuery(fullQuery, { - returnPartialData, - }); + const [queryRef] = useInteractiveQuery(fullQuery, { + returnPartialData, + }); - return ( - <> - - }> - - - - ); - } + return ( + <> + + }> + + + + ); + } - function Character({ queryRef }: { queryRef: QueryReference }) { - const { data } = useReadQuery(queryRef); + function Character({ queryRef }: { queryRef: QueryReference }) { + const { data } = useReadQuery(queryRef); - return ( - {data.character.name ?? "unknown"} - ); - } + return ( + {data.character.name ?? "unknown"} + ); + } - function App() { - return ( - - - - ); - } + function App() { + return ( + + + + ); + } - render(); + render(); - const character = await screen.findByTestId("character"); + const character = await screen.findByTestId("character"); - expect(character).toHaveTextContent("Doctor Strange"); + expect(character).toHaveTextContent("Doctor Strange"); - await act(() => user.click(screen.getByText("Update partial data"))); + await act(() => user.click(screen.getByText("Update partial data"))); - cache.modify({ - id: cache.identify({ __typename: "Character", id: "1" }), - fields: { - name: (_, { DELETE }) => DELETE, - }, - }); + cache.modify({ + id: cache.identify({ __typename: "Character", id: "1" }), + fields: { + name: (_, { DELETE }) => DELETE, + }, + }); - await waitFor(() => { - expect(character).toHaveTextContent("unknown"); - }); + await waitFor(() => { + expect(character).toHaveTextContent("unknown"); + }); - await waitFor(() => { - expect(character).toHaveTextContent("Doctor Strange (refetched)"); - }); + await waitFor(() => { + expect(character).toHaveTextContent("Doctor Strange (refetched)"); }); +}); - it("applies updated `fetchPolicy` on next fetch when it changes between renders", async () => { - interface Data { - character: { - __typename: "Character"; - id: string; - name: string; - }; - } +it("applies updated `fetchPolicy` on next fetch when it changes between renders", async () => { + interface Data { + character: { + __typename: "Character"; + id: string; + name: string; + }; + } - const user = userEvent.setup(); + const user = userEvent.setup(); - const query: TypedDocumentNode = gql` - query { - character { - __typename - id - name - } + const query: TypedDocumentNode = gql` + query { + character { + __typename + id + name } - `; + } + `; - const mocks = [ - { - request: { query }, - result: { - data: { - character: { - __typename: "Character", - id: "1", - name: "Doctor Strange", - }, + const mocks = [ + { + request: { query }, + result: { + data: { + character: { + __typename: "Character", + id: "1", + name: "Doctor Strange", }, }, - delay: 10, }, - ]; + delay: 10, + }, + ]; - const cache = new InMemoryCache(); + const cache = new InMemoryCache(); - cache.writeQuery({ - query, - data: { - character: { - __typename: "Character", - id: "1", - name: "Doctor Strangecache", - }, + cache.writeQuery({ + query, + data: { + character: { + __typename: "Character", + id: "1", + name: "Doctor Strangecache", }, - }); + }, + }); - const client = new ApolloClient({ - link: new MockLink(mocks), - cache, + const client = new ApolloClient({ + link: new MockLink(mocks), + cache, + }); + + function SuspenseFallback() { + return
Loading...
; + } + + function Parent() { + const [fetchPolicy, setFetchPolicy] = + React.useState("cache-first"); + + const [queryRef, { refetch }] = useInteractiveQuery(query, { + fetchPolicy, }); - function SuspenseFallback() { - return
Loading...
; - } + return ( + <> + + + }> + + + + ); + } - function Parent() { - const [fetchPolicy, setFetchPolicy] = - React.useState("cache-first"); + function Character({ queryRef }: { queryRef: QueryReference }) { + const { data } = useReadQuery(queryRef); - const [queryRef, { refetch }] = useInteractiveQuery(query, { - fetchPolicy, - }); + return {data.character.name}; + } - return ( - <> - - - }> - - - - ); - } + function App() { + return ( + + + + ); + } - function Character({ queryRef }: { queryRef: QueryReference }) { - const { data } = useReadQuery(queryRef); + render(); - return {data.character.name}; - } + const character = await screen.findByTestId("character"); - function App() { - return ( - - - - ); - } + expect(character).toHaveTextContent("Doctor Strangecache"); - render(); + await act(() => user.click(screen.getByText("Change fetch policy"))); + await act(() => user.click(screen.getByText("Refetch"))); + await waitFor(() => { + expect(character).toHaveTextContent("Doctor Strange"); + }); - const character = await screen.findByTestId("character"); + // Because we switched to a `no-cache` fetch policy, we should not see the + // newly fetched data in the cache after the fetch occured. + expect(cache.readQuery({ query })).toEqual({ + character: { + __typename: "Character", + id: "1", + name: "Doctor Strangecache", + }, + }); +}); - expect(character).toHaveTextContent("Doctor Strangecache"); +it("properly handles changing options along with changing `variables`", async () => { + interface Data { + character: { + __typename: "Character"; + id: string; + name: string; + }; + } - await act(() => user.click(screen.getByText("Change fetch policy"))); - await act(() => user.click(screen.getByText("Refetch"))); - await waitFor(() => { - expect(character).toHaveTextContent("Doctor Strange"); - }); + const user = userEvent.setup(); + const query: TypedDocumentNode = gql` + query ($id: ID!) { + character(id: $id) { + __typename + id + name + } + } + `; + + const mocks = [ + { + request: { query, variables: { id: "1" } }, + result: { + errors: [new GraphQLError("oops")], + }, + delay: 10, + }, + { + request: { query, variables: { id: "2" } }, + result: { + data: { + character: { + __typename: "Character", + id: "2", + name: "Hulk", + }, + }, + }, + delay: 10, + }, + ]; + + const cache = new InMemoryCache(); - // Because we switched to a `no-cache` fetch policy, we should not see the - // newly fetched data in the cache after the fetch occured. - expect(cache.readQuery({ query })).toEqual({ + cache.writeQuery({ + query, + variables: { + id: "1", + }, + data: { character: { __typename: "Character", id: "1", name: "Doctor Strangecache", }, + }, + }); + + const client = new ApolloClient({ + link: new MockLink(mocks), + cache, + }); + + function SuspenseFallback() { + return
Loading...
; + } + + function Parent() { + const [id, setId] = React.useState("1"); + + const [queryRef, { refetch }] = useInteractiveQuery(query, { + errorPolicy: id === "1" ? "all" : "none", + variables: { id }, }); + + return ( + <> + + + + Error boundary}> + }> + + + + + ); + } + + function Character({ queryRef }: { queryRef: QueryReference }) { + const { data, error } = useReadQuery(queryRef); + + return error ? ( +
{error.message}
+ ) : ( + {data.character.name} + ); + } + + function App() { + return ( + + + + ); + } + + render(); + + const character = await screen.findByTestId("character"); + + expect(character).toHaveTextContent("Doctor Strangecache"); + + await act(() => user.click(screen.getByText("Get second character"))); + + await waitFor(() => { + expect(character).toHaveTextContent("Hulk"); }); - it("properly handles changing options along with changing `variables`", async () => { - interface Data { + await act(() => user.click(screen.getByText("Get first character"))); + + await waitFor(() => { + expect(character).toHaveTextContent("Doctor Strangecache"); + }); + + await act(() => user.click(screen.getByText("Refetch"))); + + // Ensure we render the inline error instead of the error boundary, which + // tells us the error policy was properly applied. + expect(await screen.findByTestId("error")).toHaveTextContent("oops"); +}); + +describe("refetch", () => { + it("re-suspends when calling `refetch`", async () => { + const { renders } = renderVariablesIntegrationTest({ + variables: { id: "1" }, + }); + + expect(renders.suspenseCount).toBe(1); + expect(screen.getByText("loading")).toBeInTheDocument(); + + expect(await screen.findByText("1 - Spider-Man")).toBeInTheDocument(); + + const button = screen.getByText("Refetch"); + const user = userEvent.setup(); + await act(() => user.click(button)); + + // parent component re-suspends + expect(renders.suspenseCount).toBe(2); + expect(renders.count).toBe(2); + + expect( + await screen.findByText("1 - Spider-Man (updated)") + ).toBeInTheDocument(); + }); + it("re-suspends when calling `refetch` with new variables", async () => { + interface QueryData { character: { - __typename: "Character"; id: string; name: string; }; } - const user = userEvent.setup(); - const query: TypedDocumentNode = gql` - query ($id: ID!) { + interface QueryVariables { + id: string; + } + const query: TypedDocumentNode = gql` + query CharacterQuery($id: ID!) { character(id: $id) { - __typename id name } @@ -2213,2350 +2364,2181 @@ describe("useInteractiveQuery", () => { { request: { query, variables: { id: "1" } }, result: { - errors: [new GraphQLError("oops")], + data: { character: { id: "1", name: "Captain Marvel" } }, }, - delay: 10, }, { request: { query, variables: { id: "2" } }, result: { - data: { - character: { - __typename: "Character", - id: "2", - name: "Hulk", - }, - }, + data: { character: { id: "2", name: "Captain America" } }, }, - delay: 10, }, ]; - const cache = new InMemoryCache(); - - cache.writeQuery({ - query, - variables: { - id: "1", - }, - data: { - character: { - __typename: "Character", - id: "1", - name: "Doctor Strangecache", - }, - }, + const { renders } = renderVariablesIntegrationTest({ + variables: { id: "1" }, + mocks, }); - const client = new ApolloClient({ - link: new MockLink(mocks), - cache, - }); + expect(renders.suspenseCount).toBe(1); + expect(screen.getByText("loading")).toBeInTheDocument(); - function SuspenseFallback() { - return
Loading...
; - } + expect(await screen.findByText("1 - Captain Marvel")).toBeInTheDocument(); - function Parent() { - const [id, setId] = React.useState("1"); + const newVariablesRefetchButton = screen.getByText( + "Set variables to id: 2" + ); + const refetchButton = screen.getByText("Refetch"); + const user = userEvent.setup(); + await act(() => user.click(newVariablesRefetchButton)); + await act(() => user.click(refetchButton)); - const [queryRef, { refetch }] = useInteractiveQuery(query, { - errorPolicy: id === "1" ? "all" : "none", - variables: { id }, - }); - - return ( - <> - - - - Error boundary} - > - }> - - - - - ); - } - - function Character({ queryRef }: { queryRef: QueryReference }) { - const { data, error } = useReadQuery(queryRef); + expect(await screen.findByText("2 - Captain America")).toBeInTheDocument(); - return error ? ( -
{error.message}
- ) : ( - {data.character.name} - ); - } - - function App() { - return ( - - - - ); - } + // parent component re-suspends + expect(renders.suspenseCount).toBe(2); + expect(renders.count).toBe(3); - render(); + // extra render puts an additional frame into the array + expect(renders.frames).toMatchObject([ + { + ...mocks[0].result, + networkStatus: NetworkStatus.ready, + error: undefined, + }, + { + ...mocks[0].result, + networkStatus: NetworkStatus.ready, + error: undefined, + }, + { + ...mocks[1].result, + networkStatus: NetworkStatus.ready, + error: undefined, + }, + ]); + }); + it("re-suspends multiple times when calling `refetch` multiple times", async () => { + const { renders } = renderVariablesIntegrationTest({ + variables: { id: "1" }, + }); - const character = await screen.findByTestId("character"); + expect(renders.suspenseCount).toBe(1); + expect(screen.getByText("loading")).toBeInTheDocument(); - expect(character).toHaveTextContent("Doctor Strangecache"); + expect(await screen.findByText("1 - Spider-Man")).toBeInTheDocument(); - await act(() => user.click(screen.getByText("Get second character"))); + const button = screen.getByText("Refetch"); + const user = userEvent.setup(); + await act(() => user.click(button)); - await waitFor(() => { - expect(character).toHaveTextContent("Hulk"); - }); + // parent component re-suspends + expect(renders.suspenseCount).toBe(2); + expect(renders.count).toBe(2); - await act(() => user.click(screen.getByText("Get first character"))); + expect( + await screen.findByText("1 - Spider-Man (updated)") + ).toBeInTheDocument(); - await waitFor(() => { - expect(character).toHaveTextContent("Doctor Strangecache"); - }); + await act(() => user.click(button)); - await act(() => user.click(screen.getByText("Refetch"))); + // parent component re-suspends + expect(renders.suspenseCount).toBe(3); + expect(renders.count).toBe(3); - // Ensure we render the inline error instead of the error boundary, which - // tells us the error policy was properly applied. - expect(await screen.findByTestId("error")).toHaveTextContent("oops"); + expect( + await screen.findByText("1 - Spider-Man (updated again)") + ).toBeInTheDocument(); }); + it("throws errors when errors are returned after calling `refetch`", async () => { + const consoleSpy = jest.spyOn(console, "error").mockImplementation(); + interface QueryData { + character: { + id: string; + name: string; + }; + } - describe("refetch", () => { - it("re-suspends when calling `refetch`", async () => { - const { renders } = renderVariablesIntegrationTest({ - variables: { id: "1" }, - }); - - expect(renders.suspenseCount).toBe(1); - expect(screen.getByText("loading")).toBeInTheDocument(); + interface QueryVariables { + id: string; + } + const query: TypedDocumentNode = gql` + query CharacterQuery($id: ID!) { + character(id: $id) { + id + name + } + } + `; + const mocks = [ + { + request: { query, variables: { id: "1" } }, + result: { + data: { character: { id: "1", name: "Captain Marvel" } }, + }, + }, + { + request: { query, variables: { id: "1" } }, + result: { + errors: [new GraphQLError("Something went wrong")], + }, + }, + ]; + const { renders } = renderVariablesIntegrationTest({ + variables: { id: "1" }, + mocks, + }); - expect(await screen.findByText("1 - Spider-Man")).toBeInTheDocument(); + expect(renders.suspenseCount).toBe(1); + expect(screen.getByText("loading")).toBeInTheDocument(); - const button = screen.getByText("Refetch"); - const user = userEvent.setup(); - await act(() => user.click(button)); + expect(await screen.findByText("1 - Captain Marvel")).toBeInTheDocument(); - // parent component re-suspends - expect(renders.suspenseCount).toBe(2); - expect(renders.count).toBe(2); + const button = screen.getByText("Refetch"); + const user = userEvent.setup(); + await act(() => user.click(button)); - expect( - await screen.findByText("1 - Spider-Man (updated)") - ).toBeInTheDocument(); + await waitFor(() => { + expect(renders.errorCount).toBe(1); }); - it("re-suspends when calling `refetch` with new variables", async () => { - interface QueryData { - character: { - id: string; - name: string; - }; - } - interface QueryVariables { + expect(renders.errors).toEqual([ + new ApolloError({ + graphQLErrors: [new GraphQLError("Something went wrong")], + }), + ]); + + consoleSpy.mockRestore(); + }); + it('ignores errors returned after calling `refetch` when errorPolicy is set to "ignore"', async () => { + interface QueryData { + character: { id: string; - } - const query: TypedDocumentNode = gql` - query CharacterQuery($id: ID!) { - character(id: $id) { - id - name - } - } - `; + name: string; + }; + } - const mocks = [ - { - request: { query, variables: { id: "1" } }, - result: { - data: { character: { id: "1", name: "Captain Marvel" } }, - }, + interface QueryVariables { + id: string; + } + const query: TypedDocumentNode = gql` + query CharacterQuery($id: ID!) { + character(id: $id) { + id + name + } + } + `; + const mocks = [ + { + request: { query, variables: { id: "1" } }, + result: { + data: { character: { id: "1", name: "Captain Marvel" } }, }, - { - request: { query, variables: { id: "2" } }, - result: { - data: { character: { id: "2", name: "Captain America" } }, - }, + }, + { + request: { query, variables: { id: "1" } }, + result: { + errors: [new GraphQLError("Something went wrong")], }, - ]; - - const { renders } = renderVariablesIntegrationTest({ - variables: { id: "1" }, - mocks, - }); - - expect(renders.suspenseCount).toBe(1); - expect(screen.getByText("loading")).toBeInTheDocument(); - - expect(await screen.findByText("1 - Captain Marvel")).toBeInTheDocument(); + }, + ]; - const newVariablesRefetchButton = screen.getByText( - "Set variables to id: 2" - ); - const refetchButton = screen.getByText("Refetch"); - const user = userEvent.setup(); - await act(() => user.click(newVariablesRefetchButton)); - await act(() => user.click(refetchButton)); - - expect( - await screen.findByText("2 - Captain America") - ).toBeInTheDocument(); - - // parent component re-suspends - expect(renders.suspenseCount).toBe(2); - expect(renders.count).toBe(3); - - // extra render puts an additional frame into the array - expect(renders.frames).toMatchObject([ - { - ...mocks[0].result, - networkStatus: NetworkStatus.ready, - error: undefined, - }, - { - ...mocks[0].result, - networkStatus: NetworkStatus.ready, - error: undefined, - }, - { - ...mocks[1].result, - networkStatus: NetworkStatus.ready, - error: undefined, - }, - ]); + const { renders } = renderVariablesIntegrationTest({ + variables: { id: "1" }, + errorPolicy: "ignore", + mocks, }); - it("re-suspends multiple times when calling `refetch` multiple times", async () => { - const { renders } = renderVariablesIntegrationTest({ - variables: { id: "1" }, - }); - expect(renders.suspenseCount).toBe(1); - expect(screen.getByText("loading")).toBeInTheDocument(); + expect(await screen.findByText("1 - Captain Marvel")).toBeInTheDocument(); - expect(await screen.findByText("1 - Spider-Man")).toBeInTheDocument(); + const button = screen.getByText("Refetch"); + const user = userEvent.setup(); + await act(() => user.click(button)); - const button = screen.getByText("Refetch"); - const user = userEvent.setup(); - await act(() => user.click(button)); + expect(renders.errorCount).toBe(0); + expect(renders.errors).toEqual([]); + }); + it('returns errors after calling `refetch` when errorPolicy is set to "all"', async () => { + interface QueryData { + character: { + id: string; + name: string; + }; + } - // parent component re-suspends - expect(renders.suspenseCount).toBe(2); - expect(renders.count).toBe(2); + interface QueryVariables { + id: string; + } + const query: TypedDocumentNode = gql` + query CharacterQuery($id: ID!) { + character(id: $id) { + id + name + } + } + `; + const mocks = [ + { + request: { query, variables: { id: "1" } }, + result: { + data: { character: { id: "1", name: "Captain Marvel" } }, + }, + }, + { + request: { query, variables: { id: "1" } }, + result: { + errors: [new GraphQLError("Something went wrong")], + }, + }, + ]; - expect( - await screen.findByText("1 - Spider-Man (updated)") - ).toBeInTheDocument(); + const { renders } = renderVariablesIntegrationTest({ + variables: { id: "1" }, + errorPolicy: "all", + mocks, + }); - await act(() => user.click(button)); + expect(await screen.findByText("1 - Captain Marvel")).toBeInTheDocument(); - // parent component re-suspends - expect(renders.suspenseCount).toBe(3); - expect(renders.count).toBe(3); + const button = screen.getByText("Refetch"); + const user = userEvent.setup(); + await act(() => user.click(button)); - expect( - await screen.findByText("1 - Spider-Man (updated again)") - ).toBeInTheDocument(); - }); - it("throws errors when errors are returned after calling `refetch`", async () => { - const consoleSpy = jest.spyOn(console, "error").mockImplementation(); - interface QueryData { - character: { - id: string; - name: string; - }; - } + expect(renders.errorCount).toBe(0); + expect(renders.errors).toEqual([]); - interface QueryVariables { + expect(await screen.findByText("Something went wrong")).toBeInTheDocument(); + }); + it('handles partial data results after calling `refetch` when errorPolicy is set to "all"', async () => { + interface QueryData { + character: { id: string; - } - const query: TypedDocumentNode = gql` - query CharacterQuery($id: ID!) { - character(id: $id) { - id - name - } + name: string; + }; + } + + interface QueryVariables { + id: string; + } + const query: TypedDocumentNode = gql` + query CharacterQuery($id: ID!) { + character(id: $id) { + id + name } - `; - const mocks = [ - { - request: { query, variables: { id: "1" } }, - result: { - data: { character: { id: "1", name: "Captain Marvel" } }, - }, + } + `; + const mocks = [ + { + request: { query, variables: { id: "1" } }, + result: { + data: { character: { id: "1", name: "Captain Marvel" } }, }, - { - request: { query, variables: { id: "1" } }, - result: { - errors: [new GraphQLError("Something went wrong")], - }, + }, + { + request: { query, variables: { id: "1" } }, + result: { + data: { character: { id: "1", name: null } }, + errors: [new GraphQLError("Something went wrong")], }, - ]; - const { renders } = renderVariablesIntegrationTest({ - variables: { id: "1" }, - mocks, - }); + }, + ]; - expect(renders.suspenseCount).toBe(1); - expect(screen.getByText("loading")).toBeInTheDocument(); + const { renders } = renderVariablesIntegrationTest({ + variables: { id: "1" }, + errorPolicy: "all", + mocks, + }); - expect(await screen.findByText("1 - Captain Marvel")).toBeInTheDocument(); + expect(await screen.findByText("1 - Captain Marvel")).toBeInTheDocument(); - const button = screen.getByText("Refetch"); - const user = userEvent.setup(); - await act(() => user.click(button)); + const button = screen.getByText("Refetch"); + const user = userEvent.setup(); + await act(() => user.click(button)); - await waitFor(() => { - expect(renders.errorCount).toBe(1); - }); + expect(renders.errorCount).toBe(0); + expect(renders.errors).toEqual([]); - expect(renders.errors).toEqual([ - new ApolloError({ - graphQLErrors: [new GraphQLError("Something went wrong")], - }), - ]); + expect(await screen.findByText("Something went wrong")).toBeInTheDocument(); - consoleSpy.mockRestore(); + const expectedError = new ApolloError({ + graphQLErrors: [new GraphQLError("Something went wrong")], }); - it('ignores errors returned after calling `refetch` when errorPolicy is set to "ignore"', async () => { - interface QueryData { - character: { - id: string; - name: string; - }; - } - interface QueryVariables { + expect(renders.frames).toMatchObject([ + { + ...mocks[0].result, + networkStatus: NetworkStatus.ready, + error: undefined, + }, + { + data: mocks[1].result.data, + networkStatus: NetworkStatus.error, + error: expectedError, + }, + ]); + }); + it("`refetch` works with startTransition to allow React to show stale UI until finished suspending", async () => { + type Variables = { + id: string; + }; + + interface Data { + todo: { id: string; - } - const query: TypedDocumentNode = gql` - query CharacterQuery($id: ID!) { - character(id: $id) { - id - name - } + name: string; + completed: boolean; + }; + } + const user = userEvent.setup(); + + const query: TypedDocumentNode = gql` + query TodoItemQuery($id: ID!) { + todo(id: $id) { + id + name + completed } - `; - const mocks = [ - { - request: { query, variables: { id: "1" } }, - result: { - data: { character: { id: "1", name: "Captain Marvel" } }, - }, + } + `; + + const mocks: MockedResponse[] = [ + { + request: { query, variables: { id: "1" } }, + result: { + data: { todo: { id: "1", name: "Clean room", completed: false } }, }, - { - request: { query, variables: { id: "1" } }, - result: { - errors: [new GraphQLError("Something went wrong")], - }, + delay: 10, + }, + { + request: { query, variables: { id: "1" } }, + result: { + data: { todo: { id: "1", name: "Clean room", completed: true } }, }, - ]; - - const { renders } = renderVariablesIntegrationTest({ - variables: { id: "1" }, - errorPolicy: "ignore", - mocks, - }); - - expect(await screen.findByText("1 - Captain Marvel")).toBeInTheDocument(); - - const button = screen.getByText("Refetch"); - const user = userEvent.setup(); - await act(() => user.click(button)); + delay: 10, + }, + ]; - expect(renders.errorCount).toBe(0); - expect(renders.errors).toEqual([]); + const client = new ApolloClient({ + link: new MockLink(mocks), + cache: new InMemoryCache(), }); - it('returns errors after calling `refetch` when errorPolicy is set to "all"', async () => { - interface QueryData { - character: { - id: string; - name: string; - }; - } - interface QueryVariables { - id: string; - } - const query: TypedDocumentNode = gql` - query CharacterQuery($id: ID!) { - character(id: $id) { - id - name - } - } - `; - const mocks = [ - { - request: { query, variables: { id: "1" } }, - result: { - data: { character: { id: "1", name: "Captain Marvel" } }, - }, - }, - { - request: { query, variables: { id: "1" } }, - result: { - errors: [new GraphQLError("Something went wrong")], - }, - }, - ]; + function App() { + return ( + + }> + + + + ); + } - const { renders } = renderVariablesIntegrationTest({ - variables: { id: "1" }, - errorPolicy: "all", - mocks, + function SuspenseFallback() { + return

Loading

; + } + + function Parent() { + const [id, setId] = React.useState("1"); + const [queryRef, { refetch }] = useInteractiveQuery(query, { + variables: { id }, }); + return ; + } - expect(await screen.findByText("1 - Captain Marvel")).toBeInTheDocument(); + function Todo({ + queryRef, + refetch, + }: { + refetch: RefetchFunction; + queryRef: QueryReference; + onChange: (id: string) => void; + }) { + const { data } = useReadQuery(queryRef); + const [isPending, startTransition] = React.useTransition(); + const { todo } = data; - const button = screen.getByText("Refetch"); - const user = userEvent.setup(); - await act(() => user.click(button)); + return ( + <> + +
+ {todo.name} + {todo.completed && " (completed)"} +
+ + ); + } - expect(renders.errorCount).toBe(0); - expect(renders.errors).toEqual([]); + render(); - expect( - await screen.findByText("Something went wrong") - ).toBeInTheDocument(); - }); - it('handles partial data results after calling `refetch` when errorPolicy is set to "all"', async () => { - interface QueryData { - character: { - id: string; - name: string; - }; - } + expect(screen.getByText("Loading")).toBeInTheDocument(); - interface QueryVariables { - id: string; - } - const query: TypedDocumentNode = gql` - query CharacterQuery($id: ID!) { - character(id: $id) { - id - name - } - } - `; - const mocks = [ - { - request: { query, variables: { id: "1" } }, - result: { - data: { character: { id: "1", name: "Captain Marvel" } }, - }, - }, - { - request: { query, variables: { id: "1" } }, - result: { - data: { character: { id: "1", name: null } }, - errors: [new GraphQLError("Something went wrong")], - }, - }, - ]; + expect(await screen.findByTestId("todo")).toBeInTheDocument(); - const { renders } = renderVariablesIntegrationTest({ - variables: { id: "1" }, - errorPolicy: "all", - mocks, - }); + const todo = screen.getByTestId("todo"); + const button = screen.getByText("Refresh"); - expect(await screen.findByText("1 - Captain Marvel")).toBeInTheDocument(); + expect(todo).toHaveTextContent("Clean room"); - const button = screen.getByText("Refetch"); - const user = userEvent.setup(); - await act(() => user.click(button)); + await act(() => user.click(button)); - expect(renders.errorCount).toBe(0); - expect(renders.errors).toEqual([]); + // 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(); - expect( - await screen.findByText("Something went wrong") - ).toBeInTheDocument(); + // We can ensure this works with isPending from useTransition in the process + expect(todo).toHaveAttribute("aria-busy", "true"); - const expectedError = new ApolloError({ - graphQLErrors: [new GraphQLError("Something went wrong")], - }); + // Ensure we are showing the stale UI until the new todo has loaded + expect(todo).toHaveTextContent("Clean room"); - expect(renders.frames).toMatchObject([ - { - ...mocks[0].result, - networkStatus: NetworkStatus.ready, - error: undefined, - }, - { - data: mocks[1].result.data, - networkStatus: NetworkStatus.error, - error: expectedError, - }, - ]); + // Eventually we should see the updated todo content once its done + // suspending. + await waitFor(() => { + expect(todo).toHaveTextContent("Clean room (completed)"); }); - it("`refetch` works with startTransition to allow React to show stale UI until finished suspending", async () => { - type Variables = { - id: string; - }; - - interface Data { - todo: { - id: string; - name: string; - completed: boolean; - }; - } - const user = userEvent.setup(); - - const query: TypedDocumentNode = gql` - query TodoItemQuery($id: ID!) { - todo(id: $id) { - id - name - completed - } - } - `; - - const mocks: MockedResponse[] = [ - { - request: { query, variables: { id: "1" } }, - result: { - data: { todo: { id: "1", name: "Clean room", completed: false } }, - }, - delay: 10, - }, - { - request: { query, variables: { id: "1" } }, - result: { - data: { todo: { id: "1", name: "Clean room", completed: true } }, - }, - delay: 10, - }, - ]; + }); +}); - const client = new ApolloClient({ - link: new MockLink(mocks), - cache: new InMemoryCache(), - }); +describe("fetchMore", () => { + function getItemTexts() { + return screen.getAllByTestId(/letter/).map( + // eslint-disable-next-line testing-library/no-node-access + (li) => li.firstChild!.textContent + ); + } + it("re-suspends when calling `fetchMore` with different variables", async () => { + const { renders } = renderPaginatedIntegrationTest(); - function App() { - return ( - - }> - - - - ); - } + expect(renders.suspenseCount).toBe(1); + expect(screen.getByText("loading")).toBeInTheDocument(); - function SuspenseFallback() { - return

Loading

; - } + const items = await screen.findAllByTestId(/letter/i); + expect(items).toHaveLength(2); + expect(getItemTexts()).toStrictEqual(["A", "B"]); - function Parent() { - const [id, setId] = React.useState("1"); - const [queryRef, { refetch }] = useInteractiveQuery(query, { - variables: { id }, - }); - return ; - } + const button = screen.getByText("Fetch more"); + const user = userEvent.setup(); + await act(() => user.click(button)); - function Todo({ - queryRef, - refetch, - }: { - refetch: RefetchFunction; - queryRef: QueryReference; - onChange: (id: string) => void; - }) { - const { data } = useReadQuery(queryRef); - const [isPending, startTransition] = React.useTransition(); - const { todo } = data; + // parent component re-suspends + expect(renders.suspenseCount).toBe(2); + await waitFor(() => { + expect(renders.count).toBe(2); + }); - return ( - <> - -
- {todo.name} - {todo.completed && " (completed)"} -
- - ); - } + expect(getItemTexts()).toStrictEqual(["C", "D"]); + }); + it("properly uses `updateQuery` when calling `fetchMore`", async () => { + const { renders } = renderPaginatedIntegrationTest({ + updateQuery: true, + }); - render(); + expect(renders.suspenseCount).toBe(1); + expect(screen.getByText("loading")).toBeInTheDocument(); - expect(screen.getByText("Loading")).toBeInTheDocument(); + const items = await screen.findAllByTestId(/letter/i); - expect(await screen.findByTestId("todo")).toBeInTheDocument(); + expect(items).toHaveLength(2); + expect(getItemTexts()).toStrictEqual(["A", "B"]); - const todo = screen.getByTestId("todo"); - const button = screen.getByText("Refresh"); + const button = screen.getByText("Fetch more"); + const user = userEvent.setup(); + await act(() => user.click(button)); - expect(todo).toHaveTextContent("Clean room"); + // parent component re-suspends + expect(renders.suspenseCount).toBe(2); + await waitFor(() => { + expect(renders.count).toBe(2); + }); - await act(() => user.click(button)); + const moreItems = await screen.findAllByTestId(/letter/i); + expect(moreItems).toHaveLength(4); + expect(getItemTexts()).toStrictEqual(["A", "B", "C", "D"]); + }); + it("properly uses cache field policies when calling `fetchMore` without `updateQuery`", async () => { + const { renders } = renderPaginatedIntegrationTest({ + fieldPolicies: true, + }); + expect(renders.suspenseCount).toBe(1); + expect(screen.getByText("loading")).toBeInTheDocument(); - // 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(); + const items = await screen.findAllByTestId(/letter/i); - // We can ensure this works with isPending from useTransition in the process - expect(todo).toHaveAttribute("aria-busy", "true"); + expect(items).toHaveLength(2); + expect(getItemTexts()).toStrictEqual(["A", "B"]); - // Ensure we are showing the stale UI until the new todo has loaded - expect(todo).toHaveTextContent("Clean room"); + const button = screen.getByText("Fetch more"); + const user = userEvent.setup(); + await act(() => user.click(button)); - // Eventually we should see the updated todo content once its done - // suspending. - await waitFor(() => { - expect(todo).toHaveTextContent("Clean room (completed)"); - }); + // parent component re-suspends + expect(renders.suspenseCount).toBe(2); + await waitFor(() => { + expect(renders.count).toBe(2); }); + + const moreItems = await screen.findAllByTestId(/letter/i); + expect(moreItems).toHaveLength(4); + expect(getItemTexts()).toStrictEqual(["A", "B", "C", "D"]); }); + it("`fetchMore` works with startTransition to allow React to show stale UI until finished suspending", async () => { + type Variables = { + offset: number; + }; - describe("fetchMore", () => { - function getItemTexts() { - return screen.getAllByTestId(/letter/).map( - // eslint-disable-next-line testing-library/no-node-access - (li) => li.firstChild!.textContent - ); + interface Todo { + __typename: "Todo"; + id: string; + name: string; + completed: boolean; } - it("re-suspends when calling `fetchMore` with different variables", async () => { - const { renders } = renderPaginatedIntegrationTest(); + interface Data { + todos: Todo[]; + } + const user = userEvent.setup(); - expect(renders.suspenseCount).toBe(1); - expect(screen.getByText("loading")).toBeInTheDocument(); + const query: TypedDocumentNode = gql` + query TodosQuery($offset: Int!) { + todos(offset: $offset) { + id + name + completed + } + } + `; - const items = await screen.findAllByTestId(/letter/i); - expect(items).toHaveLength(2); - expect(getItemTexts()).toStrictEqual(["A", "B"]); + const mocks: MockedResponse[] = [ + { + request: { query, variables: { offset: 0 } }, + result: { + data: { + todos: [ + { + __typename: "Todo", + id: "1", + name: "Clean room", + completed: false, + }, + ], + }, + }, + delay: 10, + }, + { + request: { query, variables: { offset: 1 } }, + result: { + data: { + todos: [ + { + __typename: "Todo", + id: "2", + name: "Take out trash", + completed: true, + }, + ], + }, + }, + delay: 10, + }, + ]; - const button = screen.getByText("Fetch more"); - const user = userEvent.setup(); - await act(() => user.click(button)); + const client = new ApolloClient({ + link: new MockLink(mocks), + cache: new InMemoryCache({ + typePolicies: { + Query: { + fields: { + todos: offsetLimitPagination(), + }, + }, + }, + }), + }); - // parent component re-suspends - expect(renders.suspenseCount).toBe(2); - await waitFor(() => { - expect(renders.count).toBe(2); - }); + function App() { + return ( + + }> + + + + ); + } - expect(getItemTexts()).toStrictEqual(["C", "D"]); - }); - it("properly uses `updateQuery` when calling `fetchMore`", async () => { - const { renders } = renderPaginatedIntegrationTest({ - updateQuery: true, - }); + function SuspenseFallback() { + return

Loading

; + } - expect(renders.suspenseCount).toBe(1); - expect(screen.getByText("loading")).toBeInTheDocument(); + function Parent() { + const [queryRef, { fetchMore }] = useInteractiveQuery(query, { + variables: { offset: 0 }, + }); + return ; + } - const items = await screen.findAllByTestId(/letter/i); + function Todo({ + queryRef, + fetchMore, + }: { + fetchMore: FetchMoreFunction; + queryRef: QueryReference; + }) { + const { data } = useReadQuery(queryRef); + const [isPending, startTransition] = React.useTransition(); + const { todos } = data; - expect(items).toHaveLength(2); - expect(getItemTexts()).toStrictEqual(["A", "B"]); + return ( + <> + +
+ {todos.map((todo) => ( +
+ {todo.name} + {todo.completed && " (completed)"} +
+ ))} +
+ + ); + } - const button = screen.getByText("Fetch more"); - const user = userEvent.setup(); - await act(() => user.click(button)); + render(); - // parent component re-suspends - expect(renders.suspenseCount).toBe(2); - await waitFor(() => { - expect(renders.count).toBe(2); - }); + expect(screen.getByText("Loading")).toBeInTheDocument(); - const moreItems = await screen.findAllByTestId(/letter/i); - expect(moreItems).toHaveLength(4); - expect(getItemTexts()).toStrictEqual(["A", "B", "C", "D"]); - }); - it("properly uses cache field policies when calling `fetchMore` without `updateQuery`", async () => { - const { renders } = renderPaginatedIntegrationTest({ - fieldPolicies: true, - }); - expect(renders.suspenseCount).toBe(1); - expect(screen.getByText("loading")).toBeInTheDocument(); + expect(await screen.findByTestId("todos")).toBeInTheDocument(); - const items = await screen.findAllByTestId(/letter/i); + const todos = screen.getByTestId("todos"); + const todo1 = screen.getByTestId("todo:1"); + const button = screen.getByText("Load more"); - expect(items).toHaveLength(2); - expect(getItemTexts()).toStrictEqual(["A", "B"]); + expect(todo1).toBeInTheDocument(); - const button = screen.getByText("Fetch more"); - const user = userEvent.setup(); - await act(() => user.click(button)); + await act(() => user.click(button)); - // parent component re-suspends - expect(renders.suspenseCount).toBe(2); - await waitFor(() => { - expect(renders.count).toBe(2); - }); + // 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(); - const moreItems = await screen.findAllByTestId(/letter/i); - expect(moreItems).toHaveLength(4); - expect(getItemTexts()).toStrictEqual(["A", "B", "C", "D"]); - }); - it("`fetchMore` works with startTransition to allow React to show stale UI until finished suspending", async () => { - type Variables = { - offset: number; - }; + // We can ensure this works with isPending from useTransition in the process + expect(todos).toHaveAttribute("aria-busy", "true"); - interface Todo { - __typename: "Todo"; - id: string; - name: string; - completed: boolean; - } - interface Data { - todos: Todo[]; - } - const user = userEvent.setup(); - - const query: TypedDocumentNode = gql` - query TodosQuery($offset: Int!) { - todos(offset: $offset) { - id - name - completed - } - } - `; + // Ensure we are showing the stale UI until the new todo has loaded + expect(todo1).toHaveTextContent("Clean room"); - const mocks: MockedResponse[] = [ - { - request: { query, variables: { offset: 0 } }, - result: { - data: { - todos: [ - { - __typename: "Todo", - id: "1", - name: "Clean room", - completed: false, - }, - ], - }, - }, - delay: 10, - }, - { - request: { query, variables: { offset: 1 } }, - result: { - data: { - todos: [ - { - __typename: "Todo", - id: "2", - name: "Take out trash", - completed: true, - }, - ], - }, - }, - delay: 10, - }, - ]; - - const client = new ApolloClient({ - link: new MockLink(mocks), - cache: new InMemoryCache({ - typePolicies: { - Query: { - fields: { - todos: offsetLimitPagination(), - }, - }, - }, - }), - }); + // 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"); + }); + }); - function App() { - return ( - - }> - - - - ); - } + it('honors refetchWritePolicy set to "merge"', async () => { + const user = userEvent.setup(); - function SuspenseFallback() { - return

Loading

; + const query: TypedDocumentNode< + { primes: number[] }, + { min: number; max: number } + > = gql` + query GetPrimes($min: number, $max: number) { + primes(min: $min, max: $max) } + `; - function Parent() { - const [queryRef, { fetchMore }] = useInteractiveQuery(query, { - variables: { offset: 0 }, - }); - return ; - } + interface QueryData { + primes: number[]; + } - function Todo({ - queryRef, - fetchMore, - }: { - fetchMore: FetchMoreFunction; - queryRef: QueryReference; - }) { - const { data } = useReadQuery(queryRef); - const [isPending, startTransition] = React.useTransition(); - const { todos } = data; + 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, + }, + ]; - return ( - <> - -
- {todos.map((todo) => ( -
- {todo.name} - {todo.completed && " (completed)"} -
- ))} -
- - ); - } + 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; + }, + }, + }, + }, + }, + }); - render(); + function SuspenseFallback() { + return
loading
; + } - expect(screen.getByText("Loading")).toBeInTheDocument(); + const client = new ApolloClient({ + link: new MockLink(mocks), + cache, + }); - expect(await screen.findByTestId("todos")).toBeInTheDocument(); + function Child({ + refetch, + queryRef, + }: { + refetch: ( + variables?: Partial | undefined + ) => Promise>; + queryRef: QueryReference; + }) { + const { data, error, networkStatus } = useReadQuery(queryRef); - const todos = screen.getByTestId("todos"); - const todo1 = screen.getByTestId("todo:1"); - const button = screen.getByText("Load more"); + return ( +
+ +
{data?.primes.join(", ")}
+
{networkStatus}
+
{error?.message || "undefined"}
+
+ ); + } - expect(todo1).toBeInTheDocument(); + function Parent() { + const [queryRef, { refetch }] = useInteractiveQuery(query, { + variables: { min: 0, max: 12 }, + refetchWritePolicy: "merge", + }); + return ; + } - await act(() => user.click(button)); + function App() { + return ( + + }> + + + + ); + } - // 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(); + render(); - // We can ensure this works with isPending from useTransition in the process - expect(todos).toHaveAttribute("aria-busy", "true"); + 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]]]); - // Ensure we are showing the stale UI until the new todo has loaded - expect(todo1).toHaveTextContent("Clean room"); + await act(() => user.click(screen.getByText("Refetch"))); - // 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"); - }); + 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('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) - } - `; + it('defaults refetchWritePolicy to "overwrite"', async () => { + const user = userEvent.setup(); - interface QueryData { - primes: number[]; + const query: TypedDocumentNode< + { primes: number[] }, + { min: number; max: number } + > = gql` + query GetPrimes($min: number, $max: number) { + primes(min: $min, max: $max) } + `; - 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, - }, - ]; + interface QueryData { + primes: number[]; + } - 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; - }, + 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
; - } + function SuspenseFallback() { + return
loading
; + } - const client = new ApolloClient({ - link: new MockLink(mocks), - cache, - }); + const client = new ApolloClient({ + link: new MockLink(mocks), + cache, + }); - function Child({ - refetch, - queryRef, - }: { - refetch: ( - variables?: Partial | undefined - ) => Promise>; - queryRef: QueryReference; - }) { - const { data, error, networkStatus } = useReadQuery(queryRef); + 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"}
-
- ); - } + return ( +
+ +
{data?.primes.join(", ")}
+
{networkStatus}
+
{error?.message || "undefined"}
+
+ ); + } - function Parent() { - const [queryRef, { refetch }] = useInteractiveQuery(query, { - variables: { min: 0, max: 12 }, - refetchWritePolicy: "merge", - }); - return ; - } + function Parent() { + const [queryRef, { refetch }] = useInteractiveQuery(query, { + variables: { min: 0, max: 12 }, + }); + return ; + } - function App() { - return ( - - }> - - - - ); - } + function App() { + return ( + + }> + + + + ); + } - render(); + 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 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 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 + await waitFor(() => { + expect(screen.getByTestId("primes")).toHaveTextContent( + "13, 17, 19, 23, 29" ); - 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], - ], - ]); }); + 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]], + ]); + }); - it('defaults refetchWritePolicy to "overwrite"', async () => { - const user = userEvent.setup(); + it('does not suspend when partial data is in the cache and using a "cache-first" fetch policy with returnPartialData', async () => { + interface Data { + character: { + id: string; + name: string; + }; + } - const query: TypedDocumentNode< - { primes: number[] }, - { min: number; max: number } - > = gql` - query GetPrimes($min: number, $max: number) { - primes(min: $min, max: $max) + const fullQuery: TypedDocumentNode = gql` + query { + character { + id + name } - `; + } + `; - interface QueryData { - primes: number[]; + const partialQuery = gql` + query { + character { + id + } } + `; + const mocks = [ + { + request: { query: fullQuery }, + result: { data: { character: { id: "1", name: "Doctor Strange" } } }, + }, + ]; - 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, - }, - ]; + interface Renders { + errors: Error[]; + errorCount: number; + suspenseCount: number; + count: number; + } + const renders: Renders = { + errors: [], + errorCount: 0, + suspenseCount: 0, + count: 0, + }; - 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; - }, - }, - }, - }, - }, - }); + const cache = new InMemoryCache(); - function SuspenseFallback() { - return
loading
; - } + cache.writeQuery({ + query: partialQuery, + data: { character: { id: "1" } }, + }); - const client = new ApolloClient({ - link: new MockLink(mocks), - cache, - }); + const client = new ApolloClient({ + link: new MockLink(mocks), + cache, + }); - function Child({ - refetch, - queryRef, - }: { - refetch: ( - variables?: Partial | undefined - ) => Promise>; - queryRef: QueryReference; - }) { - const { data, error, networkStatus } = useReadQuery(queryRef); + function App() { + return ( + + }> + + + + ); + } - return ( -
- -
{data?.primes.join(", ")}
-
{networkStatus}
-
{error?.message || "undefined"}
-
- ); - } + function SuspenseFallback() { + renders.suspenseCount++; + return

Loading

; + } - function Parent() { - const [queryRef, { refetch }] = useInteractiveQuery(query, { - variables: { min: 0, max: 12 }, - }); - return ; - } + function Parent() { + const [queryRef] = useInteractiveQuery(fullQuery, { + fetchPolicy: "cache-first", + returnPartialData: true, + }); + return ; + } - function App() { - return ( - - }> - - - - ); - } + function Todo({ + queryRef, + }: { + queryRef: QueryReference>; + }) { + const { data, networkStatus, error } = useReadQuery(queryRef); + renders.count++; - render(); + return ( + <> +
{data.character?.id}
+
{data.character?.name}
+
{networkStatus}
+
{error?.message || "undefined"}
+ + ); + } - await waitFor(() => { - expect(screen.getByTestId("primes")).toHaveTextContent( - "2, 3, 5, 7, 11" - ); - }); - expect(screen.getByTestId("network-status")).toHaveTextContent( - "7" // ready + render(); + + 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"); + + await waitFor(() => { + expect(screen.getByTestId("character-name")).toHaveTextContent( + "Doctor Strange" ); - expect(screen.getByTestId("error")).toHaveTextContent("undefined"); - expect(mergeParams).toEqual([[undefined, [2, 3, 5, 7, 11]]]); + }); + 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); + }); + + 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 + } + } + `; - await act(() => user.click(screen.getByText("Refetch"))); + const cache = new InMemoryCache(); - 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]], - ]); + cache.writeQuery({ + query: partialQuery, + data: { character: { id: "1" } }, + variables: { id: "1" }, }); - it('does not suspend when partial data is in the cache and using a "cache-first" fetch policy with returnPartialData', async () => { - interface Data { - character: { - id: string; - name: string; - }; - } + const { renders, mocks, rerender } = renderVariablesIntegrationTest({ + variables: { id: "1" }, + cache, + options: { + fetchPolicy: "cache-first", + returnPartialData: true, + }, + }); + expect(renders.suspenseCount).toBe(0); - const fullQuery: TypedDocumentNode = gql` - query { - character { - id - name - } - } - `; + expect(await screen.findByText("1 - Spider-Man")).toBeInTheDocument(); - const partialQuery = gql` - query { - character { - id - } - } - `; - const mocks = [ - { - request: { query: fullQuery }, - result: { data: { character: { id: "1", name: "Doctor Strange" } } }, - }, - ]; + rerender({ variables: { id: "2" } }); - interface Renders { - errors: Error[]; - errorCount: number; - suspenseCount: number; - count: number; - } - const renders: Renders = { - errors: [], - errorCount: 0, - suspenseCount: 0, - count: 0, - }; + expect(await screen.findByText("2 - Black Widow")).toBeInTheDocument(); - const cache = new InMemoryCache(); + expect(renders.frames[2]).toMatchObject({ + ...mocks[1].result, + networkStatus: NetworkStatus.ready, + error: undefined, + }); - cache.writeQuery({ - query: partialQuery, + expect(renders.count).toBe(3); + expect(renders.suspenseCount).toBe(1); + expect(renders.frames).toMatchObject([ + { data: { character: { id: "1" } }, - }); - - const client = new ApolloClient({ - link: new MockLink(mocks), - cache, - }); - - function App() { - return ( - - }> - - - - ); - } + networkStatus: NetworkStatus.loading, + error: undefined, + }, + { + ...mocks[0].result, + networkStatus: NetworkStatus.ready, + error: undefined, + }, + { + ...mocks[1].result, + networkStatus: NetworkStatus.ready, + error: undefined, + }, + ]); + }); - function SuspenseFallback() { - renders.suspenseCount++; - return

Loading

; - } + 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; + }; + } - function Parent() { - const [queryRef] = useInteractiveQuery(fullQuery, { - fetchPolicy: "cache-first", - returnPartialData: true, - }); - return ; + const fullQuery: TypedDocumentNode = gql` + query { + character { + id + name + } } + `; - function Todo({ - queryRef, - }: { - queryRef: QueryReference>; - }) { - const { data, networkStatus, error } = useReadQuery(queryRef); - renders.count++; - - return ( - <> -
{data.character?.id}
-
{data.character?.name}
-
{networkStatus}
-
{error?.message || "undefined"}
- - ); + const partialQuery = gql` + query { + character { + id + } } + `; + const mocks = [ + { + request: { query: fullQuery }, + result: { data: { character: { id: "1", name: "Doctor Strange" } } }, + }, + ]; - render(); + 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: [], + }; - 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"); + const cache = new InMemoryCache(); - 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"); + cache.writeQuery({ + query: partialQuery, + data: { character: { id: "1" } }, + }); - expect(renders.count).toBe(2); - expect(renders.suspenseCount).toBe(0); + const client = new ApolloClient({ + link: new MockLink(mocks), + cache, }); - 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 - } - } - `; + function App() { + return ( + + }> + + + + ); + } - const cache = new InMemoryCache(); + function SuspenseFallback() { + renders.suspenseCount++; + return

Loading

; + } - cache.writeQuery({ - query: partialQuery, - data: { character: { id: "1" } }, - variables: { id: "1" }, + function Parent() { + const [queryRef] = useInteractiveQuery(fullQuery, { + fetchPolicy: "network-only", + returnPartialData: true, }); - const { renders, mocks, rerender } = renderVariablesIntegrationTest({ - variables: { id: "1" }, - cache, - options: { - fetchPolicy: "cache-first", - returnPartialData: true, - }, - }); - expect(renders.suspenseCount).toBe(0); + return ; + } - expect(await screen.findByText("1 - Spider-Man")).toBeInTheDocument(); + 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"}
+ + ); + } - rerender({ variables: { id: "2" } }); + render(); - expect(await screen.findByText("2 - Black Widow")).toBeInTheDocument(); + expect(renders.suspenseCount).toBe(1); - expect(renders.frames[2]).toMatchObject({ - ...mocks[1].result, + 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, - }); - - 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; - }; - } + 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; + }; + } - const fullQuery: TypedDocumentNode = gql` - query { - character { - id - name - } + const fullQuery: TypedDocumentNode = gql` + query { + character { + id + name } - `; + } + `; - const partialQuery = gql` - query { - character { - id - } + 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 mocks = [ + { + request: { query: fullQuery }, + result: { data: { character: { id: "1", name: "Doctor Strange" } } }, + }, + ]; - const client = new ApolloClient({ - link: new MockLink(mocks), - cache, - }); + 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: [], + }; - function App() { - return ( - - }> - - - - ); - } + const cache = new InMemoryCache(); - function SuspenseFallback() { - renders.suspenseCount++; - return

Loading

; - } + cache.writeQuery({ + query: partialQuery, + data: { character: { id: "1" } }, + }); - function Parent() { - const [queryRef] = useInteractiveQuery(fullQuery, { - fetchPolicy: "network-only", - returnPartialData: true, - }); + const client = new ApolloClient({ + link: new MockLink(mocks), + cache, + }); - return ; - } + function App() { + 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"}
- - ); - } + function SuspenseFallback() { + renders.suspenseCount++; + return

Loading

; + } - render(); + function Parent() { + const [queryRef] = useInteractiveQuery(fullQuery, { + fetchPolicy: "no-cache", + returnPartialData: true, + }); - expect(renders.suspenseCount).toBe(1); + return ; + } - 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, - }, - ]); - }); + 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"}
+ + ); + } - 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; - }; - } + render(); - const fullQuery: TypedDocumentNode = gql` - query { - character { - id - name - } - } - `; + expect(renders.suspenseCount).toBe(1); - 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: [], - }; + 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"); - const cache = new InMemoryCache(); + expect(renders.count).toBe(1); + expect(renders.suspenseCount).toBe(1); - cache.writeQuery({ - query: partialQuery, - data: { character: { id: "1" } }, - }); + expect(renders.frames).toMatchObject([ + { + ...mocks[0].result, + networkStatus: NetworkStatus.ready, + error: undefined, + }, + ]); - const client = new ApolloClient({ - link: new MockLink(mocks), - cache, - }); + consoleSpy.mockRestore(); + }); - function App() { - return ( - - }> - - - - ); - } + it('warns when using returnPartialData with a "no-cache" fetch policy', async () => { + const consoleSpy = jest.spyOn(console, "warn").mockImplementation(); - function SuspenseFallback() { - renders.suspenseCount++; - return

Loading

; + const query: TypedDocumentNode = gql` + query UserQuery { + greeting } + `; + const mocks = [ + { + request: { query }, + result: { data: { greeting: "Hello" } }, + }, + ]; - function Parent() { - const [queryRef] = useInteractiveQuery(fullQuery, { + renderSuspenseHook( + () => + useInteractiveQuery(query, { 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); + }), + { mocks } + ); - 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, - }, - ]); + 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(); - }); + consoleSpy.mockRestore(); + }); - it('warns when using returnPartialData with a "no-cache" fetch policy', async () => { - const consoleSpy = jest.spyOn(console, "warn").mockImplementation(); + 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; + }; + } - const query: TypedDocumentNode = gql` - query UserQuery { - greeting + const fullQuery: TypedDocumentNode = gql` + query { + character { + id + name } - `; - const mocks = [ - { - request: { query }, - result: { data: { greeting: "Hello" } }, - }, - ]; - - renderSuspenseHook( - () => - useInteractiveQuery(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('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; - }; } + `; - const fullQuery: TypedDocumentNode = gql` - query { - character { - id - name - } - } - `; - - const partialQuery = gql` - query { - character { - id - } + 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(); + `; + const mocks = [ + { + request: { query: fullQuery }, + result: { data: { character: { id: "1", name: "Doctor Strange" } } }, + }, + ]; - cache.writeQuery({ - query: partialQuery, - data: { character: { id: "1" } }, - }); + 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: new MockLink(mocks), - cache, - }); + const cache = new InMemoryCache(); - function App() { - return ( - - }> - - - - ); - } + cache.writeQuery({ + query: partialQuery, + data: { character: { id: "1" } }, + }); - function SuspenseFallback() { - renders.suspenseCount++; - return

Loading

; - } + const client = new ApolloClient({ + link: new MockLink(mocks), + cache, + }); - function Parent() { - const [queryRef] = useInteractiveQuery(fullQuery, { - fetchPolicy: "cache-and-network", - returnPartialData: true, - }); + function App() { + return ( + + }> + + + + ); + } - return ; - } + function SuspenseFallback() { + renders.suspenseCount++; + return

Loading

; + } - 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"}
- - ); - } + function Parent() { + const [queryRef] = useInteractiveQuery(fullQuery, { + fetchPolicy: "cache-and-network", + returnPartialData: true, + }); - render(); + return ; + } - 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"); + 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"}
+ + ); + } - 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"); + render(); - expect(renders.count).toBe(2); - expect(renders.suspenseCount).toBe(0); + 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"); - expect(renders.frames).toMatchObject([ - { - data: { character: { id: "1" } }, - networkStatus: NetworkStatus.loading, - error: undefined, - }, - { - ...mocks[0].result, - networkStatus: NetworkStatus.ready, - error: 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"); - 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 - } + 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(); + const cache = new InMemoryCache(); - cache.writeQuery({ - query: partialQuery, - data: { character: { id: "1" } }, - variables: { id: "1" }, - }); + 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, - }, - }); + const { renders, mocks, rerender } = renderVariablesIntegrationTest({ + variables: { id: "1" }, + cache, + options: { + fetchPolicy: "cache-and-network", + returnPartialData: true, + }, + }); - expect(renders.suspenseCount).toBe(0); + expect(renders.suspenseCount).toBe(0); - expect(await screen.findByText("1 - Spider-Man")).toBeInTheDocument(); + expect(await screen.findByText("1 - Spider-Man")).toBeInTheDocument(); - rerender({ variables: { id: "2" } }); + rerender({ variables: { id: "2" } }); - expect(await screen.findByText("2 - Black Widow")).toBeInTheDocument(); + 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, - }, - ]); - }); + 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('does not suspend deferred queries with partial data in the cache and using a "cache-first" fetch policy with `returnPartialData`', async () => { - interface QueryData { - greeting: { + 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; - message?: string; - recipient?: { - __typename: string; - name: string; - }; + name: string; }; - } + }; + } - const user = userEvent.setup(); + const user = userEvent.setup(); - const query: TypedDocumentNode = gql` - query { - greeting { - message - ... on Greeting @defer { - recipient { - name - } + const query: TypedDocumentNode = gql` + query { + greeting { + message + ... on Greeting @defer { + recipient { + name } } } - `; + } + `; - const link = new MockSubscriptionLink(); - const cache = new InMemoryCache(); + 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" }, - }, + // 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, - }); + }, + }); + consoleSpy.mockRestore(); - function App() { - return ( - - }> - - - - ); - } + 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: [], + }; - function SuspenseFallback() { - renders.suspenseCount++; - return

Loading

; - } + const client = new ApolloClient({ + link, + cache, + }); - function Parent() { - const [queryRef, loadTodo] = useInteractiveQuery(query, { - fetchPolicy: "cache-first", - returnPartialData: true, - }); + function App() { + return ( + + }> + + + + ); + } - return ( -
- - {queryRef && } -
- ); - } + function SuspenseFallback() { + renders.suspenseCount++; + return

Loading

; + } - 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"}
- - ); - } + function Parent() { + const [queryRef, loadTodo] = useInteractiveQuery(query, { + fetchPolicy: "cache-first", + returnPartialData: true, + }); - render(); + return ( +
+ + {queryRef && } +
+ ); + } - await act(() => user.click(screen.getByText("Load todo"))); + 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"}
+ + ); + } - 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"); + render(); - link.simulateResult({ - result: { - data: { - greeting: { message: "Hello world", __typename: "Greeting" }, - }, - hasNext: true, - }, - }); + await act(() => user.click(screen.getByText("Load todo"))); - 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"); + 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: { - incremental: [ - { - data: { - __typename: "Greeting", - recipient: { name: "Alice", __typename: "Person" }, - }, - path: ["greeting"], - }, - ], - hasNext: false, + link.simulateResult({ + result: { + data: { + greeting: { message: "Hello world", __typename: "Greeting" }, }, - }); + hasNext: true, + }, + }); - await waitFor(() => { - expect(screen.getByTestId("recipient").textContent).toEqual("Alice"); - }); + await waitFor(() => { expect(screen.getByTestId("message")).toHaveTextContent("Hello world"); - expect(screen.getByTestId("network-status")).toHaveTextContent("7"); // ready - expect(screen.getByTestId("error")).toHaveTextContent("undefined"); + }); + expect(screen.getByTestId("recipient")).toHaveTextContent("Cached Alice"); + 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: { + link.simulateResult({ + result: { + incremental: [ + { + data: { __typename: "Greeting", - recipient: { __typename: "Person", name: "Cached Alice" }, + 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.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, + }, + { + data: { + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Alice" }, }, - networkStatus: NetworkStatus.ready, - error: undefined, }, - ]); - }); + networkStatus: NetworkStatus.ready, + error: undefined, + }, + ]); }); +}); - describe.skip("type tests", () => { - it("returns unknown when TData cannot be inferred", () => { - const query = gql` - query { - hello - } - `; +describe.skip("type tests", () => { + it("returns unknown when TData cannot be inferred", () => { + const query = gql` + query { + hello + } + `; - const [queryRef, loadQuery] = useInteractiveQuery(query); + const [queryRef, loadQuery] = useInteractiveQuery(query); - invariant(queryRef); + invariant(queryRef); - const { data } = useReadQuery(queryRef); + const { data } = useReadQuery(queryRef); - expectTypeOf(data).toEqualTypeOf(); - expectTypeOf(loadQuery).toEqualTypeOf< - (variables: OperationVariables) => void - >(); - }); + expectTypeOf(data).toEqualTypeOf(); + expectTypeOf(loadQuery).toEqualTypeOf< + (variables: OperationVariables) => void + >(); + }); - it("enforces variables argument to loadQuery function when TVariables is specified", () => { - const { query } = useVariablesIntegrationTestCase(); + it("enforces variables argument to loadQuery function when TVariables is specified", () => { + const { query } = useVariablesIntegrationTestCase(); - const [, loadQuery] = useInteractiveQuery(query); + const [, loadQuery] = useInteractiveQuery(query); - expectTypeOf(loadQuery).toEqualTypeOf< - (variables: VariablesCaseVariables) => void - >(); - // @ts-expect-error enforces variables argument when type is specified - loadQuery(); - }); + expectTypeOf(loadQuery).toEqualTypeOf< + (variables: VariablesCaseVariables) => void + >(); + // @ts-expect-error enforces variables argument when type is specified + loadQuery(); + }); - it("disallows wider variables type", () => { - const { query } = useVariablesIntegrationTestCase(); + it("disallows wider variables type", () => { + const { query } = useVariablesIntegrationTestCase(); - const [, loadQuery] = useInteractiveQuery(query); + const [, loadQuery] = useInteractiveQuery(query); - expectTypeOf(loadQuery).toEqualTypeOf< - (variables: VariablesCaseVariables) => void - >(); - // @ts-expect-error does not allow wider TVariables type - loadQuery({ id: "1", foo: "bar" }); - }); + expectTypeOf(loadQuery).toEqualTypeOf< + (variables: VariablesCaseVariables) => void + >(); + // @ts-expect-error does not allow wider TVariables type + loadQuery({ id: "1", foo: "bar" }); + }); - it("does not allow variables argument to loadQuery when TVariables is `never`", () => { - const query: TypedDocumentNode = gql` - query { - greeting - } - `; + it("does not allow variables argument to loadQuery when TVariables is `never`", () => { + const query: TypedDocumentNode = gql` + query { + greeting + } + `; - const [, loadQuery] = useInteractiveQuery(query); + const [, loadQuery] = useInteractiveQuery(query); - expectTypeOf(loadQuery).toEqualTypeOf<() => void>(); - // @ts-expect-error does not allow variables argument when TVariables is `never` - loadQuery({}); - }); + expectTypeOf(loadQuery).toEqualTypeOf<() => void>(); + // @ts-expect-error does not allow variables argument when TVariables is `never` + loadQuery({}); + }); - it("returns TData in default case", () => { - const { query } = useVariablesIntegrationTestCase(); + it("returns TData in default case", () => { + const { query } = useVariablesIntegrationTestCase(); - { - const [queryRef] = useInteractiveQuery(query); + { + const [queryRef] = useInteractiveQuery(query); - invariant(queryRef); + invariant(queryRef); - const { data } = useReadQuery(queryRef); + const { data } = useReadQuery(queryRef); - expectTypeOf(data).toEqualTypeOf(); - } + expectTypeOf(data).toEqualTypeOf(); + } - { - const [queryRef] = useInteractiveQuery< - VariablesCaseData, - VariablesCaseVariables - >(query); + { + const [queryRef] = useInteractiveQuery< + VariablesCaseData, + VariablesCaseVariables + >(query); - invariant(queryRef); + invariant(queryRef); - const { data } = useReadQuery(queryRef); + const { data } = useReadQuery(queryRef); - expectTypeOf(data).toEqualTypeOf(); - } - }); + expectTypeOf(data).toEqualTypeOf(); + } + }); - it('returns TData | undefined with errorPolicy: "ignore"', () => { - const { query } = useVariablesIntegrationTestCase(); + it('returns TData | undefined with errorPolicy: "ignore"', () => { + const { query } = useVariablesIntegrationTestCase(); - { - const [queryRef] = useInteractiveQuery(query, { - errorPolicy: "ignore", - }); + { + const [queryRef] = useInteractiveQuery(query, { + errorPolicy: "ignore", + }); - invariant(queryRef); + invariant(queryRef); - const { data } = useReadQuery(queryRef); + const { data } = useReadQuery(queryRef); - expectTypeOf(data).toEqualTypeOf(); - } + expectTypeOf(data).toEqualTypeOf(); + } - { - const [queryRef] = useInteractiveQuery< - VariablesCaseData, - VariablesCaseVariables - >(query, { errorPolicy: "ignore" }); + { + const [queryRef] = useInteractiveQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { errorPolicy: "ignore" }); - invariant(queryRef); + invariant(queryRef); - const { data } = useReadQuery(queryRef); + const { data } = useReadQuery(queryRef); - expectTypeOf(data).toEqualTypeOf(); - } - }); + expectTypeOf(data).toEqualTypeOf(); + } + }); - it('returns TData | undefined with errorPolicy: "all"', () => { - const { query } = useVariablesIntegrationTestCase(); + it('returns TData | undefined with errorPolicy: "all"', () => { + const { query } = useVariablesIntegrationTestCase(); - { - const [queryRef] = useInteractiveQuery(query, { - errorPolicy: "all", - }); + { + const [queryRef] = useInteractiveQuery(query, { + errorPolicy: "all", + }); - invariant(queryRef); + invariant(queryRef); - const { data } = useReadQuery(queryRef); + const { data } = useReadQuery(queryRef); - expectTypeOf(data).toEqualTypeOf(); - } + expectTypeOf(data).toEqualTypeOf(); + } - { - const [queryRef] = useInteractiveQuery< - VariablesCaseData, - VariablesCaseVariables - >(query, { errorPolicy: "all" }); + { + const [queryRef] = useInteractiveQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { errorPolicy: "all" }); - invariant(queryRef); + invariant(queryRef); - const { data } = useReadQuery(queryRef); + const { data } = useReadQuery(queryRef); - expectTypeOf(data).toEqualTypeOf(); - } - }); + expectTypeOf(data).toEqualTypeOf(); + } + }); - it('returns TData with errorPolicy: "none"', () => { - const { query } = useVariablesIntegrationTestCase(); + it('returns TData with errorPolicy: "none"', () => { + const { query } = useVariablesIntegrationTestCase(); - { - const [queryRef] = useInteractiveQuery(query, { - errorPolicy: "none", - }); + { + const [queryRef] = useInteractiveQuery(query, { + errorPolicy: "none", + }); - invariant(queryRef); + invariant(queryRef); - const { data } = useReadQuery(queryRef); + const { data } = useReadQuery(queryRef); - expectTypeOf(data).toEqualTypeOf(); - } + expectTypeOf(data).toEqualTypeOf(); + } - { - const [queryRef] = useInteractiveQuery< - VariablesCaseData, - VariablesCaseVariables - >(query, { errorPolicy: "none" }); + { + const [queryRef] = useInteractiveQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { errorPolicy: "none" }); - invariant(queryRef); + invariant(queryRef); - const { data } = useReadQuery(queryRef); + const { data } = useReadQuery(queryRef); - expectTypeOf(data).toEqualTypeOf(); - } - }); + expectTypeOf(data).toEqualTypeOf(); + } + }); - it("returns DeepPartial with returnPartialData: true", () => { - const { query } = useVariablesIntegrationTestCase(); + it("returns DeepPartial with returnPartialData: true", () => { + const { query } = useVariablesIntegrationTestCase(); - { - const [queryRef] = useInteractiveQuery(query, { - returnPartialData: true, - }); + { + const [queryRef] = useInteractiveQuery(query, { + returnPartialData: true, + }); - invariant(queryRef); + invariant(queryRef); - const { data } = useReadQuery(queryRef); + const { data } = useReadQuery(queryRef); - expectTypeOf(data).toEqualTypeOf>(); - } + expectTypeOf(data).toEqualTypeOf>(); + } - { - const [queryRef] = useInteractiveQuery< - VariablesCaseData, - VariablesCaseVariables - >(query, { returnPartialData: true }); + { + const [queryRef] = useInteractiveQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { returnPartialData: true }); - invariant(queryRef); + invariant(queryRef); - const { data } = useReadQuery(queryRef); + const { data } = useReadQuery(queryRef); - expectTypeOf(data).toEqualTypeOf>(); - } - }); + expectTypeOf(data).toEqualTypeOf>(); + } + }); - it("returns TData with returnPartialData: false", () => { - const { query } = useVariablesIntegrationTestCase(); + it("returns TData with returnPartialData: false", () => { + const { query } = useVariablesIntegrationTestCase(); - { - const [queryRef] = useInteractiveQuery(query, { - returnPartialData: false, - }); + { + const [queryRef] = useInteractiveQuery(query, { + returnPartialData: false, + }); - invariant(queryRef); + invariant(queryRef); - const { data } = useReadQuery(queryRef); + const { data } = useReadQuery(queryRef); - expectTypeOf(data).toEqualTypeOf(); - } + expectTypeOf(data).toEqualTypeOf(); + } - { - const [queryRef] = useInteractiveQuery< - VariablesCaseData, - VariablesCaseVariables - >(query, { returnPartialData: false }); + { + const [queryRef] = useInteractiveQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { returnPartialData: false }); - invariant(queryRef); + invariant(queryRef); - const { data } = useReadQuery(queryRef); + const { data } = useReadQuery(queryRef); - expectTypeOf(data).toEqualTypeOf(); - } - }); + expectTypeOf(data).toEqualTypeOf(); + } + }); - it("returns TData when passing an option that does not affect TData", () => { - const { query } = useVariablesIntegrationTestCase(); + it("returns TData when passing an option that does not affect TData", () => { + const { query } = useVariablesIntegrationTestCase(); - { - const [queryRef] = useInteractiveQuery(query, { - fetchPolicy: "no-cache", - }); + { + const [queryRef] = useInteractiveQuery(query, { + fetchPolicy: "no-cache", + }); - invariant(queryRef); + invariant(queryRef); - const { data } = useReadQuery(queryRef); + const { data } = useReadQuery(queryRef); - expectTypeOf(data).toEqualTypeOf(); - } + expectTypeOf(data).toEqualTypeOf(); + } - { - const [queryRef] = useInteractiveQuery< - VariablesCaseData, - VariablesCaseVariables - >(query, { fetchPolicy: "no-cache" }); + { + const [queryRef] = useInteractiveQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { fetchPolicy: "no-cache" }); - invariant(queryRef); + invariant(queryRef); - const { data } = useReadQuery(queryRef); + const { data } = useReadQuery(queryRef); - expectTypeOf(data).toEqualTypeOf(); - } - }); + expectTypeOf(data).toEqualTypeOf(); + } + }); - it("handles combinations of options", () => { - const { query } = useVariablesIntegrationTestCase(); + it("handles combinations of options", () => { + const { query } = useVariablesIntegrationTestCase(); - { - const [queryRef] = useInteractiveQuery(query, { - returnPartialData: true, - errorPolicy: "ignore", - }); + { + const [queryRef] = useInteractiveQuery(query, { + returnPartialData: true, + errorPolicy: "ignore", + }); - invariant(queryRef); + invariant(queryRef); - const { data } = useReadQuery(queryRef); + const { data } = useReadQuery(queryRef); - expectTypeOf(data).toEqualTypeOf< - DeepPartial | undefined - >(); - } + expectTypeOf(data).toEqualTypeOf< + DeepPartial | undefined + >(); + } - { - const [queryRef] = useInteractiveQuery< - VariablesCaseData, - VariablesCaseVariables - >(query, { returnPartialData: true, errorPolicy: "ignore" }); + { + const [queryRef] = useInteractiveQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { returnPartialData: true, errorPolicy: "ignore" }); - invariant(queryRef); + invariant(queryRef); - const { data } = useReadQuery(queryRef); + const { data } = useReadQuery(queryRef); - expectTypeOf(data).toEqualTypeOf< - DeepPartial | undefined - >(); - } + expectTypeOf(data).toEqualTypeOf< + DeepPartial | undefined + >(); + } - { - const [queryRef] = useInteractiveQuery(query, { - returnPartialData: true, - errorPolicy: "none", - }); + { + const [queryRef] = useInteractiveQuery(query, { + returnPartialData: true, + errorPolicy: "none", + }); - invariant(queryRef); + invariant(queryRef); - const { data } = useReadQuery(queryRef); + const { data } = useReadQuery(queryRef); - expectTypeOf(data).toEqualTypeOf>(); - } + expectTypeOf(data).toEqualTypeOf>(); + } - { - const [queryRef] = useInteractiveQuery< - VariablesCaseData, - VariablesCaseVariables - >(query, { returnPartialData: true, errorPolicy: "none" }); + { + const [queryRef] = useInteractiveQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { returnPartialData: true, errorPolicy: "none" }); - invariant(queryRef); + invariant(queryRef); - const { data } = useReadQuery(queryRef); + const { data } = useReadQuery(queryRef); - expectTypeOf(data).toEqualTypeOf>(); - } - }); + expectTypeOf(data).toEqualTypeOf>(); + } + }); - it("returns correct TData type when combined options that do not affect TData", () => { - const { query } = useVariablesIntegrationTestCase(); + it("returns correct TData type when combined options that do not affect TData", () => { + const { query } = useVariablesIntegrationTestCase(); - { - const [queryRef] = useInteractiveQuery(query, { - fetchPolicy: "no-cache", - returnPartialData: true, - errorPolicy: "none", - }); + { + const [queryRef] = useInteractiveQuery(query, { + fetchPolicy: "no-cache", + returnPartialData: true, + errorPolicy: "none", + }); - invariant(queryRef); + invariant(queryRef); - const { data } = useReadQuery(queryRef); + const { data } = useReadQuery(queryRef); - expectTypeOf(data).toEqualTypeOf>(); - } + expectTypeOf(data).toEqualTypeOf>(); + } - { - const [queryRef] = useInteractiveQuery< - VariablesCaseData, - VariablesCaseVariables - >(query, { - fetchPolicy: "no-cache", - returnPartialData: true, - errorPolicy: "none", - }); + { + const [queryRef] = useInteractiveQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { + fetchPolicy: "no-cache", + returnPartialData: true, + errorPolicy: "none", + }); - invariant(queryRef); + invariant(queryRef); - const { data } = useReadQuery(queryRef); + const { data } = useReadQuery(queryRef); - expectTypeOf(data).toEqualTypeOf>(); - } - }); + expectTypeOf(data).toEqualTypeOf>(); + } }); }); From bfedb9600170223047acf155c2148e95b54b3ba7 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 16 Oct 2023 16:59:21 -0600 Subject: [PATCH 010/199] Add tests for overriding client and adding context --- .../__tests__/useInteractiveQuery.test.tsx | 238 +++++++++++++----- 1 file changed, 173 insertions(+), 65 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index 8ba5523a1d5..e3e1aec1bf6 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -734,84 +734,192 @@ it("loads a query when the load query function is called", async () => { expect(ProfiledApp).not.toRerender(); }); -// it('allows the client to be overridden', async () => { -// const query: TypedDocumentNode = gql` -// query UserQuery { -// greeting -// } -// `; +it("allows the client to be overridden", async () => { + const user = userEvent.setup(); + const { query } = useSimpleQueryCase(); -// const globalClient = new ApolloClient({ -// link: new ApolloLink(() => -// Observable.of({ data: { greeting: 'global hello' } }) -// ), -// cache: new InMemoryCache(), -// }); + const globalClient = new ApolloClient({ + link: new ApolloLink(() => + Observable.of({ data: { greeting: "global hello" } }) + ), + cache: new InMemoryCache(), + }); -// const localClient = new ApolloClient({ -// link: new ApolloLink(() => -// Observable.of({ data: { greeting: 'local hello' } }) -// ), -// cache: new InMemoryCache(), -// }); + const localClient = new ApolloClient({ + link: new ApolloLink(() => + Observable.of({ data: { greeting: "local hello" } }) + ), + cache: new InMemoryCache(), + }); -// const { result } = renderSuspenseHook( -// () => useInteractiveQuery(query, { client: localClient }), -// { client: globalClient } -// ); + function SuspenseFallback() { + return

Loading

; + } -// const [queryRef] = result.current; + function Parent() { + const [queryRef, loadQuery] = useInteractiveQuery(query, { + client: localClient, + }); -// const _result = await queryRef[QUERY_REFERENCE_SYMBOL].promise; + return ( + <> + + }> + {queryRef && } + + + ); + } -// await waitFor(() => { -// expect(_result).toEqual({ -// data: { greeting: 'local hello' }, -// loading: false, -// networkStatus: NetworkStatus.ready, -// }); -// }); -// }); + function Greeting({ + queryRef, + }: { + queryRef: QueryReference; + }) { + const result = useReadQuery(queryRef); -// it('passes context to the link', async () => { -// const query = gql` -// query ContextQuery { -// context -// } -// `; + ProfiledApp.updateSnapshot({ result }); -// const link = new ApolloLink((operation) => { -// return new Observable((observer) => { -// const { valueA, valueB } = operation.getContext(); + return
{result.data.greeting}
; + } -// observer.next({ data: { context: { valueA, valueB } } }); -// observer.complete(); -// }); -// }); + const ProfiledApp = profile<{ + result: ReturnType | null; + }>({ + Component: () => ( + + + + ), + snapshotDOM: true, + initialSnapshot: { + result: null, + }, + }); -// const { result } = renderHook( -// () => -// useInteractiveQuery(query, { -// context: { valueA: 'A', valueB: 'B' }, -// }), -// { -// wrapper: ({ children }) => ( -// {children} -// ), -// } -// ); + render(); -// const [queryRef] = result.current; + { + const { snapshot } = await ProfiledApp.takeRender(); + expect(snapshot.result).toEqual(null); + } -// const _result = await queryRef[QUERY_REFERENCE_SYMBOL].promise; + await act(() => user.click(screen.getByText("Load query"))); -// await waitFor(() => { -// expect(_result).toMatchObject({ -// data: { context: { valueA: 'A', valueB: 'B' } }, -// networkStatus: NetworkStatus.ready, -// }); -// }); -// }); + { + const { snapshot, withinDOM } = await ProfiledApp.takeRender(); + + expect(withinDOM().getByText("Loading")).toBeInTheDocument(); + expect(snapshot.result).toEqual(null); + } + + { + const { snapshot, withinDOM } = await ProfiledApp.takeRender(); + + expect(withinDOM().getByText("local hello")).toBeInTheDocument(); + expect(snapshot.result).toEqual({ + data: { greeting: "local hello" }, + networkStatus: NetworkStatus.ready, + error: undefined, + }); + } + + expect(ProfiledApp).not.toRerender(); +}); + +it("passes context to the link", async () => { + interface QueryData { + context: Record; + } + + const user = userEvent.setup(); + const query: TypedDocumentNode = gql` + query ContextQuery { + context + } + `; + + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: new ApolloLink((operation) => { + return new Observable((observer) => { + const { valueA, valueB } = operation.getContext(); + + observer.next({ data: { context: { valueA, valueB } } }); + observer.complete(); + }); + }), + }); + + function SuspenseFallback() { + return

Loading

; + } + + function Parent() { + const [queryRef, loadQuery] = useInteractiveQuery(query, { + context: { valueA: "A", valueB: "B" }, + }); + + return ( + <> + + }> + {queryRef && } + + + ); + } + + function Child({ queryRef }: { queryRef: QueryReference }) { + const result = useReadQuery(queryRef); + + ProfiledApp.updateSnapshot({ result }); + + return null; + } + + const ProfiledApp = profile<{ + result: ReturnType | null; + }>({ + Component: () => ( + + + + ), + snapshotDOM: true, + initialSnapshot: { + result: null, + }, + }); + + render(); + + { + const { snapshot } = await ProfiledApp.takeRender(); + expect(snapshot.result).toEqual(null); + } + + await act(() => user.click(screen.getByText("Load query"))); + + { + const { snapshot, withinDOM } = await ProfiledApp.takeRender(); + + expect(withinDOM().getByText("Loading")).toBeInTheDocument(); + expect(snapshot.result).toEqual(null); + } + + { + const { snapshot } = await ProfiledApp.takeRender(); + + expect(snapshot.result).toEqual({ + data: { context: { valueA: "A", valueB: "B" } }, + networkStatus: NetworkStatus.ready, + error: undefined, + }); + } + + expect(ProfiledApp).not.toRerender(); +}); // it('enables canonical results when canonizeResults is "true"', async () => { // interface Result { From 3d30f2a03d528599619869924c04d391aecff076 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 16 Oct 2023 17:12:42 -0600 Subject: [PATCH 011/199] Add test for canonicalResults option for useInteractiveQuery --- .../__tests__/useInteractiveQuery.test.tsx | 156 ++++++++++++------ 1 file changed, 105 insertions(+), 51 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index e3e1aec1bf6..62124d41a09 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -39,6 +39,7 @@ import { DeepPartial, } from "../../../utilities"; import { useInteractiveQuery } from "../useInteractiveQuery"; +import type { UseReadQueryResult } from "../useReadQuery"; import { useReadQuery } from "../useReadQuery"; import { ApolloProvider } from "../../context"; import { InMemoryCache } from "../../../cache"; @@ -921,65 +922,118 @@ it("passes context to the link", async () => { expect(ProfiledApp).not.toRerender(); }); -// it('enables canonical results when canonizeResults is "true"', async () => { -// interface Result { -// __typename: string; -// value: number; -// } +it('enables canonical results when canonizeResults is "true"', async () => { + interface Result { + __typename: string; + value: number; + } -// const cache = new InMemoryCache({ -// typePolicies: { -// Result: { -// keyFields: false, -// }, -// }, -// }); + interface QueryData { + results: Result[]; + } -// const query: TypedDocumentNode<{ results: Result[] }> = gql` -// query { -// results { -// value -// } -// } -// `; + const user = userEvent.setup(); + const cache = new InMemoryCache({ + typePolicies: { + Result: { + keyFields: false, + }, + }, + }); -// const results: Result[] = [ -// { __typename: 'Result', value: 0 }, -// { __typename: 'Result', value: 1 }, -// { __typename: 'Result', value: 1 }, -// { __typename: 'Result', value: 2 }, -// { __typename: 'Result', value: 3 }, -// { __typename: 'Result', value: 5 }, -// ]; + const query: TypedDocumentNode = gql` + query { + results { + value + } + } + `; -// cache.writeQuery({ -// query, -// data: { results }, -// }); + const results: Result[] = [ + { __typename: "Result", value: 0 }, + { __typename: "Result", value: 1 }, + { __typename: "Result", value: 1 }, + { __typename: "Result", value: 2 }, + { __typename: "Result", value: 3 }, + { __typename: "Result", value: 5 }, + ]; -// const { result } = renderHook( -// () => -// useInteractiveQuery(query, { -// canonizeResults: true, -// }), -// { -// wrapper: ({ children }) => ( -// {children} -// ), -// } -// ); + cache.writeQuery({ + query, + data: { results }, + }); -// const [queryRef] = result.current; + const client = new ApolloClient({ + cache, + link: new MockLink([]), + }); -// const _result = await queryRef[QUERY_REFERENCE_SYMBOL].promise; -// const resultSet = new Set(_result.data.results); -// const values = Array.from(resultSet).map((item) => item.value); + function SuspenseFallback() { + return

Loading

; + } -// expect(_result.data).toEqual({ results }); -// expect(_result.data.results.length).toBe(6); -// expect(resultSet.size).toBe(5); -// expect(values).toEqual([0, 1, 2, 3, 5]); -// }); + function Parent() { + const [queryRef, loadQuery] = useInteractiveQuery(query, { + canonizeResults: true, + }); + + return ( + <> + + }> + {queryRef && } + + + ); + } + + function Child({ queryRef }: { queryRef: QueryReference }) { + const result = useReadQuery(queryRef); + + ProfiledApp.updateSnapshot({ result }); + + return null; + } + + const ProfiledApp = profile<{ + result: UseReadQueryResult | null; + }>({ + Component: () => ( + + + + ), + initialSnapshot: { + result: null, + }, + }); + + render(); + + { + const { snapshot } = await ProfiledApp.takeRender(); + expect(snapshot.result).toEqual(null); + } + + await act(() => user.click(screen.getByText("Load query"))); + + { + const { snapshot } = await ProfiledApp.takeRender(); + + const resultSet = new Set(snapshot.result!.data.results); + const values = Array.from(resultSet).map((item) => item.value); + + expect(snapshot.result).toEqual({ + data: { results }, + networkStatus: NetworkStatus.ready, + error: undefined, + }); + expect(resultSet.size).toBe(5); + expect(values).toEqual([0, 1, 2, 3, 5]); + } + + expect(ProfiledApp).not.toRerender(); +}); // it("can disable canonical results when the cache's canonizeResults setting is true", async () => { // interface Result { From 9c64cd16130b08498fa2994e6997f00a48aa54ba Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 16 Oct 2023 17:19:08 -0600 Subject: [PATCH 012/199] Add test for disabling canonical results --- .../__tests__/useInteractiveQuery.test.tsx | 156 ++++++++++++------ 1 file changed, 102 insertions(+), 54 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index 62124d41a09..7bf9d9a1b97 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -1035,66 +1035,114 @@ it('enables canonical results when canonizeResults is "true"', async () => { expect(ProfiledApp).not.toRerender(); }); -// it("can disable canonical results when the cache's canonizeResults setting is true", async () => { -// interface Result { -// __typename: string; -// value: number; -// } - -// const cache = new InMemoryCache({ -// canonizeResults: true, -// typePolicies: { -// Result: { -// keyFields: false, -// }, -// }, -// }); +it("can disable canonical results when the cache's canonizeResults setting is true", async () => { + interface Result { + __typename: string; + value: number; + } -// const query: TypedDocumentNode<{ results: Result[] }> = gql` -// query { -// results { -// value -// } -// } -// `; + interface QueryData { + results: Result[]; + } -// const results: Result[] = [ -// { __typename: 'Result', value: 0 }, -// { __typename: 'Result', value: 1 }, -// { __typename: 'Result', value: 1 }, -// { __typename: 'Result', value: 2 }, -// { __typename: 'Result', value: 3 }, -// { __typename: 'Result', value: 5 }, -// ]; - -// cache.writeQuery({ -// query, -// data: { results }, -// }); + const cache = new InMemoryCache({ + canonizeResults: true, + typePolicies: { + Result: { + keyFields: false, + }, + }, + }); -// const { result } = renderHook( -// () => -// useInteractiveQuery(query, { -// canonizeResults: false, -// }), -// { -// wrapper: ({ children }) => ( -// {children} -// ), -// } -// ); + const query: TypedDocumentNode<{ results: Result[] }, never> = gql` + query { + results { + value + } + } + `; -// const [queryRef] = result.current; + const user = userEvent.setup(); + const results: Result[] = [ + { __typename: "Result", value: 0 }, + { __typename: "Result", value: 1 }, + { __typename: "Result", value: 1 }, + { __typename: "Result", value: 2 }, + { __typename: "Result", value: 3 }, + { __typename: "Result", value: 5 }, + ]; -// const _result = await queryRef[QUERY_REFERENCE_SYMBOL].promise; -// const resultSet = new Set(_result.data.results); -// const values = Array.from(resultSet).map((item) => item.value); + cache.writeQuery({ + query, + data: { results }, + }); -// expect(_result.data).toEqual({ results }); -// expect(_result.data.results.length).toBe(6); -// expect(resultSet.size).toBe(6); -// expect(values).toEqual([0, 1, 1, 2, 3, 5]); -// }); + function SuspenseFallback() { + return

Loading

; + } + + function Parent() { + const [queryRef, loadQuery] = useInteractiveQuery(query, { + canonizeResults: false, + }); + + return ( + <> + + }> + {queryRef && } + + + ); + } + + function Child({ queryRef }: { queryRef: QueryReference }) { + const result = useReadQuery(queryRef); + + ProfiledApp.updateSnapshot({ result }); + + return null; + } + + const ProfiledApp = profile<{ + result: UseReadQueryResult | null; + }>({ + Component: () => ( + + + + ), + initialSnapshot: { + result: null, + }, + }); + + render(); + + { + const { snapshot } = await ProfiledApp.takeRender(); + expect(snapshot.result).toEqual(null); + } + + await act(() => user.click(screen.getByText("Load query"))); + + { + const { snapshot } = await ProfiledApp.takeRender(); + + const resultSet = new Set(snapshot.result!.data.results); + const values = Array.from(resultSet).map((item) => item.value); + + expect(snapshot.result).toEqual({ + data: { results }, + networkStatus: NetworkStatus.ready, + error: undefined, + }); + expect(resultSet.size).toBe(6); + expect(values).toEqual([0, 1, 1, 2, 3, 5]); + } + + expect(ProfiledApp).not.toRerender(); +}); // // TODO(FIXME): test fails, should return cache data first if it exists // it.skip('returns initial cache data followed by network data when the fetch policy is `cache-and-network`', async () => { From 0ad05df7566c63b7bd1c7980b1f72109c0caeab8 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 16 Oct 2023 17:40:38 -0600 Subject: [PATCH 013/199] Use more robust type for `result` in snapshots --- src/react/hooks/__tests__/useInteractiveQuery.test.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index 7bf9d9a1b97..eff24beedca 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -671,7 +671,7 @@ it("loads a query when the load query function is called", async () => { } const ProfiledApp = profile<{ - result: ReturnType | null; + result: UseReadQueryResult | null; suspenseCount: number; parentRenderCount: number; childRenderCount: number; @@ -785,7 +785,7 @@ it("allows the client to be overridden", async () => { } const ProfiledApp = profile<{ - result: ReturnType | null; + result: UseReadQueryResult | null; }>({ Component: () => ( @@ -880,7 +880,7 @@ it("passes context to the link", async () => { } const ProfiledApp = profile<{ - result: ReturnType | null; + result: UseReadQueryResult | null; }>({ Component: () => ( From 8ad8c5519d9685277cd93ece9963e1d9f879dd23 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 16 Oct 2023 18:31:47 -0600 Subject: [PATCH 014/199] Add cache test --- .../__tests__/useInteractiveQuery.test.tsx | 116 +++++++++++++----- 1 file changed, 83 insertions(+), 33 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index eff24beedca..84441255b45 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -1185,45 +1185,95 @@ it("can disable canonical results when the cache's canonizeResults setting is tr // }); // }); -// it('all data is present in the cache, no network request is made', async () => { -// const query = gql` -// { -// hello -// } -// `; -// const cache = new InMemoryCache(); -// const link = mockSingleLink({ -// request: { query }, -// result: { data: { hello: 'from link' } }, -// delay: 20, -// }); +it("all data is present in the cache, no network request is made", async () => { + const query = gql` + query { + hello + } + `; + const user = userEvent.setup(); + const cache = new InMemoryCache(); + const link = new MockLink([ + { + request: { query }, + result: { data: { hello: "from link" } }, + delay: 20, + }, + ]); -// const client = new ApolloClient({ -// link, -// cache, -// }); + const client = new ApolloClient({ + link, + cache, + }); -// cache.writeQuery({ query, data: { hello: 'from cache' } }); + cache.writeQuery({ query, data: { hello: "from cache" } }); -// const { result } = renderHook( -// () => useInteractiveQuery(query, { fetchPolicy: 'cache-first' }), -// { -// wrapper: ({ children }) => ( -// {children} -// ), -// } -// ); + function SuspenseFallback() { + ProfiledApp.updateSnapshot((snapshot) => ({ + ...snapshot, + suspenseCount: snapshot.suspenseCount + 1, + })); -// const [queryRef] = result.current; + return

Loading

; + } -// const _result = await queryRef[QUERY_REFERENCE_SYMBOL].promise; + function Parent() { + const [queryRef, loadQuery] = useInteractiveQuery(query); + + return ( + <> + + }> + {queryRef && } + + + ); + } + + function Child({ queryRef }: { queryRef: QueryReference }) { + const result = useReadQuery(queryRef); + + ProfiledApp.updateSnapshot((snapshot) => ({ ...snapshot, result })); + + return null; + } + + const ProfiledApp = profile<{ + result: UseReadQueryResult | null; + suspenseCount: number; + }>({ + Component: () => ( + + + + ), + initialSnapshot: { + result: null, + suspenseCount: 0, + }, + }); + + render(); + + { + const { snapshot } = await ProfiledApp.takeRender(); + expect(snapshot.result).toEqual(null); + } + + await act(() => user.click(screen.getByText("Load query"))); + + { + const { snapshot } = await ProfiledApp.takeRender(); + + expect(snapshot.suspenseCount).toBe(0); + expect(snapshot.result).toEqual({ + data: { hello: "from cache" }, + networkStatus: NetworkStatus.ready, + error: undefined, + }); + } +}); -// expect(_result).toEqual({ -// data: { hello: 'from cache' }, -// loading: false, -// networkStatus: 7, -// }); -// }); // it('partial data is present in the cache so it is ignored and network request is made', async () => { // const query = gql` // { From 1808468b119a3b283d73c738b31433ae8b7beea2 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 17 Oct 2023 12:39:33 -0600 Subject: [PATCH 015/199] Add test to check partial data --- .../__tests__/useInteractiveQuery.test.tsx | 141 ++++++++++++------ 1 file changed, 99 insertions(+), 42 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index 84441255b45..621b4de34b7 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -51,7 +51,7 @@ import { import { FetchMoreFunction, RefetchFunction } from "../useSuspenseQuery"; import { RefetchWritePolicy } from "../../../core/watchQueryOptions"; import invariant from "ts-invariant"; -import { profile } from "../../../testing/internal"; +import { profile, spyOnConsole } from "../../../testing/internal"; interface SimpleQueryData { greeting: string; @@ -1274,53 +1274,110 @@ it("all data is present in the cache, no network request is made", async () => { } }); -// it('partial data is present in the cache so it is ignored and network request is made', async () => { -// const query = gql` -// { -// hello -// foo -// } -// `; -// const cache = new InMemoryCache(); -// const link = mockSingleLink({ -// request: { query }, -// result: { data: { hello: 'from link', foo: 'bar' } }, -// delay: 20, -// }); +it("partial data is present in the cache so it is ignored and network request is made", async () => { + const user = userEvent.setup(); + const query = gql` + { + hello + foo + } + `; + const cache = new InMemoryCache(); + const link = new MockLink([ + { + request: { query }, + result: { data: { hello: "from link", foo: "bar" } }, + delay: 20, + }, + ]); -// const client = new ApolloClient({ -// link, -// cache, -// }); + const client = new ApolloClient({ + link, + cache, + }); -// // we expect a "Missing field 'foo' while writing result..." error -// // when writing hello to the cache, so we'll silence the console.error -// const originalConsoleError = console.error; -// console.error = () => { -// /* noop */ -// }; -// cache.writeQuery({ query, data: { hello: 'from cache' } }); -// console.error = originalConsoleError; + { + // we expect a "Missing field 'foo' while writing result..." error + // when writing hello to the cache, so we'll silence the console.error + using _consoleSpy = spyOnConsole("error"); + cache.writeQuery({ query, data: { hello: "from cache" } }); + } -// const { result } = renderHook( -// () => useInteractiveQuery(query, { fetchPolicy: 'cache-first' }), -// { -// wrapper: ({ children }) => ( -// {children} -// ), -// } -// ); + function SuspenseFallback() { + ProfiledApp.updateSnapshot((snapshot) => ({ + ...snapshot, + suspenseCount: snapshot.suspenseCount + 1, + })); -// const [queryRef] = result.current; + return

Loading

; + } -// const _result = await queryRef[QUERY_REFERENCE_SYMBOL].promise; + function Parent() { + const [queryRef, loadQuery] = useInteractiveQuery(query); -// expect(_result).toEqual({ -// data: { foo: 'bar', hello: 'from link' }, -// loading: false, -// networkStatus: 7, -// }); -// }); + return ( + <> + + }> + {queryRef && } + + + ); + } + + function Child({ queryRef }: { queryRef: QueryReference }) { + const result = useReadQuery(queryRef); + + ProfiledApp.updateSnapshot((snapshot) => ({ ...snapshot, result })); + + return null; + } + + const ProfiledApp = profile<{ + result: UseReadQueryResult | null; + suspenseCount: number; + }>({ + Component: () => ( + + + + ), + initialSnapshot: { + result: null, + suspenseCount: 0, + }, + }); + + render(); + + { + const { snapshot } = await ProfiledApp.takeRender(); + expect(snapshot.suspenseCount).toBe(0); + expect(snapshot.result).toBe(null); + } + + await act(() => user.click(screen.getByText("Load query"))); + + { + const { snapshot } = await ProfiledApp.takeRender(); + + expect(snapshot.suspenseCount).toBe(1); + expect(snapshot.result).toBe(null); + } + + { + const { snapshot } = await ProfiledApp.takeRender(); + + expect(snapshot.suspenseCount).toBe(1); + expect(snapshot.result).toEqual({ + data: { foo: "bar", hello: "from link" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await expect(ProfiledApp).not.toRerender(); +}); // it('existing data in the cache is ignored', async () => { // const query = gql` From deb928eb20b6176afbdd7eae6f755f4a9fe6e666 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 17 Oct 2023 12:46:38 -0600 Subject: [PATCH 016/199] Use await on toRerender assertion --- .../hooks/__tests__/useInteractiveQuery.test.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index 621b4de34b7..eb3a323f6ed 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -732,7 +732,7 @@ it("loads a query when the load query function is called", async () => { }); } - expect(ProfiledApp).not.toRerender(); + await expect(ProfiledApp).not.toRerender(); }); it("allows the client to be overridden", async () => { @@ -825,7 +825,7 @@ it("allows the client to be overridden", async () => { }); } - expect(ProfiledApp).not.toRerender(); + await expect(ProfiledApp).not.toRerender(); }); it("passes context to the link", async () => { @@ -919,7 +919,7 @@ it("passes context to the link", async () => { }); } - expect(ProfiledApp).not.toRerender(); + await expect(ProfiledApp).not.toRerender(); }); it('enables canonical results when canonizeResults is "true"', async () => { @@ -1032,7 +1032,7 @@ it('enables canonical results when canonizeResults is "true"', async () => { expect(values).toEqual([0, 1, 2, 3, 5]); } - expect(ProfiledApp).not.toRerender(); + await expect(ProfiledApp).not.toRerender(); }); it("can disable canonical results when the cache's canonizeResults setting is true", async () => { @@ -1141,7 +1141,7 @@ it("can disable canonical results when the cache's canonizeResults setting is tr expect(values).toEqual([0, 1, 1, 2, 3, 5]); } - expect(ProfiledApp).not.toRerender(); + await expect(ProfiledApp).not.toRerender(); }); // // TODO(FIXME): test fails, should return cache data first if it exists @@ -1272,6 +1272,8 @@ it("all data is present in the cache, no network request is made", async () => { error: undefined, }); } + + await expect(ProfiledApp).not.toRerender(); }); it("partial data is present in the cache so it is ignored and network request is made", async () => { From e7d94464ee9cf13ebbe7f20b2cee1ee03647b27c Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 17 Oct 2023 14:18:06 -0600 Subject: [PATCH 017/199] Add tests to check network-only and no-cache fetch policies --- .../__tests__/useInteractiveQuery.test.tsx | 261 +++++++++++++----- 1 file changed, 188 insertions(+), 73 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index eb3a323f6ed..a6d01dbfc0a 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -1381,92 +1381,207 @@ it("partial data is present in the cache so it is ignored and network request is await expect(ProfiledApp).not.toRerender(); }); -// it('existing data in the cache is ignored', async () => { -// const query = gql` -// { -// hello -// } -// `; -// const cache = new InMemoryCache(); -// const link = mockSingleLink({ -// request: { query }, -// result: { data: { hello: 'from link' } }, -// delay: 20, -// }); +it("existing data in the cache is ignored when `fetchPolicy` is 'network-only'", async () => { + const user = userEvent.setup(); + const query = gql` + query { + hello + } + `; + const cache = new InMemoryCache(); + const link = new MockLink([ + { + request: { query }, + result: { data: { hello: "from link" } }, + delay: 20, + }, + ]); -// const client = new ApolloClient({ -// link, -// cache, -// }); + const client = new ApolloClient({ + link, + cache, + }); -// cache.writeQuery({ query, data: { hello: 'from cache' } }); + cache.writeQuery({ query, data: { hello: "from cache" } }); -// const { result } = renderHook( -// () => useInteractiveQuery(query, { fetchPolicy: 'network-only' }), -// { -// wrapper: ({ children }) => ( -// {children} -// ), -// } -// ); + function SuspenseFallback() { + ProfiledApp.updateSnapshot((snapshot) => ({ + ...snapshot, + suspenseCount: snapshot.suspenseCount + 1, + })); -// const [queryRef] = result.current; + return

Loading

; + } -// const _result = await queryRef[QUERY_REFERENCE_SYMBOL].promise; + function Parent() { + const [queryRef, loadQuery] = useInteractiveQuery(query, { + fetchPolicy: "network-only", + }); -// expect(_result).toEqual({ -// data: { hello: 'from link' }, -// loading: false, -// networkStatus: 7, -// }); -// expect(client.cache.extract()).toEqual({ -// ROOT_QUERY: { __typename: 'Query', hello: 'from link' }, -// }); -// }); + return ( + <> + + }> + {queryRef && } + + + ); + } -// it('fetches data from the network but does not update the cache', async () => { -// const query = gql` -// { -// hello -// } -// `; -// const cache = new InMemoryCache(); -// const link = mockSingleLink({ -// request: { query }, -// result: { data: { hello: 'from link' } }, -// delay: 20, -// }); + function Child({ queryRef }: { queryRef: QueryReference }) { + const result = useReadQuery(queryRef); -// const client = new ApolloClient({ -// link, -// cache, -// }); + ProfiledApp.updateSnapshot((snapshot) => ({ ...snapshot, result })); -// cache.writeQuery({ query, data: { hello: 'from cache' } }); + return null; + } -// const { result } = renderHook( -// () => useInteractiveQuery(query, { fetchPolicy: 'no-cache' }), -// { -// wrapper: ({ children }) => ( -// {children} -// ), -// } -// ); + const ProfiledApp = profile<{ + result: UseReadQueryResult | null; + suspenseCount: number; + }>({ + Component: () => ( + + + + ), + initialSnapshot: { + result: null, + suspenseCount: 0, + }, + }); -// const [queryRef] = result.current; + render(); -// const _result = await queryRef[QUERY_REFERENCE_SYMBOL].promise; + { + const { snapshot } = await ProfiledApp.takeRender(); + expect(snapshot.suspenseCount).toBe(0); + expect(snapshot.result).toBe(null); + } -// expect(_result).toEqual({ -// data: { hello: 'from link' }, -// loading: false, -// networkStatus: 7, -// }); -// // ...but not updated in the cache -// expect(client.cache.extract()).toEqual({ -// ROOT_QUERY: { __typename: 'Query', hello: 'from cache' }, -// }); -// }); + await act(() => user.click(screen.getByText("Load query"))); + + { + const { snapshot } = await ProfiledApp.takeRender(); + + expect(snapshot.suspenseCount).toBe(1); + expect(snapshot.result).toBe(null); + } + + { + const { snapshot } = await ProfiledApp.takeRender(); + + expect(snapshot.suspenseCount).toBe(1); + expect(snapshot.result).toEqual({ + data: { hello: "from link" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await expect(ProfiledApp).not.toRerender(); +}); + +it("fetches data from the network but does not update the cache when `fetchPolicy` is 'no-cache'", async () => { + const user = userEvent.setup(); + const query = gql` + query { + hello + } + `; + const cache = new InMemoryCache(); + const link = new MockLink([ + { + request: { query }, + result: { data: { hello: "from link" } }, + delay: 20, + }, + ]); + + const client = new ApolloClient({ link, cache }); + + cache.writeQuery({ query, data: { hello: "from cache" } }); + + function SuspenseFallback() { + ProfiledApp.updateSnapshot((snapshot) => ({ + ...snapshot, + suspenseCount: snapshot.suspenseCount + 1, + })); + + return

Loading

; + } + + function Parent() { + const [queryRef, loadQuery] = useInteractiveQuery(query, { + fetchPolicy: "no-cache", + }); + + return ( + <> + + }> + {queryRef && } + + + ); + } + + function Child({ queryRef }: { queryRef: QueryReference }) { + const result = useReadQuery(queryRef); + + ProfiledApp.updateSnapshot((snapshot) => ({ ...snapshot, result })); + + return null; + } + + const ProfiledApp = profile<{ + result: UseReadQueryResult | null; + suspenseCount: number; + }>({ + Component: () => ( + + + + ), + initialSnapshot: { + result: null, + suspenseCount: 0, + }, + }); + + render(); + + { + const { snapshot } = await ProfiledApp.takeRender(); + expect(snapshot.suspenseCount).toBe(0); + expect(snapshot.result).toBe(null); + } + + await act(() => user.click(screen.getByText("Load query"))); + + { + const { snapshot } = await ProfiledApp.takeRender(); + + expect(snapshot.suspenseCount).toBe(1); + expect(snapshot.result).toBe(null); + } + + { + const { snapshot } = await ProfiledApp.takeRender(); + + expect(snapshot.suspenseCount).toBe(1); + expect(snapshot.result).toEqual({ + data: { hello: "from link" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + expect(client.extract()).toEqual({ + ROOT_QUERY: { __typename: "Query", hello: "from cache" }, + }); + } + + await expect(ProfiledApp).not.toRerender(); +}); describe("integration tests with useReadQuery", () => { it("suspends and renders hello", async () => { From 17563d287eeeaedc1144b620673d41a01e626dbc Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 17 Oct 2023 14:18:42 -0600 Subject: [PATCH 018/199] Remove outer describe for integration with useReadQuery --- .../__tests__/useInteractiveQuery.test.tsx | 504 +++++++++--------- 1 file changed, 250 insertions(+), 254 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index a6d01dbfc0a..48387054cf3 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -1583,316 +1583,312 @@ it("fetches data from the network but does not update the cache when `fetchPolic await expect(ProfiledApp).not.toRerender(); }); -describe("integration tests with useReadQuery", () => { - it("suspends and renders hello", async () => { - const user = userEvent.setup(); - const { renders, loadQueryButton } = renderIntegrationTest(); +it("suspends and renders hello", async () => { + const user = userEvent.setup(); + const { renders, loadQueryButton } = renderIntegrationTest(); - expect(renders.suspenseCount).toBe(0); + expect(renders.suspenseCount).toBe(0); - await act(() => user.click(loadQueryButton)); + await act(() => user.click(loadQueryButton)); - expect(await screen.findByText("loading")).toBeInTheDocument(); + expect(await screen.findByText("loading")).toBeInTheDocument(); - // the parent component re-renders when promise fulfilled - expect(await screen.findByText("hello")).toBeInTheDocument(); - expect(renders.suspenseCount).toBe(1); - expect(renders.count).toBe(1); - }); + // the parent component re-renders when promise fulfilled + expect(await screen.findByText("hello")).toBeInTheDocument(); + expect(renders.suspenseCount).toBe(1); + expect(renders.count).toBe(1); +}); - it("works with startTransition to change variables", async () => { - type Variables = { +it("works with startTransition to change variables", async () => { + type Variables = { + id: string; + }; + + interface Data { + todo: { id: string; + name: string; + completed: boolean; }; + } + const user = userEvent.setup(); - interface Data { - todo: { - id: string; - name: string; - completed: boolean; - }; - } - const user = userEvent.setup(); - - const query: TypedDocumentNode = gql` - query TodoItemQuery($id: ID!) { - todo(id: $id) { - id - name - completed - } + const query: TypedDocumentNode = gql` + query TodoItemQuery($id: ID!) { + todo(id: $id) { + id + name + completed } - `; + } + `; - const mocks: MockedResponse[] = [ - { - request: { query, variables: { id: "1" } }, - result: { - data: { todo: { id: "1", name: "Clean room", completed: false } }, - }, - delay: 10, + const mocks: MockedResponse[] = [ + { + request: { query, variables: { id: "1" } }, + result: { + data: { todo: { id: "1", name: "Clean room", completed: false } }, }, - { - request: { query, variables: { id: "2" } }, - result: { - data: { - todo: { id: "2", name: "Take out trash", completed: true }, - }, + delay: 10, + }, + { + request: { query, variables: { id: "2" } }, + result: { + data: { + todo: { id: "2", name: "Take out trash", completed: true }, }, - delay: 10, }, - ]; + delay: 10, + }, + ]; - const client = new ApolloClient({ - link: new MockLink(mocks), - cache: new InMemoryCache(), - }); + const client = new ApolloClient({ + link: new MockLink(mocks), + cache: new InMemoryCache(), + }); - function App() { - return ( - - }> - - - - ); - } + function App() { + return ( + + }> + + + + ); + } - function SuspenseFallback() { - return

Loading

; - } + function SuspenseFallback() { + return

Loading

; + } - function Parent() { - const [queryRef, loadQuery] = useInteractiveQuery(query); + function Parent() { + const [queryRef, loadQuery] = useInteractiveQuery(query); - return ( -
- - {queryRef && ( - loadQuery({ id })} /> - )} -
- ); - } + return ( +
+ + {queryRef && ( + loadQuery({ id })} /> + )} +
+ ); + } - function Todo({ - queryRef, - onChange, - }: { - queryRef: QueryReference; - onChange: (id: string) => void; - }) { - const { data } = useReadQuery(queryRef); - const [isPending, startTransition] = React.useTransition(); - const { todo } = data; + function Todo({ + queryRef, + onChange, + }: { + queryRef: QueryReference; + onChange: (id: string) => void; + }) { + const { data } = useReadQuery(queryRef); + const [isPending, startTransition] = React.useTransition(); + const { todo } = data; - return ( - <> - -
- {todo.name} - {todo.completed && " (completed)"} -
- - ); - } + return ( + <> + +
+ {todo.name} + {todo.completed && " (completed)"} +
+ + ); + } - render(); + render(); - await act(() => user.click(screen.getByText("Load first todo"))); + await act(() => user.click(screen.getByText("Load first todo"))); - expect(screen.getByText("Loading")).toBeInTheDocument(); - expect(await screen.findByTestId("todo")).toBeInTheDocument(); + expect(screen.getByText("Loading")).toBeInTheDocument(); + expect(await screen.findByTestId("todo")).toBeInTheDocument(); - const todo = screen.getByTestId("todo"); - const button = screen.getByText("Refresh"); + const todo = screen.getByTestId("todo"); + const button = screen.getByText("Refresh"); - expect(todo).toHaveTextContent("Clean room"); + expect(todo).toHaveTextContent("Clean room"); - await act(() => user.click(button)); + 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(); + // 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(todo).toHaveAttribute("aria-busy", "true"); + // We can ensure this works with isPending from useTransition in the process + expect(todo).toHaveAttribute("aria-busy", "true"); - // Ensure we are showing the stale UI until the new todo has loaded - expect(todo).toHaveTextContent("Clean room"); + // Ensure we are showing the stale UI until the new todo has loaded + expect(todo).toHaveTextContent("Clean room"); - // Eventually we should see the updated todo content once its done - // suspending. - await waitFor(() => { - expect(todo).toHaveTextContent("Take out trash (completed)"); - }); + // Eventually we should see the updated todo content once its done + // suspending. + await waitFor(() => { + expect(todo).toHaveTextContent("Take out trash (completed)"); }); +}); - it('does not suspend deferred queries with data in the cache and using a "cache-and-network" fetch policy', async () => { - interface Data { - greeting: { - __typename: string; - message: string; - recipient: { name: string; __typename: string }; - }; - } +it('does not suspend deferred queries with data in the cache and using a "cache-and-network" fetch policy', async () => { + interface Data { + greeting: { + __typename: string; + message: string; + recipient: { name: string; __typename: string }; + }; + } - const user = userEvent.setup(); + const user = userEvent.setup(); - const query: TypedDocumentNode = gql` - query { - greeting { - message - ... on Greeting @defer { - recipient { - name - } + const query: TypedDocumentNode = gql` + query { + greeting { + message + ... on Greeting @defer { + recipient { + name } } } - `; + } + `; - const link = new MockSubscriptionLink(); - const cache = new InMemoryCache(); - cache.writeQuery({ - query, - data: { - greeting: { - __typename: "Greeting", - message: "Hello cached", - recipient: { __typename: "Person", name: "Cached Alice" }, - }, + const link = new MockSubscriptionLink(); + const cache = new InMemoryCache(); + cache.writeQuery({ + query, + data: { + greeting: { + __typename: "Greeting", + message: "Hello cached", + recipient: { __typename: "Person", name: "Cached Alice" }, }, - }); - const client = new ApolloClient({ cache, link }); - let renders = 0; - let suspenseCount = 0; + }, + }); + const client = new ApolloClient({ cache, link }); + let renders = 0; + let suspenseCount = 0; - function App() { - return ( - - }> - - - - ); - } + function App() { + return ( + + }> + + + + ); + } - function SuspenseFallback() { - suspenseCount++; - return

Loading

; - } + function SuspenseFallback() { + suspenseCount++; + return

Loading

; + } - function Parent() { - const [queryRef, loadQuery] = useInteractiveQuery(query, { - fetchPolicy: "cache-and-network", - }); - return ( -
- - {queryRef && } -
- ); - } + function Parent() { + const [queryRef, loadQuery] = useInteractiveQuery(query, { + fetchPolicy: "cache-and-network", + }); + return ( +
+ + {queryRef && } +
+ ); + } - function Todo({ queryRef }: { queryRef: QueryReference }) { - const { data, networkStatus, error } = useReadQuery(queryRef); - const { greeting } = data; - renders++; + function Todo({ queryRef }: { queryRef: QueryReference }) { + const { data, networkStatus, error } = useReadQuery(queryRef); + const { greeting } = data; + renders++; - return ( - <> -
Message: {greeting.message}
-
Recipient: {greeting.recipient.name}
-
Network status: {networkStatus}
-
Error: {error ? error.message : "none"}
- - ); - } + return ( + <> +
Message: {greeting.message}
+
Recipient: {greeting.recipient.name}
+
Network status: {networkStatus}
+
Error: {error ? error.message : "none"}
+ + ); + } - render(); + render(); - await act(() => user.click(screen.getByText("Load todo"))); + await act(() => user.click(screen.getByText("Load todo"))); - expect(screen.getByText(/Message/i)).toHaveTextContent( - "Message: Hello cached" - ); - expect(screen.getByText(/Recipient/i)).toHaveTextContent( - "Recipient: Cached Alice" - ); - expect(screen.getByText(/Network status/i)).toHaveTextContent( - "Network status: 1" // loading - ); - expect(screen.getByText(/Error/i)).toHaveTextContent("none"); + expect(screen.getByText(/Message/i)).toHaveTextContent( + "Message: Hello cached" + ); + expect(screen.getByText(/Recipient/i)).toHaveTextContent( + "Recipient: Cached Alice" + ); + expect(screen.getByText(/Network status/i)).toHaveTextContent( + "Network status: 1" // loading + ); + expect(screen.getByText(/Error/i)).toHaveTextContent("none"); - link.simulateResult({ - result: { - data: { - greeting: { __typename: "Greeting", message: "Hello world" }, - }, - hasNext: true, + link.simulateResult({ + result: { + data: { + greeting: { __typename: "Greeting", message: "Hello world" }, }, - }); + hasNext: true, + }, + }); - await waitFor(() => { - expect(screen.getByText(/Message/i)).toHaveTextContent( - "Message: Hello world" - ); - }); - expect(screen.getByText(/Recipient/i)).toHaveTextContent( - "Recipient: Cached Alice" - ); - expect(screen.getByText(/Network status/i)).toHaveTextContent( - "Network status: 7" // ready + await waitFor(() => { + expect(screen.getByText(/Message/i)).toHaveTextContent( + "Message: Hello world" ); - expect(screen.getByText(/Error/i)).toHaveTextContent("none"); + }); + expect(screen.getByText(/Recipient/i)).toHaveTextContent( + "Recipient: Cached Alice" + ); + expect(screen.getByText(/Network status/i)).toHaveTextContent( + "Network status: 7" // ready + ); + expect(screen.getByText(/Error/i)).toHaveTextContent("none"); - link.simulateResult({ - result: { - incremental: [ - { - data: { - recipient: { name: "Alice", __typename: "Person" }, - __typename: "Greeting", - }, - path: ["greeting"], + link.simulateResult({ + result: { + incremental: [ + { + data: { + recipient: { name: "Alice", __typename: "Person" }, + __typename: "Greeting", }, - ], - hasNext: false, - }, - }); + path: ["greeting"], + }, + ], + hasNext: false, + }, + }); - await waitFor(() => { - expect(screen.getByText(/Recipient/i)).toHaveTextContent( - "Recipient: Alice" - ); - }); - expect(screen.getByText(/Message/i)).toHaveTextContent( - "Message: Hello world" - ); - expect(screen.getByText(/Network status/i)).toHaveTextContent( - "Network status: 7" // ready + await waitFor(() => { + expect(screen.getByText(/Recipient/i)).toHaveTextContent( + "Recipient: Alice" ); - expect(screen.getByText(/Error/i)).toHaveTextContent("none"); - - expect(renders).toBe(3); - expect(suspenseCount).toBe(0); }); + expect(screen.getByText(/Message/i)).toHaveTextContent( + "Message: Hello world" + ); + expect(screen.getByText(/Network status/i)).toHaveTextContent( + "Network status: 7" // ready + ); + expect(screen.getByText(/Error/i)).toHaveTextContent("none"); + + expect(renders).toBe(3); + expect(suspenseCount).toBe(0); }); it("reacts to cache updates", async () => { From c001bfdffa935eea5dbe92344e016e59c9087431 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 17 Oct 2023 14:22:34 -0600 Subject: [PATCH 019/199] Remove duplicate test --- .../hooks/__tests__/useInteractiveQuery.test.tsx | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index 48387054cf3..aa039502093 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -1583,22 +1583,6 @@ it("fetches data from the network but does not update the cache when `fetchPolic await expect(ProfiledApp).not.toRerender(); }); -it("suspends and renders hello", async () => { - const user = userEvent.setup(); - const { renders, loadQueryButton } = renderIntegrationTest(); - - expect(renders.suspenseCount).toBe(0); - - await act(() => user.click(loadQueryButton)); - - expect(await screen.findByText("loading")).toBeInTheDocument(); - - // the parent component re-renders when promise fulfilled - expect(await screen.findByText("hello")).toBeInTheDocument(); - expect(renders.suspenseCount).toBe(1); - expect(renders.count).toBe(1); -}); - it("works with startTransition to change variables", async () => { type Variables = { id: string; From 948015b3d587df6b633711921767711b01cc2187 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 17 Oct 2023 14:22:40 -0600 Subject: [PATCH 020/199] Get test with startTransition and changing variables working --- src/react/hooks/__tests__/useInteractiveQuery.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index aa039502093..9e46a4b8051 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -1607,7 +1607,7 @@ it("works with startTransition to change variables", async () => { } `; - const mocks: MockedResponse[] = [ + const mocks = [ { request: { query, variables: { id: "1" } }, result: { From 65654294e1f85048bada5580efd001367e6a04a6 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 17 Oct 2023 14:26:28 -0600 Subject: [PATCH 021/199] Add test and missing implementation for changing error policies --- src/react/hooks/__tests__/useInteractiveQuery.test.tsx | 9 ++++++--- src/react/hooks/useInteractiveQuery.ts | 5 +++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index 9e46a4b8051..14ac640ded4 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -1936,7 +1936,7 @@ it("applies `errorPolicy` on next fetch when it changes between renders", async const user = userEvent.setup(); - const query: TypedDocumentNode = gql` + const query: TypedDocumentNode = gql` query { greeting } @@ -1966,7 +1966,7 @@ it("applies `errorPolicy` on next fetch when it changes between renders", async function Parent() { const [errorPolicy, setErrorPolicy] = React.useState("none"); - const [queryRef, { refetch }] = useInteractiveQuery(query, { + const [queryRef, loadQuery, { refetch }] = useInteractiveQuery(query, { errorPolicy, }); @@ -1976,8 +1976,9 @@ it("applies `errorPolicy` on next fetch when it changes between renders", async Change error policy + }> - + {queryRef && } ); @@ -2005,6 +2006,8 @@ it("applies `errorPolicy` on next fetch when it changes between renders", async render(); + await act(() => user.click(screen.getByText("Load greeting"))); + expect(await screen.findByTestId("greeting")).toHaveTextContent("Hello"); await act(() => user.click(screen.getByText("Change error policy"))); diff --git a/src/react/hooks/useInteractiveQuery.ts b/src/react/hooks/useInteractiveQuery.ts index 23f97c52de6..6fe99e84ae9 100644 --- a/src/react/hooks/useInteractiveQuery.ts +++ b/src/react/hooks/useInteractiveQuery.ts @@ -115,6 +115,11 @@ export function useInteractiveQuery< queryRef ? new Map([[queryRef.key, queryRef.promise]]) : new Map() ); + if (queryRef?.didChangeOptions(watchQueryOptions)) { + const promise = queryRef.applyOptions(watchQueryOptions); + promiseCache.set(queryRef.key, promise); + } + if (queryRef) { queryRef.promiseCache = promiseCache; } From d9a3d1271d89cdaddfa756f16c4775ef08869131 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 17 Oct 2023 14:42:21 -0600 Subject: [PATCH 022/199] Update test that checks changes to context between renders --- src/react/hooks/__tests__/useInteractiveQuery.test.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index 14ac640ded4..04dbb027571 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -2025,7 +2025,7 @@ it("applies `context` on next fetch when it changes between renders", async () = const user = userEvent.setup(); - const query: TypedDocumentNode = gql` + const query: TypedDocumentNode = gql` query { context } @@ -2050,7 +2050,7 @@ it("applies `context` on next fetch when it changes between renders", async () = function Parent() { const [phase, setPhase] = React.useState("initial"); - const [queryRef, { refetch }] = useInteractiveQuery(query, { + const [queryRef, loadQuery, { refetch }] = useInteractiveQuery(query, { context: { phase }, }); @@ -2058,8 +2058,9 @@ it("applies `context` on next fetch when it changes between renders", async () = <> + }> - + {queryRef && } ); @@ -2081,6 +2082,8 @@ it("applies `context` on next fetch when it changes between renders", async () = render(); + await act(() => user.click(screen.getByText("Load query"))); + expect(await screen.findByTestId("context")).toHaveTextContent("initial"); await act(() => user.click(screen.getByText("Update context"))); From 95141037f01f7fc2f2b172cf9ada93bebe22ab5b Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 19 Oct 2023 17:02:12 +0100 Subject: [PATCH 023/199] Move suspense boundary in test to better reflect recommended usage --- .../hooks/__tests__/useInteractiveQuery.test.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index 04dbb027571..f1cf5c440cc 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -1634,9 +1634,7 @@ it("works with startTransition to change variables", async () => { function App() { return ( - }> - - + ); } @@ -1651,9 +1649,11 @@ it("works with startTransition to change variables", async () => { return (
- {queryRef && ( - loadQuery({ id })} /> - )} + }> + {queryRef && ( + loadQuery({ id })} /> + )} +
); } From e3cb1e3bef27b685cace14544be41ea3cbe01f32 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 25 Oct 2023 17:09:25 +0100 Subject: [PATCH 024/199] Temp skip all failing tests to update one-by-one --- .../__tests__/useInteractiveQuery.test.tsx | 148 +++++++++--------- 1 file changed, 74 insertions(+), 74 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index f1cf5c440cc..89ba4be2be1 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -1144,46 +1144,46 @@ it("can disable canonical results when the cache's canonizeResults setting is tr await expect(ProfiledApp).not.toRerender(); }); -// // TODO(FIXME): test fails, should return cache data first if it exists -// it.skip('returns initial cache data followed by network data when the fetch policy is `cache-and-network`', async () => { -// const query = gql` -// { -// hello -// } -// `; -// const cache = new InMemoryCache(); -// const link = mockSingleLink({ -// request: { query }, -// result: { data: { hello: 'from link' } }, -// delay: 20, -// }); - -// const client = new ApolloClient({ -// link, -// cache, -// }); - -// cache.writeQuery({ query, data: { hello: 'from cache' } }); - -// const { result } = renderHook( -// () => useInteractiveQuery(query, { fetchPolicy: 'cache-and-network' }), -// { -// wrapper: ({ children }) => ( -// {children} -// ), -// } -// ); - -// const [queryRef] = result.current; - -// const _result = await queryRef[QUERY_REFERENCE_SYMBOL].promise; - -// expect(_result).toEqual({ -// data: { hello: 'from link' }, -// loading: false, -// networkStatus: 7, -// }); -// }); +// TODO(FIXME): test fails, should return cache data first if it exists +it.skip("returns initial cache data followed by network data when the fetch policy is `cache-and-network`", async () => { + const query = gql` + { + hello + } + `; + const cache = new InMemoryCache(); + const link = mockSingleLink({ + request: { query }, + result: { data: { hello: "from link" } }, + delay: 20, + }); + + const client = new ApolloClient({ + link, + cache, + }); + + cache.writeQuery({ query, data: { hello: "from cache" } }); + + const { result } = renderHook( + () => useInteractiveQuery(query, { fetchPolicy: "cache-and-network" }), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + const [queryRef] = result.current; + + const _result = await queryRef[QUERY_REFERENCE_SYMBOL].promise; + + expect(_result).toEqual({ + data: { hello: "from link" }, + loading: false, + networkStatus: 7, + }); +}); it("all data is present in the cache, no network request is made", async () => { const query = gql` @@ -1724,7 +1724,7 @@ it("works with startTransition to change variables", async () => { }); }); -it('does not suspend deferred queries with data in the cache and using a "cache-and-network" fetch policy', async () => { +it.skip('does not suspend deferred queries with data in the cache and using a "cache-and-network" fetch policy', async () => { interface Data { greeting: { __typename: string; @@ -1875,7 +1875,7 @@ it('does not suspend deferred queries with data in the cache and using a "cache- expect(suspenseCount).toBe(0); }); -it("reacts to cache updates", async () => { +it.skip("reacts to cache updates", async () => { const { renders, client, query, loadQueryButton, user } = renderIntegrationTest(); @@ -1909,7 +1909,7 @@ it("reacts to cache updates", async () => { expect(renders.suspenseCount).toBe(1); }); -it("reacts to variables updates", async () => { +it.skip("reacts to variables updates", async () => { const { renders, user, loadQueryButton } = renderVariablesIntegrationTest({ variables: { id: "1" }, }); @@ -1929,7 +1929,7 @@ it("reacts to variables updates", async () => { expect(await screen.findByText("2 - Black Widow")).toBeInTheDocument(); }); -it("applies `errorPolicy` on next fetch when it changes between renders", async () => { +it.skip("applies `errorPolicy` on next fetch when it changes between renders", async () => { interface Data { greeting: string; } @@ -2018,7 +2018,7 @@ it("applies `errorPolicy` on next fetch when it changes between renders", async expect(await screen.findByTestId("error")).toHaveTextContent("oops"); }); -it("applies `context` on next fetch when it changes between renders", async () => { +it.skip("applies `context` on next fetch when it changes between renders", async () => { interface Data { context: Record; } @@ -2095,7 +2095,7 @@ it("applies `context` on next fetch when it changes between renders", async () = // NOTE: We only test the `false` -> `true` path here. If the option changes // from `true` -> `false`, the data has already been canonized, so it has no // effect on the output. -it("returns canonical results immediately when `canonizeResults` changes from `false` to `true` between renders", async () => { +it.skip("returns canonical results immediately when `canonizeResults` changes from `false` to `true` between renders", async () => { interface Result { __typename: string; value: number; @@ -2210,7 +2210,7 @@ it("returns canonical results immediately when `canonizeResults` changes from `f verifyCanonicalResults(result.current!, true); }); -it("applies changed `refetchWritePolicy` to next fetch when changing between renders", async () => { +it.skip("applies changed `refetchWritePolicy` to next fetch when changing between renders", async () => { interface Data { primes: number[]; } @@ -2347,7 +2347,7 @@ it("applies changed `refetchWritePolicy` to next fetch when changing between ren ]); }); -it("applies `returnPartialData` on next fetch when it changes between renders", async () => { +it.skip("applies `returnPartialData` on next fetch when it changes between renders", async () => { interface Data { character: { __typename: "Character"; @@ -2487,7 +2487,7 @@ it("applies `returnPartialData` on next fetch when it changes between renders", }); }); -it("applies updated `fetchPolicy` on next fetch when it changes between renders", async () => { +it.skip("applies updated `fetchPolicy` on next fetch when it changes between renders", async () => { interface Data { character: { __typename: "Character"; @@ -2604,7 +2604,7 @@ it("applies updated `fetchPolicy` on next fetch when it changes between renders" }); }); -it("properly handles changing options along with changing `variables`", async () => { +it.skip("properly handles changing options along with changing `variables`", async () => { interface Data { character: { __typename: "Character"; @@ -2737,8 +2737,8 @@ it("properly handles changing options along with changing `variables`", async () expect(await screen.findByTestId("error")).toHaveTextContent("oops"); }); -describe("refetch", () => { - it("re-suspends when calling `refetch`", async () => { +describe.skip("refetch", () => { + it.skip("re-suspends when calling `refetch`", async () => { const { renders } = renderVariablesIntegrationTest({ variables: { id: "1" }, }); @@ -2760,7 +2760,7 @@ describe("refetch", () => { await screen.findByText("1 - Spider-Man (updated)") ).toBeInTheDocument(); }); - it("re-suspends when calling `refetch` with new variables", async () => { + it.skip("re-suspends when calling `refetch` with new variables", async () => { interface QueryData { character: { id: string; @@ -2838,7 +2838,7 @@ describe("refetch", () => { }, ]); }); - it("re-suspends multiple times when calling `refetch` multiple times", async () => { + it.skip("re-suspends multiple times when calling `refetch` multiple times", async () => { const { renders } = renderVariablesIntegrationTest({ variables: { id: "1" }, }); @@ -2870,7 +2870,7 @@ describe("refetch", () => { await screen.findByText("1 - Spider-Man (updated again)") ).toBeInTheDocument(); }); - it("throws errors when errors are returned after calling `refetch`", async () => { + it.skip("throws errors when errors are returned after calling `refetch`", async () => { const consoleSpy = jest.spyOn(console, "error").mockImplementation(); interface QueryData { character: { @@ -2930,7 +2930,7 @@ describe("refetch", () => { consoleSpy.mockRestore(); }); - it('ignores errors returned after calling `refetch` when errorPolicy is set to "ignore"', async () => { + it.skip('ignores errors returned after calling `refetch` when errorPolicy is set to "ignore"', async () => { interface QueryData { character: { id: string; @@ -2979,7 +2979,7 @@ describe("refetch", () => { expect(renders.errorCount).toBe(0); expect(renders.errors).toEqual([]); }); - it('returns errors after calling `refetch` when errorPolicy is set to "all"', async () => { + it.skip('returns errors after calling `refetch` when errorPolicy is set to "all"', async () => { interface QueryData { character: { id: string; @@ -3030,7 +3030,7 @@ describe("refetch", () => { expect(await screen.findByText("Something went wrong")).toBeInTheDocument(); }); - it('handles partial data results after calling `refetch` when errorPolicy is set to "all"', async () => { + it.skip('handles partial data results after calling `refetch` when errorPolicy is set to "all"', async () => { interface QueryData { character: { id: string; @@ -3099,7 +3099,7 @@ describe("refetch", () => { }, ]); }); - it("`refetch` works with startTransition to allow React to show stale UI until finished suspending", async () => { + it.skip("`refetch` works with startTransition to allow React to show stale UI until finished suspending", async () => { type Variables = { id: string; }; @@ -3234,14 +3234,14 @@ describe("refetch", () => { }); }); -describe("fetchMore", () => { +describe.skip("fetchMore", () => { function getItemTexts() { return screen.getAllByTestId(/letter/).map( // eslint-disable-next-line testing-library/no-node-access (li) => li.firstChild!.textContent ); } - it("re-suspends when calling `fetchMore` with different variables", async () => { + it.skip("re-suspends when calling `fetchMore` with different variables", async () => { const { renders } = renderPaginatedIntegrationTest(); expect(renders.suspenseCount).toBe(1); @@ -3263,7 +3263,7 @@ describe("fetchMore", () => { expect(getItemTexts()).toStrictEqual(["C", "D"]); }); - it("properly uses `updateQuery` when calling `fetchMore`", async () => { + it.skip("properly uses `updateQuery` when calling `fetchMore`", async () => { const { renders } = renderPaginatedIntegrationTest({ updateQuery: true, }); @@ -3290,7 +3290,7 @@ describe("fetchMore", () => { expect(moreItems).toHaveLength(4); expect(getItemTexts()).toStrictEqual(["A", "B", "C", "D"]); }); - it("properly uses cache field policies when calling `fetchMore` without `updateQuery`", async () => { + it.skip("properly uses cache field policies when calling `fetchMore` without `updateQuery`", async () => { const { renders } = renderPaginatedIntegrationTest({ fieldPolicies: true, }); @@ -3316,7 +3316,7 @@ describe("fetchMore", () => { expect(moreItems).toHaveLength(4); expect(getItemTexts()).toStrictEqual(["A", "B", "C", "D"]); }); - it("`fetchMore` works with startTransition to allow React to show stale UI until finished suspending", async () => { + it.skip("`fetchMore` works with startTransition to allow React to show stale UI until finished suspending", async () => { type Variables = { offset: number; }; @@ -3484,7 +3484,7 @@ describe("fetchMore", () => { }); }); - it('honors refetchWritePolicy set to "merge"', async () => { + it.skip('honors refetchWritePolicy set to "merge"', async () => { const user = userEvent.setup(); const query: TypedDocumentNode< @@ -3614,7 +3614,7 @@ describe("fetchMore", () => { ]); }); - it('defaults refetchWritePolicy to "overwrite"', async () => { + it.skip('defaults refetchWritePolicy to "overwrite"', async () => { const user = userEvent.setup(); const query: TypedDocumentNode< @@ -3740,7 +3740,7 @@ describe("fetchMore", () => { ]); }); - it('does not suspend when partial data is in the cache and using a "cache-first" fetch policy with returnPartialData', async () => { + it.skip('does not suspend when partial data is in the cache and using a "cache-first" fetch policy with returnPartialData', async () => { interface Data { character: { id: string; @@ -3858,7 +3858,7 @@ describe("fetchMore", () => { expect(renders.suspenseCount).toBe(0); }); - it('suspends and does not use partial data when changing variables and using a "cache-first" fetch policy with returnPartialData', async () => { + it.skip('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) { @@ -3918,7 +3918,7 @@ describe("fetchMore", () => { ]); }); - it('suspends when partial data is in the cache and using a "network-only" fetch policy with returnPartialData', async () => { + it.skip('suspends when partial data is in the cache and using a "network-only" fetch policy with returnPartialData', async () => { interface Data { character: { id: string; @@ -4047,7 +4047,7 @@ describe("fetchMore", () => { ]); }); - it('suspends when partial data is in the cache and using a "no-cache" fetch policy with returnPartialData', async () => { + it.skip('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: { @@ -4179,7 +4179,7 @@ describe("fetchMore", () => { consoleSpy.mockRestore(); }); - it('warns when using returnPartialData with a "no-cache" fetch policy', async () => { + it.skip('warns when using returnPartialData with a "no-cache" fetch policy', async () => { const consoleSpy = jest.spyOn(console, "warn").mockImplementation(); const query: TypedDocumentNode = gql` @@ -4211,7 +4211,7 @@ describe("fetchMore", () => { consoleSpy.mockRestore(); }); - it('does not suspend when partial data is in the cache and using a "cache-and-network" fetch policy with returnPartialData', async () => { + it.skip('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; @@ -4350,7 +4350,7 @@ describe("fetchMore", () => { ]); }); - it('suspends and does not use partial data when changing variables and using a "cache-and-network" fetch policy with returnPartialData', async () => { + it.skip('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) { @@ -4405,7 +4405,7 @@ describe("fetchMore", () => { ]); }); - it('does not suspend deferred queries with partial data in the cache and using a "cache-first" fetch policy with `returnPartialData`', async () => { + it.skip('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; From c6bd65523c8a361cfe694fa60f287b7dba767427 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 7 Nov 2023 19:03:57 -0700 Subject: [PATCH 025/199] Get test for cache-and-network updated to work correctly --- .../__tests__/useInteractiveQuery.test.tsx | 118 ++++++++++++++---- 1 file changed, 91 insertions(+), 27 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index 89ba4be2be1..44a32a541ee 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -1144,45 +1144,109 @@ it("can disable canonical results when the cache's canonizeResults setting is tr await expect(ProfiledApp).not.toRerender(); }); -// TODO(FIXME): test fails, should return cache data first if it exists -it.skip("returns initial cache data followed by network data when the fetch policy is `cache-and-network`", async () => { - const query = gql` - { +it("returns initial cache data followed by network data when the fetch policy is `cache-and-network`", async () => { + const user = userEvent.setup(); + const query: TypedDocumentNode<{ hello: string }, never> = gql` + query { hello } `; const cache = new InMemoryCache(); - const link = mockSingleLink({ - request: { query }, - result: { data: { hello: "from link" } }, - delay: 20, - }); + const link = new MockLink([ + { + request: { query }, + result: { data: { hello: "from link" } }, + delay: 20, + }, + ]); - const client = new ApolloClient({ - link, - cache, - }); + const client = new ApolloClient({ link, cache }); cache.writeQuery({ query, data: { hello: "from cache" } }); - const { result } = renderHook( - () => useInteractiveQuery(query, { fetchPolicy: "cache-and-network" }), - { - wrapper: ({ children }) => ( - {children} - ), - } - ); + function SuspenseFallback() { + ProfiledApp.updateSnapshot((snapshot) => ({ + ...snapshot, + suspenseCount: snapshot.suspenseCount + 1, + })); + + return

Loading

; + } + + function Parent() { + const [queryRef, loadQuery] = useInteractiveQuery(query, { + fetchPolicy: "cache-and-network", + }); + + return ( + <> + + }> + {queryRef && } + + + ); + } + + function Child({ + queryRef, + }: { + queryRef: QueryReference<{ hello: string }>; + }) { + const result = useReadQuery(queryRef); - const [queryRef] = result.current; + ProfiledApp.updateSnapshot((snapshot) => ({ ...snapshot, result })); - const _result = await queryRef[QUERY_REFERENCE_SYMBOL].promise; + return null; + } - expect(_result).toEqual({ - data: { hello: "from link" }, - loading: false, - networkStatus: 7, + const ProfiledApp = profile<{ + result: UseReadQueryResult | null; + suspenseCount: number; + }>({ + Component: () => ( + + + + ), + initialSnapshot: { + result: null, + suspenseCount: 0, + }, }); + + render(); + + { + const { snapshot } = await ProfiledApp.takeRender(); + expect(snapshot.result).toEqual(null); + } + + await act(() => user.click(screen.getByText("Load query"))); + + { + const { snapshot } = await ProfiledApp.takeRender(); + + expect(snapshot.suspenseCount).toBe(0); + expect(snapshot.result).toEqual({ + data: { hello: "from cache" }, + networkStatus: NetworkStatus.loading, + error: undefined, + }); + } + + { + const { snapshot } = await ProfiledApp.takeRender(); + + expect(snapshot.suspenseCount).toBe(0); + expect(snapshot.result).toEqual({ + data: { hello: "from link" }, + networkStatus: NetworkStatus.ready, + error: undefined, + }); + } + + await expect(ProfiledApp).not.toRerender(); }); it("all data is present in the cache, no network request is made", async () => { From 8f145a1cb9bad409be2cf628490a3bf0619a97f7 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 7 Nov 2023 19:23:29 -0700 Subject: [PATCH 026/199] Update test to ensure deferred queries with cache-and-network behave properly --- .../__tests__/useInteractiveQuery.test.tsx | 173 ++++++++++-------- 1 file changed, 101 insertions(+), 72 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index 44a32a541ee..98d5d0eafd0 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -1788,7 +1788,7 @@ it("works with startTransition to change variables", async () => { }); }); -it.skip('does not suspend deferred queries with data in the cache and using a "cache-and-network" fetch policy', async () => { +it('does not suspend deferred queries with data in the cache and using a "cache-and-network" fetch policy', async () => { interface Data { greeting: { __typename: string; @@ -1825,21 +1825,32 @@ it.skip('does not suspend deferred queries with data in the cache and using a "c }, }); const client = new ApolloClient({ cache, link }); - let renders = 0; - let suspenseCount = 0; - function App() { - return ( - - }> - - - - ); - } + const ProfiledApp = profile<{ + result: UseReadQueryResult | null; + suspenseCount: number; + }>({ + Component: () => { + return ( + + }> + + + + ); + }, + initialSnapshot: { + suspenseCount: 0, + result: null, + }, + }); function SuspenseFallback() { - suspenseCount++; + ProfiledApp.updateSnapshot((snapshot) => ({ + ...snapshot, + suspenseCount: snapshot.suspenseCount + 1, + })); + return

Loading

; } @@ -1856,34 +1867,42 @@ it.skip('does not suspend deferred queries with data in the cache and using a "c } function Todo({ queryRef }: { queryRef: QueryReference }) { - const { data, networkStatus, error } = useReadQuery(queryRef); + const result = useReadQuery(queryRef); + const { data, networkStatus, error } = result; const { greeting } = data; - renders++; - return ( - <> -
Message: {greeting.message}
-
Recipient: {greeting.recipient.name}
-
Network status: {networkStatus}
-
Error: {error ? error.message : "none"}
- - ); + ProfiledApp.updateSnapshot((snapshot) => ({ ...snapshot, result })); + + return null; } - render(); + render(); + + { + const { snapshot } = await ProfiledApp.takeRender(); + + expect(snapshot.suspenseCount).toBe(0); + expect(snapshot.result).toBeNull(); + } await act(() => user.click(screen.getByText("Load todo"))); - expect(screen.getByText(/Message/i)).toHaveTextContent( - "Message: Hello cached" - ); - expect(screen.getByText(/Recipient/i)).toHaveTextContent( - "Recipient: Cached Alice" - ); - expect(screen.getByText(/Network status/i)).toHaveTextContent( - "Network status: 1" // loading - ); - expect(screen.getByText(/Error/i)).toHaveTextContent("none"); + { + const { snapshot } = await ProfiledApp.takeRender(); + + expect(snapshot.suspenseCount).toBe(0); + expect(snapshot.result).toEqual({ + data: { + greeting: { + __typename: "Greeting", + message: "Hello cached", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + error: undefined, + networkStatus: NetworkStatus.loading, + }); + } link.simulateResult({ result: { @@ -1894,49 +1913,59 @@ it.skip('does not suspend deferred queries with data in the cache and using a "c }, }); - await waitFor(() => { - expect(screen.getByText(/Message/i)).toHaveTextContent( - "Message: Hello world" - ); - }); - expect(screen.getByText(/Recipient/i)).toHaveTextContent( - "Recipient: Cached Alice" - ); - expect(screen.getByText(/Network status/i)).toHaveTextContent( - "Network status: 7" // ready - ); - expect(screen.getByText(/Error/i)).toHaveTextContent("none"); + { + const { snapshot } = await ProfiledApp.takeRender(); - link.simulateResult({ - result: { - incremental: [ - { - data: { - recipient: { name: "Alice", __typename: "Person" }, - __typename: "Greeting", - }, - path: ["greeting"], + expect(snapshot.suspenseCount).toBe(0); + expect(snapshot.result).toEqual({ + data: { + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Cached Alice" }, }, - ], - hasNext: false, - }, - }); + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } - await waitFor(() => { - expect(screen.getByText(/Recipient/i)).toHaveTextContent( - "Recipient: Alice" - ); - }); - expect(screen.getByText(/Message/i)).toHaveTextContent( - "Message: Hello world" - ); - expect(screen.getByText(/Network status/i)).toHaveTextContent( - "Network status: 7" // ready + link.simulateResult( + { + result: { + incremental: [ + { + data: { + recipient: { name: "Alice", __typename: "Person" }, + __typename: "Greeting", + }, + path: ["greeting"], + }, + ], + hasNext: false, + }, + }, + true ); - expect(screen.getByText(/Error/i)).toHaveTextContent("none"); - expect(renders).toBe(3); - expect(suspenseCount).toBe(0); + { + const { snapshot } = await ProfiledApp.takeRender(); + + expect(snapshot.suspenseCount).toBe(0); + expect(snapshot.result).toEqual({ + data: { + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Alice" }, + }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await expect(ProfiledApp).not.toRerender(); }); it.skip("reacts to cache updates", async () => { From 29fd6cd35e2dd26e59040d07a477118fc5e4caf6 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 7 Nov 2023 20:09:17 -0700 Subject: [PATCH 027/199] Update test that checks cache updates --- .../__tests__/useInteractiveQuery.test.tsx | 104 ++++++++++++++---- 1 file changed, 85 insertions(+), 19 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index 98d5d0eafd0..f43884fe878 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -1968,38 +1968,104 @@ it('does not suspend deferred queries with data in the cache and using a "cache- await expect(ProfiledApp).not.toRerender(); }); -it.skip("reacts to cache updates", async () => { - const { renders, client, query, loadQueryButton, user } = - renderIntegrationTest(); +it("reacts to cache updates", async () => { + const { query, mocks } = useSimpleQueryCase(); + const user = userEvent.setup(); + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: new MockLink(mocks), + }); - await act(() => user.click(loadQueryButton)); + function SuspenseFallback() { + ProfiledApp.updateSnapshot((snapshot) => ({ + ...snapshot, + suspenseCount: snapshot.suspenseCount + 1, + })); - expect(renders.suspenseCount).toBe(1); - expect(screen.getByText("loading")).toBeInTheDocument(); + return

Loading

; + } + + function Parent() { + const [queryRef, loadQuery] = useInteractiveQuery(query); + return ( + <> + + }> + {queryRef && } + + + ); + } - // the parent component re-renders when promise fulfilled - expect(await screen.findByText("hello")).toBeInTheDocument(); - expect(renders.count).toBe(1); + function Child({ queryRef }: { queryRef: QueryReference }) { + const result = useReadQuery(queryRef); - client.writeQuery({ - query, - data: { foo: { bar: "baz" } }, + ProfiledApp.updateSnapshot((snapshot) => ({ ...snapshot, result })); + + return null; + } + + const ProfiledApp = profile<{ + result: UseReadQueryResult | null; + suspenseCount: number; + }>({ + Component: () => ( + + + + ), + initialSnapshot: { + result: null, + suspenseCount: 0, + }, }); - // the parent component re-renders when promise fulfilled - expect(await screen.findByText("baz")).toBeInTheDocument(); + render(); - expect(renders.count).toBe(2); - expect(renders.suspenseCount).toBe(1); + { + const { snapshot } = await ProfiledApp.takeRender(); + + expect(snapshot.suspenseCount).toBe(0); + expect(snapshot.result).toBe(null); + } + + await act(() => user.click(screen.getByText("Load query"))); + + { + const { snapshot } = await ProfiledApp.takeRender(); + + expect(snapshot.suspenseCount).toBe(1); + expect(snapshot.result).toBe(null); + } + + { + const { snapshot } = await ProfiledApp.takeRender(); + + expect(snapshot.suspenseCount).toBe(1); + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } client.writeQuery({ query, - data: { foo: { bar: "bat" } }, + data: { greeting: "Updated Hello" }, }); - expect(await screen.findByText("bat")).toBeInTheDocument(); + { + const { snapshot } = await ProfiledApp.takeRender(); - expect(renders.suspenseCount).toBe(1); + expect(snapshot.suspenseCount).toBe(1); + expect(snapshot.result).toEqual({ + data: { greeting: "Updated Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await expect(ProfiledApp).not.toRerender(); }); it.skip("reacts to variables updates", async () => { From 4cf688d93529f986ed4aea0744769a8601c4b60f Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 7 Nov 2023 20:19:53 -0700 Subject: [PATCH 028/199] Remove unused renderIntegrationTest helper in useInteractiveQuery tests --- .../__tests__/useInteractiveQuery.test.tsx | 98 ------------------- 1 file changed, 98 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index f43884fe878..5bb4e8bc56b 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -77,104 +77,6 @@ function useSimpleQueryCase( return { query, mocks }; } -function renderIntegrationTest({ - client, -}: { - client?: ApolloClient; -} = {}) { - const query: TypedDocumentNode = gql` - query SimpleQuery { - foo { - bar - } - } - `; - - const user = userEvent.setup(); - - const mocks = [ - { - request: { query }, - result: { data: { foo: { bar: "hello" } } }, - delay: 10, - }, - ]; - const _client = - client || - new ApolloClient({ - cache: new InMemoryCache(), - link: new MockLink(mocks), - }); - interface Renders { - errors: Error[]; - errorCount: number; - suspenseCount: number; - count: number; - } - const renders: Renders = { - errors: [], - errorCount: 0, - suspenseCount: 0, - count: 0, - }; - const errorBoundaryProps: ErrorBoundaryProps = { - fallback:
Error
, - onError: (error) => { - renders.errorCount++; - renders.errors.push(error); - }, - }; - - interface QueryData { - foo: { bar: string }; - } - - function SuspenseFallback() { - renders.suspenseCount++; - return
loading
; - } - - 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, loadQuery] = useInteractiveQuery(query); - return ( -
- - {queryRef && } -
- ); - } - - function App() { - return ( - - - }> - - - - - ); - } - - const utils = render(); - - return { - ...utils, - query, - client: _client, - renders, - user, - loadQueryButton: screen.getByText("Load query"), - }; -} - interface VariablesCaseData { character: { id: string; From ed39fa1ca7ea5b39fb372b1c6d05f103d1f93af2 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 7 Nov 2023 21:08:44 -0700 Subject: [PATCH 029/199] Rename helper to useVariablesQueryCase to better match convention --- .../__tests__/useInteractiveQuery.test.tsx | 42 +++++++++++-------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index 5bb4e8bc56b..b1e315b54c8 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -88,7 +88,9 @@ interface VariablesCaseVariables { id: string; } -function useVariablesIntegrationTestCase() { +function useVariablesQueryCase( + mockOverrides?: MockedResponse[] +) { const query: TypedDocumentNode< VariablesCaseData, VariablesCaseVariables @@ -101,11 +103,15 @@ function useVariablesIntegrationTestCase() { } `; 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 } } }, - delay: 20, - })); + + const mocks = + mockOverrides ?? + [...CHARACTERS].map((name, index) => ({ + request: { query, variables: { id: String(index + 1) } }, + result: { data: { character: { id: String(index + 1), name } } }, + delay: 20, + })); + return { mocks, query }; } @@ -133,7 +139,7 @@ function renderVariablesIntegrationTest({ errorPolicy?: ErrorPolicy; }) { const user = userEvent.setup(); - let { mocks: _mocks, query } = useVariablesIntegrationTestCase(); + let { mocks: _mocks, query } = useVariablesQueryCase(); // duplicate mocks with (updated) in the name for refetches _mocks = [..._mocks, ..._mocks, ..._mocks].map((mock, index) => { @@ -4690,7 +4696,7 @@ describe.skip("type tests", () => { }); it("enforces variables argument to loadQuery function when TVariables is specified", () => { - const { query } = useVariablesIntegrationTestCase(); + const { query } = useVariablesQueryCase(); const [, loadQuery] = useInteractiveQuery(query); @@ -4702,7 +4708,7 @@ describe.skip("type tests", () => { }); it("disallows wider variables type", () => { - const { query } = useVariablesIntegrationTestCase(); + const { query } = useVariablesQueryCase(); const [, loadQuery] = useInteractiveQuery(query); @@ -4728,7 +4734,7 @@ describe.skip("type tests", () => { }); it("returns TData in default case", () => { - const { query } = useVariablesIntegrationTestCase(); + const { query } = useVariablesQueryCase(); { const [queryRef] = useInteractiveQuery(query); @@ -4755,7 +4761,7 @@ describe.skip("type tests", () => { }); it('returns TData | undefined with errorPolicy: "ignore"', () => { - const { query } = useVariablesIntegrationTestCase(); + const { query } = useVariablesQueryCase(); { const [queryRef] = useInteractiveQuery(query, { @@ -4784,7 +4790,7 @@ describe.skip("type tests", () => { }); it('returns TData | undefined with errorPolicy: "all"', () => { - const { query } = useVariablesIntegrationTestCase(); + const { query } = useVariablesQueryCase(); { const [queryRef] = useInteractiveQuery(query, { @@ -4813,7 +4819,7 @@ describe.skip("type tests", () => { }); it('returns TData with errorPolicy: "none"', () => { - const { query } = useVariablesIntegrationTestCase(); + const { query } = useVariablesQueryCase(); { const [queryRef] = useInteractiveQuery(query, { @@ -4842,7 +4848,7 @@ describe.skip("type tests", () => { }); it("returns DeepPartial with returnPartialData: true", () => { - const { query } = useVariablesIntegrationTestCase(); + const { query } = useVariablesQueryCase(); { const [queryRef] = useInteractiveQuery(query, { @@ -4871,7 +4877,7 @@ describe.skip("type tests", () => { }); it("returns TData with returnPartialData: false", () => { - const { query } = useVariablesIntegrationTestCase(); + const { query } = useVariablesQueryCase(); { const [queryRef] = useInteractiveQuery(query, { @@ -4900,7 +4906,7 @@ describe.skip("type tests", () => { }); it("returns TData when passing an option that does not affect TData", () => { - const { query } = useVariablesIntegrationTestCase(); + const { query } = useVariablesQueryCase(); { const [queryRef] = useInteractiveQuery(query, { @@ -4929,7 +4935,7 @@ describe.skip("type tests", () => { }); it("handles combinations of options", () => { - const { query } = useVariablesIntegrationTestCase(); + const { query } = useVariablesQueryCase(); { const [queryRef] = useInteractiveQuery(query, { @@ -4989,7 +4995,7 @@ describe.skip("type tests", () => { }); it("returns correct TData type when combined options that do not affect TData", () => { - const { query } = useVariablesIntegrationTestCase(); + const { query } = useVariablesQueryCase(); { const [queryRef] = useInteractiveQuery(query, { From 67092e21d4a6f5f0681837a7e0c1da2b97eaa96b Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 7 Nov 2023 21:15:39 -0700 Subject: [PATCH 030/199] Add test to validate variables can be used with loadQuery --- .../__tests__/useInteractiveQuery.test.tsx | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index b1e315b54c8..eed5f14dc4f 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -643,6 +643,118 @@ it("loads a query when the load query function is called", async () => { await expect(ProfiledApp).not.toRerender(); }); +it("loads a query with variables and suspends by passing variables to the loadQuery function", async () => { + const user = userEvent.setup(); + const { query, mocks } = useVariablesQueryCase(); + + function SuspenseFallback() { + ProfiledApp.updateSnapshot((snapshot) => ({ + ...snapshot, + suspenseCount: snapshot.suspenseCount + 1, + })); + + return

Loading

; + } + + function Parent() { + const [queryRef, loadQuery] = useInteractiveQuery(query); + + ProfiledApp.updateSnapshot((snapshot) => ({ + ...snapshot, + parentRenderCount: snapshot.parentRenderCount + 1, + })); + + return ( + <> + + }> + {queryRef && } + + + ); + } + + function Child({ + queryRef, + }: { + queryRef: QueryReference; + }) { + const result = useReadQuery(queryRef); + + ProfiledApp.updateSnapshot((snapshot) => ({ + ...snapshot, + result, + childRenderCount: snapshot.childRenderCount + 1, + })); + + return null; + } + + const ProfiledApp = profile<{ + result: UseReadQueryResult | null; + suspenseCount: number; + parentRenderCount: number; + childRenderCount: number; + }>({ + Component: () => ( + + + + ), + snapshotDOM: true, + initialSnapshot: { + result: null, + suspenseCount: 0, + parentRenderCount: 0, + childRenderCount: 0, + }, + }); + + render(); + + { + const { snapshot } = await ProfiledApp.takeRender(); + + expect(snapshot).toEqual({ + result: null, + suspenseCount: 0, + parentRenderCount: 1, + childRenderCount: 0, + }); + } + + await act(() => user.click(screen.getByText("Load query"))); + + { + const { snapshot, withinDOM } = await ProfiledApp.takeRender(); + + expect(withinDOM().getByText("Loading")).toBeInTheDocument(); + expect(snapshot).toEqual({ + result: null, + suspenseCount: 1, + parentRenderCount: 2, + childRenderCount: 0, + }); + } + + { + const { snapshot } = await ProfiledApp.takeRender(); + + expect(snapshot).toEqual({ + result: { + data: { character: { id: "1", name: "Spider-Man" } }, + networkStatus: NetworkStatus.ready, + error: undefined, + }, + suspenseCount: 1, + parentRenderCount: 2, + childRenderCount: 1, + }); + } + + await expect(ProfiledApp).not.toRerender(); +}); + it("allows the client to be overridden", async () => { const user = userEvent.setup(); const { query } = useSimpleQueryCase(); From a1caec27a4c857e7b0710af2528d8e2cae7ae6f5 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 7 Nov 2023 21:20:27 -0700 Subject: [PATCH 031/199] Add test to check that variables can be changed by calling loadQuery with different variables --- .../__tests__/useInteractiveQuery.test.tsx | 150 ++++++++++++++++++ 1 file changed, 150 insertions(+) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index eed5f14dc4f..26ee98af41f 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -755,6 +755,156 @@ it("loads a query with variables and suspends by passing variables to the loadQu await expect(ProfiledApp).not.toRerender(); }); +it("can change variables on a query and resuspend by passing new variables to the loadQuery function", async () => { + const user = userEvent.setup(); + const { query, mocks } = useVariablesQueryCase(); + + function SuspenseFallback() { + ProfiledApp.updateSnapshot((snapshot) => ({ + ...snapshot, + suspenseCount: snapshot.suspenseCount + 1, + })); + + return

Loading

; + } + + function Parent() { + const [queryRef, loadQuery] = useInteractiveQuery(query); + + ProfiledApp.updateSnapshot((snapshot) => ({ + ...snapshot, + parentRenderCount: snapshot.parentRenderCount + 1, + })); + + return ( + <> + + + }> + {queryRef && } + + + ); + } + + function Child({ + queryRef, + }: { + queryRef: QueryReference; + }) { + const result = useReadQuery(queryRef); + + ProfiledApp.updateSnapshot((snapshot) => ({ + ...snapshot, + result, + childRenderCount: snapshot.childRenderCount + 1, + })); + + return null; + } + + const ProfiledApp = profile<{ + result: UseReadQueryResult | null; + suspenseCount: number; + parentRenderCount: number; + childRenderCount: number; + }>({ + Component: () => ( + + + + ), + snapshotDOM: true, + initialSnapshot: { + result: null, + suspenseCount: 0, + parentRenderCount: 0, + childRenderCount: 0, + }, + }); + + render(); + + { + const { snapshot } = await ProfiledApp.takeRender(); + + expect(snapshot).toEqual({ + result: null, + suspenseCount: 0, + parentRenderCount: 1, + childRenderCount: 0, + }); + } + + await act(() => user.click(screen.getByText("Load 1st character"))); + + { + const { snapshot, withinDOM } = await ProfiledApp.takeRender(); + + expect(withinDOM().getByText("Loading")).toBeInTheDocument(); + expect(snapshot).toEqual({ + result: null, + suspenseCount: 1, + parentRenderCount: 2, + childRenderCount: 0, + }); + } + + { + const { snapshot } = await ProfiledApp.takeRender(); + + expect(snapshot).toEqual({ + result: { + data: { character: { id: "1", name: "Spider-Man" } }, + networkStatus: NetworkStatus.ready, + error: undefined, + }, + suspenseCount: 1, + parentRenderCount: 2, + childRenderCount: 1, + }); + } + + await act(() => user.click(screen.getByText("Load 2nd character"))); + + { + const { snapshot, withinDOM } = await ProfiledApp.takeRender(); + + expect(withinDOM().getByText("Loading")).toBeInTheDocument(); + expect(snapshot).toEqual({ + result: { + data: { character: { id: "1", name: "Spider-Man" } }, + networkStatus: NetworkStatus.ready, + error: undefined, + }, + suspenseCount: 2, + parentRenderCount: 3, + childRenderCount: 1, + }); + } + + { + const { snapshot } = await ProfiledApp.takeRender(); + + expect(snapshot).toEqual({ + result: { + data: { character: { id: "2", name: "Black Widow" } }, + networkStatus: NetworkStatus.ready, + error: undefined, + }, + suspenseCount: 2, + parentRenderCount: 3, + childRenderCount: 2, + }); + } + + await expect(ProfiledApp).not.toRerender(); +}); + it("allows the client to be overridden", async () => { const user = userEvent.setup(); const { query } = useSimpleQueryCase(); From c769437832ee6750d27685040606bc2dc4353bd4 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 8 Nov 2023 12:35:22 -0700 Subject: [PATCH 032/199] Update test that checks that error policy is applied between renders --- .../__tests__/useInteractiveQuery.test.tsx | 194 ++++++++++++------ 1 file changed, 131 insertions(+), 63 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index 26ee98af41f..180680129e5 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -1,4 +1,4 @@ -import React, { Fragment, StrictMode, Suspense } from "react"; +import React, { Fragment, StrictMode, Suspense, useState } from "react"; import { act, render, @@ -2238,40 +2238,10 @@ it("reacts to cache updates", async () => { await expect(ProfiledApp).not.toRerender(); }); -it.skip("reacts to variables updates", async () => { - const { renders, user, loadQueryButton } = renderVariablesIntegrationTest({ - variables: { id: "1" }, - }); - - await act(() => user.click(loadQueryButton)); - - expect(renders.suspenseCount).toBe(1); - expect(screen.getByText("loading")).toBeInTheDocument(); - - expect(await screen.findByText("1 - Spider-Man")).toBeInTheDocument(); - - await act(() => user.click(screen.getByText("Change variables"))); - - expect(renders.suspenseCount).toBe(2); - expect(screen.getByText("loading")).toBeInTheDocument(); - - expect(await screen.findByText("2 - Black Widow")).toBeInTheDocument(); -}); - -it.skip("applies `errorPolicy` on next fetch when it changes between renders", async () => { - interface Data { - greeting: string; - } - - const user = userEvent.setup(); - - const query: TypedDocumentNode = gql` - query { - greeting - } - `; +it("applies `errorPolicy` on next fetch when it changes between renders", async () => { + const { query } = useSimpleQueryCase(); - const mocks = [ + const mocks: MockedResponse[] = [ { request: { query }, result: { data: { greeting: "Hello" } }, @@ -2284,17 +2254,19 @@ it.skip("applies `errorPolicy` on next fetch when it changes between renders", a }, ]; - const client = new ApolloClient({ - link: new MockLink(mocks), - cache: new InMemoryCache(), - }); + const user = userEvent.setup(); function SuspenseFallback() { - return
Loading...
; + ProfiledApp.updateSnapshot((snapshot) => ({ + ...snapshot, + suspenseCount: snapshot.suspenseCount + 1, + })); + + return

Loading

; } function Parent() { - const [errorPolicy, setErrorPolicy] = React.useState("none"); + const [errorPolicy, setErrorPolicy] = useState("none"); const [queryRef, loadQuery, { refetch }] = useInteractiveQuery(query, { errorPolicy, }); @@ -2305,46 +2277,142 @@ it.skip("applies `errorPolicy` on next fetch when it changes between renders", a Change error policy - + }> - {queryRef && } + Error boundary} + onError={(error) => { + ProfiledApp.updateSnapshot((snapshot) => ({ + ...snapshot, + error, + errorBoundaryCount: snapshot.errorBoundaryCount + 1, + })); + }} + > + {queryRef && } + ); } - function Greeting({ queryRef }: { queryRef: QueryReference }) { - const { data, error } = useReadQuery(queryRef); + function Child({ queryRef }: { queryRef: QueryReference }) { + const result = useReadQuery(queryRef); - return error ? ( -
{error.message}
- ) : ( -
{data.greeting}
- ); + ProfiledApp.updateSnapshot((snapshot) => ({ ...snapshot, result })); + + return null; } - function App() { - return ( - - Error boundary}> - - - - ); + const ProfiledApp = profile<{ + error: Error | undefined; + errorBoundaryCount: number; + result: UseReadQueryResult | null; + suspenseCount: number; + }>({ + Component: () => ( + + + + ), + initialSnapshot: { + error: undefined, + errorBoundaryCount: 0, + result: null, + suspenseCount: 0, + }, + }); + + render(); + + { + const { snapshot } = await ProfiledApp.takeRender(); + + expect(snapshot).toEqual({ + result: null, + suspenseCount: 0, + error: undefined, + errorBoundaryCount: 0, + }); } - render(); + await act(() => user.click(screen.getByText("Load query"))); + + { + const { snapshot } = await ProfiledApp.takeRender(); + + expect(snapshot).toEqual({ + result: null, + suspenseCount: 1, + error: undefined, + errorBoundaryCount: 0, + }); + } - await act(() => user.click(screen.getByText("Load greeting"))); + { + const { snapshot } = await ProfiledApp.takeRender(); - expect(await screen.findByTestId("greeting")).toHaveTextContent("Hello"); + expect(snapshot).toEqual({ + result: { + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }, + suspenseCount: 1, + error: undefined, + errorBoundaryCount: 0, + }); + } await act(() => user.click(screen.getByText("Change error policy"))); + { + const { snapshot } = await ProfiledApp.takeRender(); + + expect(snapshot).toEqual({ + result: { + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }, + suspenseCount: 1, + error: undefined, + errorBoundaryCount: 0, + }); + } + await act(() => user.click(screen.getByText("Refetch greeting"))); - // Ensure we aren't rendering the error boundary and instead rendering the - // error message in the Greeting component. - expect(await screen.findByTestId("error")).toHaveTextContent("oops"); + { + const { snapshot } = await ProfiledApp.takeRender(); + + expect(snapshot).toEqual({ + result: { + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }, + suspenseCount: 2, + error: undefined, + errorBoundaryCount: 0, + }); + } + + { + const { snapshot } = await ProfiledApp.takeRender(); + + // Ensure we aren't rendering the error boundary and instead rendering the + // error message in the Child component. + expect(snapshot).toEqual({ + result: { + data: { greeting: "Hello" }, + error: new ApolloError({ graphQLErrors: [new GraphQLError("oops")] }), + networkStatus: NetworkStatus.error, + }, + suspenseCount: 2, + error: undefined, + errorBoundaryCount: 0, + }); + } }); it.skip("applies `context` on next fetch when it changes between renders", async () => { From e0a1f89ced28d45584760e422211b20bd41ec4ce Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 8 Nov 2023 14:21:09 -0700 Subject: [PATCH 033/199] More robust variables argument definition when using optional variables --- .../__tests__/useInteractiveQuery.test.tsx | 140 ++++++++++++++---- src/react/hooks/useInteractiveQuery.ts | 10 +- 2 files changed, 120 insertions(+), 30 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index 180680129e5..459f7acac76 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -5007,11 +5007,7 @@ describe.skip("fetchMore", () => { describe.skip("type tests", () => { it("returns unknown when TData cannot be inferred", () => { - const query = gql` - query { - hello - } - `; + const query = gql``; const [queryRef, loadQuery] = useInteractiveQuery(query); @@ -5020,47 +5016,133 @@ describe.skip("type tests", () => { const { data } = useReadQuery(queryRef); expectTypeOf(data).toEqualTypeOf(); - expectTypeOf(loadQuery).toEqualTypeOf< - (variables: OperationVariables) => void - >(); }); - it("enforces variables argument to loadQuery function when TVariables is specified", () => { - const { query } = useVariablesQueryCase(); + it("variables are optional and can be anything with an untyped DocumentNode", () => { + const query = gql``; const [, loadQuery] = useInteractiveQuery(query); - expectTypeOf(loadQuery).toEqualTypeOf< - (variables: VariablesCaseVariables) => void - >(); - // @ts-expect-error enforces variables argument when type is specified loadQuery(); + loadQuery({}); + loadQuery({ foo: "bar" }); + loadQuery({ bar: "baz" }); }); - it("disallows wider variables type", () => { - const { query } = useVariablesQueryCase(); + it("variables are optional and can be anything with unspecified TVariables on a TypedDocumentNode", () => { + const query: TypedDocumentNode<{ greeting: string }> = gql``; const [, loadQuery] = useInteractiveQuery(query); - expectTypeOf(loadQuery).toEqualTypeOf< - (variables: VariablesCaseVariables) => void - >(); - // @ts-expect-error does not allow wider TVariables type - loadQuery({ id: "1", foo: "bar" }); + loadQuery(); + loadQuery({}); + loadQuery({ foo: "bar" }); + loadQuery({ bar: "baz" }); }); - it("does not allow variables argument to loadQuery when TVariables is `never`", () => { - const query: TypedDocumentNode = gql` - query { - greeting - } - `; + it("variables are optional when TVariables are empty", () => { + const query: TypedDocumentNode< + { greeting: string }, + Record + > = gql``; + + const [, loadQuery] = useInteractiveQuery(query); + + loadQuery(); + loadQuery({}); + // @ts-expect-error unknown variable + loadQuery({ foo: "bar" }); + }); + + it("does not allow variables when TVariables is `never`", () => { + const query: TypedDocumentNode<{ greeting: string }, never> = gql``; const [, loadQuery] = useInteractiveQuery(query); - expectTypeOf(loadQuery).toEqualTypeOf<() => void>(); - // @ts-expect-error does not allow variables argument when TVariables is `never` + loadQuery(); + // @ts-expect-error no variables argument allowed loadQuery({}); + // @ts-expect-error no variables argument allowed + loadQuery({ foo: "bar" }); + }); + + it("optional variables are optional to loadQuery", () => { + const query: TypedDocumentNode< + { posts: string[] }, + { limit?: number } + > = gql``; + + const [, loadQuery] = useInteractiveQuery(query); + + loadQuery(); + loadQuery({}); + loadQuery({ limit: 10 }); + loadQuery({ + // @ts-expect-error unknown variable + foo: "bar", + }); + loadQuery({ + limit: 10, + // @ts-expect-error unknown variable + foo: "bar", + }); + }); + + it("enforces required variables when TVariables includes required variables", () => { + const query: TypedDocumentNode< + { character: string }, + { id: string } + > = gql``; + + const [, loadQuery] = useInteractiveQuery(query); + + // @ts-expect-error missing variables argument + loadQuery(); + // @ts-expect-error empty variables + loadQuery({}); + loadQuery({ id: "1" }); + loadQuery({ + // @ts-expect-error unknown variable + foo: "bar", + }); + loadQuery({ + id: "1", + // @ts-expect-error unknown variable + foo: "bar", + }); + }); + + it("requires variables with mixed TVariables", () => { + const query: TypedDocumentNode< + { character: string }, + { id: string; language?: string } + > = gql``; + + const [, loadQuery] = useInteractiveQuery(query); + + // @ts-expect-error missing variables argument + loadQuery(); + // @ts-expect-error empty variables + loadQuery({}); + loadQuery({ id: "1" }); + // @ts-expect-error missing required variable + loadQuery({ language: "en" }); + loadQuery({ id: "1", language: "en" }); + loadQuery({ + // @ts-expect-error unknown variable + foo: "bar", + }); + loadQuery({ + id: "1", + // @ts-expect-error unknown variable + foo: "bar", + }); + loadQuery({ + id: "1", + language: "en", + // @ts-expect-error unknown variable + foo: "bar", + }); }); it("returns TData in default case", () => { diff --git a/src/react/hooks/useInteractiveQuery.ts b/src/react/hooks/useInteractiveQuery.ts index 6fe99e84ae9..32ee0235a88 100644 --- a/src/react/hooks/useInteractiveQuery.ts +++ b/src/react/hooks/useInteractiveQuery.ts @@ -19,12 +19,20 @@ import { canonicalStringify } from "../../cache/index.js"; import type { DeepPartial } from "../../utilities/index.js"; import type { CacheKey } from "../cache/types.js"; +type OnlyRequiredProperties = { + [K in keyof T as {} extends Pick ? never : K]: T[K]; +}; + type LoadQuery = ( // Use variadic args to handle cases where TVariables is type `never`, in // which case we don't want to allow a variables argument. In other // words, we don't want to allow variables to be passed as an argument to this // function if the query does not expect variables in the document. - ...args: [TVariables] extends [never] ? [] : [variables: TVariables] + ...args: [TVariables] extends [never] + ? [] + : {} extends OnlyRequiredProperties + ? [variables?: TVariables] + : [variables: TVariables] ) => void; export type UseInteractiveQueryResult< From 98dc88286d1d45d623e6964b7be5012c34add172 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 8 Nov 2023 14:26:02 -0700 Subject: [PATCH 034/199] Remove mock overrides in case helpers and fix MockedResponse types --- .../__tests__/useInteractiveQuery.test.tsx | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index 459f7acac76..f03f061b52e 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -57,16 +57,14 @@ interface SimpleQueryData { greeting: string; } -function useSimpleQueryCase( - mockOverrides?: MockedResponse[] -) { +function useSimpleQueryCase() { const query: TypedDocumentNode = gql` query GreetingQuery { greeting } `; - const mocks: MockedResponse[] = mockOverrides || [ + const mocks: MockedResponse[] = [ { request: { query }, result: { data: { greeting: "Hello" } }, @@ -88,9 +86,7 @@ interface VariablesCaseVariables { id: string; } -function useVariablesQueryCase( - mockOverrides?: MockedResponse[] -) { +function useVariablesQueryCase() { const query: TypedDocumentNode< VariablesCaseData, VariablesCaseVariables @@ -104,13 +100,13 @@ function useVariablesQueryCase( `; const CHARACTERS = ["Spider-Man", "Black Widow", "Iron Man", "Hulk"]; - const mocks = - mockOverrides ?? - [...CHARACTERS].map((name, index) => ({ + const mocks: MockedResponse[] = [...CHARACTERS].map( + (name, index) => ({ request: { query, variables: { id: String(index + 1) } }, result: { data: { character: { id: String(index + 1), name } } }, delay: 20, - })); + }) + ); return { mocks, query }; } From 46d894c1d76e7715922415bdbb4269c03d342c73 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 8 Nov 2023 14:31:07 -0700 Subject: [PATCH 035/199] Update result canonization test to use useInteractiveQuery --- src/react/hooks/__tests__/useInteractiveQuery.test.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index f03f061b52e..30b02d2e55d 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -2488,7 +2488,7 @@ it.skip("applies `context` on next fetch when it changes between renders", async // NOTE: We only test the `false` -> `true` path here. If the option changes // from `true` -> `false`, the data has already been canonized, so it has no // effect on the output. -it.skip("returns canonical results immediately when `canonizeResults` changes from `false` to `true` between renders", async () => { +it("returns canonical results immediately when `canonizeResults` changes from `false` to `true` between renders", async () => { interface Result { __typename: string; value: number; @@ -2545,17 +2545,18 @@ it.skip("returns canonical results immediately when `canonizeResults` changes fr function Parent() { const [canonizeResults, setCanonizeResults] = React.useState(false); - const [queryRef] = useInteractiveQuery(query, { + const [queryRef, loadQuery] = useInteractiveQuery(query, { canonizeResults, }); return ( <> + }> - + {queryRef && } ); @@ -2596,6 +2597,8 @@ it.skip("returns canonical results immediately when `canonizeResults` changes fr } } + await act(() => user.click(screen.getByText("Load query"))); + verifyCanonicalResults(result.current!, false); await act(() => user.click(screen.getByText("Canonize results"))); From 11ac8770649f1157f965d6fb891abe80b4f2939f Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 8 Nov 2023 14:31:46 -0700 Subject: [PATCH 036/199] Enable context test --- src/react/hooks/__tests__/useInteractiveQuery.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index 30b02d2e55d..f93659346fa 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -2411,7 +2411,7 @@ it("applies `errorPolicy` on next fetch when it changes between renders", async } }); -it.skip("applies `context` on next fetch when it changes between renders", async () => { +it("applies `context` on next fetch when it changes between renders", async () => { interface Data { context: Record; } From 39b8a0b83977717b95a6f0bacc5c0ff5847cfe19 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 8 Nov 2023 14:35:55 -0700 Subject: [PATCH 037/199] Enable test that checks refetchWritePolicy changes --- .../hooks/__tests__/useInteractiveQuery.test.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index f93659346fa..9cb17f74f75 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -2606,7 +2606,7 @@ it("returns canonical results immediately when `canonizeResults` changes from `f verifyCanonicalResults(result.current!, true); }); -it.skip("applies changed `refetchWritePolicy` to next fetch when changing between renders", async () => { +it("applies changed `refetchWritePolicy` to next fetch when changing between renders", async () => { interface Data { primes: number[]; } @@ -2667,13 +2667,15 @@ it.skip("applies changed `refetchWritePolicy` to next fetch when changing betwee const [refetchWritePolicy, setRefetchWritePolicy] = React.useState("merge"); - const [queryRef, { refetch }] = useInteractiveQuery(query, { + const [queryRef, loadQuery, { refetch }] = useInteractiveQuery(query, { refetchWritePolicy, - variables: { min: 0, max: 12 }, }); return ( <> + @@ -2684,7 +2686,7 @@ it.skip("applies changed `refetchWritePolicy` to next fetch when changing betwee Refetch last }> - + {queryRef && } ); @@ -2706,6 +2708,8 @@ it.skip("applies changed `refetchWritePolicy` to next fetch when changing betwee render(); + await act(() => user.click(screen.getByText("Load query"))); + const primes = await screen.findByTestId("primes"); expect(primes).toHaveTextContent("2, 3, 5, 7, 11"); @@ -2726,7 +2730,6 @@ it.skip("applies changed `refetchWritePolicy` to next fetch when changing betwee ]); await act(() => user.click(screen.getByText("Change refetch write policy"))); - await act(() => user.click(screen.getByText("Refetch last"))); await waitFor(() => { From dc6daa7937cdfe4079265fadcb57a46587e4712b Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 8 Nov 2023 14:38:18 -0700 Subject: [PATCH 038/199] Enable test that checks returnPartialData changing --- src/react/hooks/__tests__/useInteractiveQuery.test.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index 9cb17f74f75..588866f01e6 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -2746,7 +2746,7 @@ it("applies changed `refetchWritePolicy` to next fetch when changing between ren ]); }); -it.skip("applies `returnPartialData` on next fetch when it changes between renders", async () => { +it("applies `returnPartialData` on next fetch when it changes between renders", async () => { interface Data { character: { __typename: "Character"; @@ -2830,17 +2830,18 @@ it.skip("applies `returnPartialData` on next fetch when it changes between rende function Parent() { const [returnPartialData, setReturnPartialData] = React.useState(false); - const [queryRef] = useInteractiveQuery(fullQuery, { + const [queryRef, loadQuery] = useInteractiveQuery(fullQuery, { returnPartialData, }); return ( <> + }> - + {queryRef && } ); @@ -2864,6 +2865,8 @@ it.skip("applies `returnPartialData` on next fetch when it changes between rende render(); + await act(() => user.click(screen.getByText("Load query"))); + const character = await screen.findByTestId("character"); expect(character).toHaveTextContent("Doctor Strange"); From 287e599935193754ce3deffbb13a6c2101cadffd Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 8 Nov 2023 14:40:08 -0700 Subject: [PATCH 039/199] Enable test that checks changes to fetchPolicy --- src/react/hooks/__tests__/useInteractiveQuery.test.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index 588866f01e6..57fd372a6ad 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -2889,7 +2889,7 @@ it("applies `returnPartialData` on next fetch when it changes between renders", }); }); -it.skip("applies updated `fetchPolicy` on next fetch when it changes between renders", async () => { +it("applies updated `fetchPolicy` on next fetch when it changes between renders", async () => { interface Data { character: { __typename: "Character"; @@ -2952,18 +2952,19 @@ it.skip("applies updated `fetchPolicy` on next fetch when it changes between ren const [fetchPolicy, setFetchPolicy] = React.useState("cache-first"); - const [queryRef, { refetch }] = useInteractiveQuery(query, { + const [queryRef, loadQuery, { refetch }] = useInteractiveQuery(query, { fetchPolicy, }); return ( <> + }> - + {queryRef && } ); @@ -2985,6 +2986,8 @@ it.skip("applies updated `fetchPolicy` on next fetch when it changes between ren render(); + await act(() => user.click(screen.getByText("Load query"))); + const character = await screen.findByTestId("character"); expect(character).toHaveTextContent("Doctor Strangecache"); From 66ada14ff3659eb12d24c0cb6ebc8fcb34372630 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 8 Nov 2023 14:52:47 -0700 Subject: [PATCH 040/199] Flatten refetch and fetchMore tests --- .../__tests__/useInteractiveQuery.test.tsx | 3209 ++++++++--------- 1 file changed, 1527 insertions(+), 1682 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index 57fd372a6ad..56a01c41387 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -3009,20 +3009,43 @@ it("applies updated `fetchPolicy` on next fetch when it changes between renders" }); }); -it.skip("properly handles changing options along with changing `variables`", async () => { - interface Data { +it.skip("re-suspends when calling `refetch`", async () => { + const { renders } = renderVariablesIntegrationTest({ + variables: { id: "1" }, + }); + + expect(renders.suspenseCount).toBe(1); + expect(screen.getByText("loading")).toBeInTheDocument(); + + expect(await screen.findByText("1 - Spider-Man")).toBeInTheDocument(); + + const button = screen.getByText("Refetch"); + const user = userEvent.setup(); + await act(() => user.click(button)); + + // parent component re-suspends + expect(renders.suspenseCount).toBe(2); + expect(renders.count).toBe(2); + + expect( + await screen.findByText("1 - Spider-Man (updated)") + ).toBeInTheDocument(); +}); + +it.skip("re-suspends when calling `refetch` with new variables", async () => { + interface QueryData { character: { - __typename: "Character"; id: string; name: string; }; } - const user = userEvent.setup(); - const query: TypedDocumentNode = gql` - query ($id: ID!) { + interface QueryVariables { + id: string; + } + const query: TypedDocumentNode = gql` + query CharacterQuery($id: ID!) { character(id: $id) { - __typename id name } @@ -3033,1984 +3056,1806 @@ it.skip("properly handles changing options along with changing `variables`", asy { request: { query, variables: { id: "1" } }, result: { - errors: [new GraphQLError("oops")], + data: { character: { id: "1", name: "Captain Marvel" } }, }, - delay: 10, }, { request: { query, variables: { id: "2" } }, result: { - data: { - character: { - __typename: "Character", - id: "2", - name: "Hulk", - }, - }, + data: { character: { id: "2", name: "Captain America" } }, }, - delay: 10, }, ]; - const cache = new InMemoryCache(); + const { renders } = renderVariablesIntegrationTest({ + variables: { id: "1" }, + mocks, + }); - cache.writeQuery({ - query, - variables: { - id: "1", + expect(renders.suspenseCount).toBe(1); + expect(screen.getByText("loading")).toBeInTheDocument(); + + expect(await screen.findByText("1 - Captain Marvel")).toBeInTheDocument(); + + const newVariablesRefetchButton = screen.getByText("Set variables to id: 2"); + const refetchButton = screen.getByText("Refetch"); + const user = userEvent.setup(); + await act(() => user.click(newVariablesRefetchButton)); + await act(() => user.click(refetchButton)); + + expect(await screen.findByText("2 - Captain America")).toBeInTheDocument(); + + // parent component re-suspends + expect(renders.suspenseCount).toBe(2); + expect(renders.count).toBe(3); + + // extra render puts an additional frame into the array + expect(renders.frames).toMatchObject([ + { + ...mocks[0].result, + networkStatus: NetworkStatus.ready, + error: undefined, }, - data: { - character: { - __typename: "Character", - id: "1", - name: "Doctor Strangecache", - }, + { + ...mocks[0].result, + networkStatus: NetworkStatus.ready, + error: undefined, + }, + { + ...mocks[1].result, + networkStatus: NetworkStatus.ready, + error: undefined, }, + ]); +}); +it.skip("re-suspends multiple times when calling `refetch` multiple times", async () => { + const { renders } = renderVariablesIntegrationTest({ + variables: { id: "1" }, }); - const client = new ApolloClient({ - link: new MockLink(mocks), - cache, - }); + expect(renders.suspenseCount).toBe(1); + expect(screen.getByText("loading")).toBeInTheDocument(); - function SuspenseFallback() { - return
Loading...
; - } + expect(await screen.findByText("1 - Spider-Man")).toBeInTheDocument(); - function Parent() { - const [id, setId] = React.useState("1"); + const button = screen.getByText("Refetch"); + const user = userEvent.setup(); + await act(() => user.click(button)); - const [queryRef, { refetch }] = useInteractiveQuery(query, { - errorPolicy: id === "1" ? "all" : "none", - variables: { id }, - }); + // parent component re-suspends + expect(renders.suspenseCount).toBe(2); + expect(renders.count).toBe(2); - return ( - <> - - - - Error boundary}> - }> - - - - - ); - } + expect( + await screen.findByText("1 - Spider-Man (updated)") + ).toBeInTheDocument(); - function Character({ queryRef }: { queryRef: QueryReference }) { - const { data, error } = useReadQuery(queryRef); + await act(() => user.click(button)); - return error ? ( -
{error.message}
- ) : ( - {data.character.name} - ); - } + // parent component re-suspends + expect(renders.suspenseCount).toBe(3); + expect(renders.count).toBe(3); - function App() { - return ( - - - - ); + expect( + await screen.findByText("1 - Spider-Man (updated again)") + ).toBeInTheDocument(); +}); +it.skip("throws errors when errors are returned after calling `refetch`", async () => { + const consoleSpy = jest.spyOn(console, "error").mockImplementation(); + interface QueryData { + character: { + id: string; + name: string; + }; } - render(); - - const character = await screen.findByTestId("character"); - - expect(character).toHaveTextContent("Doctor Strangecache"); + interface QueryVariables { + id: string; + } + const query: TypedDocumentNode = gql` + query CharacterQuery($id: ID!) { + character(id: $id) { + id + name + } + } + `; + const mocks = [ + { + request: { query, variables: { id: "1" } }, + result: { + data: { character: { id: "1", name: "Captain Marvel" } }, + }, + }, + { + request: { query, variables: { id: "1" } }, + result: { + errors: [new GraphQLError("Something went wrong")], + }, + }, + ]; + const { renders } = renderVariablesIntegrationTest({ + variables: { id: "1" }, + mocks, + }); - await act(() => user.click(screen.getByText("Get second character"))); + expect(renders.suspenseCount).toBe(1); + expect(screen.getByText("loading")).toBeInTheDocument(); - await waitFor(() => { - expect(character).toHaveTextContent("Hulk"); - }); + expect(await screen.findByText("1 - Captain Marvel")).toBeInTheDocument(); - await act(() => user.click(screen.getByText("Get first character"))); + const button = screen.getByText("Refetch"); + const user = userEvent.setup(); + await act(() => user.click(button)); await waitFor(() => { - expect(character).toHaveTextContent("Doctor Strangecache"); + expect(renders.errorCount).toBe(1); }); - await act(() => user.click(screen.getByText("Refetch"))); + expect(renders.errors).toEqual([ + new ApolloError({ + graphQLErrors: [new GraphQLError("Something went wrong")], + }), + ]); - // Ensure we render the inline error instead of the error boundary, which - // tells us the error policy was properly applied. - expect(await screen.findByTestId("error")).toHaveTextContent("oops"); + consoleSpy.mockRestore(); }); +it.skip('ignores errors returned after calling `refetch` when errorPolicy is set to "ignore"', async () => { + interface QueryData { + character: { + id: string; + name: string; + }; + } -describe.skip("refetch", () => { - it.skip("re-suspends when calling `refetch`", async () => { - const { renders } = renderVariablesIntegrationTest({ - variables: { id: "1" }, - }); - - expect(renders.suspenseCount).toBe(1); - expect(screen.getByText("loading")).toBeInTheDocument(); - - expect(await screen.findByText("1 - Spider-Man")).toBeInTheDocument(); + interface QueryVariables { + id: string; + } + const query: TypedDocumentNode = gql` + query CharacterQuery($id: ID!) { + character(id: $id) { + id + name + } + } + `; + const mocks = [ + { + request: { query, variables: { id: "1" } }, + result: { + data: { character: { id: "1", name: "Captain Marvel" } }, + }, + }, + { + request: { query, variables: { id: "1" } }, + result: { + errors: [new GraphQLError("Something went wrong")], + }, + }, + ]; - const button = screen.getByText("Refetch"); - const user = userEvent.setup(); - await act(() => user.click(button)); + const { renders } = renderVariablesIntegrationTest({ + variables: { id: "1" }, + errorPolicy: "ignore", + mocks, + }); - // parent component re-suspends - expect(renders.suspenseCount).toBe(2); - expect(renders.count).toBe(2); + expect(await screen.findByText("1 - Captain Marvel")).toBeInTheDocument(); - expect( - await screen.findByText("1 - Spider-Man (updated)") - ).toBeInTheDocument(); - }); - it.skip("re-suspends when calling `refetch` with new variables", async () => { - interface QueryData { - character: { - id: string; - name: string; - }; - } + const button = screen.getByText("Refetch"); + const user = userEvent.setup(); + await act(() => user.click(button)); - interface QueryVariables { + expect(renders.errorCount).toBe(0); + expect(renders.errors).toEqual([]); +}); +it.skip('returns errors after calling `refetch` when errorPolicy is set to "all"', async () => { + interface QueryData { + character: { id: string; - } - const query: TypedDocumentNode = gql` - query CharacterQuery($id: ID!) { - character(id: $id) { - id - name - } - } - `; + name: string; + }; + } - const mocks = [ - { - request: { query, variables: { id: "1" } }, - result: { - data: { character: { id: "1", name: "Captain Marvel" } }, - }, + interface QueryVariables { + id: string; + } + const query: TypedDocumentNode = gql` + query CharacterQuery($id: ID!) { + character(id: $id) { + id + name + } + } + `; + const mocks = [ + { + request: { query, variables: { id: "1" } }, + result: { + data: { character: { id: "1", name: "Captain Marvel" } }, }, - { - request: { query, variables: { id: "2" } }, - result: { - data: { character: { id: "2", name: "Captain America" } }, - }, + }, + { + request: { query, variables: { id: "1" } }, + result: { + errors: [new GraphQLError("Something went wrong")], }, - ]; - - const { renders } = renderVariablesIntegrationTest({ - variables: { id: "1" }, - mocks, - }); + }, + ]; - expect(renders.suspenseCount).toBe(1); - expect(screen.getByText("loading")).toBeInTheDocument(); + const { renders } = renderVariablesIntegrationTest({ + variables: { id: "1" }, + errorPolicy: "all", + mocks, + }); - expect(await screen.findByText("1 - Captain Marvel")).toBeInTheDocument(); + expect(await screen.findByText("1 - Captain Marvel")).toBeInTheDocument(); - const newVariablesRefetchButton = screen.getByText( - "Set variables to id: 2" - ); - const refetchButton = screen.getByText("Refetch"); - const user = userEvent.setup(); - await act(() => user.click(newVariablesRefetchButton)); - await act(() => user.click(refetchButton)); + const button = screen.getByText("Refetch"); + const user = userEvent.setup(); + await act(() => user.click(button)); - expect(await screen.findByText("2 - Captain America")).toBeInTheDocument(); + expect(renders.errorCount).toBe(0); + expect(renders.errors).toEqual([]); - // parent component re-suspends - expect(renders.suspenseCount).toBe(2); - expect(renders.count).toBe(3); + expect(await screen.findByText("Something went wrong")).toBeInTheDocument(); +}); +it.skip('handles partial data results after calling `refetch` when errorPolicy is set to "all"', async () => { + interface QueryData { + character: { + id: string; + name: string; + }; + } - // extra render puts an additional frame into the array - expect(renders.frames).toMatchObject([ - { - ...mocks[0].result, - networkStatus: NetworkStatus.ready, - error: undefined, - }, - { - ...mocks[0].result, - networkStatus: NetworkStatus.ready, - error: undefined, + interface QueryVariables { + id: string; + } + const query: TypedDocumentNode = gql` + query CharacterQuery($id: ID!) { + character(id: $id) { + id + name + } + } + `; + const mocks = [ + { + request: { query, variables: { id: "1" } }, + result: { + data: { character: { id: "1", name: "Captain Marvel" } }, }, - { - ...mocks[1].result, - networkStatus: NetworkStatus.ready, - error: undefined, + }, + { + request: { query, variables: { id: "1" } }, + result: { + data: { character: { id: "1", name: null } }, + errors: [new GraphQLError("Something went wrong")], }, - ]); - }); - it.skip("re-suspends multiple times when calling `refetch` multiple times", async () => { - const { renders } = renderVariablesIntegrationTest({ - variables: { id: "1" }, - }); - - expect(renders.suspenseCount).toBe(1); - expect(screen.getByText("loading")).toBeInTheDocument(); - - expect(await screen.findByText("1 - Spider-Man")).toBeInTheDocument(); + }, + ]; - const button = screen.getByText("Refetch"); - const user = userEvent.setup(); - await act(() => user.click(button)); + const { renders } = renderVariablesIntegrationTest({ + variables: { id: "1" }, + errorPolicy: "all", + mocks, + }); - // parent component re-suspends - expect(renders.suspenseCount).toBe(2); - expect(renders.count).toBe(2); + expect(await screen.findByText("1 - Captain Marvel")).toBeInTheDocument(); - expect( - await screen.findByText("1 - Spider-Man (updated)") - ).toBeInTheDocument(); + const button = screen.getByText("Refetch"); + const user = userEvent.setup(); + await act(() => user.click(button)); - await act(() => user.click(button)); + expect(renders.errorCount).toBe(0); + expect(renders.errors).toEqual([]); - // parent component re-suspends - expect(renders.suspenseCount).toBe(3); - expect(renders.count).toBe(3); + expect(await screen.findByText("Something went wrong")).toBeInTheDocument(); - expect( - await screen.findByText("1 - Spider-Man (updated again)") - ).toBeInTheDocument(); + const expectedError = new ApolloError({ + graphQLErrors: [new GraphQLError("Something went wrong")], }); - it.skip("throws errors when errors are returned after calling `refetch`", async () => { - const consoleSpy = jest.spyOn(console, "error").mockImplementation(); - interface QueryData { - character: { - id: string; - name: string; - }; - } - interface QueryVariables { - id: string; - } - const query: TypedDocumentNode = gql` - query CharacterQuery($id: ID!) { - character(id: $id) { - id - name - } - } - `; - const mocks = [ - { - request: { query, variables: { id: "1" } }, - result: { - data: { character: { id: "1", name: "Captain Marvel" } }, - }, - }, - { - request: { query, variables: { id: "1" } }, - result: { - errors: [new GraphQLError("Something went wrong")], - }, - }, - ]; - const { renders } = renderVariablesIntegrationTest({ - variables: { id: "1" }, - mocks, - }); - - expect(renders.suspenseCount).toBe(1); - expect(screen.getByText("loading")).toBeInTheDocument(); - - expect(await screen.findByText("1 - Captain Marvel")).toBeInTheDocument(); - - const button = screen.getByText("Refetch"); - const user = userEvent.setup(); - await act(() => user.click(button)); - - await waitFor(() => { - expect(renders.errorCount).toBe(1); - }); - - expect(renders.errors).toEqual([ - new ApolloError({ - graphQLErrors: [new GraphQLError("Something went wrong")], - }), - ]); - - consoleSpy.mockRestore(); - }); - it.skip('ignores errors returned after calling `refetch` when errorPolicy is set to "ignore"', async () => { - interface QueryData { - character: { - id: string; - name: string; - }; - } + expect(renders.frames).toMatchObject([ + { + ...mocks[0].result, + networkStatus: NetworkStatus.ready, + error: undefined, + }, + { + data: mocks[1].result.data, + networkStatus: NetworkStatus.error, + error: expectedError, + }, + ]); +}); +it.skip("`refetch` works with startTransition to allow React to show stale UI until finished suspending", async () => { + type Variables = { + id: string; + }; - interface QueryVariables { + interface Data { + todo: { id: string; - } - const query: TypedDocumentNode = gql` - query CharacterQuery($id: ID!) { - character(id: $id) { - id - name - } - } - `; - const mocks = [ - { - request: { query, variables: { id: "1" } }, - result: { - data: { character: { id: "1", name: "Captain Marvel" } }, - }, - }, - { - request: { query, variables: { id: "1" } }, - result: { - errors: [new GraphQLError("Something went wrong")], - }, - }, - ]; - - const { renders } = renderVariablesIntegrationTest({ - variables: { id: "1" }, - errorPolicy: "ignore", - mocks, - }); - - expect(await screen.findByText("1 - Captain Marvel")).toBeInTheDocument(); - - const button = screen.getByText("Refetch"); - const user = userEvent.setup(); - await act(() => user.click(button)); - - expect(renders.errorCount).toBe(0); - expect(renders.errors).toEqual([]); - }); - it.skip('returns errors after calling `refetch` when errorPolicy is set to "all"', async () => { - interface QueryData { - character: { - id: string; - name: string; - }; - } + name: string; + completed: boolean; + }; + } + const user = userEvent.setup(); - interface QueryVariables { - id: string; - } - const query: TypedDocumentNode = gql` - query CharacterQuery($id: ID!) { - character(id: $id) { - id - name - } + const query: TypedDocumentNode = gql` + query TodoItemQuery($id: ID!) { + todo(id: $id) { + id + name + completed } - `; - const mocks = [ - { - request: { query, variables: { id: "1" } }, - result: { - data: { character: { id: "1", name: "Captain Marvel" } }, - }, - }, - { - request: { query, variables: { id: "1" } }, - result: { - errors: [new GraphQLError("Something went wrong")], - }, - }, - ]; - - const { renders } = renderVariablesIntegrationTest({ - variables: { id: "1" }, - errorPolicy: "all", - mocks, - }); - - expect(await screen.findByText("1 - Captain Marvel")).toBeInTheDocument(); - - const button = screen.getByText("Refetch"); - const user = userEvent.setup(); - await act(() => user.click(button)); - - expect(renders.errorCount).toBe(0); - expect(renders.errors).toEqual([]); - - expect(await screen.findByText("Something went wrong")).toBeInTheDocument(); - }); - it.skip('handles partial data results after calling `refetch` when errorPolicy is set to "all"', async () => { - interface QueryData { - character: { - id: string; - name: string; - }; } + `; - interface QueryVariables { - id: string; - } - const query: TypedDocumentNode = gql` - query CharacterQuery($id: ID!) { - character(id: $id) { - id - name - } - } - `; - const mocks = [ - { - request: { query, variables: { id: "1" } }, - result: { - data: { character: { id: "1", name: "Captain Marvel" } }, - }, + const mocks: MockedResponse[] = [ + { + request: { query, variables: { id: "1" } }, + result: { + data: { todo: { id: "1", name: "Clean room", completed: false } }, }, - { - request: { query, variables: { id: "1" } }, - result: { - data: { character: { id: "1", name: null } }, - errors: [new GraphQLError("Something went wrong")], - }, + delay: 10, + }, + { + request: { query, variables: { id: "1" } }, + result: { + data: { todo: { id: "1", name: "Clean room", completed: true } }, }, - ]; - - const { renders } = renderVariablesIntegrationTest({ - variables: { id: "1" }, - errorPolicy: "all", - mocks, - }); - - expect(await screen.findByText("1 - Captain Marvel")).toBeInTheDocument(); - - const button = screen.getByText("Refetch"); - const user = userEvent.setup(); - await act(() => user.click(button)); - - expect(renders.errorCount).toBe(0); - expect(renders.errors).toEqual([]); - - expect(await screen.findByText("Something went wrong")).toBeInTheDocument(); - - const expectedError = new ApolloError({ - graphQLErrors: [new GraphQLError("Something went wrong")], - }); + delay: 10, + }, + ]; - expect(renders.frames).toMatchObject([ - { - ...mocks[0].result, - networkStatus: NetworkStatus.ready, - error: undefined, - }, - { - data: mocks[1].result.data, - networkStatus: NetworkStatus.error, - error: expectedError, - }, - ]); + const client = new ApolloClient({ + link: new MockLink(mocks), + cache: new InMemoryCache(), }); - it.skip("`refetch` works with startTransition to allow React to show stale UI until finished suspending", async () => { - type Variables = { - id: string; - }; - interface Data { - todo: { - id: string; - name: string; - completed: boolean; - }; - } - const user = userEvent.setup(); - - const query: TypedDocumentNode = gql` - query TodoItemQuery($id: ID!) { - todo(id: $id) { - id - name - completed - } - } - `; + function App() { + return ( + + }> + + + + ); + } - const mocks: MockedResponse[] = [ - { - request: { query, variables: { id: "1" } }, - result: { - data: { todo: { id: "1", name: "Clean room", completed: false } }, - }, - delay: 10, - }, - { - request: { query, variables: { id: "1" } }, - result: { - data: { todo: { id: "1", name: "Clean room", completed: true } }, - }, - delay: 10, - }, - ]; + function SuspenseFallback() { + return

Loading

; + } - const client = new ApolloClient({ - link: new MockLink(mocks), - cache: new InMemoryCache(), + function Parent() { + const [id, setId] = React.useState("1"); + const [queryRef, { refetch }] = useInteractiveQuery(query, { + variables: { id }, }); + return ; + } - function App() { - return ( - - }> - - - - ); - } - - function SuspenseFallback() { - return

Loading

; - } - - function Parent() { - const [id, setId] = React.useState("1"); - const [queryRef, { refetch }] = useInteractiveQuery(query, { - variables: { id }, - }); - return ; - } - - function Todo({ - queryRef, - refetch, - }: { - refetch: RefetchFunction; - queryRef: QueryReference; - onChange: (id: string) => void; - }) { - const { data } = useReadQuery(queryRef); - const [isPending, startTransition] = React.useTransition(); - const { todo } = data; + function Todo({ + queryRef, + refetch, + }: { + refetch: RefetchFunction; + queryRef: QueryReference; + onChange: (id: string) => void; + }) { + const { data } = useReadQuery(queryRef); + const [isPending, startTransition] = React.useTransition(); + const { todo } = data; - return ( - <> - -
- {todo.name} - {todo.completed && " (completed)"} -
- - ); - } + return ( + <> + +
+ {todo.name} + {todo.completed && " (completed)"} +
+ + ); + } - render(); + render(); - expect(screen.getByText("Loading")).toBeInTheDocument(); + expect(screen.getByText("Loading")).toBeInTheDocument(); - expect(await screen.findByTestId("todo")).toBeInTheDocument(); + expect(await screen.findByTestId("todo")).toBeInTheDocument(); - const todo = screen.getByTestId("todo"); - const button = screen.getByText("Refresh"); + const todo = screen.getByTestId("todo"); + const button = screen.getByText("Refresh"); - expect(todo).toHaveTextContent("Clean room"); + expect(todo).toHaveTextContent("Clean room"); - await act(() => user.click(button)); + 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(); + // 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(todo).toHaveAttribute("aria-busy", "true"); + // We can ensure this works with isPending from useTransition in the process + expect(todo).toHaveAttribute("aria-busy", "true"); - // Ensure we are showing the stale UI until the new todo has loaded - expect(todo).toHaveTextContent("Clean room"); + // Ensure we are showing the stale UI until the new todo has loaded + expect(todo).toHaveTextContent("Clean room"); - // Eventually we should see the updated todo content once its done - // suspending. - await waitFor(() => { - expect(todo).toHaveTextContent("Clean room (completed)"); - }); + // Eventually we should see the updated todo content once its done + // suspending. + await waitFor(() => { + expect(todo).toHaveTextContent("Clean room (completed)"); }); }); +function getItemTexts() { + return screen.getAllByTestId(/letter/).map( + // eslint-disable-next-line testing-library/no-node-access + (li) => li.firstChild!.textContent + ); +} +it.skip("re-suspends when calling `fetchMore` with different variables", async () => { + const { renders } = renderPaginatedIntegrationTest(); -describe.skip("fetchMore", () => { - function getItemTexts() { - return screen.getAllByTestId(/letter/).map( - // eslint-disable-next-line testing-library/no-node-access - (li) => li.firstChild!.textContent - ); - } - it.skip("re-suspends when calling `fetchMore` with different variables", async () => { - const { renders } = renderPaginatedIntegrationTest(); - - expect(renders.suspenseCount).toBe(1); - expect(screen.getByText("loading")).toBeInTheDocument(); - - const items = await screen.findAllByTestId(/letter/i); - expect(items).toHaveLength(2); - expect(getItemTexts()).toStrictEqual(["A", "B"]); + expect(renders.suspenseCount).toBe(1); + expect(screen.getByText("loading")).toBeInTheDocument(); - const button = screen.getByText("Fetch more"); - const user = userEvent.setup(); - await act(() => user.click(button)); + const items = await screen.findAllByTestId(/letter/i); + expect(items).toHaveLength(2); + expect(getItemTexts()).toStrictEqual(["A", "B"]); - // parent component re-suspends - expect(renders.suspenseCount).toBe(2); - await waitFor(() => { - expect(renders.count).toBe(2); - }); + const button = screen.getByText("Fetch more"); + const user = userEvent.setup(); + await act(() => user.click(button)); - expect(getItemTexts()).toStrictEqual(["C", "D"]); + // parent component re-suspends + expect(renders.suspenseCount).toBe(2); + await waitFor(() => { + expect(renders.count).toBe(2); }); - it.skip("properly uses `updateQuery` when calling `fetchMore`", async () => { - const { renders } = renderPaginatedIntegrationTest({ - updateQuery: true, - }); - expect(renders.suspenseCount).toBe(1); - expect(screen.getByText("loading")).toBeInTheDocument(); + expect(getItemTexts()).toStrictEqual(["C", "D"]); +}); +it.skip("properly uses `updateQuery` when calling `fetchMore`", async () => { + const { renders } = renderPaginatedIntegrationTest({ + updateQuery: true, + }); - const items = await screen.findAllByTestId(/letter/i); + expect(renders.suspenseCount).toBe(1); + expect(screen.getByText("loading")).toBeInTheDocument(); - expect(items).toHaveLength(2); - expect(getItemTexts()).toStrictEqual(["A", "B"]); + const items = await screen.findAllByTestId(/letter/i); - const button = screen.getByText("Fetch more"); - const user = userEvent.setup(); - await act(() => user.click(button)); + expect(items).toHaveLength(2); + expect(getItemTexts()).toStrictEqual(["A", "B"]); - // parent component re-suspends - expect(renders.suspenseCount).toBe(2); - await waitFor(() => { - expect(renders.count).toBe(2); - }); + const button = screen.getByText("Fetch more"); + const user = userEvent.setup(); + await act(() => user.click(button)); - const moreItems = await screen.findAllByTestId(/letter/i); - expect(moreItems).toHaveLength(4); - expect(getItemTexts()).toStrictEqual(["A", "B", "C", "D"]); + // parent component re-suspends + expect(renders.suspenseCount).toBe(2); + await waitFor(() => { + expect(renders.count).toBe(2); }); - it.skip("properly uses cache field policies when calling `fetchMore` without `updateQuery`", async () => { - const { renders } = renderPaginatedIntegrationTest({ - fieldPolicies: true, - }); - expect(renders.suspenseCount).toBe(1); - expect(screen.getByText("loading")).toBeInTheDocument(); - const items = await screen.findAllByTestId(/letter/i); + const moreItems = await screen.findAllByTestId(/letter/i); + expect(moreItems).toHaveLength(4); + expect(getItemTexts()).toStrictEqual(["A", "B", "C", "D"]); +}); +it.skip("properly uses cache field policies when calling `fetchMore` without `updateQuery`", async () => { + const { renders } = renderPaginatedIntegrationTest({ + fieldPolicies: true, + }); + expect(renders.suspenseCount).toBe(1); + expect(screen.getByText("loading")).toBeInTheDocument(); - expect(items).toHaveLength(2); - expect(getItemTexts()).toStrictEqual(["A", "B"]); + const items = await screen.findAllByTestId(/letter/i); - const button = screen.getByText("Fetch more"); - const user = userEvent.setup(); - await act(() => user.click(button)); + expect(items).toHaveLength(2); + expect(getItemTexts()).toStrictEqual(["A", "B"]); - // parent component re-suspends - expect(renders.suspenseCount).toBe(2); - await waitFor(() => { - expect(renders.count).toBe(2); - }); + const button = screen.getByText("Fetch more"); + const user = userEvent.setup(); + await act(() => user.click(button)); - const moreItems = await screen.findAllByTestId(/letter/i); - expect(moreItems).toHaveLength(4); - expect(getItemTexts()).toStrictEqual(["A", "B", "C", "D"]); + // parent component re-suspends + expect(renders.suspenseCount).toBe(2); + await waitFor(() => { + expect(renders.count).toBe(2); }); - it.skip("`fetchMore` works with startTransition to allow React to show stale UI until finished suspending", async () => { - type Variables = { - offset: number; - }; - interface Todo { - __typename: "Todo"; - id: string; - name: string; - completed: boolean; - } - interface Data { - todos: Todo[]; - } - const user = userEvent.setup(); - - const query: TypedDocumentNode = gql` - query TodosQuery($offset: Int!) { - todos(offset: $offset) { - id - name - completed - } - } - `; + const moreItems = await screen.findAllByTestId(/letter/i); + expect(moreItems).toHaveLength(4); + expect(getItemTexts()).toStrictEqual(["A", "B", "C", "D"]); +}); +it.skip("`fetchMore` works with startTransition to allow React to show stale UI until finished suspending", async () => { + type Variables = { + offset: number; + }; - const mocks: MockedResponse[] = [ - { - request: { query, variables: { offset: 0 } }, - result: { - data: { - todos: [ - { - __typename: "Todo", - id: "1", - name: "Clean room", - completed: false, - }, - ], - }, - }, - delay: 10, - }, - { - request: { query, variables: { offset: 1 } }, - result: { - data: { - todos: [ - { - __typename: "Todo", - id: "2", - name: "Take out trash", - completed: true, - }, - ], - }, - }, - delay: 10, - }, - ]; - - const client = new ApolloClient({ - link: new MockLink(mocks), - cache: new InMemoryCache({ - typePolicies: { - Query: { - fields: { - todos: offsetLimitPagination(), - }, + interface Todo { + __typename: "Todo"; + id: string; + name: string; + completed: boolean; + } + interface Data { + todos: Todo[]; + } + const user = userEvent.setup(); + + const query: TypedDocumentNode = gql` + query TodosQuery($offset: Int!) { + todos(offset: $offset) { + id + name + completed + } + } + `; + + const mocks: MockedResponse[] = [ + { + request: { query, variables: { offset: 0 } }, + result: { + data: { + todos: [ + { + __typename: "Todo", + id: "1", + name: "Clean room", + completed: false, + }, + ], + }, + }, + delay: 10, + }, + { + request: { query, variables: { offset: 1 } }, + result: { + data: { + todos: [ + { + __typename: "Todo", + id: "2", + name: "Take out trash", + completed: true, + }, + ], + }, + }, + delay: 10, + }, + ]; + + const client = new ApolloClient({ + link: new MockLink(mocks), + cache: new InMemoryCache({ + typePolicies: { + Query: { + fields: { + todos: offsetLimitPagination(), }, }, - }), - }); + }, + }), + }); - function App() { - return ( - - }> - - - - ); - } + function App() { + return ( + + }> + + + + ); + } - function SuspenseFallback() { - return

Loading

; - } + function SuspenseFallback() { + return

Loading

; + } - function Parent() { - const [queryRef, { fetchMore }] = useInteractiveQuery(query, { - variables: { offset: 0 }, - }); - return ; - } + function Parent() { + const [queryRef, { fetchMore }] = useInteractiveQuery(query, { + variables: { offset: 0 }, + }); + return ; + } - function Todo({ - queryRef, - fetchMore, - }: { - fetchMore: FetchMoreFunction; - queryRef: QueryReference; - }) { - const { data } = useReadQuery(queryRef); - const [isPending, startTransition] = React.useTransition(); - const { todos } = data; + function Todo({ + queryRef, + fetchMore, + }: { + fetchMore: FetchMoreFunction; + queryRef: QueryReference; + }) { + const { data } = useReadQuery(queryRef); + const [isPending, startTransition] = React.useTransition(); + const { todos } = data; - return ( - <> - -
- {todos.map((todo) => ( -
- {todo.name} - {todo.completed && " (completed)"} -
- ))} -
- - ); - } + return ( + <> + +
+ {todos.map((todo) => ( +
+ {todo.name} + {todo.completed && " (completed)"} +
+ ))} +
+ + ); + } + + render(); - render(); + expect(screen.getByText("Loading")).toBeInTheDocument(); - expect(screen.getByText("Loading")).toBeInTheDocument(); + expect(await screen.findByTestId("todos")).toBeInTheDocument(); - expect(await screen.findByTestId("todos")).toBeInTheDocument(); + const todos = screen.getByTestId("todos"); + const todo1 = screen.getByTestId("todo:1"); + const button = screen.getByText("Load more"); - const todos = screen.getByTestId("todos"); - const todo1 = screen.getByTestId("todo:1"); - const button = screen.getByText("Load more"); + expect(todo1).toBeInTheDocument(); - expect(todo1).toBeInTheDocument(); + await act(() => user.click(button)); - 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(); - // 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"); - // 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"); - // Ensure we are showing the stale UI until the new todo has loaded + // 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"); - - // 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.skip('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) - } - `; +it.skip('honors refetchWritePolicy set to "merge"', async () => { + const user = userEvent.setup(); - interface QueryData { - primes: number[]; + const query: TypedDocumentNode< + { primes: number[] }, + { min: number; max: number } + > = gql` + query GetPrimes($min: number, $max: number) { + primes(min: $min, max: $max) } + `; - 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, - }, - ]; + interface QueryData { + primes: number[]; + } - 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; - }, + 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
; - } + function SuspenseFallback() { + return
loading
; + } - const client = new ApolloClient({ - link: new MockLink(mocks), - cache, - }); + const client = new ApolloClient({ + link: new MockLink(mocks), + cache, + }); - function Child({ - refetch, - queryRef, - }: { - refetch: ( - variables?: Partial | undefined - ) => Promise>; - queryRef: QueryReference; - }) { - const { data, error, networkStatus } = useReadQuery(queryRef); + 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"}
-
- ); - } + return ( +
+ +
{data?.primes.join(", ")}
+
{networkStatus}
+
{error?.message || "undefined"}
+
+ ); + } - function Parent() { - const [queryRef, { refetch }] = useInteractiveQuery(query, { - variables: { min: 0, max: 12 }, - refetchWritePolicy: "merge", - }); - return ; - } + function Parent() { + const [queryRef, { refetch }] = useInteractiveQuery(query, { + variables: { min: 0, max: 12 }, + refetchWritePolicy: "merge", + }); + return ; + } - function App() { - return ( - - }> - - - - ); - } + function App() { + return ( + + }> + + + + ); + } - render(); + 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 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 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 + await waitFor(() => { + expect(screen.getByTestId("primes")).toHaveTextContent( + "2, 3, 5, 7, 11, 13, 17, 19, 23, 29" ); - 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], - ], - ]); }); + 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.skip('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) - } - `; +it.skip('defaults refetchWritePolicy to "overwrite"', async () => { + const user = userEvent.setup(); - interface QueryData { - primes: number[]; + const query: TypedDocumentNode< + { primes: number[] }, + { min: number; max: number } + > = gql` + query GetPrimes($min: number, $max: number) { + primes(min: $min, max: $max) } + `; - 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, - }, - ]; + interface QueryData { + primes: number[]; + } - 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; - }, + 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
; - } + function SuspenseFallback() { + return
loading
; + } - const client = new ApolloClient({ - link: new MockLink(mocks), - cache, - }); + const client = new ApolloClient({ + link: new MockLink(mocks), + cache, + }); - function Child({ - refetch, - queryRef, - }: { - refetch: ( - variables?: Partial | undefined - ) => Promise>; - queryRef: QueryReference; - }) { - const { data, error, networkStatus } = useReadQuery(queryRef); + 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"}
-
- ); - } + return ( +
+ +
{data?.primes.join(", ")}
+
{networkStatus}
+
{error?.message || "undefined"}
+
+ ); + } - function Parent() { - const [queryRef, { refetch }] = useInteractiveQuery(query, { - variables: { min: 0, max: 12 }, - }); - return ; - } + function Parent() { + const [queryRef, { refetch }] = useInteractiveQuery(query, { + variables: { min: 0, max: 12 }, + }); + return ; + } - function App() { - return ( - - }> - - - - ); - } + function App() { + return ( + + }> + + + + ); + } - render(); + 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 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 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 + await waitFor(() => { + expect(screen.getByTestId("primes")).toHaveTextContent( + "13, 17, 19, 23, 29" ); - expect(screen.getByTestId("error")).toHaveTextContent("undefined"); - expect(mergeParams).toEqual([ - [undefined, [2, 3, 5, 7, 11]], - [undefined, [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]], + ]); +}); - it.skip('does not suspend when partial data is in the cache and using a "cache-first" fetch policy with returnPartialData', async () => { - interface Data { - character: { - id: string; - name: string; - }; - } +it.skip('does not suspend when partial data is in the cache and using a "cache-first" fetch policy with returnPartialData', async () => { + interface Data { + character: { + id: string; + name: string; + }; + } - const fullQuery: TypedDocumentNode = gql` - query { - character { - id - name - } + const fullQuery: TypedDocumentNode = gql` + query { + character { + id + name } - `; + } + `; - const partialQuery = gql` - query { - character { - id - } + 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; } - const renders: Renders = { - errors: [], - errorCount: 0, - suspenseCount: 0, - count: 0, - }; + `; + const mocks = [ + { + request: { query: fullQuery }, + result: { data: { character: { id: "1", name: "Doctor Strange" } } }, + }, + ]; - const cache = new InMemoryCache(); + interface Renders { + errors: Error[]; + errorCount: number; + suspenseCount: number; + count: number; + } + const renders: Renders = { + errors: [], + errorCount: 0, + suspenseCount: 0, + count: 0, + }; - cache.writeQuery({ - query: partialQuery, - data: { character: { id: "1" } }, - }); + const cache = new InMemoryCache(); - const client = new ApolloClient({ - link: new MockLink(mocks), - cache, - }); + cache.writeQuery({ + query: partialQuery, + data: { character: { id: "1" } }, + }); - function App() { - return ( - - }> - - - - ); - } + const client = new ApolloClient({ + link: new MockLink(mocks), + cache, + }); - function SuspenseFallback() { - renders.suspenseCount++; - return

Loading

; - } + function App() { + return ( + + }> + + + + ); + } - function Parent() { - const [queryRef] = useInteractiveQuery(fullQuery, { - fetchPolicy: "cache-first", - returnPartialData: true, - }); - return ; - } + function SuspenseFallback() { + renders.suspenseCount++; + return

Loading

; + } - function Todo({ - queryRef, - }: { - queryRef: QueryReference>; - }) { - const { data, networkStatus, error } = useReadQuery(queryRef); - renders.count++; + function Parent() { + const [queryRef] = useInteractiveQuery(fullQuery, { + fetchPolicy: "cache-first", + returnPartialData: true, + }); + return ; + } - return ( - <> -
{data.character?.id}
-
{data.character?.name}
-
{networkStatus}
-
{error?.message || "undefined"}
- - ); - } + function Todo({ queryRef }: { queryRef: QueryReference> }) { + const { data, networkStatus, error } = useReadQuery(queryRef); + renders.count++; - render(); + return ( + <> +
{data.character?.id}
+
{data.character?.name}
+
{networkStatus}
+
{error?.message || "undefined"}
+ + ); + } - 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"); + render(); - 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.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"); - expect(renders.count).toBe(2); - expect(renders.suspenseCount).toBe(0); + 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"); - it.skip('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 - } + expect(renders.count).toBe(2); + expect(renders.suspenseCount).toBe(0); +}); + +it.skip('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 } - `; + } + `; - const cache = new InMemoryCache(); + const cache = new InMemoryCache(); - cache.writeQuery({ - query: partialQuery, - data: { character: { id: "1" } }, - variables: { id: "1" }, - }); + 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); + 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(); - expect(await screen.findByText("1 - Spider-Man")).toBeInTheDocument(); + rerender({ variables: { id: "2" } }); - rerender({ variables: { id: "2" } }); + expect(await screen.findByText("2 - Black Widow")).toBeInTheDocument(); - expect(await screen.findByText("2 - Black Widow")).toBeInTheDocument(); + expect(renders.frames[2]).toMatchObject({ + ...mocks[1].result, + networkStatus: NetworkStatus.ready, + error: undefined, + }); - expect(renders.frames[2]).toMatchObject({ + 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, - }); - - 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.skip('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; - }; - } +it.skip('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 fullQuery: TypedDocumentNode = gql` + query { + character { + id + name } - `; + } + `; - const partialQuery = gql` - query { - character { - id - } + 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(); + `; + const mocks = [ + { + request: { query: fullQuery }, + result: { data: { character: { id: "1", name: "Doctor Strange" } } }, + }, + ]; - cache.writeQuery({ - query: partialQuery, - data: { character: { id: "1" } }, - }); + 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: new MockLink(mocks), - cache, - }); + const cache = new InMemoryCache(); - function App() { - return ( - - }> - - - - ); - } + cache.writeQuery({ + query: partialQuery, + data: { character: { id: "1" } }, + }); - function SuspenseFallback() { - renders.suspenseCount++; - return

Loading

; - } + const client = new ApolloClient({ + link: new MockLink(mocks), + cache, + }); - function Parent() { - const [queryRef] = useInteractiveQuery(fullQuery, { - fetchPolicy: "network-only", - returnPartialData: true, - }); + function App() { + return ( + + }> + + + + ); + } - return ; - } + function SuspenseFallback() { + renders.suspenseCount++; + return

Loading

; + } - 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"}
- - ); - } + function Parent() { + const [queryRef] = useInteractiveQuery(fullQuery, { + fetchPolicy: "network-only", + returnPartialData: true, + }); - render(); + return ; + } - expect(renders.suspenseCount).toBe(1); + 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"}
+ + ); + } - 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"); + render(); - expect(renders.count).toBe(1); - expect(renders.suspenseCount).toBe(1); + expect(renders.suspenseCount).toBe(1); - expect(renders.frames).toMatchObject([ - { - ...mocks[0].result, - networkStatus: NetworkStatus.ready, - error: 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"); - it.skip('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; - }; - } + expect(renders.count).toBe(1); + expect(renders.suspenseCount).toBe(1); - const fullQuery: TypedDocumentNode = gql` - query { - character { - id - name - } - } - `; + expect(renders.frames).toMatchObject([ + { + ...mocks[0].result, + networkStatus: NetworkStatus.ready, + error: undefined, + }, + ]); +}); - 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: [], +it.skip('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; }; + } - const cache = new InMemoryCache(); - - cache.writeQuery({ - query: partialQuery, - data: { character: { id: "1" } }, - }); - - const client = new ApolloClient({ - link: new MockLink(mocks), - cache, - }); - - function App() { - return ( - - }> - - - - ); + const fullQuery: TypedDocumentNode = gql` + query { + character { + id + name + } } + `; - function SuspenseFallback() { - renders.suspenseCount++; - return

Loading

; + const partialQuery = gql` + query { + character { + id + } } + `; + const mocks = [ + { + request: { query: fullQuery }, + result: { data: { character: { id: "1", name: "Doctor Strange" } } }, + }, + ]; - function Parent() { - const [queryRef] = useInteractiveQuery(fullQuery, { - fetchPolicy: "no-cache", - returnPartialData: true, - }); + 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: [], + }; - return ; - } + const cache = new InMemoryCache(); - 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"}
- - ); - } + cache.writeQuery({ + query: partialQuery, + data: { character: { id: "1" } }, + }); + + const client = new ApolloClient({ + link: new MockLink(mocks), + cache, + }); - render(); + function App() { + return ( + + }> + + + + ); + } - expect(renders.suspenseCount).toBe(1); + function SuspenseFallback() { + renders.suspenseCount++; + return

Loading

; + } - await waitFor(() => { - expect(screen.getByTestId("character-name")).toHaveTextContent( - "Doctor Strange" - ); + function Parent() { + const [queryRef] = useInteractiveQuery(fullQuery, { + fetchPolicy: "no-cache", + returnPartialData: true, }); - 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); + return ; + } - expect(renders.frames).toMatchObject([ - { - ...mocks[0].result, - networkStatus: NetworkStatus.ready, - error: undefined, - }, - ]); + 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"}
+ + ); + } - consoleSpy.mockRestore(); - }); + render(); - it.skip('warns when using returnPartialData with a "no-cache" fetch policy', async () => { - const consoleSpy = jest.spyOn(console, "warn").mockImplementation(); + expect(renders.suspenseCount).toBe(1); - const query: TypedDocumentNode = gql` - query UserQuery { - greeting - } - `; - const mocks = [ - { - request: { query }, - result: { data: { greeting: "Hello" } }, - }, - ]; - - renderSuspenseHook( - () => - useInteractiveQuery(query, { - fetchPolicy: "no-cache", - returnPartialData: true, - }), - { mocks } + 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(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." - ); + expect(renders.count).toBe(1); + expect(renders.suspenseCount).toBe(1); - consoleSpy.mockRestore(); - }); + expect(renders.frames).toMatchObject([ + { + ...mocks[0].result, + networkStatus: NetworkStatus.ready, + error: undefined, + }, + ]); - it.skip('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; - }; - } + consoleSpy.mockRestore(); +}); - const fullQuery: TypedDocumentNode = gql` - query { - character { - id - name - } - } - `; +it.skip('warns when using returnPartialData with a "no-cache" fetch policy', async () => { + const consoleSpy = jest.spyOn(console, "warn").mockImplementation(); - 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 query: TypedDocumentNode = gql` + query UserQuery { + greeting } - const renders: Renders = { - errors: [], - errorCount: 0, - suspenseCount: 0, - count: 0, - frames: [], - }; + `; + const mocks = [ + { + request: { query }, + result: { data: { greeting: "Hello" } }, + }, + ]; - const cache = new InMemoryCache(); + renderSuspenseHook( + () => + useInteractiveQuery(query, { + fetchPolicy: "no-cache", + returnPartialData: true, + }), + { mocks } + ); - cache.writeQuery({ - query: partialQuery, - data: { character: { id: "1" } }, - }); + 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." + ); - const client = new ApolloClient({ - link: new MockLink(mocks), - cache, - }); + consoleSpy.mockRestore(); +}); - function App() { - return ( - - }> - - - - ); +it.skip('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; + }; + } + + const fullQuery: TypedDocumentNode = gql` + query { + character { + id + name + } } + `; - function SuspenseFallback() { - renders.suspenseCount++; - return

Loading

; + const partialQuery = gql` + query { + character { + id + } } + `; + const mocks = [ + { + request: { query: fullQuery }, + result: { data: { character: { id: "1", name: "Doctor Strange" } } }, + }, + ]; - function Parent() { - const [queryRef] = useInteractiveQuery(fullQuery, { - fetchPolicy: "cache-and-network", - returnPartialData: true, - }); + 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: [], + }; - return ; - } + const cache = new InMemoryCache(); - 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"}
- - ); - } + cache.writeQuery({ + query: partialQuery, + data: { character: { id: "1" } }, + }); + + const client = new ApolloClient({ + link: new MockLink(mocks), + cache, + }); - render(); + function App() { + return ( + + }> + + + + ); + } - 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"); + function SuspenseFallback() { + renders.suspenseCount++; + return

Loading

; + } - await waitFor(() => { - expect(screen.getByTestId("character-name")).toHaveTextContent( - "Doctor Strange" - ); + function Parent() { + const [queryRef] = useInteractiveQuery(fullQuery, { + fetchPolicy: "cache-and-network", + returnPartialData: true, }); - 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); + 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"); - expect(renders.frames).toMatchObject([ - { - data: { character: { id: "1" } }, - networkStatus: NetworkStatus.loading, - error: undefined, - }, - { - ...mocks[0].result, - networkStatus: NetworkStatus.ready, - error: 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"); - it.skip('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 - } + 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.skip('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(); + const cache = new InMemoryCache(); - cache.writeQuery({ - query: partialQuery, - data: { character: { id: "1" } }, - variables: { id: "1" }, - }); + 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, - }, - }); + const { renders, mocks, rerender } = renderVariablesIntegrationTest({ + variables: { id: "1" }, + cache, + options: { + fetchPolicy: "cache-and-network", + returnPartialData: true, + }, + }); - expect(renders.suspenseCount).toBe(0); + expect(renders.suspenseCount).toBe(0); - expect(await screen.findByText("1 - Spider-Man")).toBeInTheDocument(); + expect(await screen.findByText("1 - Spider-Man")).toBeInTheDocument(); - rerender({ variables: { id: "2" } }); + rerender({ variables: { id: "2" } }); - expect(await screen.findByText("2 - Black Widow")).toBeInTheDocument(); + 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, - }, - ]); - }); + 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.skip('does not suspend deferred queries with partial data in the cache and using a "cache-first" fetch policy with `returnPartialData`', async () => { - interface QueryData { - greeting: { +it.skip('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; - message?: string; - recipient?: { - __typename: string; - name: string; - }; + name: string; }; - } + }; + } - const user = userEvent.setup(); + const user = userEvent.setup(); - const query: TypedDocumentNode = gql` - query { - greeting { - message - ... on Greeting @defer { - recipient { - name - } + const query: TypedDocumentNode = gql` + query { + greeting { + message + ... on Greeting @defer { + recipient { + name } } } - `; + } + `; - const link = new MockSubscriptionLink(); - const cache = new InMemoryCache(); + 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" }, - }, + // 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, - }); + }, + }); + consoleSpy.mockRestore(); - function App() { - return ( - - }> - - - - ); - } + 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: [], + }; - function SuspenseFallback() { - renders.suspenseCount++; - return

Loading

; - } + const client = new ApolloClient({ + link, + cache, + }); - function Parent() { - const [queryRef, loadTodo] = useInteractiveQuery(query, { - fetchPolicy: "cache-first", - returnPartialData: true, - }); + function App() { + return ( + + }> + + + + ); + } - return ( -
- - {queryRef && } -
- ); - } + function SuspenseFallback() { + renders.suspenseCount++; + return

Loading

; + } - 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"}
- - ); - } + function Parent() { + const [queryRef, loadTodo] = useInteractiveQuery(query, { + fetchPolicy: "cache-first", + returnPartialData: true, + }); - render(); + return ( +
+ + {queryRef && } +
+ ); + } - await act(() => user.click(screen.getByText("Load todo"))); + 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"}
+ + ); + } - 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"); + render(); - link.simulateResult({ - result: { - data: { - greeting: { message: "Hello world", __typename: "Greeting" }, - }, - hasNext: true, - }, - }); + await act(() => user.click(screen.getByText("Load todo"))); - 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"); + 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: { - incremental: [ - { - data: { - __typename: "Greeting", - recipient: { name: "Alice", __typename: "Person" }, - }, - path: ["greeting"], - }, - ], - hasNext: false, + link.simulateResult({ + result: { + data: { + greeting: { message: "Hello world", __typename: "Greeting" }, }, - }); + hasNext: true, + }, + }); - await waitFor(() => { - expect(screen.getByTestId("recipient").textContent).toEqual("Alice"); - }); + await waitFor(() => { expect(screen.getByTestId("message")).toHaveTextContent("Hello world"); - expect(screen.getByTestId("network-status")).toHaveTextContent("7"); // ready - expect(screen.getByTestId("error")).toHaveTextContent("undefined"); + }); + expect(screen.getByTestId("recipient")).toHaveTextContent("Cached Alice"); + 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: { + link.simulateResult({ + result: { + incremental: [ + { + data: { __typename: "Greeting", - recipient: { __typename: "Person", name: "Cached Alice" }, + 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.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, + }, + { + data: { + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Alice" }, }, - networkStatus: NetworkStatus.ready, - error: undefined, }, - ]); - }); + networkStatus: NetworkStatus.ready, + error: undefined, + }, + ]); }); describe.skip("type tests", () => { From f14a33daaa6caa68f5ab96e2c6df2a6fd46bf8dd Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 8 Nov 2023 16:49:31 -0700 Subject: [PATCH 041/199] Add custom matchers to check if profiled component has rendered or has rendered a certain number of times --- src/testing/matchers/ProfiledComponent.ts | 51 +++++++++++++++++++++++ src/testing/matchers/index.d.ts | 3 ++ src/testing/matchers/index.ts | 9 +++- 3 files changed, 62 insertions(+), 1 deletion(-) diff --git a/src/testing/matchers/ProfiledComponent.ts b/src/testing/matchers/ProfiledComponent.ts index 8a4e72025a9..c9bc99be675 100644 --- a/src/testing/matchers/ProfiledComponent.ts +++ b/src/testing/matchers/ProfiledComponent.ts @@ -5,6 +5,57 @@ import type { ProfiledComponent, ProfiledHook, } from "../internal/index.js"; + +export const toHaveRendered: MatcherFunction = function ( + profiled: ProfiledComponent +) { + const hint = this.utils.matcherHint( + "toHaveRendered", + "ProfiledComponent", + "" + ); + const pass = profiled.currentRenderCount() > 0; + + return { + pass, + message() { + return ( + hint + + `\n\nExpected profiled component to${pass ? " not" : ""} have rendered.` + ); + }, + }; +}; + +export const toHaveRenderedTimes: MatcherFunction<[count: number]> = function ( + profiled: ProfiledComponent, + count: number +) { + const hint = this.utils.matcherHint( + "toHaveRenderedTimes", + "ProfiledComponent", + "renderCount" + ); + const actualRenderCount = profiled.currentRenderCount(); + const pass = actualRenderCount === count; + + return { + pass, + message: () => { + return ( + hint + + `\n\nExpected profiled component to${ + pass ? " not" : "" + } have rendered times ${this.utils.printExpected( + count + )}, but it rendered times ${this.utils.printReceived( + actualRenderCount + )}.` + ); + }, + }; +}; + export const toRerender: MatcherFunction<[options?: NextRenderOptions]> = async function (actual, options) { const _profiled = actual as diff --git a/src/testing/matchers/index.d.ts b/src/testing/matchers/index.d.ts index 715f7d3dbdf..59d5693d888 100644 --- a/src/testing/matchers/index.d.ts +++ b/src/testing/matchers/index.d.ts @@ -16,6 +16,9 @@ interface ApolloCustomMatchers { */ toMatchDocument(document: DocumentNode): R; + toHaveRendered(): R; + toHaveRenderedTimes(count: number): R; + /** * Used to determine if the Suspense cache has a cache entry. */ diff --git a/src/testing/matchers/index.ts b/src/testing/matchers/index.ts index d2ebd8ce7c2..6aa673b0fc2 100644 --- a/src/testing/matchers/index.ts +++ b/src/testing/matchers/index.ts @@ -1,9 +1,16 @@ import { expect } from "@jest/globals"; import { toMatchDocument } from "./toMatchDocument.js"; import { toHaveSuspenseCacheEntryUsing } from "./toHaveSuspenseCacheEntryUsing.js"; -import { toRerender, toRenderExactlyTimes } from "./ProfiledComponent.js"; +import { + toHaveRendered, + toHaveRenderedTimes, + toRerender, + toRenderExactlyTimes, +} from "./ProfiledComponent.js"; expect.extend({ + toHaveRendered, + toHaveRenderedTimes, toHaveSuspenseCacheEntryUsing, toMatchDocument, toRerender, From 7785ab5e4ae7ade80ff0a800e8ae753a062faf7c Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 8 Nov 2023 17:18:37 -0700 Subject: [PATCH 042/199] Use a nicer matcher hint on toRerender --- src/testing/matchers/ProfiledComponent.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/testing/matchers/ProfiledComponent.ts b/src/testing/matchers/ProfiledComponent.ts index c9bc99be675..de45b2d9b7f 100644 --- a/src/testing/matchers/ProfiledComponent.ts +++ b/src/testing/matchers/ProfiledComponent.ts @@ -65,7 +65,7 @@ export const toRerender: MatcherFunction<[options?: NextRenderOptions]> = "ProfiledComponent" in _profiled ? _profiled.ProfiledComponent : _profiled; - const hint = this.utils.matcherHint("toRerender"); + const hint = this.utils.matcherHint("toRerender", "ProfiledComponent", ""); let pass = true; try { await profiled.peekRender({ timeout: 100, ...options }); From 66d03a6d22948d5ef2575b752754630f9234b31e Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 8 Nov 2023 17:18:53 -0700 Subject: [PATCH 043/199] Move failure message for toRerender below matcher hint --- src/testing/matchers/ProfiledComponent.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/testing/matchers/ProfiledComponent.ts b/src/testing/matchers/ProfiledComponent.ts index de45b2d9b7f..d780f8f6d12 100644 --- a/src/testing/matchers/ProfiledComponent.ts +++ b/src/testing/matchers/ProfiledComponent.ts @@ -76,12 +76,13 @@ export const toRerender: MatcherFunction<[options?: NextRenderOptions]> = throw e; } } + return { pass, message() { return ( hint + - ` Expected component to${pass ? " not" : ""} rerender, ` + + `\n\nExpected component to${pass ? " not" : ""} rerender, ` + `but it did${pass ? "" : " not"}.` ); }, From 1c70110150253241337b151b5d6643e112317dff Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 8 Nov 2023 17:19:48 -0700 Subject: [PATCH 044/199] Use waitForNextRender in toRerender to avoid needing to advance the iterator --- src/testing/matchers/ProfiledComponent.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/testing/matchers/ProfiledComponent.ts b/src/testing/matchers/ProfiledComponent.ts index d780f8f6d12..992651e8782 100644 --- a/src/testing/matchers/ProfiledComponent.ts +++ b/src/testing/matchers/ProfiledComponent.ts @@ -68,7 +68,7 @@ export const toRerender: MatcherFunction<[options?: NextRenderOptions]> = const hint = this.utils.matcherHint("toRerender", "ProfiledComponent", ""); let pass = true; try { - await profiled.peekRender({ timeout: 100, ...options }); + await profiled.waitForNextRender({ timeout: 100, ...options }); } catch (e) { if (e instanceof WaitForRenderTimeoutError) { pass = false; From 0a8b235428c5bd00d79831323dd9973111c24ace Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 8 Nov 2023 17:27:22 -0700 Subject: [PATCH 045/199] Use simpler style for profiling components in test --- .../__tests__/useInteractiveQuery.test.tsx | 135 ++++++------------ 1 file changed, 42 insertions(+), 93 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index 56a01c41387..762d3747762 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -527,116 +527,65 @@ function renderSuspenseHook( return { ...view, renders }; } -it("loads a query when the load query function is called", async () => { +it("loads a query and suspends when the load query function is called", async () => { const user = userEvent.setup(); const { query, mocks } = useSimpleQueryCase(); - function SuspenseFallback() { - ProfiledApp.updateSnapshot((snapshot) => ({ - ...snapshot, - suspenseCount: snapshot.suspenseCount + 1, - })); - - return

Loading

; - } - - function Parent() { - const [queryRef, loadQuery] = useInteractiveQuery(query); - - ProfiledApp.updateSnapshot((snapshot) => ({ - ...snapshot, - parentRenderCount: snapshot.parentRenderCount + 1, - })); - - return ( - <> - - }> - {queryRef && } - - - ); - } + const SuspenseFallback = profile({ + Component: () =>

Loading

, + }); - function Greeting({ - queryRef, - }: { - queryRef: QueryReference; - }) { - const result = useReadQuery(queryRef); + const App = profile({ + Component: () => { + const [queryRef, loadQuery] = useInteractiveQuery(query); - ProfiledApp.updateSnapshot((snapshot) => ({ - ...snapshot, - result, - childRenderCount: snapshot.childRenderCount + 1, - })); + return ( + <> + + }> + {queryRef && } + + + ); + }, + }); - return
{result.data.greeting}
; - } + const Greeting = profile< + { result: UseReadQueryResult }, + { queryRef: QueryReference } + >({ + Component: ({ queryRef }) => { + Greeting.updateSnapshot({ result: useReadQuery(queryRef) }); - const ProfiledApp = profile<{ - result: UseReadQueryResult | null; - suspenseCount: number; - parentRenderCount: number; - childRenderCount: number; - }>({ - Component: () => ( - - - - ), - snapshotDOM: true, - initialSnapshot: { - result: null, - suspenseCount: 0, - parentRenderCount: 0, - childRenderCount: 0, + return null; }, }); - render(); + render( + + + + ); - { - const { snapshot } = await ProfiledApp.takeRender(); - expect(snapshot).toEqual({ - result: null, - suspenseCount: 0, - parentRenderCount: 1, - childRenderCount: 0, - }); - } + expect(SuspenseFallback).not.toHaveRendered(); await act(() => user.click(screen.getByText("Load query"))); - { - const { snapshot, withinDOM } = await ProfiledApp.takeRender(); + expect(SuspenseFallback).toHaveRendered(); + expect(Greeting).not.toHaveRendered(); + expect(App).toHaveRenderedTimes(2); - expect(withinDOM().getByText("Loading")).toBeInTheDocument(); - expect(snapshot).toEqual({ - result: null, - suspenseCount: 1, - parentRenderCount: 2, - childRenderCount: 0, - }); - } + const { snapshot } = await Greeting.takeRender(); - { - const { snapshot, withinDOM } = await ProfiledApp.takeRender(); - - expect(withinDOM().getByText("Hello")).toBeInTheDocument(); - expect(snapshot).toEqual({ - result: { - data: { greeting: "Hello" }, - networkStatus: NetworkStatus.ready, - error: undefined, - }, - suspenseCount: 1, - parentRenderCount: 2, - childRenderCount: 1, - }); - } + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); - await expect(ProfiledApp).not.toRerender(); + expect(SuspenseFallback).toHaveRenderedTimes(1); + expect(Greeting).toHaveRenderedTimes(1); + expect(App).toHaveRenderedTimes(3); }); it("loads a query with variables and suspends by passing variables to the loadQuery function", async () => { From 6604504b45fd0d6cda4baedbc1f7070c17f98b5c Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 8 Nov 2023 17:36:38 -0700 Subject: [PATCH 046/199] Create helpers to render with mocks or with client --- .../__tests__/useInteractiveQuery.test.tsx | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index 762d3747762..c59107db153 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -8,6 +8,7 @@ import { waitFor, } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; +import type { Options as UserEventOptions } from "@testing-library/user-event"; import { ErrorBoundary, ErrorBoundaryProps } from "react-error-boundary"; import { expectTypeOf } from "expect-type"; import { GraphQLError } from "graphql"; @@ -29,6 +30,7 @@ import { } from "../../../core"; import { MockedProvider, + MockedProviderProps, MockedResponse, MockLink, MockSubscriptionLink, @@ -111,6 +113,38 @@ function useVariablesQueryCase() { return { mocks, query }; } +function renderWithUser(ui: React.ReactElement, options?: UserEventOptions) { + const user = userEvent.setup(options); + + return { ...render(ui), user }; +} + +function renderWithMocks( + ui: React.ReactElement, + { + userEvent, + ...props + }: MockedProviderProps & { userEvent?: UserEventOptions } +) { + return renderWithUser( + {ui}, + userEvent + ); +} + +function renderWithClient( + ui: React.ReactElement, + { + client, + userEvent, + }: { client: ApolloClient; userEvent?: UserEventOptions } +) { + return renderWithUser( + {ui}, + userEvent + ); +} + function renderVariablesIntegrationTest({ variables, mocks, From 3e8d154d646f9fe54191f58d0ec2eefb8f43cf48 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 8 Nov 2023 17:56:38 -0700 Subject: [PATCH 047/199] Use render helpers in all passing tests --- .../__tests__/useInteractiveQuery.test.tsx | 196 ++++-------------- 1 file changed, 43 insertions(+), 153 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index c59107db153..c3e8e374b16 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -562,7 +562,6 @@ function renderSuspenseHook( } it("loads a query and suspends when the load query function is called", async () => { - const user = userEvent.setup(); const { query, mocks } = useSimpleQueryCase(); const SuspenseFallback = profile({ @@ -595,11 +594,7 @@ it("loads a query and suspends when the load query function is called", async () }, }); - render( - - - - ); + const { user } = renderWithMocks(, { mocks }); expect(SuspenseFallback).not.toHaveRendered(); @@ -623,7 +618,6 @@ it("loads a query and suspends when the load query function is called", async () }); it("loads a query with variables and suspends by passing variables to the loadQuery function", async () => { - const user = userEvent.setup(); const { query, mocks } = useVariablesQueryCase(); function SuspenseFallback() { @@ -675,11 +669,7 @@ it("loads a query with variables and suspends by passing variables to the loadQu parentRenderCount: number; childRenderCount: number; }>({ - Component: () => ( - - - - ), + Component: () => , snapshotDOM: true, initialSnapshot: { result: null, @@ -689,7 +679,7 @@ it("loads a query with variables and suspends by passing variables to the loadQu }, }); - render(); + const { user } = renderWithMocks(, { mocks }); { const { snapshot } = await ProfiledApp.takeRender(); @@ -735,7 +725,6 @@ it("loads a query with variables and suspends by passing variables to the loadQu }); it("can change variables on a query and resuspend by passing new variables to the loadQuery function", async () => { - const user = userEvent.setup(); const { query, mocks } = useVariablesQueryCase(); function SuspenseFallback() { @@ -792,11 +781,7 @@ it("can change variables on a query and resuspend by passing new variables to th parentRenderCount: number; childRenderCount: number; }>({ - Component: () => ( - - - - ), + Component: () => , snapshotDOM: true, initialSnapshot: { result: null, @@ -806,7 +791,7 @@ it("can change variables on a query and resuspend by passing new variables to th }, }); - render(); + const { user } = renderWithMocks(, { mocks }); { const { snapshot } = await ProfiledApp.takeRender(); @@ -885,7 +870,6 @@ it("can change variables on a query and resuspend by passing new variables to th }); it("allows the client to be overridden", async () => { - const user = userEvent.setup(); const { query } = useSimpleQueryCase(); const globalClient = new ApolloClient({ @@ -936,18 +920,14 @@ it("allows the client to be overridden", async () => { const ProfiledApp = profile<{ result: UseReadQueryResult | null; }>({ - Component: () => ( - - - - ), + Component: () => , snapshotDOM: true, initialSnapshot: { result: null, }, }); - render(); + const { user } = renderWithClient(, { client: globalClient }); { const { snapshot } = await ProfiledApp.takeRender(); @@ -982,7 +962,6 @@ it("passes context to the link", async () => { context: Record; } - const user = userEvent.setup(); const query: TypedDocumentNode = gql` query ContextQuery { context @@ -1031,18 +1010,14 @@ it("passes context to the link", async () => { const ProfiledApp = profile<{ result: UseReadQueryResult | null; }>({ - Component: () => ( - - - - ), + Component: () => , snapshotDOM: true, initialSnapshot: { result: null, }, }); - render(); + const { user } = renderWithClient(, { client }); { const { snapshot } = await ProfiledApp.takeRender(); @@ -1081,7 +1056,6 @@ it('enables canonical results when canonizeResults is "true"', async () => { results: Result[]; } - const user = userEvent.setup(); const cache = new InMemoryCache({ typePolicies: { Result: { @@ -1147,17 +1121,13 @@ it('enables canonical results when canonizeResults is "true"', async () => { const ProfiledApp = profile<{ result: UseReadQueryResult | null; }>({ - Component: () => ( - - - - ), + Component: () => , initialSnapshot: { result: null, }, }); - render(); + const { user } = renderWithClient(, { client }); { const { snapshot } = await ProfiledApp.takeRender(); @@ -1211,7 +1181,6 @@ it("can disable canonical results when the cache's canonizeResults setting is tr } `; - const user = userEvent.setup(); const results: Result[] = [ { __typename: "Result", value: 0 }, { __typename: "Result", value: 1 }, @@ -1256,17 +1225,13 @@ it("can disable canonical results when the cache's canonizeResults setting is tr const ProfiledApp = profile<{ result: UseReadQueryResult | null; }>({ - Component: () => ( - - - - ), + Component: () => , initialSnapshot: { result: null, }, }); - render(); + const { user } = renderWithMocks(, { cache }); { const { snapshot } = await ProfiledApp.takeRender(); @@ -1294,7 +1259,6 @@ it("can disable canonical results when the cache's canonizeResults setting is tr }); it("returns initial cache data followed by network data when the fetch policy is `cache-and-network`", async () => { - const user = userEvent.setup(); const query: TypedDocumentNode<{ hello: string }, never> = gql` query { hello @@ -1353,18 +1317,14 @@ it("returns initial cache data followed by network data when the fetch policy is result: UseReadQueryResult | null; suspenseCount: number; }>({ - Component: () => ( - - - - ), + Component: () => , initialSnapshot: { result: null, suspenseCount: 0, }, }); - render(); + const { user } = renderWithClient(, { client }); { const { snapshot } = await ProfiledApp.takeRender(); @@ -1404,7 +1364,6 @@ it("all data is present in the cache, no network request is made", async () => { hello } `; - const user = userEvent.setup(); const cache = new InMemoryCache(); const link = new MockLink([ { @@ -1455,18 +1414,14 @@ it("all data is present in the cache, no network request is made", async () => { result: UseReadQueryResult | null; suspenseCount: number; }>({ - Component: () => ( - - - - ), + Component: () => , initialSnapshot: { result: null, suspenseCount: 0, }, }); - render(); + const { user } = renderWithClient(, { client }); { const { snapshot } = await ProfiledApp.takeRender(); @@ -1490,7 +1445,6 @@ it("all data is present in the cache, no network request is made", async () => { }); it("partial data is present in the cache so it is ignored and network request is made", async () => { - const user = userEvent.setup(); const query = gql` { hello @@ -1552,18 +1506,14 @@ it("partial data is present in the cache so it is ignored and network request is result: UseReadQueryResult | null; suspenseCount: number; }>({ - Component: () => ( - - - - ), + Component: () => , initialSnapshot: { result: null, suspenseCount: 0, }, }); - render(); + const { user } = renderWithClient(, { client }); { const { snapshot } = await ProfiledApp.takeRender(); @@ -1595,7 +1545,6 @@ it("partial data is present in the cache so it is ignored and network request is }); it("existing data in the cache is ignored when `fetchPolicy` is 'network-only'", async () => { - const user = userEvent.setup(); const query = gql` query { hello @@ -1653,18 +1602,14 @@ it("existing data in the cache is ignored when `fetchPolicy` is 'network-only'", result: UseReadQueryResult | null; suspenseCount: number; }>({ - Component: () => ( - - - - ), + Component: () => , initialSnapshot: { result: null, suspenseCount: 0, }, }); - render(); + const { user } = renderWithClient(, { client }); { const { snapshot } = await ProfiledApp.takeRender(); @@ -1696,7 +1641,6 @@ it("existing data in the cache is ignored when `fetchPolicy` is 'network-only'", }); it("fetches data from the network but does not update the cache when `fetchPolicy` is 'no-cache'", async () => { - const user = userEvent.setup(); const query = gql` query { hello @@ -1751,18 +1695,14 @@ it("fetches data from the network but does not update the cache when `fetchPolic result: UseReadQueryResult | null; suspenseCount: number; }>({ - Component: () => ( - - - - ), + Component: () => , initialSnapshot: { result: null, suspenseCount: 0, }, }); - render(); + const { user } = renderWithClient(, { client }); { const { snapshot } = await ProfiledApp.takeRender(); @@ -1808,7 +1748,6 @@ it("works with startTransition to change variables", async () => { completed: boolean; }; } - const user = userEvent.setup(); const query: TypedDocumentNode = gql` query TodoItemQuery($id: ID!) { @@ -1845,11 +1784,7 @@ it("works with startTransition to change variables", async () => { }); function App() { - return ( - - - - ); + return ; } function SuspenseFallback() { @@ -1901,7 +1836,7 @@ it("works with startTransition to change variables", async () => { ); } - render(); + const { user } = renderWithClient(, { client }); await act(() => user.click(screen.getByText("Load first todo"))); @@ -1946,8 +1881,6 @@ it('does not suspend deferred queries with data in the cache and using a "cache- }; } - const user = userEvent.setup(); - const query: TypedDocumentNode = gql` query { greeting { @@ -1981,11 +1914,9 @@ it('does not suspend deferred queries with data in the cache and using a "cache- }>({ Component: () => { return ( - - }> - - - + }> + + ); }, initialSnapshot: { @@ -2025,7 +1956,7 @@ it('does not suspend deferred queries with data in the cache and using a "cache- return null; } - render(); + const { user } = renderWithClient(, { client }); { const { snapshot } = await ProfiledApp.takeRender(); @@ -2119,7 +2050,6 @@ it('does not suspend deferred queries with data in the cache and using a "cache- it("reacts to cache updates", async () => { const { query, mocks } = useSimpleQueryCase(); - const user = userEvent.setup(); const client = new ApolloClient({ cache: new InMemoryCache(), link: new MockLink(mocks), @@ -2158,18 +2088,14 @@ it("reacts to cache updates", async () => { result: UseReadQueryResult | null; suspenseCount: number; }>({ - Component: () => ( - - - - ), + Component: () => , initialSnapshot: { result: null, suspenseCount: 0, }, }); - render(); + const { user } = renderWithClient(, { client }); { const { snapshot } = await ProfiledApp.takeRender(); @@ -2233,8 +2159,6 @@ it("applies `errorPolicy` on next fetch when it changes between renders", async }, ]; - const user = userEvent.setup(); - function SuspenseFallback() { ProfiledApp.updateSnapshot((snapshot) => ({ ...snapshot, @@ -2289,11 +2213,7 @@ it("applies `errorPolicy` on next fetch when it changes between renders", async result: UseReadQueryResult | null; suspenseCount: number; }>({ - Component: () => ( - - - - ), + Component: () => , initialSnapshot: { error: undefined, errorBoundaryCount: 0, @@ -2302,7 +2222,7 @@ it("applies `errorPolicy` on next fetch when it changes between renders", async }, }); - render(); + const { user } = renderWithMocks(, { mocks }); { const { snapshot } = await ProfiledApp.takeRender(); @@ -2399,8 +2319,6 @@ it("applies `context` on next fetch when it changes between renders", async () = context: Record; } - const user = userEvent.setup(); - const query: TypedDocumentNode = gql` query { context @@ -2449,14 +2367,10 @@ it("applies `context` on next fetch when it changes between renders", async () = } function App() { - return ( - - - - ); + return ; } - render(); + const { user } = renderWithClient(, { client }); await act(() => user.click(screen.getByText("Load query"))); @@ -2506,8 +2420,6 @@ it("returns canonical results immediately when `canonizeResults` changes from `f { __typename: "Result", value: 5 }, ]; - const user = userEvent.setup(); - cache.writeQuery({ query, data: { results }, @@ -2554,14 +2466,10 @@ it("returns canonical results immediately when `canonizeResults` changes from `f } function App() { - return ( - - - - ); + return ; } - render(); + const { user } = renderWithClient(, { client }); function verifyCanonicalResults(data: Data, canonized: boolean) { const resultSet = new Set(data.results); @@ -2594,8 +2502,6 @@ it("applies changed `refetchWritePolicy` to next fetch when changing between ren primes: number[]; } - const user = userEvent.setup(); - const query: TypedDocumentNode = gql` query GetPrimes($min: number, $max: number) { primes(min: $min, max: $max) @@ -2682,14 +2588,10 @@ it("applies changed `refetchWritePolicy` to next fetch when changing between ren } function App() { - return ( - - - - ); + return ; } - render(); + const { user } = renderWithClient(, { client }); await act(() => user.click(screen.getByText("Load query"))); @@ -2745,8 +2647,6 @@ it("applies `returnPartialData` on next fetch when it changes between renders", }; } - const user = userEvent.setup(); - const fullQuery: TypedDocumentNode = gql` query { character { @@ -2839,14 +2739,10 @@ it("applies `returnPartialData` on next fetch when it changes between renders", } function App() { - return ( - - - - ); + return ; } - render(); + const { user } = renderWithClient(, { client }); await act(() => user.click(screen.getByText("Load query"))); @@ -2881,8 +2777,6 @@ it("applies updated `fetchPolicy` on next fetch when it changes between renders" }; } - const user = userEvent.setup(); - const query: TypedDocumentNode = gql` query { character { @@ -2960,14 +2854,10 @@ it("applies updated `fetchPolicy` on next fetch when it changes between renders" } function App() { - return ( - - - - ); + return ; } - render(); + const { user } = renderWithClient(, { client }); await act(() => user.click(screen.getByText("Load query"))); From f123982153fc27518814054ab59734918bf5abc2 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 8 Nov 2023 18:05:09 -0700 Subject: [PATCH 048/199] Reduce redundancy and increase consistency in naming of app component in tests --- .../__tests__/useInteractiveQuery.test.tsx | 88 +++++++------------ 1 file changed, 32 insertions(+), 56 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index c3e8e374b16..1ce539b6574 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -629,7 +629,7 @@ it("loads a query with variables and suspends by passing variables to the loadQu return

Loading

; } - function Parent() { + function App() { const [queryRef, loadQuery] = useInteractiveQuery(query); ProfiledApp.updateSnapshot((snapshot) => ({ @@ -669,7 +669,7 @@ it("loads a query with variables and suspends by passing variables to the loadQu parentRenderCount: number; childRenderCount: number; }>({ - Component: () => , + Component: App, snapshotDOM: true, initialSnapshot: { result: null, @@ -736,7 +736,7 @@ it("can change variables on a query and resuspend by passing new variables to th return

Loading

; } - function Parent() { + function App() { const [queryRef, loadQuery] = useInteractiveQuery(query); ProfiledApp.updateSnapshot((snapshot) => ({ @@ -781,7 +781,7 @@ it("can change variables on a query and resuspend by passing new variables to th parentRenderCount: number; childRenderCount: number; }>({ - Component: () => , + Component: App, snapshotDOM: true, initialSnapshot: { result: null, @@ -890,7 +890,7 @@ it("allows the client to be overridden", async () => { return

Loading

; } - function Parent() { + function App() { const [queryRef, loadQuery] = useInteractiveQuery(query, { client: localClient, }); @@ -920,7 +920,7 @@ it("allows the client to be overridden", async () => { const ProfiledApp = profile<{ result: UseReadQueryResult | null; }>({ - Component: () => , + Component: App, snapshotDOM: true, initialSnapshot: { result: null, @@ -984,7 +984,7 @@ it("passes context to the link", async () => { return

Loading

; } - function Parent() { + function App() { const [queryRef, loadQuery] = useInteractiveQuery(query, { context: { valueA: "A", valueB: "B" }, }); @@ -1010,7 +1010,7 @@ it("passes context to the link", async () => { const ProfiledApp = profile<{ result: UseReadQueryResult | null; }>({ - Component: () => , + Component: App, snapshotDOM: true, initialSnapshot: { result: null, @@ -1095,7 +1095,7 @@ it('enables canonical results when canonizeResults is "true"', async () => { return

Loading

; } - function Parent() { + function App() { const [queryRef, loadQuery] = useInteractiveQuery(query, { canonizeResults: true, }); @@ -1121,7 +1121,7 @@ it('enables canonical results when canonizeResults is "true"', async () => { const ProfiledApp = profile<{ result: UseReadQueryResult | null; }>({ - Component: () => , + Component: App, initialSnapshot: { result: null, }, @@ -1199,7 +1199,7 @@ it("can disable canonical results when the cache's canonizeResults setting is tr return

Loading

; } - function Parent() { + function App() { const [queryRef, loadQuery] = useInteractiveQuery(query, { canonizeResults: false, }); @@ -1225,7 +1225,7 @@ it("can disable canonical results when the cache's canonizeResults setting is tr const ProfiledApp = profile<{ result: UseReadQueryResult | null; }>({ - Component: () => , + Component: App, initialSnapshot: { result: null, }, @@ -1286,7 +1286,7 @@ it("returns initial cache data followed by network data when the fetch policy is return

Loading

; } - function Parent() { + function App() { const [queryRef, loadQuery] = useInteractiveQuery(query, { fetchPolicy: "cache-and-network", }); @@ -1317,7 +1317,7 @@ it("returns initial cache data followed by network data when the fetch policy is result: UseReadQueryResult | null; suspenseCount: number; }>({ - Component: () => , + Component: App, initialSnapshot: { result: null, suspenseCount: 0, @@ -1389,7 +1389,7 @@ it("all data is present in the cache, no network request is made", async () => { return

Loading

; } - function Parent() { + function App() { const [queryRef, loadQuery] = useInteractiveQuery(query); return ( @@ -1414,7 +1414,7 @@ it("all data is present in the cache, no network request is made", async () => { result: UseReadQueryResult | null; suspenseCount: number; }>({ - Component: () => , + Component: App, initialSnapshot: { result: null, suspenseCount: 0, @@ -1481,7 +1481,7 @@ it("partial data is present in the cache so it is ignored and network request is return

Loading

; } - function Parent() { + function App() { const [queryRef, loadQuery] = useInteractiveQuery(query); return ( @@ -1506,7 +1506,7 @@ it("partial data is present in the cache so it is ignored and network request is result: UseReadQueryResult | null; suspenseCount: number; }>({ - Component: () => , + Component: App, initialSnapshot: { result: null, suspenseCount: 0, @@ -1575,7 +1575,7 @@ it("existing data in the cache is ignored when `fetchPolicy` is 'network-only'", return

Loading

; } - function Parent() { + function App() { const [queryRef, loadQuery] = useInteractiveQuery(query, { fetchPolicy: "network-only", }); @@ -1602,7 +1602,7 @@ it("existing data in the cache is ignored when `fetchPolicy` is 'network-only'", result: UseReadQueryResult | null; suspenseCount: number; }>({ - Component: () => , + Component: App, initialSnapshot: { result: null, suspenseCount: 0, @@ -1668,7 +1668,7 @@ it("fetches data from the network but does not update the cache when `fetchPolic return

Loading

; } - function Parent() { + function App() { const [queryRef, loadQuery] = useInteractiveQuery(query, { fetchPolicy: "no-cache", }); @@ -1695,7 +1695,7 @@ it("fetches data from the network but does not update the cache when `fetchPolic result: UseReadQueryResult | null; suspenseCount: number; }>({ - Component: () => , + Component: App, initialSnapshot: { result: null, suspenseCount: 0, @@ -1783,15 +1783,11 @@ it("works with startTransition to change variables", async () => { cache: new InMemoryCache(), }); - function App() { - return ; - } - function SuspenseFallback() { return

Loading

; } - function Parent() { + function App() { const [queryRef, loadQuery] = useInteractiveQuery(query); return ( @@ -2064,7 +2060,7 @@ it("reacts to cache updates", async () => { return

Loading

; } - function Parent() { + function App() { const [queryRef, loadQuery] = useInteractiveQuery(query); return ( <> @@ -2088,7 +2084,7 @@ it("reacts to cache updates", async () => { result: UseReadQueryResult | null; suspenseCount: number; }>({ - Component: () => , + Component: App, initialSnapshot: { result: null, suspenseCount: 0, @@ -2168,7 +2164,7 @@ it("applies `errorPolicy` on next fetch when it changes between renders", async return

Loading

; } - function Parent() { + function App() { const [errorPolicy, setErrorPolicy] = useState("none"); const [queryRef, loadQuery, { refetch }] = useInteractiveQuery(query, { errorPolicy, @@ -2213,7 +2209,7 @@ it("applies `errorPolicy` on next fetch when it changes between renders", async result: UseReadQueryResult | null; suspenseCount: number; }>({ - Component: () => , + Component: App, initialSnapshot: { error: undefined, errorBoundaryCount: 0, @@ -2342,7 +2338,7 @@ it("applies `context` on next fetch when it changes between renders", async () = return
Loading...
; } - function Parent() { + function App() { const [phase, setPhase] = React.useState("initial"); const [queryRef, loadQuery, { refetch }] = useInteractiveQuery(query, { context: { phase }, @@ -2366,10 +2362,6 @@ it("applies `context` on next fetch when it changes between renders", async () = return
{data.context.phase}
; } - function App() { - return ; - } - const { user } = renderWithClient(, { client }); await act(() => user.click(screen.getByText("Load query"))); @@ -2438,7 +2430,7 @@ it("returns canonical results immediately when `canonizeResults` changes from `f return
Loading...
; } - function Parent() { + function App() { const [canonizeResults, setCanonizeResults] = React.useState(false); const [queryRef, loadQuery] = useInteractiveQuery(query, { canonizeResults, @@ -2465,10 +2457,6 @@ it("returns canonical results immediately when `canonizeResults` changes from `f return null; } - function App() { - return ; - } - const { user } = renderWithClient(, { client }); function verifyCanonicalResults(data: Data, canonized: boolean) { @@ -2552,7 +2540,7 @@ it("applies changed `refetchWritePolicy` to next fetch when changing between ren return
Loading...
; } - function Parent() { + function App() { const [refetchWritePolicy, setRefetchWritePolicy] = React.useState("merge"); @@ -2587,10 +2575,6 @@ it("applies changed `refetchWritePolicy` to next fetch when changing between ren return {data.primes.join(", ")}; } - function App() { - return ; - } - const { user } = renderWithClient(, { client }); await act(() => user.click(screen.getByText("Load query"))); @@ -2710,7 +2694,7 @@ it("applies `returnPartialData` on next fetch when it changes between renders", return
Loading...
; } - function Parent() { + function App() { const [returnPartialData, setReturnPartialData] = React.useState(false); const [queryRef, loadQuery] = useInteractiveQuery(fullQuery, { @@ -2738,10 +2722,6 @@ it("applies `returnPartialData` on next fetch when it changes between renders", ); } - function App() { - return ; - } - const { user } = renderWithClient(, { client }); await act(() => user.click(screen.getByText("Load query"))); @@ -2825,7 +2805,7 @@ it("applies updated `fetchPolicy` on next fetch when it changes between renders" return
Loading...
; } - function Parent() { + function App() { const [fetchPolicy, setFetchPolicy] = React.useState("cache-first"); @@ -2853,10 +2833,6 @@ it("applies updated `fetchPolicy` on next fetch when it changes between renders" return {data.character.name}; } - function App() { - return ; - } - const { user } = renderWithClient(, { client }); await act(() => user.click(screen.getByText("Load query"))); From e3de8eeb217819a097835176c0cfbfe98f61b0f7 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 8 Nov 2023 18:18:09 -0700 Subject: [PATCH 049/199] Update variables test to use profile each component --- .../__tests__/useInteractiveQuery.test.tsx | 123 ++++++------------ 1 file changed, 38 insertions(+), 85 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index 1ce539b6574..bd4e038828e 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -620,108 +620,61 @@ it("loads a query and suspends when the load query function is called", async () it("loads a query with variables and suspends by passing variables to the loadQuery function", async () => { const { query, mocks } = useVariablesQueryCase(); - function SuspenseFallback() { - ProfiledApp.updateSnapshot((snapshot) => ({ - ...snapshot, - suspenseCount: snapshot.suspenseCount + 1, - })); - - return

Loading

; - } - - function App() { - const [queryRef, loadQuery] = useInteractiveQuery(query); - - ProfiledApp.updateSnapshot((snapshot) => ({ - ...snapshot, - parentRenderCount: snapshot.parentRenderCount + 1, - })); - - return ( - <> - - }> - {queryRef && } - - - ); - } + const SuspenseFallback = profile({ + Component: () =>

Loading

, + }); - function Child({ - queryRef, - }: { - queryRef: QueryReference; - }) { - const result = useReadQuery(queryRef); + const App = profile({ + Component: () => { + const [queryRef, loadQuery] = useInteractiveQuery(query); - ProfiledApp.updateSnapshot((snapshot) => ({ - ...snapshot, - result, - childRenderCount: snapshot.childRenderCount + 1, - })); + return ( + <> + + }> + {queryRef && } + + + ); + }, + }); - return null; - } + const Child = profile< + { result: UseReadQueryResult }, + { queryRef: QueryReference } + >({ + Component: ({ queryRef }) => { + Child.updateSnapshot({ result: useReadQuery(queryRef) }); - const ProfiledApp = profile<{ - result: UseReadQueryResult | null; - suspenseCount: number; - parentRenderCount: number; - childRenderCount: number; - }>({ - Component: App, - snapshotDOM: true, - initialSnapshot: { - result: null, - suspenseCount: 0, - parentRenderCount: 0, - childRenderCount: 0, + return null; }, }); - const { user } = renderWithMocks(, { mocks }); - - { - const { snapshot } = await ProfiledApp.takeRender(); + const { user } = renderWithMocks(, { mocks }); - expect(snapshot).toEqual({ - result: null, - suspenseCount: 0, - parentRenderCount: 1, - childRenderCount: 0, - }); - } + expect(SuspenseFallback).not.toHaveRendered(); await act(() => user.click(screen.getByText("Load query"))); - { - const { snapshot, withinDOM } = await ProfiledApp.takeRender(); - - expect(withinDOM().getByText("Loading")).toBeInTheDocument(); - expect(snapshot).toEqual({ - result: null, - suspenseCount: 1, - parentRenderCount: 2, - childRenderCount: 0, - }); - } + expect(SuspenseFallback).toHaveRendered(); + expect(Child).not.toHaveRendered(); + expect(App).toHaveRenderedTimes(2); { - const { snapshot } = await ProfiledApp.takeRender(); + const { snapshot } = await Child.takeRender(); - expect(snapshot).toEqual({ - result: { - data: { character: { id: "1", name: "Spider-Man" } }, - networkStatus: NetworkStatus.ready, - error: undefined, - }, - suspenseCount: 1, - parentRenderCount: 2, - childRenderCount: 1, + expect(snapshot.result).toEqual({ + data: { character: { id: "1", name: "Spider-Man" } }, + networkStatus: NetworkStatus.ready, + error: undefined, }); } - await expect(ProfiledApp).not.toRerender(); + expect(SuspenseFallback).toHaveRenderedTimes(1); + expect(Child).toHaveRenderedTimes(1); + expect(App).toHaveRenderedTimes(3); + + await expect(App).not.toRerender(); }); it("can change variables on a query and resuspend by passing new variables to the loadQuery function", async () => { From 221b498de77c4d189ac3c3b9dbd63c64d0b2ac46 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 8 Nov 2023 18:26:37 -0700 Subject: [PATCH 050/199] Simplify changing variables test --- .../__tests__/useInteractiveQuery.test.tsx | 160 +++++------------- 1 file changed, 46 insertions(+), 114 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index bd4e038828e..c1a0d861c93 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -677,149 +677,81 @@ it("loads a query with variables and suspends by passing variables to the loadQu await expect(App).not.toRerender(); }); -it("can change variables on a query and resuspend by passing new variables to the loadQuery function", async () => { +it("changes variables on a query and resuspends when passing new variables to the loadQuery function", async () => { const { query, mocks } = useVariablesQueryCase(); - function SuspenseFallback() { - ProfiledApp.updateSnapshot((snapshot) => ({ - ...snapshot, - suspenseCount: snapshot.suspenseCount + 1, - })); - - return

Loading

; - } - - function App() { - const [queryRef, loadQuery] = useInteractiveQuery(query); - - ProfiledApp.updateSnapshot((snapshot) => ({ - ...snapshot, - parentRenderCount: snapshot.parentRenderCount + 1, - })); - - return ( - <> - - - }> - {queryRef && } - - - ); - } + const SuspenseFallback = profile({ + Component: () =>

Loading

, + }); - function Child({ - queryRef, - }: { - queryRef: QueryReference; - }) { - const result = useReadQuery(queryRef); + const App = profile({ + Component: () => { + const [queryRef, loadQuery] = useInteractiveQuery(query); - ProfiledApp.updateSnapshot((snapshot) => ({ - ...snapshot, - result, - childRenderCount: snapshot.childRenderCount + 1, - })); + return ( + <> + + + }> + {queryRef && } + + + ); + }, + }); - return null; - } + const Child = profile< + UseReadQueryResult, + { queryRef: QueryReference } + >({ + Component: ({ queryRef }) => { + Child.updateSnapshot(useReadQuery(queryRef)); - const ProfiledApp = profile<{ - result: UseReadQueryResult | null; - suspenseCount: number; - parentRenderCount: number; - childRenderCount: number; - }>({ - Component: App, - snapshotDOM: true, - initialSnapshot: { - result: null, - suspenseCount: 0, - parentRenderCount: 0, - childRenderCount: 0, + return null; }, }); - const { user } = renderWithMocks(, { mocks }); - - { - const { snapshot } = await ProfiledApp.takeRender(); + const { user } = renderWithMocks(, { mocks }); - expect(snapshot).toEqual({ - result: null, - suspenseCount: 0, - parentRenderCount: 1, - childRenderCount: 0, - }); - } + expect(SuspenseFallback).not.toHaveRendered(); await act(() => user.click(screen.getByText("Load 1st character"))); - { - const { snapshot, withinDOM } = await ProfiledApp.takeRender(); - - expect(withinDOM().getByText("Loading")).toBeInTheDocument(); - expect(snapshot).toEqual({ - result: null, - suspenseCount: 1, - parentRenderCount: 2, - childRenderCount: 0, - }); - } + expect(SuspenseFallback).toHaveRendered(); + expect(Child).not.toHaveRendered(); + expect(App).toHaveRenderedTimes(2); { - const { snapshot } = await ProfiledApp.takeRender(); + const { snapshot } = await Child.takeRender(); expect(snapshot).toEqual({ - result: { - data: { character: { id: "1", name: "Spider-Man" } }, - networkStatus: NetworkStatus.ready, - error: undefined, - }, - suspenseCount: 1, - parentRenderCount: 2, - childRenderCount: 1, + data: { character: { id: "1", name: "Spider-Man" } }, + networkStatus: NetworkStatus.ready, + error: undefined, }); } await act(() => user.click(screen.getByText("Load 2nd character"))); - { - const { snapshot, withinDOM } = await ProfiledApp.takeRender(); - - expect(withinDOM().getByText("Loading")).toBeInTheDocument(); - expect(snapshot).toEqual({ - result: { - data: { character: { id: "1", name: "Spider-Man" } }, - networkStatus: NetworkStatus.ready, - error: undefined, - }, - suspenseCount: 2, - parentRenderCount: 3, - childRenderCount: 1, - }); - } + expect(SuspenseFallback).toHaveRenderedTimes(2); { - const { snapshot } = await ProfiledApp.takeRender(); + const { snapshot } = await Child.takeRender(); expect(snapshot).toEqual({ - result: { - data: { character: { id: "2", name: "Black Widow" } }, - networkStatus: NetworkStatus.ready, - error: undefined, - }, - suspenseCount: 2, - parentRenderCount: 3, - childRenderCount: 2, + data: { character: { id: "2", name: "Black Widow" } }, + networkStatus: NetworkStatus.ready, + error: undefined, }); } - await expect(ProfiledApp).not.toRerender(); + expect(SuspenseFallback).toHaveRenderedTimes(2); + expect(App).toHaveRenderedTimes(5); + expect(Child).toHaveRenderedTimes(2); }); it("allows the client to be overridden", async () => { From a05594af5c2b15a01ccedc73b3cfa841de25b492 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 8 Nov 2023 18:28:21 -0700 Subject: [PATCH 051/199] Simplify read query snapshot --- .../hooks/__tests__/useInteractiveQuery.test.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index c1a0d861c93..9f0b9d09c7d 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -584,11 +584,11 @@ it("loads a query and suspends when the load query function is called", async () }); const Greeting = profile< - { result: UseReadQueryResult }, + UseReadQueryResult, { queryRef: QueryReference } >({ Component: ({ queryRef }) => { - Greeting.updateSnapshot({ result: useReadQuery(queryRef) }); + Greeting.updateSnapshot(useReadQuery(queryRef)); return null; }, @@ -606,7 +606,7 @@ it("loads a query and suspends when the load query function is called", async () const { snapshot } = await Greeting.takeRender(); - expect(snapshot.result).toEqual({ + expect(snapshot).toEqual({ data: { greeting: "Hello" }, error: undefined, networkStatus: NetworkStatus.ready, @@ -640,11 +640,11 @@ it("loads a query with variables and suspends by passing variables to the loadQu }); const Child = profile< - { result: UseReadQueryResult }, + UseReadQueryResult, { queryRef: QueryReference } >({ Component: ({ queryRef }) => { - Child.updateSnapshot({ result: useReadQuery(queryRef) }); + Child.updateSnapshot(useReadQuery(queryRef)); return null; }, @@ -663,7 +663,7 @@ it("loads a query with variables and suspends by passing variables to the loadQu { const { snapshot } = await Child.takeRender(); - expect(snapshot.result).toEqual({ + expect(snapshot).toEqual({ data: { character: { id: "1", name: "Spider-Man" } }, networkStatus: NetworkStatus.ready, error: undefined, From d308eb042b4b177cea893f09b7720c2386f4a8e6 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 8 Nov 2023 20:49:01 -0700 Subject: [PATCH 052/199] Minor tweak to render helpers --- .../__tests__/useInteractiveQuery.test.tsx | 42 +++++++++---------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index 9f0b9d09c7d..b89f7fa7aac 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -113,36 +113,32 @@ function useVariablesQueryCase() { return { mocks, query }; } -function renderWithUser(ui: React.ReactElement, options?: UserEventOptions) { - const user = userEvent.setup(options); +function renderWithMocks(ui: React.ReactElement, props: MockedProviderProps) { + const user = userEvent.setup(); - return { ...render(ui), user }; -} + const utils = render(ui, { + wrapper: ({ children }) => ( + {children} + ), + }); -function renderWithMocks( - ui: React.ReactElement, - { - userEvent, - ...props - }: MockedProviderProps & { userEvent?: UserEventOptions } -) { - return renderWithUser( - {ui}, - userEvent - ); + return { ...utils, user }; } function renderWithClient( ui: React.ReactElement, - { - client, - userEvent, - }: { client: ApolloClient; userEvent?: UserEventOptions } + options: { client: ApolloClient } ) { - return renderWithUser( - {ui}, - userEvent - ); + const { client } = options; + const user = userEvent.setup(); + + const utils = render(ui, { + wrapper: ({ children }) => ( + {children} + ), + }); + + return { ...utils, user }; } function renderVariablesIntegrationTest({ From d2333a6761e037b86962c1f5a3fc3920be4e8a40 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 8 Nov 2023 20:53:06 -0700 Subject: [PATCH 053/199] Move new matchers over to own files --- src/testing/matchers/ProfiledComponent.ts | 50 --------------------- src/testing/matchers/index.ts | 9 ++-- src/testing/matchers/toHaveRendered.ts | 23 ++++++++++ src/testing/matchers/toHaveRenderedTimes.ts | 31 +++++++++++++ 4 files changed, 57 insertions(+), 56 deletions(-) create mode 100644 src/testing/matchers/toHaveRendered.ts create mode 100644 src/testing/matchers/toHaveRenderedTimes.ts diff --git a/src/testing/matchers/ProfiledComponent.ts b/src/testing/matchers/ProfiledComponent.ts index 992651e8782..9d9cea288ba 100644 --- a/src/testing/matchers/ProfiledComponent.ts +++ b/src/testing/matchers/ProfiledComponent.ts @@ -6,56 +6,6 @@ import type { ProfiledHook, } from "../internal/index.js"; -export const toHaveRendered: MatcherFunction = function ( - profiled: ProfiledComponent -) { - const hint = this.utils.matcherHint( - "toHaveRendered", - "ProfiledComponent", - "" - ); - const pass = profiled.currentRenderCount() > 0; - - return { - pass, - message() { - return ( - hint + - `\n\nExpected profiled component to${pass ? " not" : ""} have rendered.` - ); - }, - }; -}; - -export const toHaveRenderedTimes: MatcherFunction<[count: number]> = function ( - profiled: ProfiledComponent, - count: number -) { - const hint = this.utils.matcherHint( - "toHaveRenderedTimes", - "ProfiledComponent", - "renderCount" - ); - const actualRenderCount = profiled.currentRenderCount(); - const pass = actualRenderCount === count; - - return { - pass, - message: () => { - return ( - hint + - `\n\nExpected profiled component to${ - pass ? " not" : "" - } have rendered times ${this.utils.printExpected( - count - )}, but it rendered times ${this.utils.printReceived( - actualRenderCount - )}.` - ); - }, - }; -}; - export const toRerender: MatcherFunction<[options?: NextRenderOptions]> = async function (actual, options) { const _profiled = actual as diff --git a/src/testing/matchers/index.ts b/src/testing/matchers/index.ts index 6aa673b0fc2..4494ea3814c 100644 --- a/src/testing/matchers/index.ts +++ b/src/testing/matchers/index.ts @@ -1,12 +1,9 @@ import { expect } from "@jest/globals"; +import { toHaveRendered } from "./toHaveRendered.js"; +import { toHaveRenderedTimes } from "./toHaveRenderedTimes.js"; import { toMatchDocument } from "./toMatchDocument.js"; import { toHaveSuspenseCacheEntryUsing } from "./toHaveSuspenseCacheEntryUsing.js"; -import { - toHaveRendered, - toHaveRenderedTimes, - toRerender, - toRenderExactlyTimes, -} from "./ProfiledComponent.js"; +import { toRerender, toRenderExactlyTimes } from "./ProfiledComponent.js"; expect.extend({ toHaveRendered, diff --git a/src/testing/matchers/toHaveRendered.ts b/src/testing/matchers/toHaveRendered.ts new file mode 100644 index 00000000000..2d45828baa0 --- /dev/null +++ b/src/testing/matchers/toHaveRendered.ts @@ -0,0 +1,23 @@ +import type { MatcherFunction } from "expect"; +import type { ProfiledComponent } from "../internal/index.js"; + +export const toHaveRendered: MatcherFunction = function ( + ProfiledComponent: ProfiledComponent +) { + const hint = this.utils.matcherHint( + "toHaveRendered", + "ProfiledComponent", + "" + ); + const pass = ProfiledComponent.currentRenderCount() > 0; + + return { + pass, + message() { + return ( + hint + + `\n\nExpected profiled component to${pass ? " not" : ""} have rendered.` + ); + }, + }; +}; diff --git a/src/testing/matchers/toHaveRenderedTimes.ts b/src/testing/matchers/toHaveRenderedTimes.ts new file mode 100644 index 00000000000..725b7f66d13 --- /dev/null +++ b/src/testing/matchers/toHaveRenderedTimes.ts @@ -0,0 +1,31 @@ +import type { MatcherFunction } from "expect"; +import type { ProfiledComponent } from "../internal/index.js"; + +export const toHaveRenderedTimes: MatcherFunction<[count: number]> = function ( + ProfiledComponent: ProfiledComponent, + count: number +) { + const hint = this.utils.matcherHint( + "toHaveRenderedTimes", + "ProfiledComponent", + "renderCount" + ); + const actualRenderCount = ProfiledComponent.currentRenderCount(); + const pass = actualRenderCount === count; + + return { + pass, + message: () => { + return ( + hint + + `\n\nExpected profiled component to${ + pass ? " not" : "" + } have rendered times ${this.utils.printExpected( + count + )}, but it rendered times ${this.utils.printReceived( + actualRenderCount + )}.` + ); + }, + }; +}; From 1e10ad5f4a6380d91a153aa83bd9e9071fd84264 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 8 Nov 2023 20:57:00 -0700 Subject: [PATCH 054/199] Ensure toHaveRendered* matchers handle profiled hooks --- src/testing/matchers/toHaveRendered.ts | 11 ++++++++--- src/testing/matchers/toHaveRenderedTimes.ts | 13 +++++++++---- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/testing/matchers/toHaveRendered.ts b/src/testing/matchers/toHaveRendered.ts index 2d45828baa0..0ad53b804a8 100644 --- a/src/testing/matchers/toHaveRendered.ts +++ b/src/testing/matchers/toHaveRendered.ts @@ -1,15 +1,20 @@ import type { MatcherFunction } from "expect"; -import type { ProfiledComponent } from "../internal/index.js"; +import type { ProfiledComponent, ProfiledHook } from "../internal/index.js"; export const toHaveRendered: MatcherFunction = function ( - ProfiledComponent: ProfiledComponent + ProfiledComponent: ProfiledComponent | ProfiledHook ) { + if ("ProfiledComponent" in ProfiledComponent) { + ProfiledComponent = ProfiledComponent.ProfiledComponent; + } + + const pass = ProfiledComponent.currentRenderCount() > 0; + const hint = this.utils.matcherHint( "toHaveRendered", "ProfiledComponent", "" ); - const pass = ProfiledComponent.currentRenderCount() > 0; return { pass, diff --git a/src/testing/matchers/toHaveRenderedTimes.ts b/src/testing/matchers/toHaveRenderedTimes.ts index 725b7f66d13..daf411c0fbf 100644 --- a/src/testing/matchers/toHaveRenderedTimes.ts +++ b/src/testing/matchers/toHaveRenderedTimes.ts @@ -1,17 +1,22 @@ import type { MatcherFunction } from "expect"; -import type { ProfiledComponent } from "../internal/index.js"; +import type { ProfiledComponent, ProfiledHook } from "../internal/index.js"; export const toHaveRenderedTimes: MatcherFunction<[count: number]> = function ( - ProfiledComponent: ProfiledComponent, + ProfiledComponent: ProfiledComponent | ProfiledHook, count: number ) { + if ("ProfiledComponent" in ProfiledComponent) { + ProfiledComponent = ProfiledComponent.ProfiledComponent; + } + + const actualRenderCount = ProfiledComponent.currentRenderCount(); + const pass = actualRenderCount === count; + const hint = this.utils.matcherHint( "toHaveRenderedTimes", "ProfiledComponent", "renderCount" ); - const actualRenderCount = ProfiledComponent.currentRenderCount(); - const pass = actualRenderCount === count; return { pass, From a06ea5bd63b2159ef809e06d226b3c47e12968f5 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 8 Nov 2023 20:59:12 -0700 Subject: [PATCH 055/199] Use profileHook on hook component in first test --- .../__tests__/useInteractiveQuery.test.tsx | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index b89f7fa7aac..3c7b83dfc5d 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -53,7 +53,7 @@ import { import { FetchMoreFunction, RefetchFunction } from "../useSuspenseQuery"; import { RefetchWritePolicy } from "../../../core/watchQueryOptions"; import invariant from "ts-invariant"; -import { profile, spyOnConsole } from "../../../testing/internal"; +import { profile, profileHook, spyOnConsole } from "../../../testing/internal"; interface SimpleQueryData { greeting: string; @@ -572,23 +572,17 @@ it("loads a query and suspends when the load query function is called", async () <> }> - {queryRef && } + {queryRef && } ); }, }); - const Greeting = profile< + const ReadQueryHook = profileHook< UseReadQueryResult, { queryRef: QueryReference } - >({ - Component: ({ queryRef }) => { - Greeting.updateSnapshot(useReadQuery(queryRef)); - - return null; - }, - }); + >(({ queryRef }) => useReadQuery(queryRef)); const { user } = renderWithMocks(, { mocks }); @@ -597,10 +591,10 @@ it("loads a query and suspends when the load query function is called", async () await act(() => user.click(screen.getByText("Load query"))); expect(SuspenseFallback).toHaveRendered(); - expect(Greeting).not.toHaveRendered(); + expect(ReadQueryHook).not.toHaveRendered(); expect(App).toHaveRenderedTimes(2); - const { snapshot } = await Greeting.takeRender(); + const snapshot = await ReadQueryHook.takeSnapshot(); expect(snapshot).toEqual({ data: { greeting: "Hello" }, @@ -609,7 +603,7 @@ it("loads a query and suspends when the load query function is called", async () }); expect(SuspenseFallback).toHaveRenderedTimes(1); - expect(Greeting).toHaveRenderedTimes(1); + expect(ReadQueryHook).toHaveRenderedTimes(1); expect(App).toHaveRenderedTimes(3); }); From d99abf75a8d56d7dce1bed54ca09f97010e85c4d Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 8 Nov 2023 21:06:05 -0700 Subject: [PATCH 056/199] Add helper to create default profiled components --- .../__tests__/useInteractiveQuery.test.tsx | 73 +++++++------------ 1 file changed, 28 insertions(+), 45 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index 3c7b83dfc5d..e0858811013 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -113,6 +113,19 @@ function useVariablesQueryCase() { return { mocks, query }; } +function createDefaultProfiledComponents() { + const SuspenseFallback = profile({ + Component: () =>

Loading

, + }); + + const ReadQueryHook = profileHook< + UseReadQueryResult, + { queryRef: QueryReference } + >(({ queryRef }) => useReadQuery(queryRef)); + + return { SuspenseFallback, ReadQueryHook }; +} + function renderWithMocks(ui: React.ReactElement, props: MockedProviderProps) { const user = userEvent.setup(); @@ -560,9 +573,8 @@ function renderSuspenseHook( it("loads a query and suspends when the load query function is called", async () => { const { query, mocks } = useSimpleQueryCase(); - const SuspenseFallback = profile({ - Component: () =>

Loading

, - }); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(); const App = profile({ Component: () => { @@ -579,11 +591,6 @@ it("loads a query and suspends when the load query function is called", async () }, }); - const ReadQueryHook = profileHook< - UseReadQueryResult, - { queryRef: QueryReference } - >(({ queryRef }) => useReadQuery(queryRef)); - const { user } = renderWithMocks(, { mocks }); expect(SuspenseFallback).not.toHaveRendered(); @@ -610,9 +617,8 @@ it("loads a query and suspends when the load query function is called", async () it("loads a query with variables and suspends by passing variables to the loadQuery function", async () => { const { query, mocks } = useVariablesQueryCase(); - const SuspenseFallback = profile({ - Component: () =>

Loading

, - }); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(); const App = profile({ Component: () => { @@ -622,24 +628,13 @@ it("loads a query with variables and suspends by passing variables to the loadQu <> }> - {queryRef && } + {queryRef && } ); }, }); - const Child = profile< - UseReadQueryResult, - { queryRef: QueryReference } - >({ - Component: ({ queryRef }) => { - Child.updateSnapshot(useReadQuery(queryRef)); - - return null; - }, - }); - const { user } = renderWithMocks(, { mocks }); expect(SuspenseFallback).not.toHaveRendered(); @@ -647,11 +642,11 @@ it("loads a query with variables and suspends by passing variables to the loadQu await act(() => user.click(screen.getByText("Load query"))); expect(SuspenseFallback).toHaveRendered(); - expect(Child).not.toHaveRendered(); + expect(ReadQueryHook).not.toHaveRendered(); expect(App).toHaveRenderedTimes(2); { - const { snapshot } = await Child.takeRender(); + const snapshot = await ReadQueryHook.takeSnapshot(); expect(snapshot).toEqual({ data: { character: { id: "1", name: "Spider-Man" } }, @@ -661,7 +656,7 @@ it("loads a query with variables and suspends by passing variables to the loadQu } expect(SuspenseFallback).toHaveRenderedTimes(1); - expect(Child).toHaveRenderedTimes(1); + expect(ReadQueryHook).toHaveRenderedTimes(1); expect(App).toHaveRenderedTimes(3); await expect(App).not.toRerender(); @@ -670,9 +665,8 @@ it("loads a query with variables and suspends by passing variables to the loadQu it("changes variables on a query and resuspends when passing new variables to the loadQuery function", async () => { const { query, mocks } = useVariablesQueryCase(); - const SuspenseFallback = profile({ - Component: () =>

Loading

, - }); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(); const App = profile({ Component: () => { @@ -687,24 +681,13 @@ it("changes variables on a query and resuspends when passing new variables to th Load 2nd character }> - {queryRef && } + {queryRef && } ); }, }); - const Child = profile< - UseReadQueryResult, - { queryRef: QueryReference } - >({ - Component: ({ queryRef }) => { - Child.updateSnapshot(useReadQuery(queryRef)); - - return null; - }, - }); - const { user } = renderWithMocks(, { mocks }); expect(SuspenseFallback).not.toHaveRendered(); @@ -712,11 +695,11 @@ it("changes variables on a query and resuspends when passing new variables to th await act(() => user.click(screen.getByText("Load 1st character"))); expect(SuspenseFallback).toHaveRendered(); - expect(Child).not.toHaveRendered(); + expect(ReadQueryHook).not.toHaveRendered(); expect(App).toHaveRenderedTimes(2); { - const { snapshot } = await Child.takeRender(); + const snapshot = await ReadQueryHook.takeSnapshot(); expect(snapshot).toEqual({ data: { character: { id: "1", name: "Spider-Man" } }, @@ -730,7 +713,7 @@ it("changes variables on a query and resuspends when passing new variables to th expect(SuspenseFallback).toHaveRenderedTimes(2); { - const { snapshot } = await Child.takeRender(); + const snapshot = await ReadQueryHook.takeSnapshot(); expect(snapshot).toEqual({ data: { character: { id: "2", name: "Black Widow" } }, @@ -741,7 +724,7 @@ it("changes variables on a query and resuspends when passing new variables to th expect(SuspenseFallback).toHaveRenderedTimes(2); expect(App).toHaveRenderedTimes(5); - expect(Child).toHaveRenderedTimes(2); + expect(ReadQueryHook).toHaveRenderedTimes(2); }); it("allows the client to be overridden", async () => { From ee0fe16a4f67e46ca33c0f97ea72890f8ee3dbee Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 8 Nov 2023 21:09:33 -0700 Subject: [PATCH 057/199] Update client override test to use new helpers --- .../__tests__/useInteractiveQuery.test.tsx | 60 ++++--------------- 1 file changed, 10 insertions(+), 50 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index e0858811013..96172fa18ee 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -744,9 +744,8 @@ it("allows the client to be overridden", async () => { cache: new InMemoryCache(), }); - function SuspenseFallback() { - return

Loading

; - } + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(); function App() { const [queryRef, loadQuery] = useInteractiveQuery(query, { @@ -757,62 +756,23 @@ it("allows the client to be overridden", async () => { <> }> - {queryRef && } + {queryRef && } ); } - function Greeting({ - queryRef, - }: { - queryRef: QueryReference; - }) { - const result = useReadQuery(queryRef); - - ProfiledApp.updateSnapshot({ result }); - - return
{result.data.greeting}
; - } - - const ProfiledApp = profile<{ - result: UseReadQueryResult | null; - }>({ - Component: App, - snapshotDOM: true, - initialSnapshot: { - result: null, - }, - }); - - const { user } = renderWithClient(, { client: globalClient }); - - { - const { snapshot } = await ProfiledApp.takeRender(); - expect(snapshot.result).toEqual(null); - } + const { user } = renderWithClient(, { client: globalClient }); await act(() => user.click(screen.getByText("Load query"))); - { - const { snapshot, withinDOM } = await ProfiledApp.takeRender(); - - expect(withinDOM().getByText("Loading")).toBeInTheDocument(); - expect(snapshot.result).toEqual(null); - } - - { - const { snapshot, withinDOM } = await ProfiledApp.takeRender(); - - expect(withinDOM().getByText("local hello")).toBeInTheDocument(); - expect(snapshot.result).toEqual({ - data: { greeting: "local hello" }, - networkStatus: NetworkStatus.ready, - error: undefined, - }); - } + const snapshot = await ReadQueryHook.takeSnapshot(); - await expect(ProfiledApp).not.toRerender(); + expect(snapshot).toEqual({ + data: { greeting: "local hello" }, + networkStatus: NetworkStatus.ready, + error: undefined, + }); }); it("passes context to the link", async () => { From f9571fe0664a52b6b5c8da735ad4520f174cde68 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 8 Nov 2023 22:05:25 -0700 Subject: [PATCH 058/199] Update existing tests to use new profile helpers --- .../__tests__/useInteractiveQuery.test.tsx | 740 ++++-------------- 1 file changed, 162 insertions(+), 578 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index 96172fa18ee..3f30199f114 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -798,9 +798,8 @@ it("passes context to the link", async () => { }), }); - function SuspenseFallback() { - return

Loading

; - } + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(); function App() { const [queryRef, loadQuery] = useInteractiveQuery(query, { @@ -811,57 +810,23 @@ it("passes context to the link", async () => { <> }> - {queryRef && } + {queryRef && } ); } - function Child({ queryRef }: { queryRef: QueryReference }) { - const result = useReadQuery(queryRef); - - ProfiledApp.updateSnapshot({ result }); - - return null; - } - - const ProfiledApp = profile<{ - result: UseReadQueryResult | null; - }>({ - Component: App, - snapshotDOM: true, - initialSnapshot: { - result: null, - }, - }); - - const { user } = renderWithClient(, { client }); - - { - const { snapshot } = await ProfiledApp.takeRender(); - expect(snapshot.result).toEqual(null); - } + const { user } = renderWithClient(, { client }); await act(() => user.click(screen.getByText("Load query"))); - { - const { snapshot, withinDOM } = await ProfiledApp.takeRender(); - - expect(withinDOM().getByText("Loading")).toBeInTheDocument(); - expect(snapshot.result).toEqual(null); - } - - { - const { snapshot } = await ProfiledApp.takeRender(); - - expect(snapshot.result).toEqual({ - data: { context: { valueA: "A", valueB: "B" } }, - networkStatus: NetworkStatus.ready, - error: undefined, - }); - } + const snapshot = await ReadQueryHook.takeSnapshot(); - await expect(ProfiledApp).not.toRerender(); + expect(snapshot).toEqual({ + data: { context: { valueA: "A", valueB: "B" } }, + networkStatus: NetworkStatus.ready, + error: undefined, + }); }); it('enables canonical results when canonizeResults is "true"', async () => { @@ -909,9 +874,8 @@ it('enables canonical results when canonizeResults is "true"', async () => { link: new MockLink([]), }); - function SuspenseFallback() { - return

Loading

; - } + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(); function App() { const [queryRef, loadQuery] = useInteractiveQuery(query, { @@ -922,54 +886,28 @@ it('enables canonical results when canonizeResults is "true"', async () => { <> }> - {queryRef && } + {queryRef && } ); } - function Child({ queryRef }: { queryRef: QueryReference }) { - const result = useReadQuery(queryRef); - - ProfiledApp.updateSnapshot({ result }); - - return null; - } - - const ProfiledApp = profile<{ - result: UseReadQueryResult | null; - }>({ - Component: App, - initialSnapshot: { - result: null, - }, - }); - - const { user } = renderWithClient(, { client }); - - { - const { snapshot } = await ProfiledApp.takeRender(); - expect(snapshot.result).toEqual(null); - } + const { user } = renderWithClient(, { client }); await act(() => user.click(screen.getByText("Load query"))); - { - const { snapshot } = await ProfiledApp.takeRender(); - - const resultSet = new Set(snapshot.result!.data.results); - const values = Array.from(resultSet).map((item) => item.value); + const snapshot = await ReadQueryHook.takeSnapshot(); + const resultSet = new Set(snapshot.data.results); + const values = Array.from(resultSet).map((item) => item.value); - expect(snapshot.result).toEqual({ - data: { results }, - networkStatus: NetworkStatus.ready, - error: undefined, - }); - expect(resultSet.size).toBe(5); - expect(values).toEqual([0, 1, 2, 3, 5]); - } + expect(snapshot).toEqual({ + data: { results }, + networkStatus: NetworkStatus.ready, + error: undefined, + }); - await expect(ProfiledApp).not.toRerender(); + expect(resultSet.size).toBe(5); + expect(values).toEqual([0, 1, 2, 3, 5]); }); it("can disable canonical results when the cache's canonizeResults setting is true", async () => { @@ -1013,9 +951,8 @@ it("can disable canonical results when the cache's canonizeResults setting is tr data: { results }, }); - function SuspenseFallback() { - return

Loading

; - } + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(); function App() { const [queryRef, loadQuery] = useInteractiveQuery(query, { @@ -1026,54 +963,27 @@ it("can disable canonical results when the cache's canonizeResults setting is tr <> }> - {queryRef && } + {queryRef && } ); } - function Child({ queryRef }: { queryRef: QueryReference }) { - const result = useReadQuery(queryRef); - - ProfiledApp.updateSnapshot({ result }); - - return null; - } - - const ProfiledApp = profile<{ - result: UseReadQueryResult | null; - }>({ - Component: App, - initialSnapshot: { - result: null, - }, - }); - - const { user } = renderWithMocks(, { cache }); - - { - const { snapshot } = await ProfiledApp.takeRender(); - expect(snapshot.result).toEqual(null); - } + const { user } = renderWithMocks(, { cache }); await act(() => user.click(screen.getByText("Load query"))); - { - const { snapshot } = await ProfiledApp.takeRender(); - - const resultSet = new Set(snapshot.result!.data.results); - const values = Array.from(resultSet).map((item) => item.value); - - expect(snapshot.result).toEqual({ - data: { results }, - networkStatus: NetworkStatus.ready, - error: undefined, - }); - expect(resultSet.size).toBe(6); - expect(values).toEqual([0, 1, 1, 2, 3, 5]); - } + const snapshot = await ReadQueryHook.takeSnapshot(); + const resultSet = new Set(snapshot.data.results); + const values = Array.from(resultSet).map((item) => item.value); - await expect(ProfiledApp).not.toRerender(); + expect(snapshot).toEqual({ + data: { results }, + networkStatus: NetworkStatus.ready, + error: undefined, + }); + expect(resultSet.size).toBe(6); + expect(values).toEqual([0, 1, 1, 2, 3, 5]); }); it("returns initial cache data followed by network data when the fetch policy is `cache-and-network`", async () => { @@ -1095,67 +1005,37 @@ it("returns initial cache data followed by network data when the fetch policy is cache.writeQuery({ query, data: { hello: "from cache" } }); - function SuspenseFallback() { - ProfiledApp.updateSnapshot((snapshot) => ({ - ...snapshot, - suspenseCount: snapshot.suspenseCount + 1, - })); - - return

Loading

; - } - - function App() { - const [queryRef, loadQuery] = useInteractiveQuery(query, { - fetchPolicy: "cache-and-network", - }); - - return ( - <> - - }> - {queryRef && } - - - ); - } - - function Child({ - queryRef, - }: { - queryRef: QueryReference<{ hello: string }>; - }) { - const result = useReadQuery(queryRef); - - ProfiledApp.updateSnapshot((snapshot) => ({ ...snapshot, result })); + const { SuspenseFallback, ReadQueryHook } = createDefaultProfiledComponents<{ + hello: string; + }>(); - return null; - } + const App = profile({ + Component: () => { + const [queryRef, loadQuery] = useInteractiveQuery(query, { + fetchPolicy: "cache-and-network", + }); - const ProfiledApp = profile<{ - result: UseReadQueryResult | null; - suspenseCount: number; - }>({ - Component: App, - initialSnapshot: { - result: null, - suspenseCount: 0, + return ( + <> + + }> + {queryRef && } + + + ); }, }); - const { user } = renderWithClient(, { client }); - - { - const { snapshot } = await ProfiledApp.takeRender(); - expect(snapshot.result).toEqual(null); - } + const { user } = renderWithClient(, { client }); await act(() => user.click(screen.getByText("Load query"))); + expect(SuspenseFallback).not.toHaveRendered(); + { - const { snapshot } = await ProfiledApp.takeRender(); + const snapshot = await ReadQueryHook.takeSnapshot(); - expect(snapshot.suspenseCount).toBe(0); - expect(snapshot.result).toEqual({ + expect(snapshot).toEqual({ data: { hello: "from cache" }, networkStatus: NetworkStatus.loading, error: undefined, @@ -1163,17 +1043,14 @@ it("returns initial cache data followed by network data when the fetch policy is } { - const { snapshot } = await ProfiledApp.takeRender(); + const snapshot = await ReadQueryHook.takeSnapshot(); - expect(snapshot.suspenseCount).toBe(0); - expect(snapshot.result).toEqual({ + expect(snapshot).toEqual({ data: { hello: "from link" }, networkStatus: NetworkStatus.ready, error: undefined, }); } - - await expect(ProfiledApp).not.toRerender(); }); it("all data is present in the cache, no network request is made", async () => { @@ -1198,14 +1075,7 @@ it("all data is present in the cache, no network request is made", async () => { cache.writeQuery({ query, data: { hello: "from cache" } }); - function SuspenseFallback() { - ProfiledApp.updateSnapshot((snapshot) => ({ - ...snapshot, - suspenseCount: snapshot.suspenseCount + 1, - })); - - return

Loading

; - } + const { SuspenseFallback, ReadQueryHook } = createDefaultProfiledComponents(); function App() { const [queryRef, loadQuery] = useInteractiveQuery(query); @@ -1214,52 +1084,25 @@ it("all data is present in the cache, no network request is made", async () => { <> }> - {queryRef && } + {queryRef && } ); } - function Child({ queryRef }: { queryRef: QueryReference }) { - const result = useReadQuery(queryRef); - - ProfiledApp.updateSnapshot((snapshot) => ({ ...snapshot, result })); - - return null; - } - - const ProfiledApp = profile<{ - result: UseReadQueryResult | null; - suspenseCount: number; - }>({ - Component: App, - initialSnapshot: { - result: null, - suspenseCount: 0, - }, - }); - - const { user } = renderWithClient(, { client }); - - { - const { snapshot } = await ProfiledApp.takeRender(); - expect(snapshot.result).toEqual(null); - } + const { user } = renderWithClient(, { client }); await act(() => user.click(screen.getByText("Load query"))); - { - const { snapshot } = await ProfiledApp.takeRender(); + expect(SuspenseFallback).not.toHaveRendered(); - expect(snapshot.suspenseCount).toBe(0); - expect(snapshot.result).toEqual({ - data: { hello: "from cache" }, - networkStatus: NetworkStatus.ready, - error: undefined, - }); - } + const snapshot = await ReadQueryHook.takeSnapshot(); - await expect(ProfiledApp).not.toRerender(); + expect(snapshot).toEqual({ + data: { hello: "from cache" }, + networkStatus: NetworkStatus.ready, + error: undefined, + }); }); it("partial data is present in the cache so it is ignored and network request is made", async () => { @@ -1290,14 +1133,7 @@ it("partial data is present in the cache so it is ignored and network request is cache.writeQuery({ query, data: { hello: "from cache" } }); } - function SuspenseFallback() { - ProfiledApp.updateSnapshot((snapshot) => ({ - ...snapshot, - suspenseCount: snapshot.suspenseCount + 1, - })); - - return

Loading

; - } + const { SuspenseFallback, ReadQueryHook } = createDefaultProfiledComponents(); function App() { const [queryRef, loadQuery] = useInteractiveQuery(query); @@ -1306,60 +1142,25 @@ it("partial data is present in the cache so it is ignored and network request is <> }> - {queryRef && } + {queryRef && } ); } - function Child({ queryRef }: { queryRef: QueryReference }) { - const result = useReadQuery(queryRef); - - ProfiledApp.updateSnapshot((snapshot) => ({ ...snapshot, result })); - - return null; - } - - const ProfiledApp = profile<{ - result: UseReadQueryResult | null; - suspenseCount: number; - }>({ - Component: App, - initialSnapshot: { - result: null, - suspenseCount: 0, - }, - }); - - const { user } = renderWithClient(, { client }); - - { - const { snapshot } = await ProfiledApp.takeRender(); - expect(snapshot.suspenseCount).toBe(0); - expect(snapshot.result).toBe(null); - } + const { user } = renderWithClient(, { client }); await act(() => user.click(screen.getByText("Load query"))); - { - const { snapshot } = await ProfiledApp.takeRender(); - - expect(snapshot.suspenseCount).toBe(1); - expect(snapshot.result).toBe(null); - } - - { - const { snapshot } = await ProfiledApp.takeRender(); + expect(SuspenseFallback).toHaveRendered(); - expect(snapshot.suspenseCount).toBe(1); - expect(snapshot.result).toEqual({ - data: { foo: "bar", hello: "from link" }, - error: undefined, - networkStatus: NetworkStatus.ready, - }); - } + const snapshot = await ReadQueryHook.takeSnapshot(); - await expect(ProfiledApp).not.toRerender(); + expect(snapshot).toEqual({ + data: { foo: "bar", hello: "from link" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); }); it("existing data in the cache is ignored when `fetchPolicy` is 'network-only'", async () => { @@ -1384,14 +1185,7 @@ it("existing data in the cache is ignored when `fetchPolicy` is 'network-only'", cache.writeQuery({ query, data: { hello: "from cache" } }); - function SuspenseFallback() { - ProfiledApp.updateSnapshot((snapshot) => ({ - ...snapshot, - suspenseCount: snapshot.suspenseCount + 1, - })); - - return

Loading

; - } + const { SuspenseFallback, ReadQueryHook } = createDefaultProfiledComponents(); function App() { const [queryRef, loadQuery] = useInteractiveQuery(query, { @@ -1402,60 +1196,25 @@ it("existing data in the cache is ignored when `fetchPolicy` is 'network-only'", <> }> - {queryRef && } + {queryRef && } ); } - function Child({ queryRef }: { queryRef: QueryReference }) { - const result = useReadQuery(queryRef); - - ProfiledApp.updateSnapshot((snapshot) => ({ ...snapshot, result })); - - return null; - } - - const ProfiledApp = profile<{ - result: UseReadQueryResult | null; - suspenseCount: number; - }>({ - Component: App, - initialSnapshot: { - result: null, - suspenseCount: 0, - }, - }); - - const { user } = renderWithClient(, { client }); - - { - const { snapshot } = await ProfiledApp.takeRender(); - expect(snapshot.suspenseCount).toBe(0); - expect(snapshot.result).toBe(null); - } + const { user } = renderWithClient(, { client }); await act(() => user.click(screen.getByText("Load query"))); - { - const { snapshot } = await ProfiledApp.takeRender(); - - expect(snapshot.suspenseCount).toBe(1); - expect(snapshot.result).toBe(null); - } - - { - const { snapshot } = await ProfiledApp.takeRender(); + expect(SuspenseFallback).toHaveRendered(); - expect(snapshot.suspenseCount).toBe(1); - expect(snapshot.result).toEqual({ - data: { hello: "from link" }, - error: undefined, - networkStatus: NetworkStatus.ready, - }); - } + const snapshot = await ReadQueryHook.takeSnapshot(); - await expect(ProfiledApp).not.toRerender(); + expect(snapshot).toEqual({ + data: { hello: "from link" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); }); it("fetches data from the network but does not update the cache when `fetchPolicy` is 'no-cache'", async () => { @@ -1477,14 +1236,7 @@ it("fetches data from the network but does not update the cache when `fetchPolic cache.writeQuery({ query, data: { hello: "from cache" } }); - function SuspenseFallback() { - ProfiledApp.updateSnapshot((snapshot) => ({ - ...snapshot, - suspenseCount: snapshot.suspenseCount + 1, - })); - - return

Loading

; - } + const { SuspenseFallback, ReadQueryHook } = createDefaultProfiledComponents(); function App() { const [queryRef, loadQuery] = useInteractiveQuery(query, { @@ -1495,63 +1247,29 @@ it("fetches data from the network but does not update the cache when `fetchPolic <> }> - {queryRef && } + {queryRef && } ); } - function Child({ queryRef }: { queryRef: QueryReference }) { - const result = useReadQuery(queryRef); - - ProfiledApp.updateSnapshot((snapshot) => ({ ...snapshot, result })); - - return null; - } - - const ProfiledApp = profile<{ - result: UseReadQueryResult | null; - suspenseCount: number; - }>({ - Component: App, - initialSnapshot: { - result: null, - suspenseCount: 0, - }, - }); - - const { user } = renderWithClient(, { client }); - - { - const { snapshot } = await ProfiledApp.takeRender(); - expect(snapshot.suspenseCount).toBe(0); - expect(snapshot.result).toBe(null); - } + const { user } = renderWithClient(, { client }); await act(() => user.click(screen.getByText("Load query"))); - { - const { snapshot } = await ProfiledApp.takeRender(); - - expect(snapshot.suspenseCount).toBe(1); - expect(snapshot.result).toBe(null); - } + expect(SuspenseFallback).toHaveRendered(); - { - const { snapshot } = await ProfiledApp.takeRender(); + const snapshot = await ReadQueryHook.takeSnapshot(); - expect(snapshot.suspenseCount).toBe(1); - expect(snapshot.result).toEqual({ - data: { hello: "from link" }, - error: undefined, - networkStatus: NetworkStatus.ready, - }); - expect(client.extract()).toEqual({ - ROOT_QUERY: { __typename: "Query", hello: "from cache" }, - }); - } + expect(snapshot).toEqual({ + data: { hello: "from link" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); - await expect(ProfiledApp).not.toRerender(); + expect(client.extract()).toEqual({ + ROOT_QUERY: { __typename: "Query", hello: "from cache" }, + }); }); it("works with startTransition to change variables", async () => { @@ -1699,7 +1417,7 @@ it('does not suspend deferred queries with data in the cache and using a "cache- query { greeting { message - ... on Greeting @defer { + ... @defer { recipient { name } @@ -1722,70 +1440,33 @@ it('does not suspend deferred queries with data in the cache and using a "cache- }); const client = new ApolloClient({ cache, link }); - const ProfiledApp = profile<{ - result: UseReadQueryResult | null; - suspenseCount: number; - }>({ - Component: () => { - return ( - }> - - - ); - }, - initialSnapshot: { - suspenseCount: 0, - result: null, - }, - }); - - function SuspenseFallback() { - ProfiledApp.updateSnapshot((snapshot) => ({ - ...snapshot, - suspenseCount: snapshot.suspenseCount + 1, - })); - - return

Loading

; - } + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(); - function Parent() { + function App() { const [queryRef, loadQuery] = useInteractiveQuery(query, { fetchPolicy: "cache-and-network", }); return (
- {queryRef && } + }> + {queryRef && } +
); } - function Todo({ queryRef }: { queryRef: QueryReference }) { - const result = useReadQuery(queryRef); - const { data, networkStatus, error } = result; - const { greeting } = data; - - ProfiledApp.updateSnapshot((snapshot) => ({ ...snapshot, result })); - - return null; - } - - const { user } = renderWithClient(, { client }); - - { - const { snapshot } = await ProfiledApp.takeRender(); - - expect(snapshot.suspenseCount).toBe(0); - expect(snapshot.result).toBeNull(); - } + const { user } = renderWithClient(, { client }); await act(() => user.click(screen.getByText("Load todo"))); + expect(SuspenseFallback).not.toHaveRendered(); + { - const { snapshot } = await ProfiledApp.takeRender(); + const snapshot = await ReadQueryHook.takeSnapshot(); - expect(snapshot.suspenseCount).toBe(0); - expect(snapshot.result).toEqual({ + expect(snapshot).toEqual({ data: { greeting: { __typename: "Greeting", @@ -1808,10 +1489,9 @@ it('does not suspend deferred queries with data in the cache and using a "cache- }); { - const { snapshot } = await ProfiledApp.takeRender(); + const snapshot = await ReadQueryHook.takeSnapshot(); - expect(snapshot.suspenseCount).toBe(0); - expect(snapshot.result).toEqual({ + expect(snapshot).toEqual({ data: { greeting: { __typename: "Greeting", @@ -1843,10 +1523,9 @@ it('does not suspend deferred queries with data in the cache and using a "cache- ); { - const { snapshot } = await ProfiledApp.takeRender(); + const snapshot = await ReadQueryHook.takeSnapshot(); - expect(snapshot.suspenseCount).toBe(0); - expect(snapshot.result).toEqual({ + expect(snapshot).toEqual({ data: { greeting: { __typename: "Greeting", @@ -1858,8 +1537,6 @@ it('does not suspend deferred queries with data in the cache and using a "cache- networkStatus: NetworkStatus.ready, }); } - - await expect(ProfiledApp).not.toRerender(); }); it("reacts to cache updates", async () => { @@ -1869,69 +1546,30 @@ it("reacts to cache updates", async () => { link: new MockLink(mocks), }); - function SuspenseFallback() { - ProfiledApp.updateSnapshot((snapshot) => ({ - ...snapshot, - suspenseCount: snapshot.suspenseCount + 1, - })); - - return

Loading

; - } + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(); function App() { const [queryRef, loadQuery] = useInteractiveQuery(query); + return ( <> }> - {queryRef && } + {queryRef && } ); } - function Child({ queryRef }: { queryRef: QueryReference }) { - const result = useReadQuery(queryRef); - - ProfiledApp.updateSnapshot((snapshot) => ({ ...snapshot, result })); - - return null; - } - - const ProfiledApp = profile<{ - result: UseReadQueryResult | null; - suspenseCount: number; - }>({ - Component: App, - initialSnapshot: { - result: null, - suspenseCount: 0, - }, - }); - - const { user } = renderWithClient(, { client }); - - { - const { snapshot } = await ProfiledApp.takeRender(); - - expect(snapshot.suspenseCount).toBe(0); - expect(snapshot.result).toBe(null); - } + const { user } = renderWithClient(, { client }); await act(() => user.click(screen.getByText("Load query"))); { - const { snapshot } = await ProfiledApp.takeRender(); - - expect(snapshot.suspenseCount).toBe(1); - expect(snapshot.result).toBe(null); - } - - { - const { snapshot } = await ProfiledApp.takeRender(); + const snapshot = await ReadQueryHook.takeSnapshot(); - expect(snapshot.suspenseCount).toBe(1); - expect(snapshot.result).toEqual({ + expect(snapshot).toEqual({ data: { greeting: "Hello" }, error: undefined, networkStatus: NetworkStatus.ready, @@ -1944,17 +1582,14 @@ it("reacts to cache updates", async () => { }); { - const { snapshot } = await ProfiledApp.takeRender(); + const snapshot = await ReadQueryHook.takeSnapshot(); - expect(snapshot.suspenseCount).toBe(1); - expect(snapshot.result).toEqual({ + expect(snapshot).toEqual({ data: { greeting: "Updated Hello" }, error: undefined, networkStatus: NetworkStatus.ready, }); } - - await expect(ProfiledApp).not.toRerender(); }); it("applies `errorPolicy` on next fetch when it changes between renders", async () => { @@ -1973,14 +1608,8 @@ it("applies `errorPolicy` on next fetch when it changes between renders", async }, ]; - function SuspenseFallback() { - ProfiledApp.updateSnapshot((snapshot) => ({ - ...snapshot, - suspenseCount: snapshot.suspenseCount + 1, - })); - - return

Loading

; - } + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(); function App() { const [errorPolicy, setErrorPolicy] = useState("none"); @@ -2006,126 +1635,81 @@ it("applies `errorPolicy` on next fetch when it changes between renders", async })); }} > - {queryRef && } + {queryRef && } ); } - function Child({ queryRef }: { queryRef: QueryReference }) { - const result = useReadQuery(queryRef); - - ProfiledApp.updateSnapshot((snapshot) => ({ ...snapshot, result })); - - return null; - } - const ProfiledApp = profile<{ error: Error | undefined; errorBoundaryCount: number; - result: UseReadQueryResult | null; - suspenseCount: number; }>({ Component: App, initialSnapshot: { error: undefined, errorBoundaryCount: 0, - result: null, - suspenseCount: 0, }, }); const { user } = renderWithMocks(, { mocks }); - { - const { snapshot } = await ProfiledApp.takeRender(); - - expect(snapshot).toEqual({ - result: null, - suspenseCount: 0, - error: undefined, - errorBoundaryCount: 0, - }); - } - await act(() => user.click(screen.getByText("Load query"))); - { - const { snapshot } = await ProfiledApp.takeRender(); - - expect(snapshot).toEqual({ - result: null, - suspenseCount: 1, - error: undefined, - errorBoundaryCount: 0, - }); - } + expect(SuspenseFallback).toHaveRendered(); { - const { snapshot } = await ProfiledApp.takeRender(); + const snapshot = await ReadQueryHook.takeSnapshot(); expect(snapshot).toEqual({ - result: { - data: { greeting: "Hello" }, - error: undefined, - networkStatus: NetworkStatus.ready, - }, - suspenseCount: 1, + data: { greeting: "Hello" }, error: undefined, - errorBoundaryCount: 0, + networkStatus: NetworkStatus.ready, }); } await act(() => user.click(screen.getByText("Change error policy"))); - { - const { snapshot } = await ProfiledApp.takeRender(); - - expect(snapshot).toEqual({ - result: { - data: { greeting: "Hello" }, - error: undefined, - networkStatus: NetworkStatus.ready, - }, - suspenseCount: 1, - error: undefined, - errorBoundaryCount: 0, - }); - } - await act(() => user.click(screen.getByText("Refetch greeting"))); + expect(SuspenseFallback).toHaveRenderedTimes(2); + { - const { snapshot } = await ProfiledApp.takeRender(); + const snapshot = await ReadQueryHook.takeSnapshot(); expect(snapshot).toEqual({ - result: { - data: { greeting: "Hello" }, - error: undefined, - networkStatus: NetworkStatus.ready, - }, - suspenseCount: 2, + data: { greeting: "Hello" }, error: undefined, - errorBoundaryCount: 0, + networkStatus: NetworkStatus.ready, }); } { - const { snapshot } = await ProfiledApp.takeRender(); + const snapshot = await ReadQueryHook.takeSnapshot(); - // Ensure we aren't rendering the error boundary and instead rendering the - // error message in the Child component. expect(snapshot).toEqual({ - result: { - data: { greeting: "Hello" }, - error: new ApolloError({ graphQLErrors: [new GraphQLError("oops")] }), - networkStatus: NetworkStatus.error, - }, - suspenseCount: 2, - error: undefined, - errorBoundaryCount: 0, + data: { greeting: "Hello" }, + error: new ApolloError({ graphQLErrors: [new GraphQLError("oops")] }), + networkStatus: NetworkStatus.error, }); } + + // { + // const { snapshot } = await ProfiledApp.takeRender(); + // + // // Ensure we aren't rendering the error boundary and instead rendering the + // // error message in the Child component. + // expect(snapshot).toEqual({ + // result: { + // data: { greeting: "Hello" }, + // error: new ApolloError({ graphQLErrors: [new GraphQLError("oops")] }), + // networkStatus: NetworkStatus.error, + // }, + // suspenseCount: 2, + // error: undefined, + // errorBoundaryCount: 0, + // }); + // } }); it("applies `context` on next fetch when it changes between renders", async () => { From 0a249f7803437916bd549cfc180ddaeaf9c97bd1 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 8 Nov 2023 22:17:51 -0700 Subject: [PATCH 059/199] Create a default profiled error boundary and error fallback and replace test with it --- .../__tests__/useInteractiveQuery.test.tsx | 77 +++++++++---------- 1 file changed, 36 insertions(+), 41 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index 3f30199f114..9779eddcd8b 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -9,7 +9,11 @@ import { } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import type { Options as UserEventOptions } from "@testing-library/user-event"; -import { ErrorBoundary, ErrorBoundaryProps } from "react-error-boundary"; +import { + ErrorBoundary, + ErrorBoundary as ReactErrorBoundary, + ErrorBoundaryProps, +} from "react-error-boundary"; import { expectTypeOf } from "expect-type"; import { GraphQLError } from "graphql"; import { @@ -123,7 +127,31 @@ function createDefaultProfiledComponents() { { queryRef: QueryReference } >(({ queryRef }) => useReadQuery(queryRef)); - return { SuspenseFallback, ReadQueryHook }; + const ErrorFallback = profile<{ error: Error | null }, { error: Error }>({ + Component: ({ error }) => { + ErrorFallback.updateSnapshot({ error }); + + return
Oops
; + }, + initialSnapshot: { + error: null, + }, + }); + + function ErrorBoundary({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); + } + + return { + SuspenseFallback, + ReadQueryHook, + ErrorFallback, + ErrorBoundary, + }; } function renderWithMocks(ui: React.ReactElement, props: MockedProviderProps) { @@ -1608,7 +1636,7 @@ it("applies `errorPolicy` on next fetch when it changes between renders", async }, ]; - const { SuspenseFallback, ReadQueryHook } = + const { SuspenseFallback, ReadQueryHook, ErrorBoundary, ErrorFallback } = createDefaultProfiledComponents(); function App() { @@ -1625,16 +1653,7 @@ it("applies `errorPolicy` on next fetch when it changes between renders", async }> - Error boundary} - onError={(error) => { - ProfiledApp.updateSnapshot((snapshot) => ({ - ...snapshot, - error, - errorBoundaryCount: snapshot.errorBoundaryCount + 1, - })); - }} - > + {queryRef && } @@ -1642,18 +1661,7 @@ it("applies `errorPolicy` on next fetch when it changes between renders", async ); } - const ProfiledApp = profile<{ - error: Error | undefined; - errorBoundaryCount: number; - }>({ - Component: App, - initialSnapshot: { - error: undefined, - errorBoundaryCount: 0, - }, - }); - - const { user } = renderWithMocks(, { mocks }); + const { user } = renderWithMocks(, { mocks }); await act(() => user.click(screen.getByText("Load query"))); @@ -1694,22 +1702,9 @@ it("applies `errorPolicy` on next fetch when it changes between renders", async }); } - // { - // const { snapshot } = await ProfiledApp.takeRender(); - // - // // Ensure we aren't rendering the error boundary and instead rendering the - // // error message in the Child component. - // expect(snapshot).toEqual({ - // result: { - // data: { greeting: "Hello" }, - // error: new ApolloError({ graphQLErrors: [new GraphQLError("oops")] }), - // networkStatus: NetworkStatus.error, - // }, - // suspenseCount: 2, - // error: undefined, - // errorBoundaryCount: 0, - // }); - // } + // Ensure we aren't rendering the error boundary and instead rendering the + // error message in the hook component. + expect(ErrorFallback).not.toHaveRendered(); }); it("applies `context` on next fetch when it changes between renders", async () => { From 69eba8ea1ed9db766ec69ef18f7d13172ef17e29 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 8 Nov 2023 22:18:26 -0700 Subject: [PATCH 060/199] Fix TS issue with mocks variable --- src/react/hooks/__tests__/useInteractiveQuery.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index 9779eddcd8b..d4d097894ac 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -1623,7 +1623,7 @@ it("reacts to cache updates", async () => { it("applies `errorPolicy` on next fetch when it changes between renders", async () => { const { query } = useSimpleQueryCase(); - const mocks: MockedResponse[] = [ + const mocks: MockedResponse[] = [ { request: { query }, result: { data: { greeting: "Hello" } }, From 264ec3cf745790b32dcb080371b9676eca095a85 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 8 Nov 2023 22:25:44 -0700 Subject: [PATCH 061/199] Update context test to use profile helpers --- .../__tests__/useInteractiveQuery.test.tsx | 36 +++++++++++-------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index d4d097894ac..f5ec4615d16 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -1709,19 +1709,19 @@ it("applies `errorPolicy` on next fetch when it changes between renders", async it("applies `context` on next fetch when it changes between renders", async () => { interface Data { - context: Record; + phase: string; } const query: TypedDocumentNode = gql` query { - context + phase } `; const link = new ApolloLink((operation) => { return Observable.of({ data: { - context: operation.getContext(), + phase: operation.getContext().phase, }, }); }); @@ -1731,9 +1731,8 @@ it("applies `context` on next fetch when it changes between renders", async () = cache: new InMemoryCache(), }); - function SuspenseFallback() { - return
Loading...
; - } + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(); function App() { const [phase, setPhase] = React.useState("initial"); @@ -1747,28 +1746,35 @@ it("applies `context` on next fetch when it changes between renders", async () = }> - {queryRef && } + {queryRef && } ); } - function Context({ queryRef }: { queryRef: QueryReference }) { - const { data } = useReadQuery(queryRef); - - return
{data.context.phase}
; - } - const { user } = renderWithClient(, { client }); await act(() => user.click(screen.getByText("Load query"))); - expect(await screen.findByTestId("context")).toHaveTextContent("initial"); + { + const snapshot = await ReadQueryHook.takeSnapshot(); + + expect(snapshot.data).toEqual({ + phase: "initial", + }); + } await act(() => user.click(screen.getByText("Update context"))); await act(() => user.click(screen.getByText("Refetch"))); + await ReadQueryHook.takeSnapshot(); - expect(await screen.findByTestId("context")).toHaveTextContent("rerender"); + { + const snapshot = await ReadQueryHook.takeSnapshot(); + + expect(snapshot.data).toEqual({ + phase: "rerender", + }); + } }); // NOTE: We only test the `false` -> `true` path here. If the option changes From 92de850f801ee94a4d798c6b29869fe2184d63be Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 8 Nov 2023 22:29:34 -0700 Subject: [PATCH 062/199] Convert change to canonicalResults to profile helpers --- .../__tests__/useInteractiveQuery.test.tsx | 52 +++++++------------ 1 file changed, 19 insertions(+), 33 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index f5ec4615d16..d09fa822760 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -1825,13 +1825,8 @@ it("returns canonical results immediately when `canonizeResults` changes from `f cache, }); - const result: { current: Data | null } = { - current: null, - }; - - function SuspenseFallback() { - return
Loading...
; - } + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(); function App() { const [canonizeResults, setCanonizeResults] = React.useState(false); @@ -1846,46 +1841,37 @@ it("returns canonical results immediately when `canonizeResults` changes from `f Canonize results }> - {queryRef && } + {queryRef && } ); } - function Results({ queryRef }: { queryRef: QueryReference }) { - const { data } = useReadQuery(queryRef); - - result.current = data; - - return null; - } - const { user } = renderWithClient(, { client }); - function verifyCanonicalResults(data: Data, canonized: boolean) { + await act(() => user.click(screen.getByText("Load query"))); + + { + const { data } = await ReadQueryHook.takeSnapshot(); const resultSet = new Set(data.results); const values = Array.from(resultSet).map((item) => item.value); - expect(data).toEqual({ results }); - - if (canonized) { - expect(data.results.length).toBe(6); - expect(resultSet.size).toBe(5); - expect(values).toEqual([0, 1, 2, 3, 5]); - } else { - expect(data.results.length).toBe(6); - expect(resultSet.size).toBe(6); - expect(values).toEqual([0, 1, 1, 2, 3, 5]); - } + expect(data.results.length).toBe(6); + expect(resultSet.size).toBe(6); + expect(values).toEqual([0, 1, 1, 2, 3, 5]); } - await act(() => user.click(screen.getByText("Load query"))); - - verifyCanonicalResults(result.current!, false); - await act(() => user.click(screen.getByText("Canonize results"))); - verifyCanonicalResults(result.current!, true); + { + const { data } = await ReadQueryHook.takeSnapshot(); + const resultSet = new Set(data.results); + const values = Array.from(resultSet).map((item) => item.value); + + expect(data.results.length).toBe(6); + expect(resultSet.size).toBe(5); + expect(values).toEqual([0, 1, 2, 3, 5]); + } }); it("applies changed `refetchWritePolicy` to next fetch when changing between renders", async () => { From 93166f1f23d0346b0e14aa3f349469fa1dc5b0ec Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 8 Nov 2023 22:35:53 -0700 Subject: [PATCH 063/199] Update refetchWritePolicy test to use profile helpers --- .../__tests__/useInteractiveQuery.test.tsx | 73 ++++++++++--------- 1 file changed, 37 insertions(+), 36 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index d09fa822760..dbf37e50191 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -1925,9 +1925,8 @@ it("applies changed `refetchWritePolicy` to next fetch when changing between ren cache, }); - function SuspenseFallback() { - return
Loading...
; - } + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(); function App() { const [refetchWritePolicy, setRefetchWritePolicy] = @@ -1952,56 +1951,58 @@ it("applies changed `refetchWritePolicy` to next fetch when changing between ren Refetch last }> - {queryRef && } + {queryRef && } ); } - function Primes({ queryRef }: { queryRef: QueryReference }) { - const { data } = useReadQuery(queryRef); - - return {data.primes.join(", ")}; - } - const { user } = renderWithClient(, { client }); await act(() => user.click(screen.getByText("Load query"))); - const primes = await screen.findByTestId("primes"); + { + const snapshot = await ReadQueryHook.takeSnapshot(); + const { primes } = snapshot.data; - expect(primes).toHaveTextContent("2, 3, 5, 7, 11"); - expect(mergeParams).toEqual([[undefined, [2, 3, 5, 7, 11]]]); + expect(primes).toEqual([2, 3, 5, 7, 11]); + expect(mergeParams).toEqual([[undefined, [2, 3, 5, 7, 11]]]); + } await act(() => user.click(screen.getByText("Refetch next"))); - await waitFor(() => { - expect(primes).toHaveTextContent("2, 3, 5, 7, 11, 13, 17, 19, 23, 29"); - }); - - expect(mergeParams).toEqual([ - [undefined, [2, 3, 5, 7, 11]], - [ - [2, 3, 5, 7, 11], - [13, 17, 19, 23, 29], - ], - ]); + { + const snapshot = await ReadQueryHook.takeSnapshot(); + const { primes } = snapshot.data; + + expect(primes).toEqual([2, 3, 5, 7, 11, 13, 17, 19, 23, 29]); + expect(mergeParams).toEqual([ + [undefined, [2, 3, 5, 7, 11]], + [ + [2, 3, 5, 7, 11], + [13, 17, 19, 23, 29], + ], + ]); + } await act(() => user.click(screen.getByText("Change refetch write policy"))); + await ReadQueryHook.takeSnapshot(); await act(() => user.click(screen.getByText("Refetch last"))); - await waitFor(() => { - expect(primes).toHaveTextContent("31, 37, 41, 43, 47"); - }); - - expect(mergeParams).toEqual([ - [undefined, [2, 3, 5, 7, 11]], - [ - [2, 3, 5, 7, 11], - [13, 17, 19, 23, 29], - ], - [undefined, [31, 37, 41, 43, 47]], - ]); + { + const snapshot = await ReadQueryHook.takeSnapshot(); + const { primes } = snapshot.data; + + expect(primes).toEqual([31, 37, 41, 43, 47]); + expect(mergeParams).toEqual([ + [undefined, [2, 3, 5, 7, 11]], + [ + [2, 3, 5, 7, 11], + [13, 17, 19, 23, 29], + ], + [undefined, [31, 37, 41, 43, 47]], + ]); + } }); it("applies `returnPartialData` on next fetch when it changes between renders", async () => { From 7f5e9311e70603b779da3bf604ebfe3b4be0b0a7 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 8 Nov 2023 22:42:39 -0700 Subject: [PATCH 064/199] Use profile helpers for changing returnPartialData --- .../__tests__/useInteractiveQuery.test.tsx | 60 ++++++++++++------- 1 file changed, 40 insertions(+), 20 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index dbf37e50191..04e39501c4f 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -2080,9 +2080,8 @@ it("applies `returnPartialData` on next fetch when it changes between renders", cache, }); - function SuspenseFallback() { - return
Loading...
; - } + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(); function App() { const [returnPartialData, setReturnPartialData] = React.useState(false); @@ -2098,29 +2097,30 @@ it("applies `returnPartialData` on next fetch when it changes between renders", Update partial data }> - {queryRef && } + {queryRef && } ); } - function Character({ queryRef }: { queryRef: QueryReference }) { - const { data } = useReadQuery(queryRef); - - return ( - {data.character.name ?? "unknown"} - ); - } - const { user } = renderWithClient(, { client }); await act(() => user.click(screen.getByText("Load query"))); - const character = await screen.findByTestId("character"); + { + const snapshot = await ReadQueryHook.takeSnapshot(); - expect(character).toHaveTextContent("Doctor Strange"); + expect(snapshot).toEqual({ + data: { + character: { __typename: "Character", id: "1", name: "Doctor Strange" }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } await act(() => user.click(screen.getByText("Update partial data"))); + await ReadQueryHook.takeSnapshot(); cache.modify({ id: cache.identify({ __typename: "Character", id: "1" }), @@ -2129,13 +2129,33 @@ it("applies `returnPartialData` on next fetch when it changes between renders", }, }); - await waitFor(() => { - expect(character).toHaveTextContent("unknown"); - }); + { + const snapshot = await ReadQueryHook.takeSnapshot(); - await waitFor(() => { - expect(character).toHaveTextContent("Doctor Strange (refetched)"); - }); + expect(snapshot).toEqual({ + data: { + character: { __typename: "Character", id: "1" }, + }, + error: undefined, + networkStatus: NetworkStatus.loading, + }); + } + + { + const snapshot = await ReadQueryHook.takeSnapshot(); + + expect(snapshot).toEqual({ + data: { + character: { + __typename: "Character", + id: "1", + name: "Doctor Strange (refetched)", + }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } }); it("applies updated `fetchPolicy` on next fetch when it changes between renders", async () => { From dec33d7bf2ecc4f17075f4c177234afa7f28f34a Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 8 Nov 2023 22:46:04 -0700 Subject: [PATCH 065/199] Update changing fetch policy to use profile helpers --- .../__tests__/useInteractiveQuery.test.tsx | 49 +++++++++++++------ 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index 04e39501c4f..56bb7442da2 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -2211,9 +2211,8 @@ it("applies updated `fetchPolicy` on next fetch when it changes between renders" cache, }); - function SuspenseFallback() { - return
Loading...
; - } + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(); function App() { const [fetchPolicy, setFetchPolicy] = @@ -2231,31 +2230,51 @@ it("applies updated `fetchPolicy` on next fetch when it changes between renders" }> - {queryRef && } + {queryRef && } ); } - function Character({ queryRef }: { queryRef: QueryReference }) { - const { data } = useReadQuery(queryRef); - - return {data.character.name}; - } - const { user } = renderWithClient(, { client }); await act(() => user.click(screen.getByText("Load query"))); - const character = await screen.findByTestId("character"); + { + const snapshot = await ReadQueryHook.takeSnapshot(); - expect(character).toHaveTextContent("Doctor Strangecache"); + expect(snapshot).toEqual({ + data: { + character: { + __typename: "Character", + id: "1", + name: "Doctor Strangecache", + }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } await act(() => user.click(screen.getByText("Change fetch policy"))); + await ReadQueryHook.takeSnapshot(); await act(() => user.click(screen.getByText("Refetch"))); - await waitFor(() => { - expect(character).toHaveTextContent("Doctor Strange"); - }); + + { + const snapshot = await ReadQueryHook.takeSnapshot(); + + expect(snapshot).toEqual({ + data: { + character: { + __typename: "Character", + id: "1", + name: "Doctor Strange", + }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } // Because we switched to a `no-cache` fetch policy, we should not see the // newly fetched data in the cache after the fetch occured. From 3fd069ba6fc8de72b14c685ab5e37f1eed70768b Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 8 Nov 2023 23:06:12 -0700 Subject: [PATCH 066/199] Update tests refetching to use profile helpers --- .../__tests__/useInteractiveQuery.test.tsx | 166 +++++++++++------- 1 file changed, 102 insertions(+), 64 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index 56bb7442da2..b725af61fa2 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -2287,50 +2287,81 @@ it("applies updated `fetchPolicy` on next fetch when it changes between renders" }); }); -it.skip("re-suspends when calling `refetch`", async () => { - const { renders } = renderVariablesIntegrationTest({ - variables: { id: "1" }, - }); +it("re-suspends when calling `refetch`", async () => { + const { query } = useVariablesQueryCase(); - expect(renders.suspenseCount).toBe(1); - expect(screen.getByText("loading")).toBeInTheDocument(); + const mocks: MockedResponse[] = [ + { + request: { query, variables: { id: "1" } }, + result: { + data: { character: { id: "1", name: "Spider-Man" } }, + }, + delay: 20, + }, + // refetch + { + request: { query, variables: { id: "1" } }, + result: { + data: { character: { id: "1", name: "Spider-Man (updated)" } }, + }, + delay: 20, + }, + ]; - expect(await screen.findByText("1 - Spider-Man")).toBeInTheDocument(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(); - const button = screen.getByText("Refetch"); - const user = userEvent.setup(); - await act(() => user.click(button)); + function App() { + const [queryRef, loadQuery, { refetch }] = useInteractiveQuery(query); - // parent component re-suspends - expect(renders.suspenseCount).toBe(2); - expect(renders.count).toBe(2); + return ( + <> + + + }> + {queryRef && } + + + ); + } - expect( - await screen.findByText("1 - Spider-Man (updated)") - ).toBeInTheDocument(); -}); + const { user } = renderWithMocks(, { mocks }); -it.skip("re-suspends when calling `refetch` with new variables", async () => { - interface QueryData { - character: { - id: string; - name: string; - }; + expect(SuspenseFallback).not.toHaveRendered(); + + await act(() => user.click(screen.getByText("Load query"))); + + expect(SuspenseFallback).toHaveRendered(); + + { + const snapshot = await ReadQueryHook.takeSnapshot(); + + expect(snapshot).toEqual({ + data: { character: { id: "1", name: "Spider-Man" } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); } - interface QueryVariables { - id: string; + await act(() => user.click(screen.getByText("Refetch"))); + + expect(SuspenseFallback).toHaveRenderedTimes(2); + + { + const snapshot = await ReadQueryHook.takeSnapshot(); + + expect(snapshot).toEqual({ + data: { character: { id: "1", name: "Spider-Man (updated)" } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); } - const query: TypedDocumentNode = gql` - query CharacterQuery($id: ID!) { - character(id: $id) { - id - name - } - } - `; +}); - const mocks = [ +it("re-suspends when calling `refetch` with new variables", async () => { + const { query } = useVariablesQueryCase(); + + const mocks: MockedResponse[] = [ { request: { query, variables: { id: "1" } }, result: { @@ -2345,47 +2376,54 @@ it.skip("re-suspends when calling `refetch` with new variables", async () => { }, ]; - const { renders } = renderVariablesIntegrationTest({ - variables: { id: "1" }, - mocks, - }); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(); - expect(renders.suspenseCount).toBe(1); - expect(screen.getByText("loading")).toBeInTheDocument(); + function App() { + const [queryRef, loadQuery, { refetch }] = useInteractiveQuery(query); - expect(await screen.findByText("1 - Captain Marvel")).toBeInTheDocument(); + return ( + <> + + + }> + {queryRef && } + + + ); + } - const newVariablesRefetchButton = screen.getByText("Set variables to id: 2"); - const refetchButton = screen.getByText("Refetch"); - const user = userEvent.setup(); - await act(() => user.click(newVariablesRefetchButton)); - await act(() => user.click(refetchButton)); + const { user } = renderWithMocks(, { mocks }); - expect(await screen.findByText("2 - Captain America")).toBeInTheDocument(); + await act(() => user.click(screen.getByText("Load query"))); - // parent component re-suspends - expect(renders.suspenseCount).toBe(2); - expect(renders.count).toBe(3); + expect(SuspenseFallback).toHaveRendered(); - // extra render puts an additional frame into the array - expect(renders.frames).toMatchObject([ - { - ...mocks[0].result, - networkStatus: NetworkStatus.ready, + { + const snapshot = await ReadQueryHook.takeSnapshot(); + + expect(snapshot).toEqual({ + data: { character: { id: "1", name: "Captain Marvel" } }, error: undefined, - }, - { - ...mocks[0].result, networkStatus: NetworkStatus.ready, + }); + } + + await act(() => user.click(screen.getByText("Refetch with ID 2"))); + + expect(SuspenseFallback).toHaveRenderedTimes(2); + + { + const snapshot = await ReadQueryHook.takeSnapshot(); + + expect(snapshot).toEqual({ + data: { character: { id: "2", name: "Captain America" } }, error: undefined, - }, - { - ...mocks[1].result, networkStatus: NetworkStatus.ready, - error: undefined, - }, - ]); + }); + } }); + it.skip("re-suspends multiple times when calling `refetch` multiple times", async () => { const { renders } = renderVariablesIntegrationTest({ variables: { id: "1" }, From 9d523c49a128da811af18daaac5dd1a3d00db8f3 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 8 Nov 2023 23:12:31 -0700 Subject: [PATCH 067/199] Convert test that refetches multiple times to helpers --- .../__tests__/useInteractiveQuery.test.tsx | 60 ++++++++++++------- 1 file changed, 37 insertions(+), 23 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index b725af61fa2..f265b73c77d 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -2424,38 +2424,52 @@ it("re-suspends when calling `refetch` with new variables", async () => { } }); -it.skip("re-suspends multiple times when calling `refetch` multiple times", async () => { - const { renders } = renderVariablesIntegrationTest({ - variables: { id: "1" }, - }); +it("re-suspends multiple times when calling `refetch` multiple times", async () => { + const { query } = useVariablesQueryCase(); - expect(renders.suspenseCount).toBe(1); - expect(screen.getByText("loading")).toBeInTheDocument(); + const mocks: MockedResponse[] = [ + { + request: { query, variables: { id: "1" } }, + result: { + data: { character: { id: "1", name: "Spider-Man" } }, + }, + maxUsageCount: 3, + }, + ]; - expect(await screen.findByText("1 - Spider-Man")).toBeInTheDocument(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(); - const button = screen.getByText("Refetch"); - const user = userEvent.setup(); - await act(() => user.click(button)); + function App() { + const [queryRef, loadQuery, { refetch }] = useInteractiveQuery(query); - // parent component re-suspends - expect(renders.suspenseCount).toBe(2); - expect(renders.count).toBe(2); + return ( + <> + + + }> + {queryRef && } + + + ); + } - expect( - await screen.findByText("1 - Spider-Man (updated)") - ).toBeInTheDocument(); + const { user } = renderWithMocks(, { mocks }); - await act(() => user.click(button)); + await act(() => user.click(screen.getByText("Load query"))); - // parent component re-suspends - expect(renders.suspenseCount).toBe(3); - expect(renders.count).toBe(3); + expect(SuspenseFallback).toHaveRendered(); + await ReadQueryHook.takeSnapshot(); + + const button = screen.getByText("Refetch"); + + await act(() => user.click(button)); + expect(SuspenseFallback).toHaveRenderedTimes(2); - expect( - await screen.findByText("1 - Spider-Man (updated again)") - ).toBeInTheDocument(); + await act(() => user.click(button)); + expect(SuspenseFallback).toHaveRenderedTimes(3); }); + it.skip("throws errors when errors are returned after calling `refetch`", async () => { const consoleSpy = jest.spyOn(console, "error").mockImplementation(); interface QueryData { From 78339f1150a51975f6083a3c6328a028018233a5 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 8 Nov 2023 23:18:16 -0700 Subject: [PATCH 068/199] Use profile helpers for test that checks errors after refetch --- .../__tests__/useInteractiveQuery.test.tsx | 76 +++++++++---------- 1 file changed, 37 insertions(+), 39 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index f265b73c77d..1800d2d62b1 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -2470,66 +2470,64 @@ it("re-suspends multiple times when calling `refetch` multiple times", async () expect(SuspenseFallback).toHaveRenderedTimes(3); }); -it.skip("throws errors when errors are returned after calling `refetch`", async () => { - const consoleSpy = jest.spyOn(console, "error").mockImplementation(); - interface QueryData { - character: { - id: string; - name: string; - }; - } +it("throws errors when errors are returned after calling `refetch`", async () => { + using _consoleSpy = spyOnConsole("error"); - interface QueryVariables { - id: string; - } - const query: TypedDocumentNode = gql` - query CharacterQuery($id: ID!) { - character(id: $id) { - id - name - } - } - `; - const mocks = [ + const { query } = useVariablesQueryCase(); + + const mocks: MockedResponse[] = [ { request: { query, variables: { id: "1" } }, result: { data: { character: { id: "1", name: "Captain Marvel" } }, }, + delay: 20, }, { request: { query, variables: { id: "1" } }, result: { errors: [new GraphQLError("Something went wrong")], }, + delay: 20, }, ]; - const { renders } = renderVariablesIntegrationTest({ - variables: { id: "1" }, - mocks, - }); - expect(renders.suspenseCount).toBe(1); - expect(screen.getByText("loading")).toBeInTheDocument(); + const { SuspenseFallback, ReadQueryHook, ErrorBoundary, ErrorFallback } = + createDefaultProfiledComponents(); - expect(await screen.findByText("1 - Captain Marvel")).toBeInTheDocument(); + function App() { + const [queryRef, loadQuery, { refetch }] = useInteractiveQuery(query); - const button = screen.getByText("Refetch"); - const user = userEvent.setup(); - await act(() => user.click(button)); + return ( + <> + + + }> + + {queryRef && } + + + + ); + } - await waitFor(() => { - expect(renders.errorCount).toBe(1); - }); + const { user } = renderWithMocks(, { mocks }); - expect(renders.errors).toEqual([ - new ApolloError({ - graphQLErrors: [new GraphQLError("Something went wrong")], - }), - ]); + await act(() => user.click(screen.getByText("Load query"))); + await ReadQueryHook.waitForNextSnapshot(); + await act(() => user.click(screen.getByText("Refetch"))); - consoleSpy.mockRestore(); + { + const { snapshot } = await ErrorFallback.takeRender(); + + expect(snapshot.error).toEqual( + new ApolloError({ + graphQLErrors: [new GraphQLError("Something went wrong")], + }) + ); + } }); + it.skip('ignores errors returned after calling `refetch` when errorPolicy is set to "ignore"', async () => { interface QueryData { character: { From 0db74e8038bbdb76aca134601605a2d864b47ee3 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 8 Nov 2023 23:25:44 -0700 Subject: [PATCH 069/199] Update test that checks errorPolicy ignore to profile helpers --- .../__tests__/useInteractiveQuery.test.tsx | 66 +++++++++++-------- 1 file changed, 37 insertions(+), 29 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index 1800d2d62b1..de566fcdebd 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -2528,25 +2528,9 @@ it("throws errors when errors are returned after calling `refetch`", async () => } }); -it.skip('ignores errors returned after calling `refetch` when errorPolicy is set to "ignore"', async () => { - interface QueryData { - character: { - id: string; - name: string; - }; - } +it('ignores errors returned after calling `refetch` when errorPolicy is set to "ignore"', async () => { + const { query } = useVariablesQueryCase(); - interface QueryVariables { - id: string; - } - const query: TypedDocumentNode = gql` - query CharacterQuery($id: ID!) { - character(id: $id) { - id - name - } - } - `; const mocks = [ { request: { query, variables: { id: "1" } }, @@ -2562,21 +2546,45 @@ it.skip('ignores errors returned after calling `refetch` when errorPolicy is set }, ]; - const { renders } = renderVariablesIntegrationTest({ - variables: { id: "1" }, - errorPolicy: "ignore", - mocks, - }); + const { SuspenseFallback, ReadQueryHook, ErrorBoundary, ErrorFallback } = + createDefaultProfiledComponents(); - expect(await screen.findByText("1 - Captain Marvel")).toBeInTheDocument(); + function App() { + const [queryRef, loadQuery, { refetch }] = useInteractiveQuery(query, { + errorPolicy: "ignore", + }); - const button = screen.getByText("Refetch"); - const user = userEvent.setup(); - await act(() => user.click(button)); + return ( + <> + + + }> + + {queryRef && } + + + + ); + } - expect(renders.errorCount).toBe(0); - expect(renders.errors).toEqual([]); + const { user } = renderWithMocks(, { mocks }); + + await act(() => user.click(screen.getByText("Load query"))); + await ReadQueryHook.takeSnapshot(); + await act(() => user.click(screen.getByText("Refetch"))); + + { + const snapshot = await ReadQueryHook.takeSnapshot(); + + expect(snapshot).toEqual({ + data: { character: { id: "1", name: "Captain Marvel" } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + expect(ErrorFallback).not.toHaveRendered(); + } }); + it.skip('returns errors after calling `refetch` when errorPolicy is set to "all"', async () => { interface QueryData { character: { From 7159d9ba3209d12de961dac37eadd0acd5a322c7 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 8 Nov 2023 23:35:06 -0700 Subject: [PATCH 070/199] Update test that checks errorPolicy all to profile helpers --- .../__tests__/useInteractiveQuery.test.tsx | 72 +++++++++++-------- 1 file changed, 41 insertions(+), 31 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index de566fcdebd..da965b32808 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -2585,57 +2585,67 @@ it('ignores errors returned after calling `refetch` when errorPolicy is set to " } }); -it.skip('returns errors after calling `refetch` when errorPolicy is set to "all"', async () => { - interface QueryData { - character: { - id: string; - name: string; - }; - } +it('returns errors after calling `refetch` when errorPolicy is set to "all"', async () => { + const { query } = useVariablesQueryCase(); - interface QueryVariables { - id: string; - } - const query: TypedDocumentNode = gql` - query CharacterQuery($id: ID!) { - character(id: $id) { - id - name - } - } - `; - const mocks = [ + const mocks: MockedResponse[] = [ { request: { query, variables: { id: "1" } }, result: { data: { character: { id: "1", name: "Captain Marvel" } }, }, + delay: 20, }, { request: { query, variables: { id: "1" } }, result: { errors: [new GraphQLError("Something went wrong")], }, + delay: 20, }, ]; - const { renders } = renderVariablesIntegrationTest({ - variables: { id: "1" }, - errorPolicy: "all", - mocks, - }); + const { SuspenseFallback, ReadQueryHook, ErrorBoundary, ErrorFallback } = + createDefaultProfiledComponents(); - expect(await screen.findByText("1 - Captain Marvel")).toBeInTheDocument(); + function App() { + const [queryRef, loadQuery, { refetch }] = useInteractiveQuery(query, { + errorPolicy: "all", + }); - const button = screen.getByText("Refetch"); - const user = userEvent.setup(); - await act(() => user.click(button)); + return ( + <> + + + }> + + {queryRef && } + + + + ); + } - expect(renders.errorCount).toBe(0); - expect(renders.errors).toEqual([]); + const { user } = renderWithMocks(, { mocks }); - expect(await screen.findByText("Something went wrong")).toBeInTheDocument(); + await act(() => user.click(screen.getByText("Load query"))); + await ReadQueryHook.waitForNextSnapshot(); + await act(() => user.click(screen.getByText("Refetch"))); + + // TODO: Figure out why there is an extra render here. + // Perhaps related? https://github.com/apollographql/apollo-client/issues/11315 + await ReadQueryHook.takeSnapshot(); + const snapshot = await ReadQueryHook.takeSnapshot(); + + expect(snapshot).toEqual({ + data: { character: { id: "1", name: "Captain Marvel" } }, + error: new ApolloError({ + graphQLErrors: [new GraphQLError("Something went wrong")], + }), + networkStatus: NetworkStatus.error, + }); }); + it.skip('handles partial data results after calling `refetch` when errorPolicy is set to "all"', async () => { interface QueryData { character: { From b6dab841d104c2f4a99a34d146add9562e412a2e Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 8 Nov 2023 23:38:34 -0700 Subject: [PATCH 071/199] Update test that calls refetch with partial data to use profile helpers --- .../__tests__/useInteractiveQuery.test.tsx | 83 +++++++++---------- 1 file changed, 38 insertions(+), 45 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index da965b32808..32a2992e7a5 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -2646,31 +2646,16 @@ it('returns errors after calling `refetch` when errorPolicy is set to "all"', as }); }); -it.skip('handles partial data results after calling `refetch` when errorPolicy is set to "all"', async () => { - interface QueryData { - character: { - id: string; - name: string; - }; - } +it('handles partial data results after calling `refetch` when errorPolicy is set to "all"', async () => { + const { query } = useVariablesQueryCase(); - interface QueryVariables { - id: string; - } - const query: TypedDocumentNode = gql` - query CharacterQuery($id: ID!) { - character(id: $id) { - id - name - } - } - `; const mocks = [ { request: { query, variables: { id: "1" } }, result: { data: { character: { id: "1", name: "Captain Marvel" } }, }, + delay: 20, }, { request: { query, variables: { id: "1" } }, @@ -2678,43 +2663,51 @@ it.skip('handles partial data results after calling `refetch` when errorPolicy i data: { character: { id: "1", name: null } }, errors: [new GraphQLError("Something went wrong")], }, + delay: 20, }, ]; - const { renders } = renderVariablesIntegrationTest({ - variables: { id: "1" }, - errorPolicy: "all", - mocks, - }); + const { SuspenseFallback, ReadQueryHook, ErrorBoundary, ErrorFallback } = + createDefaultProfiledComponents(); - expect(await screen.findByText("1 - Captain Marvel")).toBeInTheDocument(); + function App() { + const [queryRef, loadQuery, { refetch }] = useInteractiveQuery(query, { + errorPolicy: "all", + }); - const button = screen.getByText("Refetch"); - const user = userEvent.setup(); - await act(() => user.click(button)); + return ( + <> + + + }> + + {queryRef && } + + + + ); + } - expect(renders.errorCount).toBe(0); - expect(renders.errors).toEqual([]); + const { user } = renderWithMocks(, { mocks }); - expect(await screen.findByText("Something went wrong")).toBeInTheDocument(); + await act(() => user.click(screen.getByText("Load query"))); + await ReadQueryHook.waitForNextSnapshot(); + await act(() => user.click(screen.getByText("Refetch"))); - const expectedError = new ApolloError({ - graphQLErrors: [new GraphQLError("Something went wrong")], - }); + // TODO: Figure out why there is an extra render here. + // Perhaps related? https://github.com/apollographql/apollo-client/issues/11315 + await ReadQueryHook.takeSnapshot(); + const snapshot = await ReadQueryHook.takeSnapshot(); - expect(renders.frames).toMatchObject([ - { - ...mocks[0].result, - networkStatus: NetworkStatus.ready, - error: undefined, - }, - { - data: mocks[1].result.data, - networkStatus: NetworkStatus.error, - error: expectedError, - }, - ]); + expect(snapshot).toEqual({ + data: { character: { id: "1", name: null } }, + error: new ApolloError({ + graphQLErrors: [new GraphQLError("Something went wrong")], + }), + networkStatus: NetworkStatus.error, + }); }); + it.skip("`refetch` works with startTransition to allow React to show stale UI until finished suspending", async () => { type Variables = { id: string; From 7cdbcd2229ae67e4d76ee76b9d8165cdcbfefc14 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 8 Nov 2023 23:43:55 -0700 Subject: [PATCH 072/199] Get refetch with startTransition working --- .../__tests__/useInteractiveQuery.test.tsx | 39 +++++++++---------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index 32a2992e7a5..aec53ca4ddc 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -2708,7 +2708,7 @@ it('handles partial data results after calling `refetch` when errorPolicy is set }); }); -it.skip("`refetch` works with startTransition to allow React to show stale UI until finished suspending", async () => { +it("`refetch` works with startTransition to allow React to show stale UI until finished suspending", async () => { type Variables = { id: string; }; @@ -2720,7 +2720,6 @@ it.skip("`refetch` works with startTransition to allow React to show stale UI un completed: boolean; }; } - const user = userEvent.setup(); const query: TypedDocumentNode = gql` query TodoItemQuery($id: ID!) { @@ -2732,7 +2731,7 @@ it.skip("`refetch` works with startTransition to allow React to show stale UI un } `; - const mocks: MockedResponse[] = [ + const mocks: MockedResponse[] = [ { request: { query, variables: { id: "1" } }, result: { @@ -2754,26 +2753,24 @@ it.skip("`refetch` works with startTransition to allow React to show stale UI un cache: new InMemoryCache(), }); - function App() { - return ( - - }> - - - - ); - } - function SuspenseFallback() { return

Loading

; } - function Parent() { + function App() { const [id, setId] = React.useState("1"); - const [queryRef, { refetch }] = useInteractiveQuery(query, { - variables: { id }, - }); - return ; + const [queryRef, loadQuery, { refetch }] = useInteractiveQuery(query); + + return ( + <> + + }> + {queryRef && ( + + )} + + + ); } function Todo({ @@ -2807,10 +2804,11 @@ it.skip("`refetch` works with startTransition to allow React to show stale UI un ); } - render(); + const { user } = renderWithMocks(, { mocks }); - expect(screen.getByText("Loading")).toBeInTheDocument(); + await act(() => user.click(screen.getByText("Load query"))); + expect(screen.getByText("Loading")).toBeInTheDocument(); expect(await screen.findByTestId("todo")).toBeInTheDocument(); const todo = screen.getByTestId("todo"); @@ -2841,6 +2839,7 @@ it.skip("`refetch` works with startTransition to allow React to show stale UI un expect(todo).toHaveTextContent("Clean room (completed)"); }); }); + function getItemTexts() { return screen.getAllByTestId(/letter/).map( // eslint-disable-next-line testing-library/no-node-access From 7c0efcb21c7a7d8f1be63979af8d03567e216e9e Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 9 Nov 2023 15:24:28 -0700 Subject: [PATCH 073/199] Create a usePaginatedQueryCase helper --- .../__tests__/useInteractiveQuery.test.tsx | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index aec53ca4ddc..9814f2ce460 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -117,6 +117,52 @@ function useVariablesQueryCase() { return { mocks, query }; } +interface PaginatedQueryData { + letters: { + letter: string; + position: number; + }[]; +} + +interface PaginatedQueryVariables { + limit?: number; + offset?: number; +} + +function usePaginatedQueryCase() { + const query: TypedDocumentNode< + PaginatedQueryData, + PaginatedQueryVariables + > = gql` + query letters($limit: Int, $offset: Int) { + letters(limit: $limit) { + letter + position + } + } + `; + + const data = "ABCDEFG" + .split("") + .map((letter, index) => ({ letter, position: index + 1 })); + + const link = new ApolloLink((operation) => { + const { offset = 0, limit = 2 } = operation.variables; + const letters = data.slice(offset, offset + limit); + + return new Observable((observer) => { + setTimeout(() => { + observer.next({ data: { letters } }); + observer.complete(); + }, 10); + }); + }); + + const client = new ApolloClient({ cache: new InMemoryCache(), link }); + + return { query, link, client }; +} + function createDefaultProfiledComponents() { const SuspenseFallback = profile({ Component: () =>

Loading

, From 453ab5d27cb1ad70928b8a8e0c4fbd21a5061eb7 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 9 Nov 2023 15:46:30 -0700 Subject: [PATCH 074/199] Update test that checks fetch more with profiler --- .../__tests__/useInteractiveQuery.test.tsx | 84 ++++++++++++++----- 1 file changed, 62 insertions(+), 22 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index 9814f2ce460..44e07f17661 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -2886,34 +2886,74 @@ it("`refetch` works with startTransition to allow React to show stale UI until f }); }); -function getItemTexts() { - return screen.getAllByTestId(/letter/).map( - // eslint-disable-next-line testing-library/no-node-access - (li) => li.firstChild!.textContent - ); -} -it.skip("re-suspends when calling `fetchMore` with different variables", async () => { - const { renders } = renderPaginatedIntegrationTest(); +it("re-suspends when calling `fetchMore` with different variables", async () => { + const { query, client } = usePaginatedQueryCase(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(); - expect(renders.suspenseCount).toBe(1); - expect(screen.getByText("loading")).toBeInTheDocument(); + function App() { + const [queryRef, loadQuery, { fetchMore }] = useInteractiveQuery(query); - const items = await screen.findAllByTestId(/letter/i); - expect(items).toHaveLength(2); - expect(getItemTexts()).toStrictEqual(["A", "B"]); + return ( + <> + + + }> + {queryRef && } + + + ); + } - const button = screen.getByText("Fetch more"); - const user = userEvent.setup(); - await act(() => user.click(button)); + const { user } = renderWithClient(, { client }); - // parent component re-suspends - expect(renders.suspenseCount).toBe(2); - await waitFor(() => { - expect(renders.count).toBe(2); - }); + await act(() => user.click(screen.getByText("Load query"))); - expect(getItemTexts()).toStrictEqual(["C", "D"]); + expect(SuspenseFallback).toHaveRendered(); + + { + const snapshot = await ReadQueryHook.waitForNextSnapshot(); + + expect(snapshot).toEqual({ + data: { + letters: [ + { letter: "A", position: 1 }, + { letter: "B", position: 2 }, + ], + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await act(() => user.click(screen.getByText("Fetch more"))); + + expect(SuspenseFallback).toHaveRenderedTimes(2); + + // TODO: Figure out why there is an extra render here. + // Perhaps related? https://github.com/apollographql/apollo-client/issues/11315 + await ReadQueryHook.takeSnapshot(); + + { + const snapshot = await ReadQueryHook.takeSnapshot(); + + expect(snapshot).toEqual({ + data: { + letters: [ + { letter: "C", position: 3 }, + { letter: "D", position: 4 }, + ], + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } }); + it.skip("properly uses `updateQuery` when calling `fetchMore`", async () => { const { renders } = renderPaginatedIntegrationTest({ updateQuery: true, From 4e985ac2bf8fc478b6a4434123b55587f05becfe Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 9 Nov 2023 15:49:04 -0700 Subject: [PATCH 075/199] Update test for fetchMore + updateQuery to use profiler helpers --- .../__tests__/useInteractiveQuery.test.tsx | 90 ++++++++++++++----- 1 file changed, 70 insertions(+), 20 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index 44e07f17661..c80faca0142 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -2954,33 +2954,83 @@ it("re-suspends when calling `fetchMore` with different variables", async () => } }); -it.skip("properly uses `updateQuery` when calling `fetchMore`", async () => { - const { renders } = renderPaginatedIntegrationTest({ - updateQuery: true, - }); +it("properly uses `updateQuery` when calling `fetchMore`", async () => { + const { query, client } = usePaginatedQueryCase(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(); - expect(renders.suspenseCount).toBe(1); - expect(screen.getByText("loading")).toBeInTheDocument(); + function App() { + const [queryRef, loadQuery, { fetchMore }] = useInteractiveQuery(query); - const items = await screen.findAllByTestId(/letter/i); + return ( + <> + + + }> + {queryRef && } + + + ); + } - expect(items).toHaveLength(2); - expect(getItemTexts()).toStrictEqual(["A", "B"]); + const { user } = renderWithClient(, { client }); - const button = screen.getByText("Fetch more"); - const user = userEvent.setup(); - await act(() => user.click(button)); + await act(() => user.click(screen.getByText("Load query"))); - // parent component re-suspends - expect(renders.suspenseCount).toBe(2); - await waitFor(() => { - expect(renders.count).toBe(2); - }); + expect(SuspenseFallback).toHaveRendered(); - const moreItems = await screen.findAllByTestId(/letter/i); - expect(moreItems).toHaveLength(4); - expect(getItemTexts()).toStrictEqual(["A", "B", "C", "D"]); + { + const snapshot = await ReadQueryHook.waitForNextSnapshot(); + + expect(snapshot).toEqual({ + data: { + letters: [ + { letter: "A", position: 1 }, + { letter: "B", position: 2 }, + ], + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await act(() => user.click(screen.getByText("Fetch more"))); + + expect(SuspenseFallback).toHaveRenderedTimes(2); + + // TODO: Figure out why there is an extra render here. + // Perhaps related? https://github.com/apollographql/apollo-client/issues/11315 + await ReadQueryHook.takeSnapshot(); + + { + const snapshot = await ReadQueryHook.takeSnapshot(); + + expect(snapshot).toEqual({ + data: { + letters: [ + { letter: "A", position: 1 }, + { letter: "B", position: 2 }, + { letter: "C", position: 3 }, + { letter: "D", position: 4 }, + ], + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } }); + it.skip("properly uses cache field policies when calling `fetchMore` without `updateQuery`", async () => { const { renders } = renderPaginatedIntegrationTest({ fieldPolicies: true, From f393b915b2d5610cc720c47f8340ba02662a92ad Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 9 Nov 2023 15:52:18 -0700 Subject: [PATCH 076/199] Update test that checks fetchMore with fieldPolicies --- .../__tests__/useInteractiveQuery.test.tsx | 95 +++++++++++++++---- 1 file changed, 76 insertions(+), 19 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index c80faca0142..ddc7d57f858 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -3031,32 +3031,89 @@ it("properly uses `updateQuery` when calling `fetchMore`", async () => { } }); -it.skip("properly uses cache field policies when calling `fetchMore` without `updateQuery`", async () => { - const { renders } = renderPaginatedIntegrationTest({ - fieldPolicies: true, +it("properly uses cache field policies when calling `fetchMore` without `updateQuery`", async () => { + const { query, link } = usePaginatedQueryCase(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(); + + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ + typePolicies: { + Query: { + fields: { + letters: concatPagination(), + }, + }, + }, + }), }); - expect(renders.suspenseCount).toBe(1); - expect(screen.getByText("loading")).toBeInTheDocument(); - const items = await screen.findAllByTestId(/letter/i); + function App() { + const [queryRef, loadQuery, { fetchMore }] = useInteractiveQuery(query); - expect(items).toHaveLength(2); - expect(getItemTexts()).toStrictEqual(["A", "B"]); + return ( + <> + + + }> + {queryRef && } + + + ); + } - const button = screen.getByText("Fetch more"); - const user = userEvent.setup(); - await act(() => user.click(button)); + const { user } = renderWithClient(, { client }); - // parent component re-suspends - expect(renders.suspenseCount).toBe(2); - await waitFor(() => { - expect(renders.count).toBe(2); - }); + await act(() => user.click(screen.getByText("Load query"))); + + expect(SuspenseFallback).toHaveRendered(); + + { + const snapshot = await ReadQueryHook.waitForNextSnapshot(); + + expect(snapshot).toEqual({ + data: { + letters: [ + { letter: "A", position: 1 }, + { letter: "B", position: 2 }, + ], + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await act(() => user.click(screen.getByText("Fetch more"))); + + expect(SuspenseFallback).toHaveRenderedTimes(2); - const moreItems = await screen.findAllByTestId(/letter/i); - expect(moreItems).toHaveLength(4); - expect(getItemTexts()).toStrictEqual(["A", "B", "C", "D"]); + // TODO: Figure out why there is an extra render here. + // Perhaps related? https://github.com/apollographql/apollo-client/issues/11315 + await ReadQueryHook.takeSnapshot(); + + { + const snapshot = await ReadQueryHook.takeSnapshot(); + + expect(snapshot).toEqual({ + data: { + letters: [ + { letter: "A", position: 1 }, + { letter: "B", position: 2 }, + { letter: "C", position: 3 }, + { letter: "D", position: 4 }, + ], + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } }); + it.skip("`fetchMore` works with startTransition to allow React to show stale UI until finished suspending", async () => { type Variables = { offset: number; From afc8af000ed48af872019e2c70f33f2806ac295b Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 9 Nov 2023 16:00:06 -0700 Subject: [PATCH 077/199] Update test that checks startTransition with fetchMore --- .../__tests__/useInteractiveQuery.test.tsx | 33 +++++++++---------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index ddc7d57f858..1501e6622fc 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -3114,7 +3114,7 @@ it("properly uses cache field policies when calling `fetchMore` without `updateQ } }); -it.skip("`fetchMore` works with startTransition to allow React to show stale UI until finished suspending", async () => { +it("`fetchMore` works with startTransition to allow React to show stale UI until finished suspending", async () => { type Variables = { offset: number; }; @@ -3128,7 +3128,6 @@ it.skip("`fetchMore` works with startTransition to allow React to show stale UI interface Data { todos: Todo[]; } - const user = userEvent.setup(); const query: TypedDocumentNode = gql` query TodosQuery($offset: Int!) { @@ -3140,7 +3139,7 @@ it.skip("`fetchMore` works with startTransition to allow React to show stale UI } `; - const mocks: MockedResponse[] = [ + const mocks: MockedResponse[] = [ { request: { query, variables: { offset: 0 } }, result: { @@ -3188,27 +3187,23 @@ it.skip("`fetchMore` works with startTransition to allow React to show stale UI }), }); + function SuspenseFallback() { + return

Loading

; + } + function App() { + const [queryRef, loadQuery, { fetchMore }] = useInteractiveQuery(query); + return ( - + <> + }> - + {queryRef && } - + ); } - function SuspenseFallback() { - return

Loading

; - } - - function Parent() { - const [queryRef, { fetchMore }] = useInteractiveQuery(query, { - variables: { offset: 0 }, - }); - return ; - } - function Todo({ queryRef, fetchMore, @@ -3243,7 +3238,9 @@ it.skip("`fetchMore` works with startTransition to allow React to show stale UI ); } - render(); + const { user } = renderWithClient(, { client }); + + await act(() => user.click(screen.getByText("Load query"))); expect(screen.getByText("Loading")).toBeInTheDocument(); From 0d7f766e9aa60753645d7ce7738d798a8381bed7 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 9 Nov 2023 16:06:59 -0700 Subject: [PATCH 078/199] Update test that checks refetchWritePolicy to use profile helpers --- .../__tests__/useInteractiveQuery.test.tsx | 108 +++++++----------- 1 file changed, 41 insertions(+), 67 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index 1501e6622fc..fa384a4c9b6 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -3279,9 +3279,7 @@ it("`fetchMore` works with startTransition to allow React to show stale UI until }); }); -it.skip('honors refetchWritePolicy set to "merge"', async () => { - const user = userEvent.setup(); - +it('honors refetchWritePolicy set to "merge"', async () => { const query: TypedDocumentNode< { primes: number[] }, { min: number; max: number } @@ -3324,89 +3322,65 @@ it.skip('honors refetchWritePolicy set to "merge"', async () => { }, }); - function SuspenseFallback() { - return
loading
; - } + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(); const client = new ApolloClient({ link: new MockLink(mocks), cache, }); - 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 }] = useInteractiveQuery(query, { - variables: { min: 0, max: 12 }, + function App() { + const [queryRef, loadQuery, { refetch }] = useInteractiveQuery(query, { refetchWritePolicy: "merge", }); - return ; - } - function App() { return ( - + <> + + }> - + {queryRef && } - + ); } - render(); + const { user } = renderWithClient(, { client }); - 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("Load query"))); + + { + const snapshot = await ReadQueryHook.takeSnapshot(); + + expect(snapshot).toEqual({ + data: { primes: [2, 3, 5, 7, 11] }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + 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], - ], - ]); + { + const snapshot = await ReadQueryHook.takeSnapshot(); + + expect(snapshot).toEqual({ + data: { primes: [2, 3, 5, 7, 11, 13, 17, 19, 23, 29] }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + expect(mergeParams).toEqual([ + [undefined, [2, 3, 5, 7, 11]], + [ + [2, 3, 5, 7, 11], + [13, 17, 19, 23, 29], + ], + ]); + } }); it.skip('defaults refetchWritePolicy to "overwrite"', async () => { From 9749538220b3a09dfd4ba96b4c26e173392db22e Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 9 Nov 2023 16:25:09 -0700 Subject: [PATCH 079/199] Update test that checks refetchWritePolicy to use profiler --- .../__tests__/useInteractiveQuery.test.tsx | 101 +++++++----------- 1 file changed, 37 insertions(+), 64 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index fa384a4c9b6..5acdede27df 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -3383,9 +3383,7 @@ it('honors refetchWritePolicy set to "merge"', async () => { } }); -it.skip('defaults refetchWritePolicy to "overwrite"', async () => { - const user = userEvent.setup(); - +it('defaults refetchWritePolicy to "overwrite"', async () => { const query: TypedDocumentNode< { primes: number[] }, { min: number; max: number } @@ -3428,85 +3426,60 @@ it.skip('defaults refetchWritePolicy to "overwrite"', async () => { }, }); - function SuspenseFallback() { - return
loading
; - } + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(); const client = new ApolloClient({ link: new MockLink(mocks), cache, }); - function Child({ - refetch, - queryRef, - }: { - refetch: ( - variables?: Partial | undefined - ) => Promise>; - queryRef: QueryReference; - }) { - const { data, error, networkStatus } = useReadQuery(queryRef); + function App() { + const [queryRef, loadQuery, { refetch }] = useInteractiveQuery(query); return ( -
- -
{data?.primes.join(", ")}
-
{networkStatus}
-
{error?.message || "undefined"}
-
- ); - } - - function Parent() { - const [queryRef, { refetch }] = useInteractiveQuery(query, { - variables: { min: 0, max: 12 }, - }); - return ; - } - - function App() { - return ( - + }> - + {queryRef && } - + ); } - render(); + const { user } = renderWithClient(, { client }); - 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("Load query"))); + + { + const snapshot = await ReadQueryHook.takeSnapshot(); + + expect(snapshot).toEqual({ + data: { primes: [2, 3, 5, 7, 11] }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + 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]], - ]); + { + const snapshot = await ReadQueryHook.takeSnapshot(); + + expect(snapshot).toEqual({ + data: { primes: [13, 17, 19, 23, 29] }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + expect(mergeParams).toEqual([ + [undefined, [2, 3, 5, 7, 11]], + [undefined, [13, 17, 19, 23, 29]], + ]); + } }); it.skip('does not suspend when partial data is in the cache and using a "cache-first" fetch policy with returnPartialData', async () => { From 0430cb6eeae532755508dbea0077590c9302e4c4 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 9 Nov 2023 16:25:26 -0700 Subject: [PATCH 080/199] Update test that checks partialData to use profiler --- .../__tests__/useInteractiveQuery.test.tsx | 93 +++++++------------ 1 file changed, 34 insertions(+), 59 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index 5acdede27df..60b19b5f4da 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -3482,7 +3482,7 @@ it('defaults refetchWritePolicy to "overwrite"', async () => { } }); -it.skip('does not suspend when partial data is in the cache and using a "cache-first" fetch policy with returnPartialData', async () => { +it('does not suspend when partial data is in the cache and using a "cache-first" fetch policy with returnPartialData', async () => { interface Data { character: { id: string; @@ -3510,21 +3510,12 @@ it.skip('does not suspend when partial data is in the cache and using a "cache-f { request: { query: fullQuery }, result: { data: { character: { id: "1", name: "Doctor Strange" } } }, + delay: 20, }, ]; - interface Renders { - errors: Error[]; - errorCount: number; - suspenseCount: number; - count: number; - } - const renders: Renders = { - errors: [], - errorCount: 0, - suspenseCount: 0, - count: 0, - }; + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents>(); const cache = new InMemoryCache(); @@ -3533,67 +3524,51 @@ it.skip('does not suspend when partial data is in the cache and using a "cache-f data: { character: { id: "1" } }, }); - const client = new ApolloClient({ - link: new MockLink(mocks), - cache, - }); + const client = new ApolloClient({ link: new MockLink(mocks), cache }); function App() { - return ( - - }> - - - - ); - } - - function SuspenseFallback() { - renders.suspenseCount++; - return

Loading

; - } - - function Parent() { - const [queryRef] = useInteractiveQuery(fullQuery, { + const [queryRef, loadQuery] = useInteractiveQuery(fullQuery, { fetchPolicy: "cache-first", returnPartialData: true, }); - return ; - } - - function Todo({ queryRef }: { queryRef: QueryReference> }) { - const { data, networkStatus, error } = useReadQuery(queryRef); - renders.count++; return ( <> -
{data.character?.id}
-
{data.character?.name}
-
{networkStatus}
-
{error?.message || "undefined"}
+ + }> + {queryRef && } + ); } - render(); + const { user } = renderWithClient(, { client }); - 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"); + await act(() => user.click(screen.getByText("Load query"))); - 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(SuspenseFallback).not.toHaveRendered(); - expect(renders.count).toBe(2); - expect(renders.suspenseCount).toBe(0); + { + const snapshot = await ReadQueryHook.takeSnapshot(); + + expect(snapshot).toEqual({ + data: { character: { id: "1" } }, + error: undefined, + networkStatus: NetworkStatus.loading, + }); + } + + { + const snapshot = await ReadQueryHook.takeSnapshot(); + + expect(snapshot).toEqual({ + data: { character: { id: "1", name: "Doctor Strange" } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + expect(SuspenseFallback).not.toHaveRendered(); }); it.skip('suspends and does not use partial data when changing variables and using a "cache-first" fetch policy with returnPartialData', async () => { From 479c348e2651baa63282a6029c7ccd6c0cb6bc06 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 9 Nov 2023 16:32:00 -0700 Subject: [PATCH 081/199] Update test that checks changing variables and partial data --- .../__tests__/useInteractiveQuery.test.tsx | 78 ++++++++++++------- 1 file changed, 49 insertions(+), 29 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index 60b19b5f4da..e583cab5830 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -3571,7 +3571,9 @@ it('does not suspend when partial data is in the cache and using a "cache-first" expect(SuspenseFallback).not.toHaveRendered(); }); -it.skip('suspends and does not use partial data when changing variables and using a "cache-first" fetch policy with returnPartialData', async () => { +it('suspends and does not use partial data when changing variables and using a "cache-first" fetch policy with returnPartialData', async () => { + const { query, mocks } = useVariablesQueryCase(); + const partialQuery = gql` query ($id: ID!) { character(id: $id) { @@ -3588,47 +3590,65 @@ it.skip('suspends and does not use partial data when changing variables and usin variables: { id: "1" }, }); - const { renders, mocks, rerender } = renderVariablesIntegrationTest({ - variables: { id: "1" }, - cache, - options: { + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents>(); + + function App() { + const [queryRef, loadQuery] = useInteractiveQuery(query, { fetchPolicy: "cache-first", returnPartialData: true, - }, - }); - expect(renders.suspenseCount).toBe(0); + }); - expect(await screen.findByText("1 - Spider-Man")).toBeInTheDocument(); + return ( + <> + + + }> + {queryRef && } + + + ); + } - rerender({ variables: { id: "2" } }); + const { user } = renderWithMocks(, { mocks, cache }); - expect(await screen.findByText("2 - Black Widow")).toBeInTheDocument(); + await act(() => user.click(screen.getByText("Load query"))); - expect(renders.frames[2]).toMatchObject({ - ...mocks[1].result, - networkStatus: NetworkStatus.ready, - error: undefined, - }); + expect(SuspenseFallback).not.toHaveRendered(); - expect(renders.count).toBe(3); - expect(renders.suspenseCount).toBe(1); - expect(renders.frames).toMatchObject([ - { + { + const snapshot = await ReadQueryHook.takeSnapshot(); + + expect(snapshot).toEqual({ data: { character: { id: "1" } }, + error: undefined, networkStatus: NetworkStatus.loading, + }); + } + + { + const snapshot = await ReadQueryHook.takeSnapshot(); + + expect(snapshot).toEqual({ + data: { character: { id: "1", name: "Spider-Man" } }, error: undefined, - }, - { - ...mocks[0].result, networkStatus: NetworkStatus.ready, + }); + } + + await act(() => user.click(screen.getByText("Change variables"))); + + expect(SuspenseFallback).toHaveRendered(); + + { + const snapshot = await ReadQueryHook.takeSnapshot(); + + expect(snapshot).toEqual({ + data: { character: { id: "2", name: "Black Widow" } }, error: undefined, - }, - { - ...mocks[1].result, networkStatus: NetworkStatus.ready, - error: undefined, - }, - ]); + }); + } }); it.skip('suspends when partial data is in the cache and using a "network-only" fetch policy with returnPartialData', async () => { From a168502f247e47624dfc42888c118c856a8fed6c Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 9 Nov 2023 16:37:20 -0700 Subject: [PATCH 082/199] Update test that checks network-only with partial data --- .../__tests__/useInteractiveQuery.test.tsx | 87 ++++--------------- 1 file changed, 17 insertions(+), 70 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index e583cab5830..aeb21469d42 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -3651,7 +3651,7 @@ it('suspends and does not use partial data when changing variables and using a " } }); -it.skip('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 "network-only" fetch policy with returnPartialData', async () => { interface Data { character: { id: string; @@ -3682,24 +3682,8 @@ it.skip('suspends when partial data is in the cache and using a "network-only" f }, ]; - 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 { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents>(); const cache = new InMemoryCache(); @@ -3708,72 +3692,35 @@ it.skip('suspends when partial data is in the cache and using a "network-only" f data: { character: { id: "1" } }, }); - const client = new ApolloClient({ - link: new MockLink(mocks), - cache, - }); - function App() { - return ( - - }> - - - - ); - } - - function SuspenseFallback() { - renders.suspenseCount++; - return

Loading

; - } - - function Parent() { - const [queryRef] = useInteractiveQuery(fullQuery, { + const [queryRef, loadQuery] = useInteractiveQuery(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"}
+ + }> + {queryRef && } + ); } - render(); + const { user } = renderWithMocks(, { mocks, cache }); - expect(renders.suspenseCount).toBe(1); + await act(() => user.click(screen.getByText("Load query"))); - 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(SuspenseFallback).toHaveRendered(); - expect(renders.count).toBe(1); - expect(renders.suspenseCount).toBe(1); + const snapshot = await ReadQueryHook.takeSnapshot(); - expect(renders.frames).toMatchObject([ - { - ...mocks[0].result, - networkStatus: NetworkStatus.ready, - error: undefined, - }, - ]); + expect(snapshot).toEqual({ + data: { character: { id: "1", name: "Doctor Strange" } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); }); it.skip('suspends when partial data is in the cache and using a "no-cache" fetch policy with returnPartialData', async () => { From 7b80be09615e8a5c0ee416d56fc0e1442fe13110 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 9 Nov 2023 16:38:50 -0700 Subject: [PATCH 083/199] Remove unused renderPaginatedTest helper --- .../__tests__/useInteractiveQuery.test.tsx | 167 ------------------ 1 file changed, 167 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index aeb21469d42..1d21406ce3e 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -390,173 +390,6 @@ function renderVariablesIntegrationTest({ }; } -function renderPaginatedIntegrationTest({ - updateQuery, - fieldPolicies, -}: { - fieldPolicies?: boolean; - updateQuery?: boolean; - mocks?: { - request: { - query: DocumentNode; - variables: { offset: number; limit: number }; - }; - result: { - data: { - letters: { - letter: string; - position: number; - }[]; - }; - }; - }[]; -} = {}) { - interface QueryData { - letters: { - letter: string; - position: number; - }[]; - } - - interface Variables { - limit?: number; - offset?: number; - } - - const query: TypedDocumentNode = gql` - query letters($limit: Int, $offset: Int) { - letters(limit: $limit) { - letter - position - } - } - `; - - const data = "ABCDEFG" - .split("") - .map((letter, index) => ({ letter, position: index + 1 })); - - const link = new ApolloLink((operation) => { - const { offset = 0, limit = 2 } = operation.variables; - const letters = data.slice(offset, offset + limit); - - return new Observable((observer) => { - setTimeout(() => { - observer.next({ data: { letters } }); - observer.complete(); - }, 10); - }); - }); - - const cacheWithTypePolicies = new InMemoryCache({ - typePolicies: { - Query: { - fields: { - letters: concatPagination(), - }, - }, - }, - }); - const client = new ApolloClient({ - cache: fieldPolicies ? cacheWithTypePolicies : new InMemoryCache(), - link, - }); - interface Renders { - errors: Error[]; - errorCount: number; - suspenseCount: number; - count: number; - } - const renders: Renders = { - errors: [], - errorCount: 0, - suspenseCount: 0, - count: 0, - }; - - const errorBoundaryProps: ErrorBoundaryProps = { - fallback:
Error
, - onError: (error) => { - renders.errorCount++; - renders.errors.push(error); - }, - }; - - function SuspenseFallback() { - renders.suspenseCount++; - return
loading
; - } - - function Child({ - queryRef, - fetchMore, - }: { - fetchMore: FetchMoreFunction; - queryRef: QueryReference; - }) { - const { data, error } = useReadQuery(queryRef); - // count renders in the child component - renders.count++; - return ( -
- {error ?
{error.message}
: null} - -
    - {data.letters.map(({ letter, position }) => ( -
  • - {letter} -
  • - ))} -
-
- ); - } - - function ParentWithVariables() { - const [queryRef, { fetchMore }] = useInteractiveQuery(query, { - variables: { limit: 2, offset: 0 }, - }); - return ; - } - - function App() { - return ( - - - }> - - - - - ); - } - - const { ...rest } = render(); - return { ...rest, data, query, client, renders }; -} - type RenderSuspenseHookOptions = Omit< RenderHookOptions, "wrapper" From 0d55f891d156eda8a8e2be0978d69c1de5ab0932 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 9 Nov 2023 17:06:04 -0700 Subject: [PATCH 084/199] Update test that checks returnPartialData and no-cache fetch policy --- .../__tests__/useInteractiveQuery.test.tsx | 92 ++++--------------- 1 file changed, 19 insertions(+), 73 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index 1d21406ce3e..663e9148dd0 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -3556,8 +3556,9 @@ it('suspends when partial data is in the cache and using a "network-only" fetch }); }); -it.skip('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(); +it('suspends when partial data is in the cache and using a "no-cache" fetch policy with returnPartialData', async () => { + using _consoleSpy = spyOnConsole("warn"); + interface Data { character: { id: string; @@ -3588,25 +3589,6 @@ it.skip('suspends when partial data is in the cache and using a "no-cache" fetch }, ]; - 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({ @@ -3614,74 +3596,38 @@ it.skip('suspends when partial data is in the cache and using a "no-cache" fetch data: { character: { id: "1" } }, }); - const client = new ApolloClient({ - link: new MockLink(mocks), - cache, - }); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents>(); function App() { - return ( - - }> - - - - ); - } - - function SuspenseFallback() { - renders.suspenseCount++; - return

Loading

; - } - - function Parent() { - const [queryRef] = useInteractiveQuery(fullQuery, { + const [queryRef, loadQuery] = useInteractiveQuery(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"}
+ + }> + {queryRef && } + ); } - render(); - - expect(renders.suspenseCount).toBe(1); + const { user } = renderWithMocks(, { mocks, cache }); - 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"); + await act(() => user.click(screen.getByText("Load query"))); - expect(renders.count).toBe(1); - expect(renders.suspenseCount).toBe(1); + expect(SuspenseFallback).toHaveRendered(); - expect(renders.frames).toMatchObject([ - { - ...mocks[0].result, - networkStatus: NetworkStatus.ready, - error: undefined, - }, - ]); + const snapshot = await ReadQueryHook.takeSnapshot(); - consoleSpy.mockRestore(); + expect(snapshot).toEqual({ + data: { character: { id: "1", name: "Doctor Strange" } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); }); it.skip('warns when using returnPartialData with a "no-cache" fetch policy', async () => { From 651e206cca1c8dbe246a288bd39b16dd2ac28e68 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 9 Nov 2023 17:09:16 -0700 Subject: [PATCH 085/199] Update test that checks warnings when using no-cache with returnPartialData --- .../__tests__/useInteractiveQuery.test.tsx | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index 663e9148dd0..3cb38c812f0 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -3630,36 +3630,32 @@ it('suspends when partial data is in the cache and using a "no-cache" fetch poli }); }); -it.skip('warns when using returnPartialData with a "no-cache" fetch policy', async () => { - const consoleSpy = jest.spyOn(console, "warn").mockImplementation(); +it('warns when using returnPartialData with a "no-cache" fetch policy', async () => { + using consoleSpy = spyOnConsole("warn"); const query: TypedDocumentNode = gql` query UserQuery { greeting } `; - const mocks = [ - { - request: { query }, - result: { data: { greeting: "Hello" } }, - }, - ]; - renderSuspenseHook( + renderHook( () => useInteractiveQuery(query, { fetchPolicy: "no-cache", returnPartialData: true, }), - { mocks } + { + wrapper: ({ children }) => ( + {children} + ), + } ); 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.skip('does not suspend when partial data is in the cache and using a "cache-and-network" fetch policy with returnPartialData', async () => { From 8e5ffa61c119159dc7f35da61f2b2a32e38ce13d Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 9 Nov 2023 17:14:43 -0700 Subject: [PATCH 086/199] Update test that checks partial data with cache-and-network --- .../__tests__/useInteractiveQuery.test.tsx | 102 +++++------------- 1 file changed, 26 insertions(+), 76 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index 3cb38c812f0..34df54bdf4f 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -3658,7 +3658,7 @@ it('warns when using returnPartialData with a "no-cache" fetch policy', async () ); }); -it.skip('does not suspend when partial data is in the cache and using a "cache-and-network" fetch policy with returnPartialData', async () => { +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; @@ -3686,28 +3686,10 @@ it.skip('does not suspend when partial data is in the cache and using a "cache-a { request: { query: fullQuery }, result: { data: { character: { id: "1", name: "Doctor Strange" } } }, + delay: 20, }, ]; - 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({ @@ -3715,82 +3697,50 @@ it.skip('does not suspend when partial data is in the cache and using a "cache-a data: { character: { id: "1" } }, }); - const client = new ApolloClient({ - link: new MockLink(mocks), - cache, - }); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents>(); function App() { - return ( - - }> - - - - ); - } - - function SuspenseFallback() { - renders.suspenseCount++; - return

Loading

; - } - - function Parent() { - const [queryRef] = useInteractiveQuery(fullQuery, { + const [queryRef, loadQuery] = useInteractiveQuery(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"}
+ + }> + {queryRef && } + ); } - render(); + const { user } = renderWithMocks(, { mocks, cache }); - 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 act(() => user.click(screen.getByText("Load query"))); - 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(SuspenseFallback).not.toHaveRendered(); - expect(renders.count).toBe(2); - expect(renders.suspenseCount).toBe(0); + { + const snapshot = await ReadQueryHook.takeSnapshot(); - expect(renders.frames).toMatchObject([ - { + expect(snapshot).toEqual({ data: { character: { id: "1" } }, + error: undefined, networkStatus: NetworkStatus.loading, + }); + } + + { + const snapshot = await ReadQueryHook.takeSnapshot(); + + expect(snapshot).toEqual({ + data: { character: { id: "1", name: "Doctor Strange" } }, error: undefined, - }, - { - ...mocks[0].result, networkStatus: NetworkStatus.ready, - error: undefined, - }, - ]); + }); + } }); it.skip('suspends and does not use partial data when changing variables and using a "cache-and-network" fetch policy with returnPartialData', async () => { From 852e02a44db5e5acf07b88fb35d0e4ed842a6de1 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 9 Nov 2023 17:17:52 -0700 Subject: [PATCH 087/199] Update test that chekcs changing variables with partial data and cache-and-network --- .../__tests__/useInteractiveQuery.test.tsx | 73 +++++++++++++------ 1 file changed, 49 insertions(+), 24 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index 34df54bdf4f..5ebbf7cb99c 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -3743,7 +3743,9 @@ it('does not suspend when partial data is in the cache and using a "cache-and-ne } }); -it.skip('suspends and does not use partial data when changing variables 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 () => { + const { query, mocks } = useVariablesQueryCase(); + const partialQuery = gql` query ($id: ID!) { character(id: $id) { @@ -3760,42 +3762,65 @@ it.skip('suspends and does not use partial data when changing variables and usin variables: { id: "1" }, }); - const { renders, mocks, rerender } = renderVariablesIntegrationTest({ - variables: { id: "1" }, - cache, - options: { + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents>(); + + function App() { + const [queryRef, loadQuery] = useInteractiveQuery(query, { fetchPolicy: "cache-and-network", returnPartialData: true, - }, - }); + }); - expect(renders.suspenseCount).toBe(0); + return ( + <> + + + }> + {queryRef && } + + + ); + } + + const { user } = renderWithMocks(, { mocks, cache }); - expect(await screen.findByText("1 - Spider-Man")).toBeInTheDocument(); + await act(() => user.click(screen.getByText("Load query"))); - rerender({ variables: { id: "2" } }); + expect(SuspenseFallback).not.toHaveRendered(); - expect(await screen.findByText("2 - Black Widow")).toBeInTheDocument(); + { + const snapshot = await ReadQueryHook.takeSnapshot(); - expect(renders.count).toBe(3); - expect(renders.suspenseCount).toBe(1); - expect(renders.frames).toMatchObject([ - { + expect(snapshot).toEqual({ data: { character: { id: "1" } }, + error: undefined, networkStatus: NetworkStatus.loading, + }); + } + + { + const snapshot = await ReadQueryHook.takeSnapshot(); + + expect(snapshot).toEqual({ + data: { character: { id: "1", name: "Spider-Man" } }, error: undefined, - }, - { - ...mocks[0].result, networkStatus: NetworkStatus.ready, + }); + } + + await act(() => user.click(screen.getByText("Change variables"))); + + expect(SuspenseFallback).toHaveRendered(); + + { + const snapshot = await ReadQueryHook.takeSnapshot(); + + expect(snapshot).toEqual({ + data: { character: { id: "2", name: "Black Widow" } }, error: undefined, - }, - { - ...mocks[1].result, networkStatus: NetworkStatus.ready, - error: undefined, - }, - ]); + }); + } }); it.skip('does not suspend deferred queries with partial data in the cache and using a "cache-first" fetch policy with `returnPartialData`', async () => { From a683fe9281b1946135c6d66b742aabd1746d81b0 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 9 Nov 2023 17:23:55 -0700 Subject: [PATCH 088/199] Update test that checks deferred query with returnPartialData --- .../__tests__/useInteractiveQuery.test.tsx | 177 ++++++------------ 1 file changed, 59 insertions(+), 118 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index 5ebbf7cb99c..5832ba34878 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -3823,7 +3823,7 @@ it('suspends and does not use partial data when changing variables and using a " } }); -it.skip('does not suspend deferred queries with partial data in the cache and using a "cache-first" 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 () => { interface QueryData { greeting: { __typename: string; @@ -3835,8 +3835,6 @@ it.skip('does not suspend deferred queries with partial data in the cache and us }; } - const user = userEvent.setup(); - const query: TypedDocumentNode = gql` query { greeting { @@ -3853,60 +3851,28 @@ it.skip('does not suspend deferred queries with partial data in the cache and us 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(); + { + // We are intentionally writing partial data to the cache. Supress console + // warnings to avoid unnecessary noise in the test. + using _consoleSpy = spyOnConsole("error"); - interface Renders { - errors: Error[]; - errorCount: number; - suspenseCount: number; - count: number; - frames: { - data: DeepPartial; - networkStatus: NetworkStatus; - error: ApolloError | undefined; - }[]; + cache.writeQuery({ + query, + data: { + greeting: { + __typename: "Greeting", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + }); } - const renders: Renders = { - errors: [], - errorCount: 0, - suspenseCount: 0, - count: 0, - frames: [], - }; - const client = new ApolloClient({ - link, - cache, - }); - - function App() { - return ( - - }> - - - - ); - } + const client = new ApolloClient({ link, cache }); - function SuspenseFallback() { - renders.suspenseCount++; - return

Loading

; - } + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents>(); - function Parent() { + function App() { const [queryRef, loadTodo] = useInteractiveQuery(query, { fetchPolicy: "cache-first", returnPartialData: true, @@ -3915,39 +3881,33 @@ it.skip('does not suspend deferred queries with partial data in the cache and us return (
- {queryRef && } + }> + {queryRef && } +
); } - 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(); + const { user } = renderWithClient(, { client }); await act(() => user.click(screen.getByText("Load todo"))); - 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"); + expect(SuspenseFallback).not.toHaveRendered(); + + { + const snapshot = await ReadQueryHook.takeSnapshot(); + + expect(snapshot).toEqual({ + data: { + greeting: { + __typename: "Greeting", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + error: undefined, + networkStatus: NetworkStatus.loading, + }); + } link.simulateResult({ result: { @@ -3958,12 +3918,21 @@ it.skip('does not suspend deferred queries with partial data in the cache and us }, }); - 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"); + { + const snapshot = await ReadQueryHook.takeSnapshot(); + + expect(snapshot).toEqual({ + data: { + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } link.simulateResult({ result: { @@ -3980,38 +3949,10 @@ it.skip('does not suspend deferred queries with partial data in the cache and us }, }); - 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"); + { + const snapshot = await ReadQueryHook.takeSnapshot(); - 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, - }, - { + expect(snapshot).toEqual({ data: { greeting: { __typename: "Greeting", @@ -4019,10 +3960,10 @@ it.skip('does not suspend deferred queries with partial data in the cache and us recipient: { __typename: "Person", name: "Alice" }, }, }, - networkStatus: NetworkStatus.ready, error: undefined, - }, - ]); + networkStatus: NetworkStatus.ready, + }); + } }); describe.skip("type tests", () => { From 500de49e34267df9d11bc897b386a70730169319 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 9 Nov 2023 17:24:48 -0700 Subject: [PATCH 089/199] Remove unused helpers in test --- .../__tests__/useInteractiveQuery.test.tsx | 249 ------------------ 1 file changed, 249 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index 5832ba34878..5bc18752680 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -228,255 +228,6 @@ function renderWithClient( return { ...utils, user }; } -function renderVariablesIntegrationTest({ - variables, - mocks, - errorPolicy, - options, - cache, -}: { - mocks?: { - request: { query: DocumentNode; variables: { id: string } }; - result: { - data?: { - character: { - id: string; - name: string | null; - }; - }; - }; - }[]; - variables: { id: string }; - options?: InteractiveQueryHookOptions; - cache?: InMemoryCache; - errorPolicy?: ErrorPolicy; -}) { - const user = userEvent.setup(); - let { mocks: _mocks, query } = useVariablesQueryCase(); - - // duplicate mocks with (updated) in the name for refetches - _mocks = [..._mocks, ..._mocks, ..._mocks].map((mock, index) => { - return { - ...mock, - request: mock.request, - result: { - data: { - character: { - ...mock.result.data.character, - name: - index > 3 - ? index > 7 - ? `${mock.result.data.character.name} (updated again)` - : `${mock.result.data.character.name} (updated)` - : mock.result.data.character.name, - }, - }, - }, - }; - }); - const client = new ApolloClient({ - cache: cache || new InMemoryCache(), - link: new MockLink(mocks || _mocks), - }); - interface Renders { - errors: Error[]; - errorCount: number; - suspenseCount: number; - count: number; - frames: { - data: VariablesCaseData; - networkStatus: NetworkStatus; - error: ApolloError | undefined; - }[]; - } - const renders: Renders = { - errors: [], - errorCount: 0, - suspenseCount: 0, - count: 0, - frames: [], - }; - - const errorBoundaryProps: ErrorBoundaryProps = { - fallback:
Error
, - onError: (error) => { - renders.errorCount++; - renders.errors.push(error); - }, - }; - - function SuspenseFallback() { - renders.suspenseCount++; - return
loading
; - } - - function Child({ - onChange, - queryRef, - }: { - onChange: (variables: VariablesCaseVariables) => void; - queryRef: QueryReference; - }) { - const { data, error, networkStatus } = useReadQuery(queryRef); - // count renders in the child component - renders.count++; - renders.frames.push({ data, networkStatus, error }); - - return ( -
- {error ?
{error.message}
: null} - - {data?.character.id} - {data?.character.name} -
- ); - } - - function ParentWithVariables({ - variables, - errorPolicy = "none", - }: { - variables: VariablesCaseVariables; - errorPolicy?: ErrorPolicy; - }) { - const [queryRef, loadQuery] = useInteractiveQuery(query, { - ...options, - variables, - errorPolicy, - }); - return ( -
- - }> - {queryRef && } - -
- ); - } - - function App({ - variables, - errorPolicy, - }: { - variables: VariablesCaseVariables; - errorPolicy?: ErrorPolicy; - }) { - return ( - - - - - - ); - } - - const { ...rest } = render( - - ); - const rerender = ({ variables }: { variables: VariablesCaseVariables }) => { - return rest.rerender(); - }; - return { - ...rest, - query, - rerender, - client, - renders, - mocks: mocks || _mocks, - user, - loadQueryButton: screen.getByText("Load query"), - }; -} - -type RenderSuspenseHookOptions = Omit< - RenderHookOptions, - "wrapper" -> & { - client?: ApolloClient; - link?: ApolloLink; - cache?: ApolloCache; - mocks?: MockedResponse[]; - strictMode?: boolean; -}; - -interface Renders { - errors: Error[]; - errorCount: number; - suspenseCount: number; - count: number; - frames: Result[]; -} - -interface SimpleQueryData { - greeting: string; -} - -function renderSuspenseHook( - render: (initialProps: Props) => Result, - options: RenderSuspenseHookOptions = Object.create(null) -) { - function SuspenseFallback() { - renders.suspenseCount++; - - return
loading
; - } - - const renders: Renders = { - errors: [], - errorCount: 0, - suspenseCount: 0, - count: 0, - frames: [], - }; - - const { mocks = [], strictMode, ...renderHookOptions } = options; - - const client = - options.client || - new ApolloClient({ - cache: options.cache || new InMemoryCache(), - link: options.link || new MockLink(mocks), - }); - - const view = renderHook( - (props) => { - renders.count++; - - const view = render(props); - - renders.frames.push(view); - - return view; - }, - { - ...renderHookOptions, - wrapper: ({ children }) => { - const Wrapper = strictMode ? StrictMode : Fragment; - - return ( - - }> - Error} - onError={(error) => { - renders.errorCount++; - renders.errors.push(error); - }} - > - {children} - - - - ); - }, - } - ); - - return { ...view, renders }; -} - it("loads a query and suspends when the load query function is called", async () => { const { query, mocks } = useSimpleQueryCase(); From 7c28e0b960bb97ea9d71ccb34c3c103f0baa749f Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Fri, 10 Nov 2023 08:56:48 -0700 Subject: [PATCH 090/199] Remove unused variables and imports --- .../__tests__/useInteractiveQuery.test.tsx | 35 +++++-------------- 1 file changed, 8 insertions(+), 27 deletions(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index 5bc18752680..85e442fd043 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -1,36 +1,25 @@ -import React, { Fragment, StrictMode, Suspense, useState } from "react"; +import React, { Suspense, useState } from "react"; import { act, render, screen, renderHook, - RenderHookOptions, waitFor, } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; -import type { Options as UserEventOptions } from "@testing-library/user-event"; -import { - ErrorBoundary, - ErrorBoundary as ReactErrorBoundary, - ErrorBoundaryProps, -} from "react-error-boundary"; +import { ErrorBoundary as ReactErrorBoundary } from "react-error-boundary"; import { expectTypeOf } from "expect-type"; import { GraphQLError } from "graphql"; import { gql, ApolloError, - DocumentNode, ApolloClient, ErrorPolicy, - NormalizedCacheObject, NetworkStatus, - ApolloCache, TypedDocumentNode, ApolloLink, Observable, - FetchMoreQueryOptions, OperationVariables, - ApolloQueryResult, } from "../../../core"; import { MockedProvider, @@ -50,10 +39,7 @@ import { useReadQuery } from "../useReadQuery"; import { ApolloProvider } from "../../context"; import { InMemoryCache } from "../../../cache"; import { QueryReference } from "../../../react"; -import { - InteractiveQueryHookOptions, - InteractiveQueryHookFetchPolicy, -} from "../../types/types"; +import { InteractiveQueryHookFetchPolicy } from "../../types/types"; import { FetchMoreFunction, RefetchFunction } from "../useSuspenseQuery"; import { RefetchWritePolicy } from "../../../core/watchQueryOptions"; import invariant from "ts-invariant"; @@ -2235,7 +2221,7 @@ it('returns errors after calling `refetch` when errorPolicy is set to "all"', as }, ]; - const { SuspenseFallback, ReadQueryHook, ErrorBoundary, ErrorFallback } = + const { SuspenseFallback, ReadQueryHook, ErrorBoundary } = createDefaultProfiledComponents(); function App() { @@ -2297,7 +2283,7 @@ it('handles partial data results after calling `refetch` when errorPolicy is set }, ]; - const { SuspenseFallback, ReadQueryHook, ErrorBoundary, ErrorFallback } = + const { SuspenseFallback, ReadQueryHook, ErrorBoundary } = createDefaultProfiledComponents(); function App() { @@ -2378,11 +2364,6 @@ it("`refetch` works with startTransition to allow React to show stale UI until f }, ]; - const client = new ApolloClient({ - link: new MockLink(mocks), - cache: new InMemoryCache(), - }); - function SuspenseFallback() { return

Loading

; } @@ -3382,7 +3363,7 @@ it('suspends when partial data is in the cache and using a "no-cache" fetch poli }); it('warns when using returnPartialData with a "no-cache" fetch policy', async () => { - using consoleSpy = spyOnConsole("warn"); + using _consoleSpy = spyOnConsole("warn"); const query: TypedDocumentNode = gql` query UserQuery { @@ -3514,7 +3495,7 @@ it('suspends and does not use partial data when changing variables and using a " }); const { SuspenseFallback, ReadQueryHook } = - createDefaultProfiledComponents>(); + createDefaultProfiledComponents>(); function App() { const [queryRef, loadQuery] = useInteractiveQuery(query, { @@ -3721,7 +3702,7 @@ describe.skip("type tests", () => { it("returns unknown when TData cannot be inferred", () => { const query = gql``; - const [queryRef, loadQuery] = useInteractiveQuery(query); + const [queryRef] = useInteractiveQuery(query); invariant(queryRef); From 7c060f9bcf26519cca4f04f0459315f48c9e2ada Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Fri, 10 Nov 2023 09:50:48 -0700 Subject: [PATCH 091/199] Use rehackt for useInteractiveQuery --- src/react/hooks/useInteractiveQuery.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/react/hooks/useInteractiveQuery.ts b/src/react/hooks/useInteractiveQuery.ts index 32ee0235a88..74f07e03018 100644 --- a/src/react/hooks/useInteractiveQuery.ts +++ b/src/react/hooks/useInteractiveQuery.ts @@ -1,4 +1,4 @@ -import * as React from "react"; +import * as React from "rehackt"; import type { DocumentNode, OperationVariables, From 7b860f77a6d2f23c5af325e44fafb2aafffe706a Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Fri, 10 Nov 2023 09:56:14 -0700 Subject: [PATCH 092/199] Import RefetchWritePolicy from core --- src/react/hooks/__tests__/useInteractiveQuery.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx index 85e442fd043..a97f916681a 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useInteractiveQuery.test.tsx @@ -20,6 +20,7 @@ import { ApolloLink, Observable, OperationVariables, + RefetchWritePolicy, } from "../../../core"; import { MockedProvider, @@ -41,7 +42,6 @@ import { InMemoryCache } from "../../../cache"; import { QueryReference } from "../../../react"; import { InteractiveQueryHookFetchPolicy } from "../../types/types"; import { FetchMoreFunction, RefetchFunction } from "../useSuspenseQuery"; -import { RefetchWritePolicy } from "../../../core/watchQueryOptions"; import invariant from "ts-invariant"; import { profile, profileHook, spyOnConsole } from "../../../testing/internal"; From 85c18dd173d6d120deb3908fa6b2e24277ab1908 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Fri, 10 Nov 2023 09:57:38 -0700 Subject: [PATCH 093/199] Update api report --- .api-reports/api-report-react.md | 31 +++++++++++++++++++++++++++---- .api-reports/api-report.md | 23 +++++++++++++++++++++++ 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/.api-reports/api-report-react.md b/.api-reports/api-report-react.md index 5d4ebef40d6..ffbd1a02975 100644 --- a/.api-reports/api-report-react.md +++ b/.api-reports/api-report-react.md @@ -901,6 +901,33 @@ interface IncrementalPayload { path: Path; } +// @public (undocumented) +export type InteractiveQueryHookFetchPolicy = Extract; + +// @public (undocumented) +export interface InteractiveQueryHookOptions { + // (undocumented) + canonizeResults?: boolean; + // (undocumented) + client?: ApolloClient; + // (undocumented) + context?: Context; + // Warning: (ae-forgotten-export) The symbol "ErrorPolicy" needs to be exported by the entry point index.d.ts + // + // (undocumented) + errorPolicy?: ErrorPolicy; + // (undocumented) + fetchPolicy?: InteractiveQueryHookFetchPolicy; + // (undocumented) + queryKey?: string | number | any[]; + // Warning: (ae-forgotten-export) The symbol "RefetchWritePolicy" needs to be exported by the entry point index.d.ts + // + // (undocumented) + refetchWritePolicy?: RefetchWritePolicy; + // (undocumented) + returnPartialData?: boolean; +} + // @public (undocumented) class InternalQueryReference { // Warning: (ae-forgotten-export) The symbol "InternalQueryReferenceOptions" needs to be exported by the entry point index.d.ts @@ -1119,8 +1146,6 @@ interface MutationBaseOptions, currentFetchPolicy: WatchQueryFetchPolicy, context: NextFetchPolicyContext) => WatchQueryFetchPolicy); - // Warning: (ae-forgotten-export) The symbol "RefetchWritePolicy" needs to be exported by the entry point index.d.ts - // // (undocumented) refetchWritePolicy?: RefetchWritePolicy; } diff --git a/.api-reports/api-report.md b/.api-reports/api-report.md index 20b14b5c8a5..09b33acfde0 100644 --- a/.api-reports/api-report.md +++ b/.api-reports/api-report.md @@ -1261,6 +1261,29 @@ export interface InMemoryCacheConfig extends ApolloReducerConfig { typePolicies?: TypePolicies; } +// @public (undocumented) +export type InteractiveQueryHookFetchPolicy = Extract; + +// @public (undocumented) +export interface InteractiveQueryHookOptions { + // (undocumented) + canonizeResults?: boolean; + // (undocumented) + client?: ApolloClient; + // (undocumented) + context?: DefaultContext; + // (undocumented) + errorPolicy?: ErrorPolicy; + // (undocumented) + fetchPolicy?: InteractiveQueryHookFetchPolicy; + // (undocumented) + queryKey?: string | number | any[]; + // (undocumented) + refetchWritePolicy?: RefetchWritePolicy; + // (undocumented) + returnPartialData?: boolean; +} + // @public (undocumented) class InternalQueryReference { // Warning: (ae-forgotten-export) The symbol "InternalQueryReferenceOptions" needs to be exported by the entry point index.d.ts From d4e9009a13332bac960f5d976ba18ea4e815db3a Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Fri, 10 Nov 2023 12:42:56 -0700 Subject: [PATCH 094/199] Rename useInteractiveQuery to useLoadableQuery --- config/jest.config.js | 2 +- ...ery.test.tsx => useLoadableQuery.test.tsx} | 148 +++++++++--------- ...nteractiveQuery.ts => useLoadableQuery.ts} | 42 ++--- src/react/types/types.ts | 6 +- 4 files changed, 99 insertions(+), 99 deletions(-) rename src/react/hooks/__tests__/{useInteractiveQuery.test.tsx => useLoadableQuery.test.tsx} (95%) rename src/react/hooks/{useInteractiveQuery.ts => useLoadableQuery.ts} (84%) diff --git a/config/jest.config.js b/config/jest.config.js index 529bafcde1b..a45df96fc48 100644 --- a/config/jest.config.js +++ b/config/jest.config.js @@ -35,7 +35,7 @@ const react17TestFileIgnoreList = [ // React 17 "src/react/hooks/__tests__/useSuspenseQuery.test.tsx", "src/react/hooks/__tests__/useBackgroundQuery.test.tsx", - "src/react/hooks/__tests__/useInteractiveQuery.test.tsx", + "src/react/hooks/__tests__/useLoadableQuery.test.tsx", ]; const tsStandardConfig = { diff --git a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx similarity index 95% rename from src/react/hooks/__tests__/useInteractiveQuery.test.tsx rename to src/react/hooks/__tests__/useLoadableQuery.test.tsx index a97f916681a..77c30f5d924 100644 --- a/src/react/hooks/__tests__/useInteractiveQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -34,13 +34,13 @@ import { offsetLimitPagination, DeepPartial, } from "../../../utilities"; -import { useInteractiveQuery } from "../useInteractiveQuery"; +import { useLoadableQuery } from "../useLoadableQuery"; import type { UseReadQueryResult } from "../useReadQuery"; import { useReadQuery } from "../useReadQuery"; import { ApolloProvider } from "../../context"; import { InMemoryCache } from "../../../cache"; +import { LoadableQueryHookFetchPolicy } from "../../types/types"; import { QueryReference } from "../../../react"; -import { InteractiveQueryHookFetchPolicy } from "../../types/types"; import { FetchMoreFunction, RefetchFunction } from "../useSuspenseQuery"; import invariant from "ts-invariant"; import { profile, profileHook, spyOnConsole } from "../../../testing/internal"; @@ -222,7 +222,7 @@ it("loads a query and suspends when the load query function is called", async () const App = profile({ Component: () => { - const [queryRef, loadQuery] = useInteractiveQuery(query); + const [queryRef, loadQuery] = useLoadableQuery(query); return ( <> @@ -266,7 +266,7 @@ it("loads a query with variables and suspends by passing variables to the loadQu const App = profile({ Component: () => { - const [queryRef, loadQuery] = useInteractiveQuery(query); + const [queryRef, loadQuery] = useLoadableQuery(query); return ( <> @@ -314,7 +314,7 @@ it("changes variables on a query and resuspends when passing new variables to th const App = profile({ Component: () => { - const [queryRef, loadQuery] = useInteractiveQuery(query); + const [queryRef, loadQuery] = useLoadableQuery(query); return ( <> @@ -392,7 +392,7 @@ it("allows the client to be overridden", async () => { createDefaultProfiledComponents(); function App() { - const [queryRef, loadQuery] = useInteractiveQuery(query, { + const [queryRef, loadQuery] = useLoadableQuery(query, { client: localClient, }); @@ -446,7 +446,7 @@ it("passes context to the link", async () => { createDefaultProfiledComponents(); function App() { - const [queryRef, loadQuery] = useInteractiveQuery(query, { + const [queryRef, loadQuery] = useLoadableQuery(query, { context: { valueA: "A", valueB: "B" }, }); @@ -522,7 +522,7 @@ it('enables canonical results when canonizeResults is "true"', async () => { createDefaultProfiledComponents(); function App() { - const [queryRef, loadQuery] = useInteractiveQuery(query, { + const [queryRef, loadQuery] = useLoadableQuery(query, { canonizeResults: true, }); @@ -599,7 +599,7 @@ it("can disable canonical results when the cache's canonizeResults setting is tr createDefaultProfiledComponents(); function App() { - const [queryRef, loadQuery] = useInteractiveQuery(query, { + const [queryRef, loadQuery] = useLoadableQuery(query, { canonizeResults: false, }); @@ -655,7 +655,7 @@ it("returns initial cache data followed by network data when the fetch policy is const App = profile({ Component: () => { - const [queryRef, loadQuery] = useInteractiveQuery(query, { + const [queryRef, loadQuery] = useLoadableQuery(query, { fetchPolicy: "cache-and-network", }); @@ -722,7 +722,7 @@ it("all data is present in the cache, no network request is made", async () => { const { SuspenseFallback, ReadQueryHook } = createDefaultProfiledComponents(); function App() { - const [queryRef, loadQuery] = useInteractiveQuery(query); + const [queryRef, loadQuery] = useLoadableQuery(query); return ( <> @@ -780,7 +780,7 @@ it("partial data is present in the cache so it is ignored and network request is const { SuspenseFallback, ReadQueryHook } = createDefaultProfiledComponents(); function App() { - const [queryRef, loadQuery] = useInteractiveQuery(query); + const [queryRef, loadQuery] = useLoadableQuery(query); return ( <> @@ -832,7 +832,7 @@ it("existing data in the cache is ignored when `fetchPolicy` is 'network-only'", const { SuspenseFallback, ReadQueryHook } = createDefaultProfiledComponents(); function App() { - const [queryRef, loadQuery] = useInteractiveQuery(query, { + const [queryRef, loadQuery] = useLoadableQuery(query, { fetchPolicy: "network-only", }); @@ -883,7 +883,7 @@ it("fetches data from the network but does not update the cache when `fetchPolic const { SuspenseFallback, ReadQueryHook } = createDefaultProfiledComponents(); function App() { - const [queryRef, loadQuery] = useInteractiveQuery(query, { + const [queryRef, loadQuery] = useLoadableQuery(query, { fetchPolicy: "no-cache", }); @@ -968,7 +968,7 @@ it("works with startTransition to change variables", async () => { } function App() { - const [queryRef, loadQuery] = useInteractiveQuery(query); + const [queryRef, loadQuery] = useLoadableQuery(query); return (
@@ -1088,7 +1088,7 @@ it('does not suspend deferred queries with data in the cache and using a "cache- createDefaultProfiledComponents(); function App() { - const [queryRef, loadQuery] = useInteractiveQuery(query, { + const [queryRef, loadQuery] = useLoadableQuery(query, { fetchPolicy: "cache-and-network", }); return ( @@ -1194,7 +1194,7 @@ it("reacts to cache updates", async () => { createDefaultProfiledComponents(); function App() { - const [queryRef, loadQuery] = useInteractiveQuery(query); + const [queryRef, loadQuery] = useLoadableQuery(query); return ( <> @@ -1257,7 +1257,7 @@ it("applies `errorPolicy` on next fetch when it changes between renders", async function App() { const [errorPolicy, setErrorPolicy] = useState("none"); - const [queryRef, loadQuery, { refetch }] = useInteractiveQuery(query, { + const [queryRef, loadQuery, { refetch }] = useLoadableQuery(query, { errorPolicy, }); @@ -1352,7 +1352,7 @@ it("applies `context` on next fetch when it changes between renders", async () = function App() { const [phase, setPhase] = React.useState("initial"); - const [queryRef, loadQuery, { refetch }] = useInteractiveQuery(query, { + const [queryRef, loadQuery, { refetch }] = useLoadableQuery(query, { context: { phase }, }); @@ -1446,7 +1446,7 @@ it("returns canonical results immediately when `canonizeResults` changes from `f function App() { const [canonizeResults, setCanonizeResults] = React.useState(false); - const [queryRef, loadQuery] = useInteractiveQuery(query, { + const [queryRef, loadQuery] = useLoadableQuery(query, { canonizeResults, }); @@ -1548,7 +1548,7 @@ it("applies changed `refetchWritePolicy` to next fetch when changing between ren const [refetchWritePolicy, setRefetchWritePolicy] = React.useState("merge"); - const [queryRef, loadQuery, { refetch }] = useInteractiveQuery(query, { + const [queryRef, loadQuery, { refetch }] = useLoadableQuery(query, { refetchWritePolicy, }); @@ -1702,7 +1702,7 @@ it("applies `returnPartialData` on next fetch when it changes between renders", function App() { const [returnPartialData, setReturnPartialData] = React.useState(false); - const [queryRef, loadQuery] = useInteractiveQuery(fullQuery, { + const [queryRef, loadQuery] = useLoadableQuery(fullQuery, { returnPartialData, }); @@ -1832,9 +1832,9 @@ it("applies updated `fetchPolicy` on next fetch when it changes between renders" function App() { const [fetchPolicy, setFetchPolicy] = - React.useState("cache-first"); + React.useState("cache-first"); - const [queryRef, loadQuery, { refetch }] = useInteractiveQuery(query, { + const [queryRef, loadQuery, { refetch }] = useLoadableQuery(query, { fetchPolicy, }); @@ -1928,7 +1928,7 @@ it("re-suspends when calling `refetch`", async () => { createDefaultProfiledComponents(); function App() { - const [queryRef, loadQuery, { refetch }] = useInteractiveQuery(query); + const [queryRef, loadQuery, { refetch }] = useLoadableQuery(query); return ( <> @@ -1996,7 +1996,7 @@ it("re-suspends when calling `refetch` with new variables", async () => { createDefaultProfiledComponents(); function App() { - const [queryRef, loadQuery, { refetch }] = useInteractiveQuery(query); + const [queryRef, loadQuery, { refetch }] = useLoadableQuery(query); return ( <> @@ -2057,7 +2057,7 @@ it("re-suspends multiple times when calling `refetch` multiple times", async () createDefaultProfiledComponents(); function App() { - const [queryRef, loadQuery, { refetch }] = useInteractiveQuery(query); + const [queryRef, loadQuery, { refetch }] = useLoadableQuery(query); return ( <> @@ -2112,7 +2112,7 @@ it("throws errors when errors are returned after calling `refetch`", async () => createDefaultProfiledComponents(); function App() { - const [queryRef, loadQuery, { refetch }] = useInteractiveQuery(query); + const [queryRef, loadQuery, { refetch }] = useLoadableQuery(query); return ( <> @@ -2166,7 +2166,7 @@ it('ignores errors returned after calling `refetch` when errorPolicy is set to " createDefaultProfiledComponents(); function App() { - const [queryRef, loadQuery, { refetch }] = useInteractiveQuery(query, { + const [queryRef, loadQuery, { refetch }] = useLoadableQuery(query, { errorPolicy: "ignore", }); @@ -2225,7 +2225,7 @@ it('returns errors after calling `refetch` when errorPolicy is set to "all"', as createDefaultProfiledComponents(); function App() { - const [queryRef, loadQuery, { refetch }] = useInteractiveQuery(query, { + const [queryRef, loadQuery, { refetch }] = useLoadableQuery(query, { errorPolicy: "all", }); @@ -2287,7 +2287,7 @@ it('handles partial data results after calling `refetch` when errorPolicy is set createDefaultProfiledComponents(); function App() { - const [queryRef, loadQuery, { refetch }] = useInteractiveQuery(query, { + const [queryRef, loadQuery, { refetch }] = useLoadableQuery(query, { errorPolicy: "all", }); @@ -2370,7 +2370,7 @@ it("`refetch` works with startTransition to allow React to show stale UI until f function App() { const [id, setId] = React.useState("1"); - const [queryRef, loadQuery, { refetch }] = useInteractiveQuery(query); + const [queryRef, loadQuery, { refetch }] = useLoadableQuery(query); return ( <> @@ -2457,7 +2457,7 @@ it("re-suspends when calling `fetchMore` with different variables", async () => createDefaultProfiledComponents(); function App() { - const [queryRef, loadQuery, { fetchMore }] = useInteractiveQuery(query); + const [queryRef, loadQuery, { fetchMore }] = useLoadableQuery(query); return ( <> @@ -2525,7 +2525,7 @@ it("properly uses `updateQuery` when calling `fetchMore`", async () => { createDefaultProfiledComponents(); function App() { - const [queryRef, loadQuery, { fetchMore }] = useInteractiveQuery(query); + const [queryRef, loadQuery, { fetchMore }] = useLoadableQuery(query); return ( <> @@ -2615,7 +2615,7 @@ it("properly uses cache field policies when calling `fetchMore` without `updateQ }); function App() { - const [queryRef, loadQuery, { fetchMore }] = useInteractiveQuery(query); + const [queryRef, loadQuery, { fetchMore }] = useLoadableQuery(query); return ( <> @@ -2757,7 +2757,7 @@ it("`fetchMore` works with startTransition to allow React to show stale UI until } function App() { - const [queryRef, loadQuery, { fetchMore }] = useInteractiveQuery(query); + const [queryRef, loadQuery, { fetchMore }] = useLoadableQuery(query); return ( <> @@ -2896,7 +2896,7 @@ it('honors refetchWritePolicy set to "merge"', async () => { }); function App() { - const [queryRef, loadQuery, { refetch }] = useInteractiveQuery(query, { + const [queryRef, loadQuery, { refetch }] = useLoadableQuery(query, { refetchWritePolicy: "merge", }); @@ -3000,7 +3000,7 @@ it('defaults refetchWritePolicy to "overwrite"', async () => { }); function App() { - const [queryRef, loadQuery, { refetch }] = useInteractiveQuery(query); + const [queryRef, loadQuery, { refetch }] = useLoadableQuery(query); return ( <> @@ -3092,7 +3092,7 @@ it('does not suspend when partial data is in the cache and using a "cache-first" const client = new ApolloClient({ link: new MockLink(mocks), cache }); function App() { - const [queryRef, loadQuery] = useInteractiveQuery(fullQuery, { + const [queryRef, loadQuery] = useLoadableQuery(fullQuery, { fetchPolicy: "cache-first", returnPartialData: true, }); @@ -3159,7 +3159,7 @@ it('suspends and does not use partial data when changing variables and using a " createDefaultProfiledComponents>(); function App() { - const [queryRef, loadQuery] = useInteractiveQuery(query, { + const [queryRef, loadQuery] = useLoadableQuery(query, { fetchPolicy: "cache-first", returnPartialData: true, }); @@ -3258,7 +3258,7 @@ it('suspends when partial data is in the cache and using a "network-only" fetch }); function App() { - const [queryRef, loadQuery] = useInteractiveQuery(fullQuery, { + const [queryRef, loadQuery] = useLoadableQuery(fullQuery, { fetchPolicy: "network-only", returnPartialData: true, }); @@ -3332,7 +3332,7 @@ it('suspends when partial data is in the cache and using a "no-cache" fetch poli createDefaultProfiledComponents>(); function App() { - const [queryRef, loadQuery] = useInteractiveQuery(fullQuery, { + const [queryRef, loadQuery] = useLoadableQuery(fullQuery, { fetchPolicy: "no-cache", returnPartialData: true, }); @@ -3373,7 +3373,7 @@ it('warns when using returnPartialData with a "no-cache" fetch policy', async () renderHook( () => - useInteractiveQuery(query, { + useLoadableQuery(query, { fetchPolicy: "no-cache", returnPartialData: true, }), @@ -3433,7 +3433,7 @@ it('does not suspend when partial data is in the cache and using a "cache-and-ne createDefaultProfiledComponents>(); function App() { - const [queryRef, loadQuery] = useInteractiveQuery(fullQuery, { + const [queryRef, loadQuery] = useLoadableQuery(fullQuery, { fetchPolicy: "cache-and-network", returnPartialData: true, }); @@ -3498,7 +3498,7 @@ it('suspends and does not use partial data when changing variables and using a " createDefaultProfiledComponents>(); function App() { - const [queryRef, loadQuery] = useInteractiveQuery(query, { + const [queryRef, loadQuery] = useLoadableQuery(query, { fetchPolicy: "cache-and-network", returnPartialData: true, }); @@ -3605,7 +3605,7 @@ it('does not suspend deferred queries with partial data in the cache and using a createDefaultProfiledComponents>(); function App() { - const [queryRef, loadTodo] = useInteractiveQuery(query, { + const [queryRef, loadTodo] = useLoadableQuery(query, { fetchPolicy: "cache-first", returnPartialData: true, }); @@ -3702,7 +3702,7 @@ describe.skip("type tests", () => { it("returns unknown when TData cannot be inferred", () => { const query = gql``; - const [queryRef] = useInteractiveQuery(query); + const [queryRef] = useLoadableQuery(query); invariant(queryRef); @@ -3714,7 +3714,7 @@ describe.skip("type tests", () => { it("variables are optional and can be anything with an untyped DocumentNode", () => { const query = gql``; - const [, loadQuery] = useInteractiveQuery(query); + const [, loadQuery] = useLoadableQuery(query); loadQuery(); loadQuery({}); @@ -3725,7 +3725,7 @@ describe.skip("type tests", () => { it("variables are optional and can be anything with unspecified TVariables on a TypedDocumentNode", () => { const query: TypedDocumentNode<{ greeting: string }> = gql``; - const [, loadQuery] = useInteractiveQuery(query); + const [, loadQuery] = useLoadableQuery(query); loadQuery(); loadQuery({}); @@ -3739,7 +3739,7 @@ describe.skip("type tests", () => { Record > = gql``; - const [, loadQuery] = useInteractiveQuery(query); + const [, loadQuery] = useLoadableQuery(query); loadQuery(); loadQuery({}); @@ -3750,7 +3750,7 @@ describe.skip("type tests", () => { it("does not allow variables when TVariables is `never`", () => { const query: TypedDocumentNode<{ greeting: string }, never> = gql``; - const [, loadQuery] = useInteractiveQuery(query); + const [, loadQuery] = useLoadableQuery(query); loadQuery(); // @ts-expect-error no variables argument allowed @@ -3765,7 +3765,7 @@ describe.skip("type tests", () => { { limit?: number } > = gql``; - const [, loadQuery] = useInteractiveQuery(query); + const [, loadQuery] = useLoadableQuery(query); loadQuery(); loadQuery({}); @@ -3787,7 +3787,7 @@ describe.skip("type tests", () => { { id: string } > = gql``; - const [, loadQuery] = useInteractiveQuery(query); + const [, loadQuery] = useLoadableQuery(query); // @ts-expect-error missing variables argument loadQuery(); @@ -3811,7 +3811,7 @@ describe.skip("type tests", () => { { id: string; language?: string } > = gql``; - const [, loadQuery] = useInteractiveQuery(query); + const [, loadQuery] = useLoadableQuery(query); // @ts-expect-error missing variables argument loadQuery(); @@ -3842,7 +3842,7 @@ describe.skip("type tests", () => { const { query } = useVariablesQueryCase(); { - const [queryRef] = useInteractiveQuery(query); + const [queryRef] = useLoadableQuery(query); invariant(queryRef); @@ -3852,7 +3852,7 @@ describe.skip("type tests", () => { } { - const [queryRef] = useInteractiveQuery< + const [queryRef] = useLoadableQuery< VariablesCaseData, VariablesCaseVariables >(query); @@ -3869,7 +3869,7 @@ describe.skip("type tests", () => { const { query } = useVariablesQueryCase(); { - const [queryRef] = useInteractiveQuery(query, { + const [queryRef] = useLoadableQuery(query, { errorPolicy: "ignore", }); @@ -3881,7 +3881,7 @@ describe.skip("type tests", () => { } { - const [queryRef] = useInteractiveQuery< + const [queryRef] = useLoadableQuery< VariablesCaseData, VariablesCaseVariables >(query, { errorPolicy: "ignore" }); @@ -3898,7 +3898,7 @@ describe.skip("type tests", () => { const { query } = useVariablesQueryCase(); { - const [queryRef] = useInteractiveQuery(query, { + const [queryRef] = useLoadableQuery(query, { errorPolicy: "all", }); @@ -3910,7 +3910,7 @@ describe.skip("type tests", () => { } { - const [queryRef] = useInteractiveQuery< + const [queryRef] = useLoadableQuery< VariablesCaseData, VariablesCaseVariables >(query, { errorPolicy: "all" }); @@ -3927,7 +3927,7 @@ describe.skip("type tests", () => { const { query } = useVariablesQueryCase(); { - const [queryRef] = useInteractiveQuery(query, { + const [queryRef] = useLoadableQuery(query, { errorPolicy: "none", }); @@ -3939,7 +3939,7 @@ describe.skip("type tests", () => { } { - const [queryRef] = useInteractiveQuery< + const [queryRef] = useLoadableQuery< VariablesCaseData, VariablesCaseVariables >(query, { errorPolicy: "none" }); @@ -3956,7 +3956,7 @@ describe.skip("type tests", () => { const { query } = useVariablesQueryCase(); { - const [queryRef] = useInteractiveQuery(query, { + const [queryRef] = useLoadableQuery(query, { returnPartialData: true, }); @@ -3968,7 +3968,7 @@ describe.skip("type tests", () => { } { - const [queryRef] = useInteractiveQuery< + const [queryRef] = useLoadableQuery< VariablesCaseData, VariablesCaseVariables >(query, { returnPartialData: true }); @@ -3985,7 +3985,7 @@ describe.skip("type tests", () => { const { query } = useVariablesQueryCase(); { - const [queryRef] = useInteractiveQuery(query, { + const [queryRef] = useLoadableQuery(query, { returnPartialData: false, }); @@ -3997,7 +3997,7 @@ describe.skip("type tests", () => { } { - const [queryRef] = useInteractiveQuery< + const [queryRef] = useLoadableQuery< VariablesCaseData, VariablesCaseVariables >(query, { returnPartialData: false }); @@ -4014,7 +4014,7 @@ describe.skip("type tests", () => { const { query } = useVariablesQueryCase(); { - const [queryRef] = useInteractiveQuery(query, { + const [queryRef] = useLoadableQuery(query, { fetchPolicy: "no-cache", }); @@ -4026,7 +4026,7 @@ describe.skip("type tests", () => { } { - const [queryRef] = useInteractiveQuery< + const [queryRef] = useLoadableQuery< VariablesCaseData, VariablesCaseVariables >(query, { fetchPolicy: "no-cache" }); @@ -4043,7 +4043,7 @@ describe.skip("type tests", () => { const { query } = useVariablesQueryCase(); { - const [queryRef] = useInteractiveQuery(query, { + const [queryRef] = useLoadableQuery(query, { returnPartialData: true, errorPolicy: "ignore", }); @@ -4058,7 +4058,7 @@ describe.skip("type tests", () => { } { - const [queryRef] = useInteractiveQuery< + const [queryRef] = useLoadableQuery< VariablesCaseData, VariablesCaseVariables >(query, { returnPartialData: true, errorPolicy: "ignore" }); @@ -4073,7 +4073,7 @@ describe.skip("type tests", () => { } { - const [queryRef] = useInteractiveQuery(query, { + const [queryRef] = useLoadableQuery(query, { returnPartialData: true, errorPolicy: "none", }); @@ -4086,7 +4086,7 @@ describe.skip("type tests", () => { } { - const [queryRef] = useInteractiveQuery< + const [queryRef] = useLoadableQuery< VariablesCaseData, VariablesCaseVariables >(query, { returnPartialData: true, errorPolicy: "none" }); @@ -4103,7 +4103,7 @@ describe.skip("type tests", () => { const { query } = useVariablesQueryCase(); { - const [queryRef] = useInteractiveQuery(query, { + const [queryRef] = useLoadableQuery(query, { fetchPolicy: "no-cache", returnPartialData: true, errorPolicy: "none", @@ -4117,7 +4117,7 @@ describe.skip("type tests", () => { } { - const [queryRef] = useInteractiveQuery< + const [queryRef] = useLoadableQuery< VariablesCaseData, VariablesCaseVariables >(query, { diff --git a/src/react/hooks/useInteractiveQuery.ts b/src/react/hooks/useLoadableQuery.ts similarity index 84% rename from src/react/hooks/useInteractiveQuery.ts rename to src/react/hooks/useLoadableQuery.ts index 74f07e03018..36616860e07 100644 --- a/src/react/hooks/useInteractiveQuery.ts +++ b/src/react/hooks/useLoadableQuery.ts @@ -10,7 +10,7 @@ import type { QueryReference, InternalQueryReference, } from "../cache/QueryReference.js"; -import type { InteractiveQueryHookOptions } from "../types/types.js"; +import type { LoadableQueryHookOptions } from "../types/types.js"; import { __use } from "./internal/index.js"; import { getSuspenseCache } from "../cache/index.js"; import { useWatchQueryOptions } from "./useSuspenseQuery.js"; @@ -35,7 +35,7 @@ type LoadQuery = ( : [variables: TVariables] ) => void; -export type UseInteractiveQueryResult< +export type UseLoadableQueryResult< TData = unknown, TVariables extends OperationVariables = OperationVariables, > = [ @@ -47,14 +47,14 @@ export type UseInteractiveQueryResult< }, ]; -export function useInteractiveQuery< +export function useLoadableQuery< TData, TVariables extends OperationVariables, - TOptions extends InteractiveQueryHookOptions, + TOptions extends LoadableQueryHookOptions, >( query: DocumentNode | TypedDocumentNode, - options?: InteractiveQueryHookOptions & TOptions -): UseInteractiveQueryResult< + options?: LoadableQueryHookOptions & TOptions +): UseLoadableQueryResult< TOptions["errorPolicy"] extends "ignore" | "all" ? TOptions["returnPartialData"] extends true ? DeepPartial | undefined @@ -65,52 +65,52 @@ export function useInteractiveQuery< TVariables >; -export function useInteractiveQuery< +export function useLoadableQuery< TData = unknown, TVariables extends OperationVariables = OperationVariables, >( query: DocumentNode | TypedDocumentNode, - options: InteractiveQueryHookOptions & { + options: LoadableQueryHookOptions & { returnPartialData: true; errorPolicy: "ignore" | "all"; } -): UseInteractiveQueryResult | undefined, TVariables>; +): UseLoadableQueryResult | undefined, TVariables>; -export function useInteractiveQuery< +export function useLoadableQuery< TData = unknown, TVariables extends OperationVariables = OperationVariables, >( query: DocumentNode | TypedDocumentNode, - options: InteractiveQueryHookOptions & { + options: LoadableQueryHookOptions & { errorPolicy: "ignore" | "all"; } -): UseInteractiveQueryResult; +): UseLoadableQueryResult; -export function useInteractiveQuery< +export function useLoadableQuery< TData = unknown, TVariables extends OperationVariables = OperationVariables, >( query: DocumentNode | TypedDocumentNode, - options: InteractiveQueryHookOptions & { + options: LoadableQueryHookOptions & { returnPartialData: true; } -): UseInteractiveQueryResult, TVariables>; +): UseLoadableQueryResult, TVariables>; -export function useInteractiveQuery< +export function useLoadableQuery< TData = unknown, TVariables extends OperationVariables = OperationVariables, >( query: DocumentNode | TypedDocumentNode, - options?: InteractiveQueryHookOptions -): UseInteractiveQueryResult; + options?: LoadableQueryHookOptions +): UseLoadableQueryResult; -export function useInteractiveQuery< +export function useLoadableQuery< TData = unknown, TVariables extends OperationVariables = OperationVariables, >( query: DocumentNode | TypedDocumentNode, - options: InteractiveQueryHookOptions = Object.create(null) -): UseInteractiveQueryResult { + options: LoadableQueryHookOptions = Object.create(null) +): UseLoadableQueryResult { const client = useApolloClient(options.client); const suspenseCache = getSuspenseCache(client); const watchQueryOptions = useWatchQueryOptions({ client, query, options }); diff --git a/src/react/types/types.ts b/src/react/types/types.ts index e3a1b6ae06f..f6f7af613aa 100644 --- a/src/react/types/types.ts +++ b/src/react/types/types.ts @@ -190,12 +190,12 @@ export interface BackgroundQueryHookOptions< skip?: boolean; } -export type InteractiveQueryHookFetchPolicy = Extract< +export type LoadableQueryHookFetchPolicy = Extract< WatchQueryFetchPolicy, "cache-first" | "network-only" | "no-cache" | "cache-and-network" >; -export interface InteractiveQueryHookOptions { +export interface LoadableQueryHookOptions { /** * Whether to canonize cache results before returning them. Canonization * takes some extra time, but it speeds up future deep equality comparisons. @@ -227,7 +227,7 @@ export interface InteractiveQueryHookOptions { * * The default value is `cache-first`. */ - fetchPolicy?: InteractiveQueryHookFetchPolicy; + fetchPolicy?: LoadableQueryHookFetchPolicy; /** * A unique identifier for the query. Each item in the array must be a stable * identifier to prevent infinite fetches. From f372417834b0863374d78f821fa9d92ec0e74761 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Fri, 10 Nov 2023 12:49:19 -0700 Subject: [PATCH 095/199] Add a changeset --- .changeset/thirty-ties-arrive.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .changeset/thirty-ties-arrive.md diff --git a/.changeset/thirty-ties-arrive.md b/.changeset/thirty-ties-arrive.md new file mode 100644 index 00000000000..d9695eb0900 --- /dev/null +++ b/.changeset/thirty-ties-arrive.md @@ -0,0 +1,24 @@ +--- +"@apollo/client": minor +--- + +Introduces a new `useLoadableQuery` hook. This hook works similarly to `useBackgroundQuery` in that it returns a `queryRef` that can be used to suspend a component via the `useReadQuery` hook. It provides a more ergonomic way to load the query during a user interaction (for example when wanting to preload some data) that would otherwise be clunky with `useBackgroundQuery`. + +```tsx +function App() { + const [queryRef, loadQuery, { refetch, fetchMore }] = useLoadableQuery(query, options) + + return ( + <> + + {queryRef && } + + ); +} + +function Child({ queryRef }) { + const { data } = useReadQuery(queryRef) + + // ... +} +``` From f2aefcd7e9d425ae53e6a1dfdf078bb75227b042 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Fri, 10 Nov 2023 15:44:40 -0700 Subject: [PATCH 096/199] Ensure useLoadableQuery is exported from hooks/index.ts --- src/react/hooks/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/react/hooks/index.ts b/src/react/hooks/index.ts index 61d50665cac..afc204e0e1b 100644 --- a/src/react/hooks/index.ts +++ b/src/react/hooks/index.ts @@ -11,6 +11,8 @@ export type { UseSuspenseQueryResult } from "./useSuspenseQuery.js"; export { useSuspenseQuery } from "./useSuspenseQuery.js"; export type { UseBackgroundQueryResult } from "./useBackgroundQuery.js"; export { useBackgroundQuery } from "./useBackgroundQuery.js"; +export type { UseLoadableQueryResult } from "./useLoadableQuery.js"; +export { useLoadableQuery } from "./useLoadableQuery.js"; export type { UseReadQueryResult } from "./useReadQuery.js"; export { useReadQuery } from "./useReadQuery.js"; export { skipToken } from "./constants.js"; From b42f616729f340a19ed5360cd01ad6fe1eae034f Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Fri, 10 Nov 2023 15:46:44 -0700 Subject: [PATCH 097/199] Regenerate API report --- .api-reports/api-report-react.md | 98 +++++++++++++++++++------- .api-reports/api-report-react_hooks.md | 79 +++++++++++++++++++-- .api-reports/api-report.md | 90 +++++++++++++++++------ 3 files changed, 213 insertions(+), 54 deletions(-) diff --git a/.api-reports/api-report-react.md b/.api-reports/api-report-react.md index ffbd1a02975..b0a246a6760 100644 --- a/.api-reports/api-report-react.md +++ b/.api-reports/api-report-react.md @@ -901,33 +901,6 @@ interface IncrementalPayload { path: Path; } -// @public (undocumented) -export type InteractiveQueryHookFetchPolicy = Extract; - -// @public (undocumented) -export interface InteractiveQueryHookOptions { - // (undocumented) - canonizeResults?: boolean; - // (undocumented) - client?: ApolloClient; - // (undocumented) - context?: Context; - // Warning: (ae-forgotten-export) The symbol "ErrorPolicy" needs to be exported by the entry point index.d.ts - // - // (undocumented) - errorPolicy?: ErrorPolicy; - // (undocumented) - fetchPolicy?: InteractiveQueryHookFetchPolicy; - // (undocumented) - queryKey?: string | number | any[]; - // Warning: (ae-forgotten-export) The symbol "RefetchWritePolicy" needs to be exported by the entry point index.d.ts - // - // (undocumented) - refetchWritePolicy?: RefetchWritePolicy; - // (undocumented) - returnPartialData?: boolean; -} - // @public (undocumented) class InternalQueryReference { // Warning: (ae-forgotten-export) The symbol "InternalQueryReferenceOptions" needs to be exported by the entry point index.d.ts @@ -1045,6 +1018,38 @@ export type LazyQueryResultTuple = // @public (undocumented) type Listener = (promise: Promise>) => void; +// @public (undocumented) +export type LoadableQueryHookFetchPolicy = Extract; + +// @public (undocumented) +export interface LoadableQueryHookOptions { + // (undocumented) + canonizeResults?: boolean; + // (undocumented) + client?: ApolloClient; + // (undocumented) + context?: Context; + // Warning: (ae-forgotten-export) The symbol "ErrorPolicy" needs to be exported by the entry point index.d.ts + // + // (undocumented) + errorPolicy?: ErrorPolicy; + // (undocumented) + fetchPolicy?: LoadableQueryHookFetchPolicy; + // (undocumented) + queryKey?: string | number | any[]; + // Warning: (ae-forgotten-export) The symbol "RefetchWritePolicy" needs to be exported by the entry point index.d.ts + // + // (undocumented) + refetchWritePolicy?: RefetchWritePolicy; + // (undocumented) + returnPartialData?: boolean; +} + +// Warning: (ae-forgotten-export) The symbol "OnlyRequiredProperties" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type LoadQuery = (...args: [TVariables] extends [never] ? [] : {} extends OnlyRequiredProperties ? [variables?: TVariables] : [variables: TVariables]) => void; + // @public (undocumented) class LocalState { // Warning: (ae-forgotten-export) The symbol "LocalStateOptions" needs to be exported by the entry point index.d.ts @@ -1386,6 +1391,11 @@ export interface OnDataOptions { data: SubscriptionResult; } +// @public (undocumented) +type OnlyRequiredProperties = { + [K in keyof T as {} extends Pick ? never : K]: T[K]; +}; + // @public (undocumented) type OnQueryUpdated = (observableQuery: ObservableQuery, diff: Cache_2.DiffResult, lastDiff: Cache_2.DiffResult | undefined) => boolean | TResult; @@ -2117,6 +2127,40 @@ export type UseFragmentResult = { // @public (undocumented) export function useLazyQuery(query: DocumentNode | TypedDocumentNode, options?: LazyQueryHookOptions, NoInfer>): LazyQueryResultTuple; +// @public (undocumented) +export function useLoadableQuery(query: DocumentNode | TypedDocumentNode, options?: LoadableQueryHookOptions & TOptions): UseLoadableQueryResult | undefined : TData | undefined : TOptions["returnPartialData"] extends true ? DeepPartial : TData, TVariables>; + +// @public (undocumented) +export function useLoadableQuery(query: DocumentNode | TypedDocumentNode, options: LoadableQueryHookOptions & { + returnPartialData: true; + errorPolicy: "ignore" | "all"; +}): UseLoadableQueryResult | undefined, TVariables>; + +// @public (undocumented) +export function useLoadableQuery(query: DocumentNode | TypedDocumentNode, options: LoadableQueryHookOptions & { + errorPolicy: "ignore" | "all"; +}): UseLoadableQueryResult; + +// @public (undocumented) +export function useLoadableQuery(query: DocumentNode | TypedDocumentNode, options: LoadableQueryHookOptions & { + returnPartialData: true; +}): UseLoadableQueryResult, TVariables>; + +// @public (undocumented) +export function useLoadableQuery(query: DocumentNode | TypedDocumentNode, options?: LoadableQueryHookOptions): UseLoadableQueryResult; + +// Warning: (ae-forgotten-export) The symbol "LoadQuery" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export type UseLoadableQueryResult = [ +QueryReference | null, +LoadQuery, + { + fetchMore: FetchMoreFunction; + refetch: RefetchFunction; +} +]; + // @public (undocumented) export function useMutation = ApolloCache>(mutation: DocumentNode | TypedDocumentNode, options?: MutationHookOptions, NoInfer, TContext, TCache>): MutationTuple; diff --git a/.api-reports/api-report-react_hooks.md b/.api-reports/api-report-react_hooks.md index b9a81d6f9f1..8bd65102160 100644 --- a/.api-reports/api-report-react_hooks.md +++ b/.api-reports/api-report-react_hooks.md @@ -969,6 +969,40 @@ type LazyQueryResultTuple = [LazyQ // @public (undocumented) type Listener = (promise: Promise>) => void; +// @public (undocumented) +type LoadableQueryHookFetchPolicy = Extract; + +// @public (undocumented) +interface LoadableQueryHookOptions { + // (undocumented) + canonizeResults?: boolean; + // (undocumented) + client?: ApolloClient; + // (undocumented) + context?: DefaultContext; + // Warning: (ae-forgotten-export) The symbol "ErrorPolicy" needs to be exported by the entry point index.d.ts + // + // (undocumented) + errorPolicy?: ErrorPolicy; + // Warning: (ae-forgotten-export) The symbol "LoadableQueryHookFetchPolicy" needs to be exported by the entry point index.d.ts + // + // (undocumented) + fetchPolicy?: LoadableQueryHookFetchPolicy; + // (undocumented) + queryKey?: string | number | any[]; + // Warning: (ae-forgotten-export) The symbol "RefetchWritePolicy" needs to be exported by the entry point index.d.ts + // + // (undocumented) + refetchWritePolicy?: RefetchWritePolicy; + // (undocumented) + returnPartialData?: boolean; +} + +// Warning: (ae-forgotten-export) The symbol "OnlyRequiredProperties" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type LoadQuery = (...args: [TVariables] extends [never] ? [] : {} extends OnlyRequiredProperties ? [variables?: TVariables] : [variables: TVariables]) => void; + // @public (undocumented) class LocalState { // Warning: (ae-forgotten-export) The symbol "LocalStateOptions" needs to be exported by the entry point index.d.ts @@ -1070,8 +1104,6 @@ interface MutationBaseOptions { data: SubscriptionResult; } +// @public (undocumented) +type OnlyRequiredProperties = { + [K in keyof T as {} extends Pick ? never : K]: T[K]; +}; + // @public (undocumented) type OnQueryUpdated = (observableQuery: ObservableQuery, diff: Cache_2.DiffResult, lastDiff: Cache_2.DiffResult | undefined) => boolean | TResult; @@ -1979,6 +2016,42 @@ export type UseFragmentResult = { // @public (undocumented) export function useLazyQuery(query: DocumentNode | TypedDocumentNode, options?: LazyQueryHookOptions, NoInfer>): LazyQueryResultTuple; +// Warning: (ae-forgotten-export) The symbol "LoadableQueryHookOptions" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export function useLoadableQuery(query: DocumentNode | TypedDocumentNode, options?: LoadableQueryHookOptions & TOptions): UseLoadableQueryResult | undefined : TData | undefined : TOptions["returnPartialData"] extends true ? DeepPartial : TData, TVariables>; + +// @public (undocumented) +export function useLoadableQuery(query: DocumentNode | TypedDocumentNode, options: LoadableQueryHookOptions & { + returnPartialData: true; + errorPolicy: "ignore" | "all"; +}): UseLoadableQueryResult | undefined, TVariables>; + +// @public (undocumented) +export function useLoadableQuery(query: DocumentNode | TypedDocumentNode, options: LoadableQueryHookOptions & { + errorPolicy: "ignore" | "all"; +}): UseLoadableQueryResult; + +// @public (undocumented) +export function useLoadableQuery(query: DocumentNode | TypedDocumentNode, options: LoadableQueryHookOptions & { + returnPartialData: true; +}): UseLoadableQueryResult, TVariables>; + +// @public (undocumented) +export function useLoadableQuery(query: DocumentNode | TypedDocumentNode, options?: LoadableQueryHookOptions): UseLoadableQueryResult; + +// Warning: (ae-forgotten-export) The symbol "LoadQuery" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export type UseLoadableQueryResult = [ +QueryReference | null, +LoadQuery, + { + fetchMore: FetchMoreFunction; + refetch: RefetchFunction; +} +]; + // Warning: (ae-forgotten-export) The symbol "MutationHookOptions" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "MutationTuple" needs to be exported by the entry point index.d.ts // @@ -2087,8 +2160,6 @@ interface WatchQueryOptions, currentFetchPolicy: WatchQueryFetchPolicy, context: NextFetchPolicyContext) => WatchQueryFetchPolicy); - // Warning: (ae-forgotten-export) The symbol "RefetchWritePolicy" needs to be exported by the entry point index.d.ts - // // (undocumented) refetchWritePolicy?: RefetchWritePolicy; } diff --git a/.api-reports/api-report.md b/.api-reports/api-report.md index 09b33acfde0..b3586d1bfe6 100644 --- a/.api-reports/api-report.md +++ b/.api-reports/api-report.md @@ -1261,29 +1261,6 @@ export interface InMemoryCacheConfig extends ApolloReducerConfig { typePolicies?: TypePolicies; } -// @public (undocumented) -export type InteractiveQueryHookFetchPolicy = Extract; - -// @public (undocumented) -export interface InteractiveQueryHookOptions { - // (undocumented) - canonizeResults?: boolean; - // (undocumented) - client?: ApolloClient; - // (undocumented) - context?: DefaultContext; - // (undocumented) - errorPolicy?: ErrorPolicy; - // (undocumented) - fetchPolicy?: InteractiveQueryHookFetchPolicy; - // (undocumented) - queryKey?: string | number | any[]; - // (undocumented) - refetchWritePolicy?: RefetchWritePolicy; - // (undocumented) - returnPartialData?: boolean; -} - // @public (undocumented) class InternalQueryReference { // Warning: (ae-forgotten-export) The symbol "InternalQueryReferenceOptions" needs to be exported by the entry point index.d.ts @@ -1447,6 +1424,34 @@ export type LazyQueryResultTuple = // @public (undocumented) type Listener = (promise: Promise>) => void; +// @public (undocumented) +export type LoadableQueryHookFetchPolicy = Extract; + +// @public (undocumented) +export interface LoadableQueryHookOptions { + // (undocumented) + canonizeResults?: boolean; + // (undocumented) + client?: ApolloClient; + // (undocumented) + context?: DefaultContext; + // (undocumented) + errorPolicy?: ErrorPolicy; + // (undocumented) + fetchPolicy?: LoadableQueryHookFetchPolicy; + // (undocumented) + queryKey?: string | number | any[]; + // (undocumented) + refetchWritePolicy?: RefetchWritePolicy; + // (undocumented) + returnPartialData?: boolean; +} + +// Warning: (ae-forgotten-export) The symbol "OnlyRequiredProperties" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type LoadQuery = (...args: [TVariables] extends [never] ? [] : {} extends OnlyRequiredProperties ? [variables?: TVariables] : [variables: TVariables]) => void; + // @public (undocumented) class LocalState { // Warning: (ae-forgotten-export) The symbol "LocalStateOptions" needs to be exported by the entry point index.d.ts @@ -1872,6 +1877,11 @@ export interface OnDataOptions { data: SubscriptionResult; } +// @public (undocumented) +type OnlyRequiredProperties = { + [K in keyof T as {} extends Pick ? never : K]: T[K]; +}; + // @public (undocumented) export type OnQueryUpdated = (observableQuery: ObservableQuery, diff: Cache_2.DiffResult, lastDiff: Cache_2.DiffResult | undefined) => boolean | TResult; @@ -2763,6 +2773,40 @@ export type UseFragmentResult = { // @public (undocumented) export function useLazyQuery(query: DocumentNode | TypedDocumentNode, options?: LazyQueryHookOptions, NoInfer>): LazyQueryResultTuple; +// @public (undocumented) +export function useLoadableQuery(query: DocumentNode | TypedDocumentNode, options?: LoadableQueryHookOptions & TOptions): UseLoadableQueryResult | undefined : TData | undefined : TOptions["returnPartialData"] extends true ? DeepPartial : TData, TVariables>; + +// @public (undocumented) +export function useLoadableQuery(query: DocumentNode | TypedDocumentNode, options: LoadableQueryHookOptions & { + returnPartialData: true; + errorPolicy: "ignore" | "all"; +}): UseLoadableQueryResult | undefined, TVariables>; + +// @public (undocumented) +export function useLoadableQuery(query: DocumentNode | TypedDocumentNode, options: LoadableQueryHookOptions & { + errorPolicy: "ignore" | "all"; +}): UseLoadableQueryResult; + +// @public (undocumented) +export function useLoadableQuery(query: DocumentNode | TypedDocumentNode, options: LoadableQueryHookOptions & { + returnPartialData: true; +}): UseLoadableQueryResult, TVariables>; + +// @public (undocumented) +export function useLoadableQuery(query: DocumentNode | TypedDocumentNode, options?: LoadableQueryHookOptions): UseLoadableQueryResult; + +// Warning: (ae-forgotten-export) The symbol "LoadQuery" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export type UseLoadableQueryResult = [ +QueryReference | null, +LoadQuery, + { + fetchMore: FetchMoreFunction; + refetch: RefetchFunction; +} +]; + // @public (undocumented) export function useMutation = ApolloCache>(mutation: DocumentNode | TypedDocumentNode, options?: MutationHookOptions, NoInfer, TContext, TCache>): MutationTuple; From 9b15fdc9681d4285d37a7a0c1f4917c904212b7a Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Fri, 10 Nov 2023 15:50:23 -0700 Subject: [PATCH 098/199] Update size-limit and include useLoadableQuery --- .size-limit.cjs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.size-limit.cjs b/.size-limit.cjs index b6edc78d7bb..e46cfd785cd 100644 --- a/.size-limit.cjs +++ b/.size-limit.cjs @@ -3,6 +3,7 @@ const limits = require("./.size-limits.json"); const checks = [ { path: "dist/apollo-client.min.cjs", + limit: "38410", }, { path: "dist/main.cjs", @@ -20,6 +21,7 @@ const checks = [ "useSubscription", "useSuspenseQuery", "useBackgroundQuery", + "useLoadableQuery", "useReadQuery", "useFragment", ].map((name) => ({ path: "dist/react/index.js", import: `{ ${name} }` })), From ad4eabe3a1579fc4302781b41ae3845a12d0182f Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Fri, 10 Nov 2023 16:29:02 -0700 Subject: [PATCH 099/199] Update snapshot test --- src/__tests__/__snapshots__/exports.ts.snap | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/__tests__/__snapshots__/exports.ts.snap b/src/__tests__/__snapshots__/exports.ts.snap index 1d9a73e0eb7..70229c88a17 100644 --- a/src/__tests__/__snapshots__/exports.ts.snap +++ b/src/__tests__/__snapshots__/exports.ts.snap @@ -59,6 +59,7 @@ Array [ "useBackgroundQuery", "useFragment", "useLazyQuery", + "useLoadableQuery", "useMutation", "useQuery", "useReactiveVar", @@ -273,6 +274,7 @@ Array [ "useBackgroundQuery", "useFragment", "useLazyQuery", + "useLoadableQuery", "useMutation", "useQuery", "useReactiveVar", @@ -316,6 +318,7 @@ Array [ "useBackgroundQuery", "useFragment", "useLazyQuery", + "useLoadableQuery", "useMutation", "useQuery", "useReactiveVar", From 3b14e59d0df8bbbb349aca1aa861a4412a908701 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 21 Nov 2023 12:23:02 -0700 Subject: [PATCH 100/199] Improve TS checks on toHaveRendered and toHaveRenderedTimes matchers --- src/testing/matchers/index.d.ts | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/testing/matchers/index.d.ts b/src/testing/matchers/index.d.ts index 59d5693d888..dcedef97de1 100644 --- a/src/testing/matchers/index.d.ts +++ b/src/testing/matchers/index.d.ts @@ -16,8 +16,22 @@ interface ApolloCustomMatchers { */ toMatchDocument(document: DocumentNode): R; - toHaveRendered(): R; - toHaveRenderedTimes(count: number): R; + /** + * Used to determine if a profiled component has rendered or not. + */ + toHaveRendered: T extends ProfiledComponent | ProfiledHook + ? () => R + : { error: "matcher needs to be called on a ProfiledComponent instance" }; + + /** + * Used to determine if a profiled component has rendered a specific amount + * of times or not. + */ + toHaveRenderedTimes: T extends + | ProfiledComponent + | ProfiledHook + ? (count: number) => R + : { error: "matcher needs to be called on a ProfiledComponent instance" }; /** * Used to determine if the Suspense cache has a cache entry. From 9ab4e9abb2b78978216d808514c7636858b5e872 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 21 Nov 2023 12:26:17 -0700 Subject: [PATCH 101/199] Revert change to toRerender to use peekRender instead of waitForNextRender --- src/testing/matchers/ProfiledComponent.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/testing/matchers/ProfiledComponent.ts b/src/testing/matchers/ProfiledComponent.ts index 9d9cea288ba..c15ed832c7c 100644 --- a/src/testing/matchers/ProfiledComponent.ts +++ b/src/testing/matchers/ProfiledComponent.ts @@ -18,7 +18,7 @@ export const toRerender: MatcherFunction<[options?: NextRenderOptions]> = const hint = this.utils.matcherHint("toRerender", "ProfiledComponent", ""); let pass = true; try { - await profiled.waitForNextRender({ timeout: 100, ...options }); + await profiled.peekRender({ timeout: 100, ...options }); } catch (e) { if (e instanceof WaitForRenderTimeoutError) { pass = false; From f687f33ece034996f801eac48353ebfa07098e4e Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 21 Nov 2023 12:43:49 -0700 Subject: [PATCH 102/199] Swap order of loadQuery and queryRef values in the return of useLoadableQuery --- .../hooks/__tests__/useLoadableQuery.test.tsx | 142 +++++++++--------- src/react/hooks/useLoadableQuery.ts | 4 +- 2 files changed, 73 insertions(+), 73 deletions(-) diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index 77c30f5d924..4d596454b64 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -161,7 +161,7 @@ function createDefaultProfiledComponents() { const ErrorFallback = profile<{ error: Error | null }, { error: Error }>({ Component: ({ error }) => { - ErrorFallback.updateSnapshot({ error }); + ErrorFallback.replaceSnapshot({ error }); return
Oops
; }, @@ -222,7 +222,7 @@ it("loads a query and suspends when the load query function is called", async () const App = profile({ Component: () => { - const [queryRef, loadQuery] = useLoadableQuery(query); + const [loadQuery, queryRef] = useLoadableQuery(query); return ( <> @@ -266,7 +266,7 @@ it("loads a query with variables and suspends by passing variables to the loadQu const App = profile({ Component: () => { - const [queryRef, loadQuery] = useLoadableQuery(query); + const [loadQuery, queryRef] = useLoadableQuery(query); return ( <> @@ -314,7 +314,7 @@ it("changes variables on a query and resuspends when passing new variables to th const App = profile({ Component: () => { - const [queryRef, loadQuery] = useLoadableQuery(query); + const [loadQuery, queryRef] = useLoadableQuery(query); return ( <> @@ -392,7 +392,7 @@ it("allows the client to be overridden", async () => { createDefaultProfiledComponents(); function App() { - const [queryRef, loadQuery] = useLoadableQuery(query, { + const [loadQuery, queryRef] = useLoadableQuery(query, { client: localClient, }); @@ -446,7 +446,7 @@ it("passes context to the link", async () => { createDefaultProfiledComponents(); function App() { - const [queryRef, loadQuery] = useLoadableQuery(query, { + const [loadQuery, queryRef] = useLoadableQuery(query, { context: { valueA: "A", valueB: "B" }, }); @@ -522,7 +522,7 @@ it('enables canonical results when canonizeResults is "true"', async () => { createDefaultProfiledComponents(); function App() { - const [queryRef, loadQuery] = useLoadableQuery(query, { + const [loadQuery, queryRef] = useLoadableQuery(query, { canonizeResults: true, }); @@ -599,7 +599,7 @@ it("can disable canonical results when the cache's canonizeResults setting is tr createDefaultProfiledComponents(); function App() { - const [queryRef, loadQuery] = useLoadableQuery(query, { + const [loadQuery, queryRef] = useLoadableQuery(query, { canonizeResults: false, }); @@ -655,7 +655,7 @@ it("returns initial cache data followed by network data when the fetch policy is const App = profile({ Component: () => { - const [queryRef, loadQuery] = useLoadableQuery(query, { + const [loadQuery, queryRef] = useLoadableQuery(query, { fetchPolicy: "cache-and-network", }); @@ -722,7 +722,7 @@ it("all data is present in the cache, no network request is made", async () => { const { SuspenseFallback, ReadQueryHook } = createDefaultProfiledComponents(); function App() { - const [queryRef, loadQuery] = useLoadableQuery(query); + const [loadQuery, queryRef] = useLoadableQuery(query); return ( <> @@ -780,7 +780,7 @@ it("partial data is present in the cache so it is ignored and network request is const { SuspenseFallback, ReadQueryHook } = createDefaultProfiledComponents(); function App() { - const [queryRef, loadQuery] = useLoadableQuery(query); + const [loadQuery, queryRef] = useLoadableQuery(query); return ( <> @@ -832,7 +832,7 @@ it("existing data in the cache is ignored when `fetchPolicy` is 'network-only'", const { SuspenseFallback, ReadQueryHook } = createDefaultProfiledComponents(); function App() { - const [queryRef, loadQuery] = useLoadableQuery(query, { + const [loadQuery, queryRef] = useLoadableQuery(query, { fetchPolicy: "network-only", }); @@ -883,7 +883,7 @@ it("fetches data from the network but does not update the cache when `fetchPolic const { SuspenseFallback, ReadQueryHook } = createDefaultProfiledComponents(); function App() { - const [queryRef, loadQuery] = useLoadableQuery(query, { + const [loadQuery, queryRef] = useLoadableQuery(query, { fetchPolicy: "no-cache", }); @@ -968,7 +968,7 @@ it("works with startTransition to change variables", async () => { } function App() { - const [queryRef, loadQuery] = useLoadableQuery(query); + const [loadQuery, queryRef] = useLoadableQuery(query); return (
@@ -1088,7 +1088,7 @@ it('does not suspend deferred queries with data in the cache and using a "cache- createDefaultProfiledComponents(); function App() { - const [queryRef, loadQuery] = useLoadableQuery(query, { + const [loadQuery, queryRef] = useLoadableQuery(query, { fetchPolicy: "cache-and-network", }); return ( @@ -1194,7 +1194,7 @@ it("reacts to cache updates", async () => { createDefaultProfiledComponents(); function App() { - const [queryRef, loadQuery] = useLoadableQuery(query); + const [loadQuery, queryRef] = useLoadableQuery(query); return ( <> @@ -1257,7 +1257,7 @@ it("applies `errorPolicy` on next fetch when it changes between renders", async function App() { const [errorPolicy, setErrorPolicy] = useState("none"); - const [queryRef, loadQuery, { refetch }] = useLoadableQuery(query, { + const [loadQuery, queryRef, { refetch }] = useLoadableQuery(query, { errorPolicy, }); @@ -1352,7 +1352,7 @@ it("applies `context` on next fetch when it changes between renders", async () = function App() { const [phase, setPhase] = React.useState("initial"); - const [queryRef, loadQuery, { refetch }] = useLoadableQuery(query, { + const [loadQuery, queryRef, { refetch }] = useLoadableQuery(query, { context: { phase }, }); @@ -1446,7 +1446,7 @@ it("returns canonical results immediately when `canonizeResults` changes from `f function App() { const [canonizeResults, setCanonizeResults] = React.useState(false); - const [queryRef, loadQuery] = useLoadableQuery(query, { + const [loadQuery, queryRef] = useLoadableQuery(query, { canonizeResults, }); @@ -1548,7 +1548,7 @@ it("applies changed `refetchWritePolicy` to next fetch when changing between ren const [refetchWritePolicy, setRefetchWritePolicy] = React.useState("merge"); - const [queryRef, loadQuery, { refetch }] = useLoadableQuery(query, { + const [loadQuery, queryRef, { refetch }] = useLoadableQuery(query, { refetchWritePolicy, }); @@ -1702,7 +1702,7 @@ it("applies `returnPartialData` on next fetch when it changes between renders", function App() { const [returnPartialData, setReturnPartialData] = React.useState(false); - const [queryRef, loadQuery] = useLoadableQuery(fullQuery, { + const [loadQuery, queryRef] = useLoadableQuery(fullQuery, { returnPartialData, }); @@ -1834,7 +1834,7 @@ it("applies updated `fetchPolicy` on next fetch when it changes between renders" const [fetchPolicy, setFetchPolicy] = React.useState("cache-first"); - const [queryRef, loadQuery, { refetch }] = useLoadableQuery(query, { + const [loadQuery, queryRef, { refetch }] = useLoadableQuery(query, { fetchPolicy, }); @@ -1928,7 +1928,7 @@ it("re-suspends when calling `refetch`", async () => { createDefaultProfiledComponents(); function App() { - const [queryRef, loadQuery, { refetch }] = useLoadableQuery(query); + const [loadQuery, queryRef, { refetch }] = useLoadableQuery(query); return ( <> @@ -1996,7 +1996,7 @@ it("re-suspends when calling `refetch` with new variables", async () => { createDefaultProfiledComponents(); function App() { - const [queryRef, loadQuery, { refetch }] = useLoadableQuery(query); + const [loadQuery, queryRef, { refetch }] = useLoadableQuery(query); return ( <> @@ -2057,7 +2057,7 @@ it("re-suspends multiple times when calling `refetch` multiple times", async () createDefaultProfiledComponents(); function App() { - const [queryRef, loadQuery, { refetch }] = useLoadableQuery(query); + const [loadQuery, queryRef, { refetch }] = useLoadableQuery(query); return ( <> @@ -2112,7 +2112,7 @@ it("throws errors when errors are returned after calling `refetch`", async () => createDefaultProfiledComponents(); function App() { - const [queryRef, loadQuery, { refetch }] = useLoadableQuery(query); + const [loadQuery, queryRef, { refetch }] = useLoadableQuery(query); return ( <> @@ -2166,7 +2166,7 @@ it('ignores errors returned after calling `refetch` when errorPolicy is set to " createDefaultProfiledComponents(); function App() { - const [queryRef, loadQuery, { refetch }] = useLoadableQuery(query, { + const [loadQuery, queryRef, { refetch }] = useLoadableQuery(query, { errorPolicy: "ignore", }); @@ -2225,7 +2225,7 @@ it('returns errors after calling `refetch` when errorPolicy is set to "all"', as createDefaultProfiledComponents(); function App() { - const [queryRef, loadQuery, { refetch }] = useLoadableQuery(query, { + const [loadQuery, queryRef, { refetch }] = useLoadableQuery(query, { errorPolicy: "all", }); @@ -2287,7 +2287,7 @@ it('handles partial data results after calling `refetch` when errorPolicy is set createDefaultProfiledComponents(); function App() { - const [queryRef, loadQuery, { refetch }] = useLoadableQuery(query, { + const [loadQuery, queryRef, { refetch }] = useLoadableQuery(query, { errorPolicy: "all", }); @@ -2370,7 +2370,7 @@ it("`refetch` works with startTransition to allow React to show stale UI until f function App() { const [id, setId] = React.useState("1"); - const [queryRef, loadQuery, { refetch }] = useLoadableQuery(query); + const [loadQuery, queryRef, { refetch }] = useLoadableQuery(query); return ( <> @@ -2457,7 +2457,7 @@ it("re-suspends when calling `fetchMore` with different variables", async () => createDefaultProfiledComponents(); function App() { - const [queryRef, loadQuery, { fetchMore }] = useLoadableQuery(query); + const [loadQuery, queryRef, { fetchMore }] = useLoadableQuery(query); return ( <> @@ -2525,7 +2525,7 @@ it("properly uses `updateQuery` when calling `fetchMore`", async () => { createDefaultProfiledComponents(); function App() { - const [queryRef, loadQuery, { fetchMore }] = useLoadableQuery(query); + const [loadQuery, queryRef, { fetchMore }] = useLoadableQuery(query); return ( <> @@ -2615,7 +2615,7 @@ it("properly uses cache field policies when calling `fetchMore` without `updateQ }); function App() { - const [queryRef, loadQuery, { fetchMore }] = useLoadableQuery(query); + const [loadQuery, queryRef, { fetchMore }] = useLoadableQuery(query); return ( <> @@ -2757,7 +2757,7 @@ it("`fetchMore` works with startTransition to allow React to show stale UI until } function App() { - const [queryRef, loadQuery, { fetchMore }] = useLoadableQuery(query); + const [loadQuery, queryRef, { fetchMore }] = useLoadableQuery(query); return ( <> @@ -2896,7 +2896,7 @@ it('honors refetchWritePolicy set to "merge"', async () => { }); function App() { - const [queryRef, loadQuery, { refetch }] = useLoadableQuery(query, { + const [loadQuery, queryRef, { refetch }] = useLoadableQuery(query, { refetchWritePolicy: "merge", }); @@ -3000,7 +3000,7 @@ it('defaults refetchWritePolicy to "overwrite"', async () => { }); function App() { - const [queryRef, loadQuery, { refetch }] = useLoadableQuery(query); + const [loadQuery, queryRef, { refetch }] = useLoadableQuery(query); return ( <> @@ -3092,7 +3092,7 @@ it('does not suspend when partial data is in the cache and using a "cache-first" const client = new ApolloClient({ link: new MockLink(mocks), cache }); function App() { - const [queryRef, loadQuery] = useLoadableQuery(fullQuery, { + const [loadQuery, queryRef] = useLoadableQuery(fullQuery, { fetchPolicy: "cache-first", returnPartialData: true, }); @@ -3159,7 +3159,7 @@ it('suspends and does not use partial data when changing variables and using a " createDefaultProfiledComponents>(); function App() { - const [queryRef, loadQuery] = useLoadableQuery(query, { + const [loadQuery, queryRef] = useLoadableQuery(query, { fetchPolicy: "cache-first", returnPartialData: true, }); @@ -3258,7 +3258,7 @@ it('suspends when partial data is in the cache and using a "network-only" fetch }); function App() { - const [queryRef, loadQuery] = useLoadableQuery(fullQuery, { + const [loadQuery, queryRef] = useLoadableQuery(fullQuery, { fetchPolicy: "network-only", returnPartialData: true, }); @@ -3332,7 +3332,7 @@ it('suspends when partial data is in the cache and using a "no-cache" fetch poli createDefaultProfiledComponents>(); function App() { - const [queryRef, loadQuery] = useLoadableQuery(fullQuery, { + const [loadQuery, queryRef] = useLoadableQuery(fullQuery, { fetchPolicy: "no-cache", returnPartialData: true, }); @@ -3433,7 +3433,7 @@ it('does not suspend when partial data is in the cache and using a "cache-and-ne createDefaultProfiledComponents>(); function App() { - const [queryRef, loadQuery] = useLoadableQuery(fullQuery, { + const [loadQuery, queryRef] = useLoadableQuery(fullQuery, { fetchPolicy: "cache-and-network", returnPartialData: true, }); @@ -3498,7 +3498,7 @@ it('suspends and does not use partial data when changing variables and using a " createDefaultProfiledComponents>(); function App() { - const [queryRef, loadQuery] = useLoadableQuery(query, { + const [loadQuery, queryRef] = useLoadableQuery(query, { fetchPolicy: "cache-and-network", returnPartialData: true, }); @@ -3605,7 +3605,7 @@ it('does not suspend deferred queries with partial data in the cache and using a createDefaultProfiledComponents>(); function App() { - const [queryRef, loadTodo] = useLoadableQuery(query, { + const [loadTodo, queryRef] = useLoadableQuery(query, { fetchPolicy: "cache-first", returnPartialData: true, }); @@ -3702,7 +3702,7 @@ describe.skip("type tests", () => { it("returns unknown when TData cannot be inferred", () => { const query = gql``; - const [queryRef] = useLoadableQuery(query); + const [, queryRef] = useLoadableQuery(query); invariant(queryRef); @@ -3714,7 +3714,7 @@ describe.skip("type tests", () => { it("variables are optional and can be anything with an untyped DocumentNode", () => { const query = gql``; - const [, loadQuery] = useLoadableQuery(query); + const [loadQuery] = useLoadableQuery(query); loadQuery(); loadQuery({}); @@ -3725,7 +3725,7 @@ describe.skip("type tests", () => { it("variables are optional and can be anything with unspecified TVariables on a TypedDocumentNode", () => { const query: TypedDocumentNode<{ greeting: string }> = gql``; - const [, loadQuery] = useLoadableQuery(query); + const [loadQuery] = useLoadableQuery(query); loadQuery(); loadQuery({}); @@ -3739,7 +3739,7 @@ describe.skip("type tests", () => { Record > = gql``; - const [, loadQuery] = useLoadableQuery(query); + const [loadQuery] = useLoadableQuery(query); loadQuery(); loadQuery({}); @@ -3750,7 +3750,7 @@ describe.skip("type tests", () => { it("does not allow variables when TVariables is `never`", () => { const query: TypedDocumentNode<{ greeting: string }, never> = gql``; - const [, loadQuery] = useLoadableQuery(query); + const [loadQuery] = useLoadableQuery(query); loadQuery(); // @ts-expect-error no variables argument allowed @@ -3765,7 +3765,7 @@ describe.skip("type tests", () => { { limit?: number } > = gql``; - const [, loadQuery] = useLoadableQuery(query); + const [loadQuery] = useLoadableQuery(query); loadQuery(); loadQuery({}); @@ -3787,7 +3787,7 @@ describe.skip("type tests", () => { { id: string } > = gql``; - const [, loadQuery] = useLoadableQuery(query); + const [loadQuery] = useLoadableQuery(query); // @ts-expect-error missing variables argument loadQuery(); @@ -3811,7 +3811,7 @@ describe.skip("type tests", () => { { id: string; language?: string } > = gql``; - const [, loadQuery] = useLoadableQuery(query); + const [loadQuery] = useLoadableQuery(query); // @ts-expect-error missing variables argument loadQuery(); @@ -3842,7 +3842,7 @@ describe.skip("type tests", () => { const { query } = useVariablesQueryCase(); { - const [queryRef] = useLoadableQuery(query); + const [, queryRef] = useLoadableQuery(query); invariant(queryRef); @@ -3852,7 +3852,7 @@ describe.skip("type tests", () => { } { - const [queryRef] = useLoadableQuery< + const [, queryRef] = useLoadableQuery< VariablesCaseData, VariablesCaseVariables >(query); @@ -3869,7 +3869,7 @@ describe.skip("type tests", () => { const { query } = useVariablesQueryCase(); { - const [queryRef] = useLoadableQuery(query, { + const [, queryRef] = useLoadableQuery(query, { errorPolicy: "ignore", }); @@ -3881,7 +3881,7 @@ describe.skip("type tests", () => { } { - const [queryRef] = useLoadableQuery< + const [, queryRef] = useLoadableQuery< VariablesCaseData, VariablesCaseVariables >(query, { errorPolicy: "ignore" }); @@ -3898,7 +3898,7 @@ describe.skip("type tests", () => { const { query } = useVariablesQueryCase(); { - const [queryRef] = useLoadableQuery(query, { + const [, queryRef] = useLoadableQuery(query, { errorPolicy: "all", }); @@ -3910,7 +3910,7 @@ describe.skip("type tests", () => { } { - const [queryRef] = useLoadableQuery< + const [, queryRef] = useLoadableQuery< VariablesCaseData, VariablesCaseVariables >(query, { errorPolicy: "all" }); @@ -3927,7 +3927,7 @@ describe.skip("type tests", () => { const { query } = useVariablesQueryCase(); { - const [queryRef] = useLoadableQuery(query, { + const [, queryRef] = useLoadableQuery(query, { errorPolicy: "none", }); @@ -3939,7 +3939,7 @@ describe.skip("type tests", () => { } { - const [queryRef] = useLoadableQuery< + const [, queryRef] = useLoadableQuery< VariablesCaseData, VariablesCaseVariables >(query, { errorPolicy: "none" }); @@ -3956,7 +3956,7 @@ describe.skip("type tests", () => { const { query } = useVariablesQueryCase(); { - const [queryRef] = useLoadableQuery(query, { + const [, queryRef] = useLoadableQuery(query, { returnPartialData: true, }); @@ -3968,7 +3968,7 @@ describe.skip("type tests", () => { } { - const [queryRef] = useLoadableQuery< + const [, queryRef] = useLoadableQuery< VariablesCaseData, VariablesCaseVariables >(query, { returnPartialData: true }); @@ -3985,7 +3985,7 @@ describe.skip("type tests", () => { const { query } = useVariablesQueryCase(); { - const [queryRef] = useLoadableQuery(query, { + const [, queryRef] = useLoadableQuery(query, { returnPartialData: false, }); @@ -3997,7 +3997,7 @@ describe.skip("type tests", () => { } { - const [queryRef] = useLoadableQuery< + const [, queryRef] = useLoadableQuery< VariablesCaseData, VariablesCaseVariables >(query, { returnPartialData: false }); @@ -4014,7 +4014,7 @@ describe.skip("type tests", () => { const { query } = useVariablesQueryCase(); { - const [queryRef] = useLoadableQuery(query, { + const [, queryRef] = useLoadableQuery(query, { fetchPolicy: "no-cache", }); @@ -4026,7 +4026,7 @@ describe.skip("type tests", () => { } { - const [queryRef] = useLoadableQuery< + const [, queryRef] = useLoadableQuery< VariablesCaseData, VariablesCaseVariables >(query, { fetchPolicy: "no-cache" }); @@ -4043,7 +4043,7 @@ describe.skip("type tests", () => { const { query } = useVariablesQueryCase(); { - const [queryRef] = useLoadableQuery(query, { + const [, queryRef] = useLoadableQuery(query, { returnPartialData: true, errorPolicy: "ignore", }); @@ -4058,7 +4058,7 @@ describe.skip("type tests", () => { } { - const [queryRef] = useLoadableQuery< + const [, queryRef] = useLoadableQuery< VariablesCaseData, VariablesCaseVariables >(query, { returnPartialData: true, errorPolicy: "ignore" }); @@ -4073,7 +4073,7 @@ describe.skip("type tests", () => { } { - const [queryRef] = useLoadableQuery(query, { + const [, queryRef] = useLoadableQuery(query, { returnPartialData: true, errorPolicy: "none", }); @@ -4086,7 +4086,7 @@ describe.skip("type tests", () => { } { - const [queryRef] = useLoadableQuery< + const [, queryRef] = useLoadableQuery< VariablesCaseData, VariablesCaseVariables >(query, { returnPartialData: true, errorPolicy: "none" }); @@ -4103,7 +4103,7 @@ describe.skip("type tests", () => { const { query } = useVariablesQueryCase(); { - const [queryRef] = useLoadableQuery(query, { + const [, queryRef] = useLoadableQuery(query, { fetchPolicy: "no-cache", returnPartialData: true, errorPolicy: "none", @@ -4117,7 +4117,7 @@ describe.skip("type tests", () => { } { - const [queryRef] = useLoadableQuery< + const [, queryRef] = useLoadableQuery< VariablesCaseData, VariablesCaseVariables >(query, { diff --git a/src/react/hooks/useLoadableQuery.ts b/src/react/hooks/useLoadableQuery.ts index 36616860e07..a6bdef3f17c 100644 --- a/src/react/hooks/useLoadableQuery.ts +++ b/src/react/hooks/useLoadableQuery.ts @@ -39,8 +39,8 @@ export type UseLoadableQueryResult< TData = unknown, TVariables extends OperationVariables = OperationVariables, > = [ - QueryReference | null, LoadQuery, + QueryReference | null, { fetchMore: FetchMoreFunction; refetch: RefetchFunction; @@ -194,8 +194,8 @@ export function useLoadableQuery< return React.useMemo(() => { return [ - queryRef && wrapQueryRef(queryRef), loadQuery, + queryRef && wrapQueryRef(queryRef), { fetchMore, refetch }, ]; }, [queryRef, loadQuery, fetchMore, refetch]); From e5ed115e475aa6473c13ce421d240b0e572e3cec Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 21 Nov 2023 13:12:39 -0700 Subject: [PATCH 103/199] Fix toHaveRendered matcher with updated profiler API --- src/testing/matchers/toHaveRendered.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/testing/matchers/toHaveRendered.ts b/src/testing/matchers/toHaveRendered.ts index 0ad53b804a8..e1c2741c4aa 100644 --- a/src/testing/matchers/toHaveRendered.ts +++ b/src/testing/matchers/toHaveRendered.ts @@ -1,14 +1,17 @@ import type { MatcherFunction } from "expect"; -import type { ProfiledComponent, ProfiledHook } from "../internal/index.js"; +import type { ProfiledComponent } from "../internal/index.js"; +import type { ProfiledHook } from "../internal/index.js"; + +export const toHaveRendered: MatcherFunction = function (actual) { + let ProfiledComponent = actual as + | ProfiledComponent + | ProfiledHook; -export const toHaveRendered: MatcherFunction = function ( - ProfiledComponent: ProfiledComponent | ProfiledHook -) { if ("ProfiledComponent" in ProfiledComponent) { ProfiledComponent = ProfiledComponent.ProfiledComponent; } - const pass = ProfiledComponent.currentRenderCount() > 0; + const pass = ProfiledComponent.totalRenderCount() > 0; const hint = this.utils.matcherHint( "toHaveRendered", From f730fb8812dfaafcbd678315fda571cd048972bf Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 21 Nov 2023 13:13:56 -0700 Subject: [PATCH 104/199] Fix toHaveRenderedTimes matcher to use updated profiler API --- src/testing/matchers/toHaveRenderedTimes.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/testing/matchers/toHaveRenderedTimes.ts b/src/testing/matchers/toHaveRenderedTimes.ts index daf411c0fbf..ffd6e775126 100644 --- a/src/testing/matchers/toHaveRenderedTimes.ts +++ b/src/testing/matchers/toHaveRenderedTimes.ts @@ -2,14 +2,18 @@ import type { MatcherFunction } from "expect"; import type { ProfiledComponent, ProfiledHook } from "../internal/index.js"; export const toHaveRenderedTimes: MatcherFunction<[count: number]> = function ( - ProfiledComponent: ProfiledComponent | ProfiledHook, - count: number + actual, + count ) { + let ProfiledComponent = actual as + | ProfiledComponent + | ProfiledHook; + if ("ProfiledComponent" in ProfiledComponent) { ProfiledComponent = ProfiledComponent.ProfiledComponent; } - const actualRenderCount = ProfiledComponent.currentRenderCount(); + const actualRenderCount = ProfiledComponent.totalRenderCount(); const pass = actualRenderCount === count; const hint = this.utils.matcherHint( From 42aae31ed0cab2f88e1a6828a6fc4e03a035d990 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 21 Nov 2023 13:17:35 -0700 Subject: [PATCH 105/199] Fix missed generic arg passed to UseLoadableQueryResult --- src/react/hooks/useLoadableQuery.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/react/hooks/useLoadableQuery.ts b/src/react/hooks/useLoadableQuery.ts index a6bdef3f17c..463df8ee85f 100644 --- a/src/react/hooks/useLoadableQuery.ts +++ b/src/react/hooks/useLoadableQuery.ts @@ -110,7 +110,7 @@ export function useLoadableQuery< >( query: DocumentNode | TypedDocumentNode, options: LoadableQueryHookOptions = Object.create(null) -): UseLoadableQueryResult { +): UseLoadableQueryResult { const client = useApolloClient(options.client); const suspenseCache = getSuspenseCache(client); const watchQueryOptions = useWatchQueryOptions({ client, query, options }); From 16cd906990859ea19a970243aa0a723ffef57d7d Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 21 Nov 2023 13:18:04 -0700 Subject: [PATCH 106/199] Fix misc type errors in useLoadableQuery with strict mode enabled --- src/react/hooks/useLoadableQuery.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/react/hooks/useLoadableQuery.ts b/src/react/hooks/useLoadableQuery.ts index 463df8ee85f..da1ca9eb114 100644 --- a/src/react/hooks/useLoadableQuery.ts +++ b/src/react/hooks/useLoadableQuery.ts @@ -1,8 +1,10 @@ import * as React from "rehackt"; import type { DocumentNode, + FetchMoreQueryOptions, OperationVariables, TypedDocumentNode, + WatchQueryOptions, } from "../../core/index.js"; import { useApolloClient } from "./useApolloClient.js"; import { wrapQueryRef } from "../cache/QueryReference.js"; @@ -142,7 +144,9 @@ export function useLoadableQuery< ); } - const promise = queryRef.fetchMore(options); + const promise = queryRef.fetchMore( + options as FetchMoreQueryOptions + ); setPromiseCache((promiseCache) => new Map(promiseCache).set(queryRef.key, queryRef.promise) @@ -183,7 +187,10 @@ export function useLoadableQuery< ]; const queryRef = suspenseCache.getQueryRef(cacheKey, () => - client.watchQuery({ ...watchQueryOptions, variables }) + client.watchQuery({ + ...watchQueryOptions, + variables, + } as WatchQueryOptions) ); promiseCache.set(queryRef.key, queryRef.promise); From 69e47ce0e5c786686b504ce957131e2e9991979a Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Tue, 21 Nov 2023 21:31:47 +0100 Subject: [PATCH 107/199] Profiler test render tracking (#11378) --- .../hooks/__tests__/useLoadableQuery.test.tsx | 31 ++++-- src/testing/internal/profile/Render.tsx | 5 +- src/testing/internal/profile/profile.tsx | 105 ++++++++++++++++-- 3 files changed, 119 insertions(+), 22 deletions(-) diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index 4d596454b64..fe63360bd69 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -151,16 +151,18 @@ function usePaginatedQueryCase() { function createDefaultProfiledComponents() { const SuspenseFallback = profile({ - Component: () =>

Loading

, + Component: function SuspenseFallback() { + return

Loading

; + }, }); const ReadQueryHook = profileHook< UseReadQueryResult, { queryRef: QueryReference } - >(({ queryRef }) => useReadQuery(queryRef)); + >(({ queryRef }) => useReadQuery(queryRef), { displayName: "UseReadQuery" }); const ErrorFallback = profile<{ error: Error | null }, { error: Error }>({ - Component: ({ error }) => { + Component: function Fallback({ error }) { ErrorFallback.replaceSnapshot({ error }); return
Oops
; @@ -265,7 +267,7 @@ it("loads a query with variables and suspends by passing variables to the loadQu createDefaultProfiledComponents(); const App = profile({ - Component: () => { + Component: function App() { const [loadQuery, queryRef] = useLoadableQuery(query); return ( @@ -281,13 +283,22 @@ it("loads a query with variables and suspends by passing variables to the loadQu const { user } = renderWithMocks(, { mocks }); - expect(SuspenseFallback).not.toHaveRendered(); + { + const { renderedComponents } = await App.takeRender(); + expect(renderedComponents).toStrictEqual(["App"]); + } await act(() => user.click(screen.getByText("Load query"))); - expect(SuspenseFallback).toHaveRendered(); - expect(ReadQueryHook).not.toHaveRendered(); - expect(App).toHaveRenderedTimes(2); + { + const { renderedComponents } = await App.takeRender(); + expect(renderedComponents).toStrictEqual(["App", "SuspenseFallback"]); + } + + { + const { renderedComponents } = await App.takeRender(); + expect(renderedComponents).toStrictEqual(["UseReadQuery"]); + } { const snapshot = await ReadQueryHook.takeSnapshot(); @@ -299,10 +310,6 @@ it("loads a query with variables and suspends by passing variables to the loadQu }); } - expect(SuspenseFallback).toHaveRenderedTimes(1); - expect(ReadQueryHook).toHaveRenderedTimes(1); - expect(App).toHaveRenderedTimes(3); - await expect(App).not.toRerender(); }); diff --git a/src/testing/internal/profile/Render.tsx b/src/testing/internal/profile/Render.tsx index 24b4737c2c0..ee4d0853431 100644 --- a/src/testing/internal/profile/Render.tsx +++ b/src/testing/internal/profile/Render.tsx @@ -62,6 +62,8 @@ export interface Render extends BaseRender { * ``` */ withinDOM: () => SyncScreen; + + renderedComponents: string[]; } /** @internal */ @@ -77,7 +79,8 @@ export class RenderInstance implements Render { constructor( baseRender: BaseRender, public snapshot: Snapshot, - private stringifiedDOM: string | undefined + private stringifiedDOM: string | undefined, + public renderedComponents: string[] ) { this.id = baseRender.id; this.phase = baseRender.phase; diff --git a/src/testing/internal/profile/profile.tsx b/src/testing/internal/profile/profile.tsx index 8ae43b64c01..f734bfca9eb 100644 --- a/src/testing/internal/profile/profile.tsx +++ b/src/testing/internal/profile/profile.tsx @@ -81,6 +81,13 @@ interface ProfiledComponentFields { waitForNextRender(options?: NextRenderOptions): Promise>; } +interface ProfilerContextValue { + renderedComponents: string[]; +} +const ProfilerContext = React.createContext( + undefined +); + /** @internal */ export function profile< Snapshot extends ValidSnapshot = void, @@ -133,6 +140,10 @@ export function profile< })); }; + const profilerContext: ProfilerContextValue = { + renderedComponents: [], + }; + const profilerOnRender: React.ProfilerOnRenderCallback = ( id, phase, @@ -169,7 +180,13 @@ export function profile< const domSnapshot = snapshotDOM ? window.document.body.innerHTML : undefined; - const render = new RenderInstance(baseRender, snapshot, domSnapshot); + const render = new RenderInstance( + baseRender, + snapshot, + domSnapshot, + profilerContext.renderedComponents + ); + profilerContext.renderedComponents = []; Profiled.renders.push(render); resolveNextRender?.(render); } catch (error) { @@ -184,13 +201,20 @@ export function profile< } }; + const Wrapped = wrapComponentWithTracking(Component); + let iteratorPosition = 0; const Profiled: ProfiledComponent = Object.assign( - (props: Props) => ( - - - - ), + (props: Props) => { + const parentContext = React.useContext(ProfilerContext); + return ( + + + + + + ); + }, { replaceSnapshot, mergeSnapshot, @@ -325,15 +349,17 @@ export interface ProfiledHook /** @internal */ export function profileHook( - renderCallback: (props: Props) => ReturnValue + renderCallback: (props: Props) => ReturnValue, + { displayName = renderCallback.name || "ProfiledHook" } = {} ): ProfiledHook { let returnValue: ReturnValue; - const Component = (props: Props) => { + const ProfiledHook = (props: Props) => { ProfiledComponent.replaceSnapshot(renderCallback(props)); return null; }; + ProfiledHook.displayName = displayName; const ProfiledComponent = profile({ - Component, + Component: ProfiledHook, onRender: () => returnValue, }); return Object.assign( @@ -361,3 +387,64 @@ export function profileHook( } satisfies ProfiledHookFields ); } + +function isReactClass( + Component: React.ComponentType +): Component is React.ComponentClass { + let proto = Component; + while (proto && proto !== Object) { + if (proto === React.Component) return true; + proto = Object.getPrototypeOf(proto); + } + return false; +} + +function getCurrentComponentName() { + const owner: React.ComponentType | undefined = (React as any) + .__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED?.ReactCurrentOwner + ?.current?.elementType; + if (owner) return owner?.displayName || owner?.name; + + try { + throw new Error(); + } catch (e) { + return (e as Error).stack?.split("\n")[1].split(":")[0] || ""; + } +} + +export function useTrackComponentRender(name = getCurrentComponentName()) { + const ctx = React.useContext(ProfilerContext); + React.useLayoutEffect(() => { + ctx?.renderedComponents.unshift(name); + }); +} + +function wrapComponentWithTracking( + Component: React.ComponentType +) { + if (!isReactClass(Component)) { + return function ComponentWithTracking(props: Props) { + useTrackComponentRender(Component.displayName || Component.name); + return Component(props); + }; + } + + let ctx: ProfilerContextValue; + class WrapperClass extends (Component as React.ComponentClass) { + constructor(props: Props) { + super(props); + } + componentDidMount() { + super.componentDidMount?.apply(this, arguments); + ctx!.renderedComponents.push(Component.displayName || Component.name); + } + componentDidUpdate() { + super.componentDidUpdate?.apply(this, arguments); + ctx!.renderedComponents.push(Component.displayName || Component.name); + } + } + return (props: any) => { + ctx = React.useContext(ProfilerContext)!; + return ; + }; +} From 4479039abe2c824b924e7f78c904d529ec20349c Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 21 Nov 2023 13:32:18 -0700 Subject: [PATCH 108/199] Remove duplicate export of RefetchWritePolicy --- src/core/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/core/index.ts b/src/core/index.ts index fd7d53aec12..5757cdb2071 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -19,7 +19,6 @@ export type { ErrorPolicy, FetchMoreQueryOptions, SubscribeToMoreOptions, - RefetchWritePolicy, } from "./watchQueryOptions.js"; export { NetworkStatus, isNetworkRequestSettled } from "./networkStatus.js"; export * from "./types.js"; From 3154b3a1a619c3ab1fc35435938273d67a0eb657 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 21 Nov 2023 13:36:53 -0700 Subject: [PATCH 109/199] Fix types on arguments for class wrapper --- src/testing/internal/profile/profile.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/testing/internal/profile/profile.tsx b/src/testing/internal/profile/profile.tsx index f734bfca9eb..7ed77e9d77e 100644 --- a/src/testing/internal/profile/profile.tsx +++ b/src/testing/internal/profile/profile.tsx @@ -435,11 +435,16 @@ function wrapComponentWithTracking( super(props); } componentDidMount() { - super.componentDidMount?.apply(this, arguments); + super.componentDidMount?.apply(this); ctx!.renderedComponents.push(Component.displayName || Component.name); } componentDidUpdate() { - super.componentDidUpdate?.apply(this, arguments); + super.componentDidUpdate?.apply( + this, + arguments as unknown as Parameters< + NonNullable["componentDidUpdate"]> + > + ); ctx!.renderedComponents.push(Component.displayName || Component.name); } } From ede5c5d5c599cd367ed7988ec90376e94baf7f9f Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 21 Nov 2023 13:38:57 -0700 Subject: [PATCH 110/199] Update size-limit --- .size-limits.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.size-limits.json b/.size-limits.json index 52108cf4fb9..ca38563abdc 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 38164, + "dist/apollo-client.min.cjs": 38452, "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32188 } From 6f27913aa06c6ab84a7711a6da39c887e93199c1 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 21 Nov 2023 13:39:46 -0700 Subject: [PATCH 111/199] Rerun api extractor --- .api-reports/api-report-react.md | 2 +- .api-reports/api-report-react_hooks.md | 2 +- .api-reports/api-report.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.api-reports/api-report-react.md b/.api-reports/api-report-react.md index b0a246a6760..ffb809098ab 100644 --- a/.api-reports/api-report-react.md +++ b/.api-reports/api-report-react.md @@ -2153,8 +2153,8 @@ export function useLoadableQuery = [ -QueryReference | null, LoadQuery, +QueryReference | null, { fetchMore: FetchMoreFunction; refetch: RefetchFunction; diff --git a/.api-reports/api-report-react_hooks.md b/.api-reports/api-report-react_hooks.md index 8bd65102160..4267b053b3a 100644 --- a/.api-reports/api-report-react_hooks.md +++ b/.api-reports/api-report-react_hooks.md @@ -2044,8 +2044,8 @@ export function useLoadableQuery = [ -QueryReference | null, LoadQuery, +QueryReference | null, { fetchMore: FetchMoreFunction; refetch: RefetchFunction; diff --git a/.api-reports/api-report.md b/.api-reports/api-report.md index b3586d1bfe6..47f488b186e 100644 --- a/.api-reports/api-report.md +++ b/.api-reports/api-report.md @@ -2799,8 +2799,8 @@ export function useLoadableQuery = [ -QueryReference | null, LoadQuery, +QueryReference | null, { fetchMore: FetchMoreFunction; refetch: RefetchFunction; From f8187683274b24291a6b9b8ad41fadde24944fd5 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 21 Nov 2023 15:02:50 -0700 Subject: [PATCH 112/199] Add check to ensure hook does not rerender --- src/react/hooks/__tests__/useLoadableQuery.test.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index fe63360bd69..f749c9f04db 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -754,6 +754,8 @@ it("all data is present in the cache, no network request is made", async () => { networkStatus: NetworkStatus.ready, error: undefined, }); + + expect(ReadQueryHook).not.toRerender(); }); it("partial data is present in the cache so it is ignored and network request is made", async () => { From b84f0d56a33758e1a34f61b5aea6c49fb670a1f7 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 21 Nov 2023 15:04:11 -0700 Subject: [PATCH 113/199] Beef up description of test that was confusing --- src/react/hooks/__tests__/useLoadableQuery.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index f749c9f04db..26794cd4ad7 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -3145,7 +3145,7 @@ it('does not suspend when partial data is in the cache and using a "cache-first" expect(SuspenseFallback).not.toHaveRendered(); }); -it('suspends and does not use partial data when changing variables and using a "cache-first" fetch policy with returnPartialData', async () => { +it('suspends and does not use partial data from other variables in the cache when changing variables and using a "cache-first" fetch policy with returnPartialData: true', async () => { const { query, mocks } = useVariablesQueryCase(); const partialQuery = gql` From 9a6c748895a3bb0fd92113f10e79c46bbd95774a Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 21 Nov 2023 16:08:04 -0700 Subject: [PATCH 114/199] Prevent loadQuery from being called in render --- .../hooks/__tests__/useLoadableQuery.test.tsx | 70 ++++++++++++++++++- src/react/hooks/useLoadableQuery.ts | 14 ++++ 2 files changed, 83 insertions(+), 1 deletion(-) diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index 26794cd4ad7..956078b0adf 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -42,7 +42,7 @@ import { InMemoryCache } from "../../../cache"; import { LoadableQueryHookFetchPolicy } from "../../types/types"; import { QueryReference } from "../../../react"; import { FetchMoreFunction, RefetchFunction } from "../useSuspenseQuery"; -import invariant from "ts-invariant"; +import invariant, { InvariantError } from "ts-invariant"; import { profile, profileHook, spyOnConsole } from "../../../testing/internal"; interface SimpleQueryData { @@ -3707,6 +3707,74 @@ it('does not suspend deferred queries with partial data in the cache and using a } }); +it("throws when calling loadQuery on first render", async () => { + using _consoleSpy = spyOnConsole("error"); + const { query, mocks } = useSimpleQueryCase(); + + function App() { + const [loadQuery] = useLoadableQuery(query); + + loadQuery(); + + return null; + } + + expect(() => renderWithMocks(, { mocks })).toThrow( + new InvariantError( + "useLoadableQuery: loadQuery should not be called during render. To load a query during render, use `useBackgroundQuery`." + ) + ); +}); + +it("throws when calling loadQuery on subsequent render", async () => { + using _consoleSpy = spyOnConsole("error"); + const { query, mocks } = useSimpleQueryCase(); + + let error!: Error; + + function App() { + const [count, setCount] = useState(0); + const [loadQuery] = useLoadableQuery(query); + + if (count === 1) { + loadQuery(); + } + + return ; + } + + const { user } = renderWithMocks( + (error = e)} fallback={
Oops
}> + +
, + { mocks } + ); + + await act(() => user.click(screen.getByText("Load query in render"))); + + expect(error).toEqual( + new InvariantError( + "useLoadableQuery: loadQuery should not be called during render. To load a query during render, use `useBackgroundQuery`." + ) + ); +}); + +it("allows loadQuery to be called in useEffect on first render", async () => { + const { query, mocks } = useSimpleQueryCase(); + + function App() { + const [loadQuery] = useLoadableQuery(query); + + React.useEffect(() => { + loadQuery(); + }, []); + + return null; + } + + expect(() => renderWithMocks(, { mocks })).not.toThrow(); +}); + describe.skip("type tests", () => { it("returns unknown when TData cannot be inferred", () => { const query = gql``; diff --git a/src/react/hooks/useLoadableQuery.ts b/src/react/hooks/useLoadableQuery.ts index da1ca9eb114..0aeb9b051fe 100644 --- a/src/react/hooks/useLoadableQuery.ts +++ b/src/react/hooks/useLoadableQuery.ts @@ -20,6 +20,9 @@ import type { FetchMoreFunction, RefetchFunction } from "./useSuspenseQuery.js"; import { canonicalStringify } from "../../cache/index.js"; import type { DeepPartial } from "../../utilities/index.js"; import type { CacheKey } from "../cache/types.js"; +import { invariant } from "../../utilities/globals/index.js"; + +let RenderDispatcher: unknown = null; type OnlyRequiredProperties = { [K in keyof T as {} extends Pick ? never : K]: T[K]; @@ -113,6 +116,7 @@ export function useLoadableQuery< query: DocumentNode | TypedDocumentNode, options: LoadableQueryHookOptions = Object.create(null) ): UseLoadableQueryResult { + RenderDispatcher = getRenderDispatcher(); const client = useApolloClient(options.client); const suspenseCache = getSuspenseCache(client); const watchQueryOptions = useWatchQueryOptions({ client, query, options }); @@ -178,6 +182,11 @@ export function useLoadableQuery< const loadQuery: LoadQuery = React.useCallback( (...args) => { + invariant( + getRenderDispatcher() !== RenderDispatcher, + "useLoadableQuery: loadQuery should not be called during render. To load a query during render, use `useBackgroundQuery`." + ); + const [variables] = args; const cacheKey: CacheKey = [ @@ -207,3 +216,8 @@ export function useLoadableQuery< ]; }, [queryRef, loadQuery, fetchMore, refetch]); } + +function getRenderDispatcher() { + return (React as any).__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED + ?.ReactCurrentDispatcher?.current; +} From 3e6843444ada786bcb58f7ef83f7ee34c56c116d Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 21 Nov 2023 16:11:55 -0700 Subject: [PATCH 115/199] Update size-limits --- .size-limits.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.size-limits.json b/.size-limits.json index ca38563abdc..8985dcaa79e 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 38452, - "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32188 + "dist/apollo-client.min.cjs": 38569, + "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32189 } From 562c3bb6e17da001c958b0e3ae3cc644a2550888 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 21 Nov 2023 18:31:19 -0700 Subject: [PATCH 116/199] Make profile return wrapper component without taking internal component --- .../hooks/__tests__/useLoadableQuery.test.tsx | 107 +++++++++++------- src/testing/internal/profile/index.ts | 7 +- src/testing/internal/profile/profile.tsx | 43 +++---- 3 files changed, 97 insertions(+), 60 deletions(-) diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index 956078b0adf..341475a472f 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -43,7 +43,12 @@ import { LoadableQueryHookFetchPolicy } from "../../types/types"; import { QueryReference } from "../../../react"; import { FetchMoreFunction, RefetchFunction } from "../useSuspenseQuery"; import invariant, { InvariantError } from "ts-invariant"; -import { profile, profileHook, spyOnConsole } from "../../../testing/internal"; +import { + ProfiledComponent, + profile, + spyOnConsole, + useTrackComponentRender, +} from "../../../testing/internal"; interface SimpleQueryData { greeting: string; @@ -149,17 +154,25 @@ function usePaginatedQueryCase() { return { query, link, client }; } -function createDefaultProfiledComponents() { - const SuspenseFallback = profile({ - Component: function SuspenseFallback() { - return

Loading

; - }, - }); +function createDefaultProfiledComponents< + Snapshot extends { result: UseReadQueryResult | null }, + TData = Snapshot["result"] extends UseReadQueryResult | null + ? TData + : unknown, +>(profiler: ProfiledComponent) { + function SuspenseFallback() { + useTrackComponentRender(); + return

Loading

; + } + + function ReadQueryHook({ queryRef }: { queryRef: QueryReference }) { + useTrackComponentRender(); + profiler.mergeSnapshot({ + result: useReadQuery(queryRef), + } as Partial); - const ReadQueryHook = profileHook< - UseReadQueryResult, - { queryRef: QueryReference } - >(({ queryRef }) => useReadQuery(queryRef), { displayName: "UseReadQuery" }); + return null; + } const ErrorFallback = profile<{ error: Error | null }, { error: Error }>({ Component: function Fallback({ error }) { @@ -219,45 +232,61 @@ function renderWithClient( it("loads a query and suspends when the load query function is called", async () => { const { query, mocks } = useSimpleQueryCase(); + const Profiler = profile({ + initialSnapshot: { + result: null as UseReadQueryResult | null, + }, + }); + const { SuspenseFallback, ReadQueryHook } = - createDefaultProfiledComponents(); + createDefaultProfiledComponents(Profiler); - const App = profile({ - Component: () => { - const [loadQuery, queryRef] = useLoadableQuery(query); + function App() { + useTrackComponentRender(); + const [loadQuery, queryRef] = useLoadableQuery(query); - return ( - <> - - }> - {queryRef && } - - - ); - }, - }); + return ( + <> + + }> + {queryRef && } + + + ); + } - const { user } = renderWithMocks(, { mocks }); + const { user } = renderWithMocks( + + + , + { mocks } + ); - expect(SuspenseFallback).not.toHaveRendered(); + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual(["App"]); + } await act(() => user.click(screen.getByText("Load query"))); - expect(SuspenseFallback).toHaveRendered(); - expect(ReadQueryHook).not.toHaveRendered(); - expect(App).toHaveRenderedTimes(2); + { + const { renderedComponents } = await Profiler.takeRender(); - const snapshot = await ReadQueryHook.takeSnapshot(); + expect(renderedComponents).toStrictEqual(["App", "SuspenseFallback"]); + } - expect(snapshot).toEqual({ - data: { greeting: "Hello" }, - error: undefined, - networkStatus: NetworkStatus.ready, - }); + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); - expect(SuspenseFallback).toHaveRenderedTimes(1); - expect(ReadQueryHook).toHaveRenderedTimes(1); - expect(App).toHaveRenderedTimes(3); + expect(renderedComponents).toStrictEqual(["ReadQueryHook"]); + } }); it("loads a query with variables and suspends by passing variables to the loadQuery function", async () => { diff --git a/src/testing/internal/profile/index.ts b/src/testing/internal/profile/index.ts index 01bb526c52c..b6aadc4ed3a 100644 --- a/src/testing/internal/profile/index.ts +++ b/src/testing/internal/profile/index.ts @@ -3,6 +3,11 @@ export type { ProfiledComponent, ProfiledHook, } from "./profile.js"; -export { profile, profileHook, WaitForRenderTimeoutError } from "./profile.js"; +export { + profile, + profileHook, + useTrackComponentRender, + WaitForRenderTimeoutError, +} from "./profile.js"; export type { SyncScreen } from "./Render.js"; diff --git a/src/testing/internal/profile/profile.tsx b/src/testing/internal/profile/profile.tsx index 7ed77e9d77e..1812ce6fe2a 100644 --- a/src/testing/internal/profile/profile.tsx +++ b/src/testing/internal/profile/profile.tsx @@ -20,10 +20,15 @@ export interface NextRenderOptions { } /** @internal */ -export interface ProfiledComponent - extends React.FC, - ProfiledComponentFields, - ProfiledComponentOnlyFields {} +interface ProfilerProps { + children: React.ReactNode; +} + +/** @internal */ +export interface ProfiledComponent + extends React.FC, + ProfiledComponentFields, + ProfiledComponentOnlyFields {} interface ReplaceSnapshot { (newSnapshot: Snapshot): void; @@ -39,13 +44,13 @@ interface MergeSnapshot { ): void; } -interface ProfiledComponentOnlyFields { +interface ProfiledComponentOnlyFields { // Allows for partial updating of the snapshot by shallow merging the results mergeSnapshot: MergeSnapshot; // Performs a full replacement of the snapshot replaceSnapshot: ReplaceSnapshot; } -interface ProfiledComponentFields { +interface ProfiledComponentFields { /** * An array of all renders that have happened so far. * Errors thrown during component render will be captured here, too. @@ -89,16 +94,11 @@ const ProfilerContext = React.createContext( ); /** @internal */ -export function profile< - Snapshot extends ValidSnapshot = void, - Props = Record, ->({ - Component, +export function profile({ onRender, snapshotDOM = false, initialSnapshot, }: { - Component: React.ComponentType; onRender?: ( info: BaseRender & { snapshot: Snapshot; @@ -201,16 +201,19 @@ export function profile< } }; - const Wrapped = wrapComponentWithTracking(Component); - let iteratorPosition = 0; - const Profiled: ProfiledComponent = Object.assign( - (props: Props) => { + const Profiled: ProfiledComponent = Object.assign( + ({ children }: ProfilerProps) => { const parentContext = React.useContext(ProfilerContext); + + if (parentContext) { + throw new Error("Should not nest profiled components."); + } + return ( - + - + {children} ); @@ -218,7 +221,7 @@ export function profile< { replaceSnapshot, mergeSnapshot, - } satisfies ProfiledComponentOnlyFields, + } satisfies ProfiledComponentOnlyFields, { renders: new Array< | Render @@ -305,7 +308,7 @@ export function profile< } return nextRender; }, - } satisfies ProfiledComponentFields + } satisfies ProfiledComponentFields ); return Profiled; } From 841a9ef170d7beec358b64db1865358dfb29538b Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 21 Nov 2023 18:33:25 -0700 Subject: [PATCH 117/199] Rename profile to createTestProfiler --- .../components/__tests__/client/Query.test.tsx | 4 ++-- .../hoc/__tests__/queries/lifecycle.test.tsx | 4 ++-- src/react/hoc/__tests__/queries/loading.test.tsx | 4 ++-- .../hooks/__tests__/useBackgroundQuery.test.tsx | 10 +++++----- src/react/hooks/__tests__/useFragment.test.tsx | 8 ++++---- .../hooks/__tests__/useLoadableQuery.test.tsx | 15 +++++++++------ .../hooks/__tests__/useSuspenseQuery.test.tsx | 6 +++--- src/testing/internal/profile/index.ts | 2 +- src/testing/internal/profile/profile.tsx | 4 ++-- 9 files changed, 30 insertions(+), 27 deletions(-) diff --git a/src/react/components/__tests__/client/Query.test.tsx b/src/react/components/__tests__/client/Query.test.tsx index acdd2015301..02cc1abb222 100644 --- a/src/react/components/__tests__/client/Query.test.tsx +++ b/src/react/components/__tests__/client/Query.test.tsx @@ -11,7 +11,7 @@ import { ApolloProvider } from "../../../context"; import { itAsync, MockedProvider, mockSingleLink } from "../../../../testing"; import { Query } from "../../Query"; import { QueryResult } from "../../../types/types"; -import { profile } from "../../../../testing/internal"; +import { createTestProfiler } from "../../../../testing/internal"; const allPeopleQuery: DocumentNode = gql` query people { @@ -1498,7 +1498,7 @@ describe("Query component", () => { ); } - const ProfiledContainer = profile({ + const ProfiledContainer = createTestProfiler({ Component: Container, }); diff --git a/src/react/hoc/__tests__/queries/lifecycle.test.tsx b/src/react/hoc/__tests__/queries/lifecycle.test.tsx index cf460af964a..34ec5a0cf51 100644 --- a/src/react/hoc/__tests__/queries/lifecycle.test.tsx +++ b/src/react/hoc/__tests__/queries/lifecycle.test.tsx @@ -10,7 +10,7 @@ import { mockSingleLink } from "../../../../testing"; import { Query as QueryComponent } from "../../../components"; import { graphql } from "../../graphql"; import { ChildProps, DataValue } from "../../types"; -import { profile } from "../../../../testing/internal"; +import { createTestProfiler } from "../../../../testing/internal"; describe("[queries] lifecycle", () => { // lifecycle @@ -58,7 +58,7 @@ describe("[queries] lifecycle", () => { } ); - const ProfiledApp = profile, Vars>({ + const ProfiledApp = createTestProfiler, Vars>({ Component: Container, }); diff --git a/src/react/hoc/__tests__/queries/loading.test.tsx b/src/react/hoc/__tests__/queries/loading.test.tsx index 387a6803fb5..bf29abc4834 100644 --- a/src/react/hoc/__tests__/queries/loading.test.tsx +++ b/src/react/hoc/__tests__/queries/loading.test.tsx @@ -13,7 +13,7 @@ import { InMemoryCache as Cache } from "../../../../cache"; import { itAsync, mockSingleLink } from "../../../../testing"; import { graphql } from "../../graphql"; import { ChildProps, DataValue } from "../../types"; -import { profile } from "../../../../testing/internal"; +import { createTestProfiler } from "../../../../testing/internal"; describe("[queries] loading", () => { // networkStatus / loading @@ -413,7 +413,7 @@ describe("[queries] loading", () => { } ); - const ProfiledContainer = profile< + const ProfiledContainer = createTestProfiler< DataValue<{ allPeople: { people: { diff --git a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx index 131364939cd..b6176e5be86 100644 --- a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx +++ b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx @@ -53,7 +53,7 @@ import { import equal from "@wry/equality"; import { RefetchWritePolicy } from "../../../core/watchQueryOptions"; import { skipToken } from "../constants"; -import { profile, spyOnConsole } from "../../../testing/internal"; +import { createTestProfiler, spyOnConsole } from "../../../testing/internal"; function renderIntegrationTest({ client, @@ -332,7 +332,7 @@ function renderVariablesIntegrationTest({ ); } - const ProfiledApp = profile>({ + const ProfiledApp = createTestProfiler>({ Component: App, snapshotDOM: true, onRender: ({ replaceSnapshot }) => replaceSnapshot(cloneDeep(renders)), @@ -516,7 +516,7 @@ function renderPaginatedIntegrationTest({ ); } - const ProfiledApp = profile({ + const ProfiledApp = createTestProfiler({ Component: App, snapshotDOM: true, initialSnapshot: { @@ -3895,7 +3895,7 @@ describe("useBackgroundQuery", () => { ); } - const ProfiledApp = profile({ Component: App, snapshotDOM: true }); + const ProfiledApp = createTestProfiler({ Component: App, snapshotDOM: true }); render(); @@ -4193,7 +4193,7 @@ describe("useBackgroundQuery", () => { ); } - const ProfiledApp = profile({ Component: App, snapshotDOM: true }); + const ProfiledApp = createTestProfiler({ Component: App, snapshotDOM: true }); render(); { diff --git a/src/react/hooks/__tests__/useFragment.test.tsx b/src/react/hooks/__tests__/useFragment.test.tsx index 21b9e083a03..edac1702d94 100644 --- a/src/react/hooks/__tests__/useFragment.test.tsx +++ b/src/react/hooks/__tests__/useFragment.test.tsx @@ -29,7 +29,7 @@ import { concatPagination } from "../../../utilities"; import assert from "assert"; import { expectTypeOf } from "expect-type"; import { SubscriptionObserver } from "zen-observable-ts"; -import { profile, spyOnConsole } from "../../../testing/internal"; +import { createTestProfiler, spyOnConsole } from "../../../testing/internal"; describe("useFragment", () => { it("is importable and callable", () => { @@ -1481,7 +1481,7 @@ describe("has the same timing as `useQuery`", () => { return complete ? JSON.stringify(fragmentData) : "loading"; } - const ProfiledComponent = profile({ + const ProfiledComponent = createTestProfiler({ Component, initialSnapshot: { queryData: undefined as any, @@ -1569,7 +1569,7 @@ describe("has the same timing as `useQuery`", () => { return <>{JSON.stringify({ item: data })}; } - const ProfiledParent = profile({ + const ProfiledParent = createTestProfiler({ Component: Parent, snapshotDOM: true, onRender() { @@ -1664,7 +1664,7 @@ describe("has the same timing as `useQuery`", () => { return <>{JSON.stringify(data)}; } - const ProfiledParent = profile({ + const ProfiledParent = createTestProfiler({ Component: Parent, onRender() { const parent = screen.getByTestId("parent"); diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index 341475a472f..db8976e35e5 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -45,7 +45,7 @@ import { FetchMoreFunction, RefetchFunction } from "../useSuspenseQuery"; import invariant, { InvariantError } from "ts-invariant"; import { ProfiledComponent, - profile, + createTestProfiler, spyOnConsole, useTrackComponentRender, } from "../../../testing/internal"; @@ -174,7 +174,10 @@ function createDefaultProfiledComponents< return null; } - const ErrorFallback = profile<{ error: Error | null }, { error: Error }>({ + const ErrorFallback = createTestProfiler< + { error: Error | null }, + { error: Error } + >({ Component: function Fallback({ error }) { ErrorFallback.replaceSnapshot({ error }); @@ -232,7 +235,7 @@ function renderWithClient( it("loads a query and suspends when the load query function is called", async () => { const { query, mocks } = useSimpleQueryCase(); - const Profiler = profile({ + const Profiler = createTestProfiler({ initialSnapshot: { result: null as UseReadQueryResult | null, }, @@ -295,7 +298,7 @@ it("loads a query with variables and suspends by passing variables to the loadQu const { SuspenseFallback, ReadQueryHook } = createDefaultProfiledComponents(); - const App = profile({ + const App = createTestProfiler({ Component: function App() { const [loadQuery, queryRef] = useLoadableQuery(query); @@ -348,7 +351,7 @@ it("changes variables on a query and resuspends when passing new variables to th const { SuspenseFallback, ReadQueryHook } = createDefaultProfiledComponents(); - const App = profile({ + const App = createTestProfiler({ Component: () => { const [loadQuery, queryRef] = useLoadableQuery(query); @@ -689,7 +692,7 @@ it("returns initial cache data followed by network data when the fetch policy is hello: string; }>(); - const App = profile({ + const App = createTestProfiler({ Component: () => { const [loadQuery, queryRef] = useLoadableQuery(query, { fetchPolicy: "cache-and-network", diff --git a/src/react/hooks/__tests__/useSuspenseQuery.test.tsx b/src/react/hooks/__tests__/useSuspenseQuery.test.tsx index 642be7d023a..7abfb005cc3 100644 --- a/src/react/hooks/__tests__/useSuspenseQuery.test.tsx +++ b/src/react/hooks/__tests__/useSuspenseQuery.test.tsx @@ -51,7 +51,7 @@ import { RefetchWritePolicy, WatchQueryFetchPolicy, } from "../../../core/watchQueryOptions"; -import { profile, spyOnConsole } from "../../../testing/internal"; +import { createTestProfiler, spyOnConsole } from "../../../testing/internal"; type RenderSuspenseHookOptions = Omit< RenderHookOptions, @@ -371,7 +371,7 @@ describe("useSuspenseQuery", () => { ); }; - const ProfiledApp = profile< + const ProfiledApp = createTestProfiler< UseSuspenseQueryResult >({ Component: App, @@ -9613,7 +9613,7 @@ describe("useSuspenseQuery", () => { ); } - const ProfiledApp = profile({ + const ProfiledApp = createTestProfiler({ Component: App, snapshotDOM: true, }); diff --git a/src/testing/internal/profile/index.ts b/src/testing/internal/profile/index.ts index b6aadc4ed3a..dec794de00f 100644 --- a/src/testing/internal/profile/index.ts +++ b/src/testing/internal/profile/index.ts @@ -4,7 +4,7 @@ export type { ProfiledHook, } from "./profile.js"; export { - profile, + createTestProfiler, profileHook, useTrackComponentRender, WaitForRenderTimeoutError, diff --git a/src/testing/internal/profile/profile.tsx b/src/testing/internal/profile/profile.tsx index 1812ce6fe2a..dd8d4130eae 100644 --- a/src/testing/internal/profile/profile.tsx +++ b/src/testing/internal/profile/profile.tsx @@ -94,7 +94,7 @@ const ProfilerContext = React.createContext( ); /** @internal */ -export function profile({ +export function createTestProfiler({ onRender, snapshotDOM = false, initialSnapshot, @@ -361,7 +361,7 @@ export function profileHook( return null; }; ProfiledHook.displayName = displayName; - const ProfiledComponent = profile({ + const ProfiledComponent = createTestProfiler({ Component: ProfiledHook, onRender: () => returnValue, }); From ed0d2fc7988aa7a382234fe37182fb085ab42666 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 21 Nov 2023 18:35:41 -0700 Subject: [PATCH 118/199] Fix error fallback in default creation --- .../hooks/__tests__/useLoadableQuery.test.tsx | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index db8976e35e5..9497ffc37b2 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -155,7 +155,10 @@ function usePaginatedQueryCase() { } function createDefaultProfiledComponents< - Snapshot extends { result: UseReadQueryResult | null }, + Snapshot extends { + result: UseReadQueryResult | null; + error?: Error | null; + }, TData = Snapshot["result"] extends UseReadQueryResult | null ? TData : unknown, @@ -174,19 +177,11 @@ function createDefaultProfiledComponents< return null; } - const ErrorFallback = createTestProfiler< - { error: Error | null }, - { error: Error } - >({ - Component: function Fallback({ error }) { - ErrorFallback.replaceSnapshot({ error }); + function ErrorFallback({ error }: { error: Error }) { + profiler.mergeSnapshot({ error } as Partial); - return
Oops
; - }, - initialSnapshot: { - error: null, - }, - }); + return
Oops
; + } function ErrorBoundary({ children }: { children: React.ReactNode }) { return ( From 895792c02bd7d4dd4bd05c9fd7ab447ada07069a Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 21 Nov 2023 18:39:48 -0700 Subject: [PATCH 119/199] Convert additional test to updated API --- .../hooks/__tests__/useLoadableQuery.test.tsx | 56 ++++++++++--------- 1 file changed, 31 insertions(+), 25 deletions(-) diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index 9497ffc37b2..99142e3301d 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -290,54 +290,60 @@ it("loads a query and suspends when the load query function is called", async () it("loads a query with variables and suspends by passing variables to the loadQuery function", async () => { const { query, mocks } = useVariablesQueryCase(); + const Profiler = createTestProfiler({ + initialSnapshot: { + result: null as UseReadQueryResult | null, + }, + }); + const { SuspenseFallback, ReadQueryHook } = - createDefaultProfiledComponents(); + createDefaultProfiledComponents(Profiler); - const App = createTestProfiler({ - Component: function App() { - const [loadQuery, queryRef] = useLoadableQuery(query); + function App() { + useTrackComponentRender(); + const [loadQuery, queryRef] = useLoadableQuery(query); - return ( - <> - - }> - {queryRef && } - - - ); - }, - }); + return ( + <> + + }> + {queryRef && } + + + ); + } - const { user } = renderWithMocks(, { mocks }); + const { user } = renderWithMocks( + + + , + { mocks } + ); { - const { renderedComponents } = await App.takeRender(); + const { renderedComponents } = await Profiler.takeRender(); expect(renderedComponents).toStrictEqual(["App"]); } await act(() => user.click(screen.getByText("Load query"))); { - const { renderedComponents } = await App.takeRender(); + const { renderedComponents } = await Profiler.takeRender(); expect(renderedComponents).toStrictEqual(["App", "SuspenseFallback"]); } { - const { renderedComponents } = await App.takeRender(); - expect(renderedComponents).toStrictEqual(["UseReadQuery"]); - } - - { - const snapshot = await ReadQueryHook.takeSnapshot(); + const { snapshot, renderedComponents } = await Profiler.takeRender(); - expect(snapshot).toEqual({ + expect(renderedComponents).toStrictEqual(["ReadQueryHook"]); + expect(snapshot.result).toEqual({ data: { character: { id: "1", name: "Spider-Man" } }, networkStatus: NetworkStatus.ready, error: undefined, }); } - await expect(App).not.toRerender(); + await expect(Profiler).not.toRerender(); }); it("changes variables on a query and resuspends when passing new variables to the loadQuery function", async () => { From ee44633b4cc172c6acae8e0f188ab0d95d9e36be Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 21 Nov 2023 18:45:40 -0700 Subject: [PATCH 120/199] Rename ProfiledComponent type to Profiler --- .../hooks/__tests__/useLoadableQuery.test.tsx | 4 +-- src/testing/internal/profile/index.ts | 6 +--- src/testing/internal/profile/profile.tsx | 34 +++++++++---------- src/testing/matchers/ProfiledComponent.ts | 10 ++---- src/testing/matchers/index.d.ts | 14 +++----- src/testing/matchers/toHaveRendered.ts | 6 ++-- src/testing/matchers/toHaveRenderedTimes.ts | 6 ++-- 7 files changed, 32 insertions(+), 48 deletions(-) diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index 99142e3301d..8ed63e7b106 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -44,7 +44,7 @@ import { QueryReference } from "../../../react"; import { FetchMoreFunction, RefetchFunction } from "../useSuspenseQuery"; import invariant, { InvariantError } from "ts-invariant"; import { - ProfiledComponent, + Profiler, createTestProfiler, spyOnConsole, useTrackComponentRender, @@ -162,7 +162,7 @@ function createDefaultProfiledComponents< TData = Snapshot["result"] extends UseReadQueryResult | null ? TData : unknown, ->(profiler: ProfiledComponent) { +>(profiler: Profiler) { function SuspenseFallback() { useTrackComponentRender(); return

Loading

; diff --git a/src/testing/internal/profile/index.ts b/src/testing/internal/profile/index.ts index dec794de00f..b7bf6716f58 100644 --- a/src/testing/internal/profile/index.ts +++ b/src/testing/internal/profile/index.ts @@ -1,8 +1,4 @@ -export type { - NextRenderOptions, - ProfiledComponent, - ProfiledHook, -} from "./profile.js"; +export type { NextRenderOptions, Profiler, ProfiledHook } from "./profile.js"; export { createTestProfiler, profileHook, diff --git a/src/testing/internal/profile/profile.tsx b/src/testing/internal/profile/profile.tsx index dd8d4130eae..2c8ce920d47 100644 --- a/src/testing/internal/profile/profile.tsx +++ b/src/testing/internal/profile/profile.tsx @@ -25,7 +25,7 @@ interface ProfilerProps { } /** @internal */ -export interface ProfiledComponent +export interface Profiler extends React.FC, ProfiledComponentFields, ProfiledComponentOnlyFields {} @@ -159,7 +159,7 @@ export function createTestProfiler({ baseDuration, startTime, commitTime, - count: Profiled.renders.length + 1, + count: Profiler.renders.length + 1, }; try { /* @@ -187,12 +187,12 @@ export function createTestProfiler({ profilerContext.renderedComponents ); profilerContext.renderedComponents = []; - Profiled.renders.push(render); + Profiler.renders.push(render); resolveNextRender?.(render); } catch (error) { - Profiled.renders.push({ + Profiler.renders.push({ phase: "snapshotError", - count: Profiled.renders.length, + count: Profiler.renders.length, error, }); rejectNextRender?.(error); @@ -202,7 +202,7 @@ export function createTestProfiler({ }; let iteratorPosition = 0; - const Profiled: ProfiledComponent = Object.assign( + const Profiler: Profiler = Object.assign( ({ children }: ProfilerProps) => { const parentContext = React.useContext(ProfilerContext); @@ -228,11 +228,11 @@ export function createTestProfiler({ | { phase: "snapshotError"; count: number; error: unknown } >(), totalRenderCount() { - return Profiled.renders.length; + return Profiler.renders.length; }, async peekRender(options: NextRenderOptions = {}) { - if (iteratorPosition < Profiled.renders.length) { - const render = Profiled.renders[iteratorPosition]; + if (iteratorPosition < Profiler.renders.length) { + const render = Profiler.renders[iteratorPosition]; if (render.phase === "snapshotError") { throw render.error; @@ -240,16 +240,16 @@ export function createTestProfiler({ return render; } - return Profiled.waitForNextRender({ - [_stackTrace]: captureStackTrace(Profiled.peekRender), + return Profiler.waitForNextRender({ + [_stackTrace]: captureStackTrace(Profiler.peekRender), ...options, }); }, async takeRender(options: NextRenderOptions = {}) { let error: unknown = undefined; try { - return await Profiled.peekRender({ - [_stackTrace]: captureStackTrace(Profiled.takeRender), + return await Profiler.peekRender({ + [_stackTrace]: captureStackTrace(Profiler.takeRender), ...options, }); } catch (e) { @@ -275,7 +275,7 @@ export function createTestProfiler({ ); } - const render = Profiled.renders[currentPosition]; + const render = Profiler.renders[currentPosition]; if (render.phase === "snapshotError") { throw render.error; @@ -286,7 +286,7 @@ export function createTestProfiler({ timeout = 1000, // capture the stack trace here so its stack trace is as close to the calling code as possible [_stackTrace]: stackTrace = captureStackTrace( - Profiled.waitForNextRender + Profiler.waitForNextRender ), }: NextRenderOptions = {}) { if (!nextRender) { @@ -310,7 +310,7 @@ export function createTestProfiler({ }, } satisfies ProfiledComponentFields ); - return Profiled; + return Profiler; } /** @internal */ @@ -347,7 +347,7 @@ type ProfiledHookFields = ProfiledComponentFields< export interface ProfiledHook extends React.FC, ProfiledHookFields { - ProfiledComponent: ProfiledComponent; + ProfiledComponent: Profiler; } /** @internal */ diff --git a/src/testing/matchers/ProfiledComponent.ts b/src/testing/matchers/ProfiledComponent.ts index c15ed832c7c..66c74480f5d 100644 --- a/src/testing/matchers/ProfiledComponent.ts +++ b/src/testing/matchers/ProfiledComponent.ts @@ -2,15 +2,13 @@ import type { MatcherFunction } from "expect"; import { WaitForRenderTimeoutError } from "../internal/index.js"; import type { NextRenderOptions, - ProfiledComponent, + Profiler, ProfiledHook, } from "../internal/index.js"; export const toRerender: MatcherFunction<[options?: NextRenderOptions]> = async function (actual, options) { - const _profiled = actual as - | ProfiledComponent - | ProfiledHook; + const _profiled = actual as Profiler | ProfiledHook; const profiled = "ProfiledComponent" in _profiled ? _profiled.ProfiledComponent @@ -45,9 +43,7 @@ const failed = {}; export const toRenderExactlyTimes: MatcherFunction< [times: number, options?: NextRenderOptions] > = async function (actual, times, optionsPerRender) { - const _profiled = actual as - | ProfiledComponent - | ProfiledHook; + const _profiled = actual as Profiler | ProfiledHook; const profiled = "ProfiledComponent" in _profiled ? _profiled.ProfiledComponent : _profiled; const options = { timeout: 100, ...optionsPerRender }; diff --git a/src/testing/matchers/index.d.ts b/src/testing/matchers/index.d.ts index dcedef97de1..ebcd8e8471e 100644 --- a/src/testing/matchers/index.d.ts +++ b/src/testing/matchers/index.d.ts @@ -5,7 +5,7 @@ import type { } from "../../core/index.js"; import { NextRenderOptions, - ProfiledComponent, + Profiler, ProfiledHook, } from "../internal/index.js"; @@ -19,7 +19,7 @@ interface ApolloCustomMatchers { /** * Used to determine if a profiled component has rendered or not. */ - toHaveRendered: T extends ProfiledComponent | ProfiledHook + toHaveRendered: T extends Profiler | ProfiledHook ? () => R : { error: "matcher needs to be called on a ProfiledComponent instance" }; @@ -27,9 +27,7 @@ interface ApolloCustomMatchers { * Used to determine if a profiled component has rendered a specific amount * of times or not. */ - toHaveRenderedTimes: T extends - | ProfiledComponent - | ProfiledHook + toHaveRenderedTimes: T extends Profiler | ProfiledHook ? (count: number) => R : { error: "matcher needs to be called on a ProfiledComponent instance" }; @@ -46,13 +44,11 @@ interface ApolloCustomMatchers { ) => R : { error: "matcher needs to be called on an ApolloClient instance" }; - toRerender: T extends ProfiledComponent | ProfiledHook + toRerender: T extends Profiler | ProfiledHook ? (options?: NextRenderOptions) => Promise : { error: "matcher needs to be called on a ProfiledComponent instance" }; - toRenderExactlyTimes: T extends - | ProfiledComponent - | ProfiledHook + toRenderExactlyTimes: T extends Profiler | ProfiledHook ? (count: number, options?: NextRenderOptions) => Promise : { error: "matcher needs to be called on a ProfiledComponent instance" }; } diff --git a/src/testing/matchers/toHaveRendered.ts b/src/testing/matchers/toHaveRendered.ts index e1c2741c4aa..29330595de7 100644 --- a/src/testing/matchers/toHaveRendered.ts +++ b/src/testing/matchers/toHaveRendered.ts @@ -1,11 +1,9 @@ import type { MatcherFunction } from "expect"; -import type { ProfiledComponent } from "../internal/index.js"; +import type { Profiler } from "../internal/index.js"; import type { ProfiledHook } from "../internal/index.js"; export const toHaveRendered: MatcherFunction = function (actual) { - let ProfiledComponent = actual as - | ProfiledComponent - | ProfiledHook; + let ProfiledComponent = actual as Profiler | ProfiledHook; if ("ProfiledComponent" in ProfiledComponent) { ProfiledComponent = ProfiledComponent.ProfiledComponent; diff --git a/src/testing/matchers/toHaveRenderedTimes.ts b/src/testing/matchers/toHaveRenderedTimes.ts index ffd6e775126..2589e271c7d 100644 --- a/src/testing/matchers/toHaveRenderedTimes.ts +++ b/src/testing/matchers/toHaveRenderedTimes.ts @@ -1,13 +1,11 @@ import type { MatcherFunction } from "expect"; -import type { ProfiledComponent, ProfiledHook } from "../internal/index.js"; +import type { Profiler, ProfiledHook } from "../internal/index.js"; export const toHaveRenderedTimes: MatcherFunction<[count: number]> = function ( actual, count ) { - let ProfiledComponent = actual as - | ProfiledComponent - | ProfiledHook; + let ProfiledComponent = actual as Profiler | ProfiledHook; if ("ProfiledComponent" in ProfiledComponent) { ProfiledComponent = ProfiledComponent.ProfiledComponent; From 2a433a472ecec29eb1a7ffd1681afff35f0d62c6 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 21 Nov 2023 20:27:45 -0700 Subject: [PATCH 121/199] Remove unneeded wrapper for tracking renders --- src/testing/internal/profile/profile.tsx | 46 ------------------------ 1 file changed, 46 deletions(-) diff --git a/src/testing/internal/profile/profile.tsx b/src/testing/internal/profile/profile.tsx index 2c8ce920d47..94d298d9c90 100644 --- a/src/testing/internal/profile/profile.tsx +++ b/src/testing/internal/profile/profile.tsx @@ -391,17 +391,6 @@ export function profileHook( ); } -function isReactClass( - Component: React.ComponentType -): Component is React.ComponentClass { - let proto = Component; - while (proto && proto !== Object) { - if (proto === React.Component) return true; - proto = Object.getPrototypeOf(proto); - } - return false; -} - function getCurrentComponentName() { const owner: React.ComponentType | undefined = (React as any) .__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED?.ReactCurrentOwner @@ -421,38 +410,3 @@ export function useTrackComponentRender(name = getCurrentComponentName()) { ctx?.renderedComponents.unshift(name); }); } - -function wrapComponentWithTracking( - Component: React.ComponentType -) { - if (!isReactClass(Component)) { - return function ComponentWithTracking(props: Props) { - useTrackComponentRender(Component.displayName || Component.name); - return Component(props); - }; - } - - let ctx: ProfilerContextValue; - class WrapperClass extends (Component as React.ComponentClass) { - constructor(props: Props) { - super(props); - } - componentDidMount() { - super.componentDidMount?.apply(this); - ctx!.renderedComponents.push(Component.displayName || Component.name); - } - componentDidUpdate() { - super.componentDidUpdate?.apply( - this, - arguments as unknown as Parameters< - NonNullable["componentDidUpdate"]> - > - ); - ctx!.renderedComponents.push(Component.displayName || Component.name); - } - } - return (props: any) => { - ctx = React.useContext(ProfilerContext)!; - return ; - }; -} From e5efcc98460c1c9f5e3b6d92ca205ce28c977e78 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 21 Nov 2023 20:30:53 -0700 Subject: [PATCH 122/199] Don't require args to createTestProfiler --- src/testing/internal/profile/profile.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/testing/internal/profile/profile.tsx b/src/testing/internal/profile/profile.tsx index 94d298d9c90..ab9f04c75b4 100644 --- a/src/testing/internal/profile/profile.tsx +++ b/src/testing/internal/profile/profile.tsx @@ -108,7 +108,7 @@ export function createTestProfiler({ ) => void; snapshotDOM?: boolean; initialSnapshot?: Snapshot; -}) { +} = {}) { let nextRender: Promise> | undefined; let resolveNextRender: ((render: Render) => void) | undefined; let rejectNextRender: ((error: unknown) => void) | undefined; From c39cba0a79d80638f22fb13e6f8cec1b7cbefc0d Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 21 Nov 2023 20:32:45 -0700 Subject: [PATCH 123/199] Fix types on profiled hook --- src/testing/internal/profile/profile.tsx | 27 +++++++++++------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/src/testing/internal/profile/profile.tsx b/src/testing/internal/profile/profile.tsx index ab9f04c75b4..5e498173fbc 100644 --- a/src/testing/internal/profile/profile.tsx +++ b/src/testing/internal/profile/profile.tsx @@ -332,22 +332,20 @@ type ResultReplaceRenderWithSnapshot = T extends ( ? (...args: Args) => Promise : T; -type ProfiledHookFields = ProfiledComponentFields< - Props, - ReturnValue -> extends infer PC - ? { - [K in keyof PC as StringReplaceRenderWithSnapshot< - K & string - >]: ResultReplaceRenderWithSnapshot; - } - : never; +type ProfiledHookFields = + ProfiledComponentFields extends infer PC + ? { + [K in keyof PC as StringReplaceRenderWithSnapshot< + K & string + >]: ResultReplaceRenderWithSnapshot; + } + : never; /** @internal */ export interface ProfiledHook extends React.FC, - ProfiledHookFields { - ProfiledComponent: Profiler; + ProfiledHookFields { + ProfiledComponent: Profiler; } /** @internal */ @@ -361,8 +359,7 @@ export function profileHook( return null; }; ProfiledHook.displayName = displayName; - const ProfiledComponent = createTestProfiler({ - Component: ProfiledHook, + const ProfiledComponent = createTestProfiler({ onRender: () => returnValue, }); return Object.assign( @@ -387,7 +384,7 @@ export function profileHook( async waitForNextSnapshot(options) { return (await ProfiledComponent.waitForNextRender(options)).snapshot; }, - } satisfies ProfiledHookFields + } satisfies ProfiledHookFields ); } From 1e646147d70bbe8f18a63d2526e3ab6e94b410ca Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 21 Nov 2023 20:42:05 -0700 Subject: [PATCH 124/199] Track component function instead of component name for rendered components --- .../hooks/__tests__/useLoadableQuery.test.tsx | 12 ++++++------ src/testing/internal/profile/Render.tsx | 4 ++-- src/testing/internal/profile/profile.tsx | 15 +++++---------- 3 files changed, 13 insertions(+), 18 deletions(-) diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index 8ed63e7b106..ebc7fcaf486 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -263,7 +263,7 @@ it("loads a query and suspends when the load query function is called", async () { const { renderedComponents } = await Profiler.takeRender(); - expect(renderedComponents).toStrictEqual(["App"]); + expect(renderedComponents).toStrictEqual([App]); } await act(() => user.click(screen.getByText("Load query"))); @@ -271,7 +271,7 @@ it("loads a query and suspends when the load query function is called", async () { const { renderedComponents } = await Profiler.takeRender(); - expect(renderedComponents).toStrictEqual(["App", "SuspenseFallback"]); + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); } { @@ -283,7 +283,7 @@ it("loads a query and suspends when the load query function is called", async () networkStatus: NetworkStatus.ready, }); - expect(renderedComponents).toStrictEqual(["ReadQueryHook"]); + expect(renderedComponents).toStrictEqual([ReadQueryHook]); } }); @@ -322,20 +322,20 @@ it("loads a query with variables and suspends by passing variables to the loadQu { const { renderedComponents } = await Profiler.takeRender(); - expect(renderedComponents).toStrictEqual(["App"]); + expect(renderedComponents).toStrictEqual([App]); } await act(() => user.click(screen.getByText("Load query"))); { const { renderedComponents } = await Profiler.takeRender(); - expect(renderedComponents).toStrictEqual(["App", "SuspenseFallback"]); + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); } { const { snapshot, renderedComponents } = await Profiler.takeRender(); - expect(renderedComponents).toStrictEqual(["ReadQueryHook"]); + expect(renderedComponents).toStrictEqual([ReadQueryHook]); expect(snapshot.result).toEqual({ data: { character: { id: "1", name: "Spider-Man" } }, networkStatus: NetworkStatus.ready, diff --git a/src/testing/internal/profile/Render.tsx b/src/testing/internal/profile/Render.tsx index ee4d0853431..4db810c0825 100644 --- a/src/testing/internal/profile/Render.tsx +++ b/src/testing/internal/profile/Render.tsx @@ -63,7 +63,7 @@ export interface Render extends BaseRender { */ withinDOM: () => SyncScreen; - renderedComponents: string[]; + renderedComponents: React.ComponentType[]; } /** @internal */ @@ -80,7 +80,7 @@ export class RenderInstance implements Render { baseRender: BaseRender, public snapshot: Snapshot, private stringifiedDOM: string | undefined, - public renderedComponents: string[] + public renderedComponents: React.ComponentType[] ) { this.id = baseRender.id; this.phase = baseRender.phase; diff --git a/src/testing/internal/profile/profile.tsx b/src/testing/internal/profile/profile.tsx index 5e498173fbc..242dff5fa47 100644 --- a/src/testing/internal/profile/profile.tsx +++ b/src/testing/internal/profile/profile.tsx @@ -87,7 +87,7 @@ interface ProfiledComponentFields { } interface ProfilerContextValue { - renderedComponents: string[]; + renderedComponents: React.ComponentType[]; } const ProfilerContext = React.createContext( undefined @@ -388,22 +388,17 @@ export function profileHook( ); } -function getCurrentComponentName() { +export function useTrackComponentRender() { const owner: React.ComponentType | undefined = (React as any) .__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED?.ReactCurrentOwner ?.current?.elementType; - if (owner) return owner?.displayName || owner?.name; - try { - throw new Error(); - } catch (e) { - return (e as Error).stack?.split("\n")[1].split(":")[0] || ""; + if (!owner) { + throw new Error("useTrackComponentRender: Unable to determine hook owner"); } -} -export function useTrackComponentRender(name = getCurrentComponentName()) { const ctx = React.useContext(ProfilerContext); React.useLayoutEffect(() => { - ctx?.renderedComponents.unshift(name); + ctx?.renderedComponents.unshift(owner); }); } From 02aa46c757c83e6eff8cb34b288e203f2a3bfe2c Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 21 Nov 2023 20:48:29 -0700 Subject: [PATCH 125/199] Fix type on matchers --- src/testing/matchers/toHaveRendered.ts | 2 +- src/testing/matchers/toHaveRenderedTimes.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/testing/matchers/toHaveRendered.ts b/src/testing/matchers/toHaveRendered.ts index 29330595de7..ec469d235d6 100644 --- a/src/testing/matchers/toHaveRendered.ts +++ b/src/testing/matchers/toHaveRendered.ts @@ -3,7 +3,7 @@ import type { Profiler } from "../internal/index.js"; import type { ProfiledHook } from "../internal/index.js"; export const toHaveRendered: MatcherFunction = function (actual) { - let ProfiledComponent = actual as Profiler | ProfiledHook; + let ProfiledComponent = actual as Profiler | ProfiledHook; if ("ProfiledComponent" in ProfiledComponent) { ProfiledComponent = ProfiledComponent.ProfiledComponent; diff --git a/src/testing/matchers/toHaveRenderedTimes.ts b/src/testing/matchers/toHaveRenderedTimes.ts index 2589e271c7d..f69ee1822b5 100644 --- a/src/testing/matchers/toHaveRenderedTimes.ts +++ b/src/testing/matchers/toHaveRenderedTimes.ts @@ -5,7 +5,7 @@ export const toHaveRenderedTimes: MatcherFunction<[count: number]> = function ( actual, count ) { - let ProfiledComponent = actual as Profiler | ProfiledHook; + let ProfiledComponent = actual as Profiler | ProfiledHook; if ("ProfiledComponent" in ProfiledComponent) { ProfiledComponent = ProfiledComponent.ProfiledComponent; From 61443bd785bf093ec5fe00693dd5819e01c83688 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 21 Nov 2023 21:20:28 -0700 Subject: [PATCH 126/199] Move render context into own file and add all of context to render instance --- .../hooks/__tests__/useLoadableQuery.test.tsx | 25 ++++++++------- src/testing/internal/profile/Render.tsx | 6 ++-- src/testing/internal/profile/context.tsx | 31 +++++++++++++++++++ src/testing/internal/profile/profile.tsx | 28 ++++++----------- 4 files changed, 57 insertions(+), 33 deletions(-) create mode 100644 src/testing/internal/profile/context.tsx diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index ebc7fcaf486..8e527645025 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -1,3 +1,4 @@ +/* eslint-disable testing-library/render-result-naming-convention */ import React, { Suspense, useState } from "react"; import { act, @@ -261,21 +262,21 @@ it("loads a query and suspends when the load query function is called", async () ); { - const { renderedComponents } = await Profiler.takeRender(); + const { context } = await Profiler.takeRender(); - expect(renderedComponents).toStrictEqual([App]); + expect(context.renderedComponents).toStrictEqual([App]); } await act(() => user.click(screen.getByText("Load query"))); { - const { renderedComponents } = await Profiler.takeRender(); + const { context } = await Profiler.takeRender(); - expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + expect(context.renderedComponents).toStrictEqual([App, SuspenseFallback]); } { - const { snapshot, renderedComponents } = await Profiler.takeRender(); + const { snapshot, context } = await Profiler.takeRender(); expect(snapshot.result).toEqual({ data: { greeting: "Hello" }, @@ -283,7 +284,7 @@ it("loads a query and suspends when the load query function is called", async () networkStatus: NetworkStatus.ready, }); - expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(context.renderedComponents).toStrictEqual([ReadQueryHook]); } }); @@ -321,21 +322,21 @@ it("loads a query with variables and suspends by passing variables to the loadQu ); { - const { renderedComponents } = await Profiler.takeRender(); - expect(renderedComponents).toStrictEqual([App]); + const { context } = await Profiler.takeRender(); + expect(context.renderedComponents).toStrictEqual([App]); } await act(() => user.click(screen.getByText("Load query"))); { - const { renderedComponents } = await Profiler.takeRender(); - expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + const { context } = await Profiler.takeRender(); + expect(context.renderedComponents).toStrictEqual([App, SuspenseFallback]); } { - const { snapshot, renderedComponents } = await Profiler.takeRender(); + const { snapshot, context } = await Profiler.takeRender(); - expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(context.renderedComponents).toStrictEqual([ReadQueryHook]); expect(snapshot.result).toEqual({ data: { character: { id: "1", name: "Spider-Man" } }, networkStatus: NetworkStatus.ready, diff --git a/src/testing/internal/profile/Render.tsx b/src/testing/internal/profile/Render.tsx index 4db810c0825..cdb534b5f3f 100644 --- a/src/testing/internal/profile/Render.tsx +++ b/src/testing/internal/profile/Render.tsx @@ -12,6 +12,7 @@ As we only use this file in our internal tests, we can safely ignore it. import { within, screen } from "@testing-library/dom"; import { JSDOM, VirtualConsole } from "jsdom"; import { applyStackTrace, captureStackTrace } from "./traces.js"; +import type { RenderContextValue } from "./context.js"; /** @internal */ export interface BaseRender { @@ -63,7 +64,7 @@ export interface Render extends BaseRender { */ withinDOM: () => SyncScreen; - renderedComponents: React.ComponentType[]; + context: RenderContextValue; } /** @internal */ @@ -80,7 +81,7 @@ export class RenderInstance implements Render { baseRender: BaseRender, public snapshot: Snapshot, private stringifiedDOM: string | undefined, - public renderedComponents: React.ComponentType[] + public context: RenderContextValue ) { this.id = baseRender.id; this.phase = baseRender.phase; @@ -89,6 +90,7 @@ export class RenderInstance implements Render { this.startTime = baseRender.startTime; this.commitTime = baseRender.commitTime; this.count = baseRender.count; + this.context = { ...context }; } private _domSnapshot: HTMLElement | undefined; diff --git a/src/testing/internal/profile/context.tsx b/src/testing/internal/profile/context.tsx new file mode 100644 index 00000000000..be13d19e17f --- /dev/null +++ b/src/testing/internal/profile/context.tsx @@ -0,0 +1,31 @@ +import * as React from "react"; + +export interface RenderContextValue { + renderedComponents: React.ComponentType[]; +} + +const RenderContext = React.createContext( + undefined +); + +export function RenderContextProvider({ + children, + value, +}: { + children: React.ReactNode; + value: RenderContextValue; +}) { + const parentContext = useRenderContext(); + + if (parentContext) { + throw new Error("Profilers should not be nested in the same tree"); + } + + return ( + {children} + ); +} + +export function useRenderContext() { + return React.useContext(RenderContext); +} diff --git a/src/testing/internal/profile/profile.tsx b/src/testing/internal/profile/profile.tsx index 242dff5fa47..68487717e72 100644 --- a/src/testing/internal/profile/profile.tsx +++ b/src/testing/internal/profile/profile.tsx @@ -8,6 +8,8 @@ global.TextDecoder ??= TextDecoder; import type { Render, BaseRender } from "./Render.js"; import { RenderInstance } from "./Render.js"; import { applyStackTrace, captureStackTrace } from "./traces.js"; +import type { RenderContextValue } from "./context.js"; +import { RenderContextProvider, useRenderContext } from "./context.js"; type ValidSnapshot = void | (object & { /* not a function */ call?: never }); @@ -86,13 +88,6 @@ interface ProfiledComponentFields { waitForNextRender(options?: NextRenderOptions): Promise>; } -interface ProfilerContextValue { - renderedComponents: React.ComponentType[]; -} -const ProfilerContext = React.createContext( - undefined -); - /** @internal */ export function createTestProfiler({ onRender, @@ -140,7 +135,7 @@ export function createTestProfiler({ })); }; - const profilerContext: ProfilerContextValue = { + const renderContext: RenderContextValue = { renderedComponents: [], }; @@ -184,9 +179,9 @@ export function createTestProfiler({ baseRender, snapshot, domSnapshot, - profilerContext.renderedComponents + renderContext ); - profilerContext.renderedComponents = []; + renderContext.renderedComponents = []; Profiler.renders.push(render); resolveNextRender?.(render); } catch (error) { @@ -204,18 +199,12 @@ export function createTestProfiler({ let iteratorPosition = 0; const Profiler: Profiler = Object.assign( ({ children }: ProfilerProps) => { - const parentContext = React.useContext(ProfilerContext); - - if (parentContext) { - throw new Error("Should not nest profiled components."); - } - return ( - + {children} - + ); }, { @@ -397,7 +386,8 @@ export function useTrackComponentRender() { throw new Error("useTrackComponentRender: Unable to determine hook owner"); } - const ctx = React.useContext(ProfilerContext); + const ctx = useRenderContext(); + React.useLayoutEffect(() => { ctx?.renderedComponents.unshift(owner); }); From d8c129ef3238da85348d482b027f935cb016ad0b Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 21 Nov 2023 21:25:02 -0700 Subject: [PATCH 127/199] Copy context before passing to RenderInstance --- src/testing/internal/profile/Render.tsx | 1 - src/testing/internal/profile/profile.tsx | 9 +++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/testing/internal/profile/Render.tsx b/src/testing/internal/profile/Render.tsx index cdb534b5f3f..5361cf371c5 100644 --- a/src/testing/internal/profile/Render.tsx +++ b/src/testing/internal/profile/Render.tsx @@ -90,7 +90,6 @@ export class RenderInstance implements Render { this.startTime = baseRender.startTime; this.commitTime = baseRender.commitTime; this.count = baseRender.count; - this.context = { ...context }; } private _domSnapshot: HTMLElement | undefined; diff --git a/src/testing/internal/profile/profile.tsx b/src/testing/internal/profile/profile.tsx index 68487717e72..7c4d8a9e7d5 100644 --- a/src/testing/internal/profile/profile.tsx +++ b/src/testing/internal/profile/profile.tsx @@ -175,12 +175,9 @@ export function createTestProfiler({ const domSnapshot = snapshotDOM ? window.document.body.innerHTML : undefined; - const render = new RenderInstance( - baseRender, - snapshot, - domSnapshot, - renderContext - ); + const render = new RenderInstance(baseRender, snapshot, domSnapshot, { + ...renderContext, + }); renderContext.renderedComponents = []; Profiler.renders.push(render); resolveNextRender?.(render); From 491404859026128a447fa9590b7a6cb5da794ab1 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 21 Nov 2023 21:29:49 -0700 Subject: [PATCH 128/199] Throw if render context is not found --- src/testing/internal/profile/profile.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/testing/internal/profile/profile.tsx b/src/testing/internal/profile/profile.tsx index 7c4d8a9e7d5..2201f26e1c6 100644 --- a/src/testing/internal/profile/profile.tsx +++ b/src/testing/internal/profile/profile.tsx @@ -385,7 +385,13 @@ export function useTrackComponentRender() { const ctx = useRenderContext(); + if (!ctx) { + throw new Error( + "useTrackComponentRender: A Profiler must be created and rendered to track component renders" + ); + } + React.useLayoutEffect(() => { - ctx?.renderedComponents.unshift(owner); + ctx.renderedComponents.unshift(owner); }); } From 87bcbb90b56e39107983d332b3fcbbecc887ae32 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 21 Nov 2023 21:30:59 -0700 Subject: [PATCH 129/199] Rename useTrackComponentRender to useTrackRender --- src/react/hooks/__tests__/useLoadableQuery.test.tsx | 10 +++++----- src/testing/internal/profile/index.ts | 2 +- src/testing/internal/profile/profile.tsx | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index 8e527645025..559963a456f 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -48,7 +48,7 @@ import { Profiler, createTestProfiler, spyOnConsole, - useTrackComponentRender, + useTrackRender, } from "../../../testing/internal"; interface SimpleQueryData { @@ -165,12 +165,12 @@ function createDefaultProfiledComponents< : unknown, >(profiler: Profiler) { function SuspenseFallback() { - useTrackComponentRender(); + useTrackRender(); return

Loading

; } function ReadQueryHook({ queryRef }: { queryRef: QueryReference }) { - useTrackComponentRender(); + useTrackRender(); profiler.mergeSnapshot({ result: useReadQuery(queryRef), } as Partial); @@ -241,7 +241,7 @@ it("loads a query and suspends when the load query function is called", async () createDefaultProfiledComponents(Profiler); function App() { - useTrackComponentRender(); + useTrackRender(); const [loadQuery, queryRef] = useLoadableQuery(query); return ( @@ -301,7 +301,7 @@ it("loads a query with variables and suspends by passing variables to the loadQu createDefaultProfiledComponents(Profiler); function App() { - useTrackComponentRender(); + useTrackRender(); const [loadQuery, queryRef] = useLoadableQuery(query); return ( diff --git a/src/testing/internal/profile/index.ts b/src/testing/internal/profile/index.ts index b7bf6716f58..764a4a33f0b 100644 --- a/src/testing/internal/profile/index.ts +++ b/src/testing/internal/profile/index.ts @@ -2,7 +2,7 @@ export type { NextRenderOptions, Profiler, ProfiledHook } from "./profile.js"; export { createTestProfiler, profileHook, - useTrackComponentRender, + useTrackRender, WaitForRenderTimeoutError, } from "./profile.js"; diff --git a/src/testing/internal/profile/profile.tsx b/src/testing/internal/profile/profile.tsx index 2201f26e1c6..3a18c9a7000 100644 --- a/src/testing/internal/profile/profile.tsx +++ b/src/testing/internal/profile/profile.tsx @@ -374,7 +374,7 @@ export function profileHook( ); } -export function useTrackComponentRender() { +export function useTrackRender() { const owner: React.ComponentType | undefined = (React as any) .__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED?.ReactCurrentOwner ?.current?.elementType; From a68308156b919be47c6afba7849e2abe0be2fd60 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 21 Nov 2023 21:46:26 -0700 Subject: [PATCH 130/199] Go back to renderedComponents directly on RenderInstance --- .../hooks/__tests__/useLoadableQuery.test.tsx | 26 ++++++++++--------- src/testing/internal/profile/Render.tsx | 4 +-- src/testing/internal/profile/profile.tsx | 9 ++++--- 3 files changed, 22 insertions(+), 17 deletions(-) diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index 559963a456f..2390053936a 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -262,21 +262,21 @@ it("loads a query and suspends when the load query function is called", async () ); { - const { context } = await Profiler.takeRender(); + const { renderedComponents } = await Profiler.takeRender(); - expect(context.renderedComponents).toStrictEqual([App]); + expect(renderedComponents).toStrictEqual([App]); } await act(() => user.click(screen.getByText("Load query"))); { - const { context } = await Profiler.takeRender(); + const { renderedComponents } = await Profiler.takeRender(); - expect(context.renderedComponents).toStrictEqual([App, SuspenseFallback]); + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); } { - const { snapshot, context } = await Profiler.takeRender(); + const { snapshot, renderedComponents } = await Profiler.takeRender(); expect(snapshot.result).toEqual({ data: { greeting: "Hello" }, @@ -284,7 +284,7 @@ it("loads a query and suspends when the load query function is called", async () networkStatus: NetworkStatus.ready, }); - expect(context.renderedComponents).toStrictEqual([ReadQueryHook]); + expect(renderedComponents).toStrictEqual([ReadQueryHook]); } }); @@ -322,21 +322,23 @@ it("loads a query with variables and suspends by passing variables to the loadQu ); { - const { context } = await Profiler.takeRender(); - expect(context.renderedComponents).toStrictEqual([App]); + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App]); } await act(() => user.click(screen.getByText("Load query"))); { - const { context } = await Profiler.takeRender(); - expect(context.renderedComponents).toStrictEqual([App, SuspenseFallback]); + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); } { - const { snapshot, context } = await Profiler.takeRender(); + const { snapshot, renderedComponents } = await Profiler.takeRender(); - expect(context.renderedComponents).toStrictEqual([ReadQueryHook]); + expect(renderedComponents).toStrictEqual([ReadQueryHook]); expect(snapshot.result).toEqual({ data: { character: { id: "1", name: "Spider-Man" } }, networkStatus: NetworkStatus.ready, diff --git a/src/testing/internal/profile/Render.tsx b/src/testing/internal/profile/Render.tsx index 5361cf371c5..c6534ea8847 100644 --- a/src/testing/internal/profile/Render.tsx +++ b/src/testing/internal/profile/Render.tsx @@ -64,7 +64,7 @@ export interface Render extends BaseRender { */ withinDOM: () => SyncScreen; - context: RenderContextValue; + renderedComponents: React.ComponentType[]; } /** @internal */ @@ -81,7 +81,7 @@ export class RenderInstance implements Render { baseRender: BaseRender, public snapshot: Snapshot, private stringifiedDOM: string | undefined, - public context: RenderContextValue + public renderedComponents: React.ComponentType[] ) { this.id = baseRender.id; this.phase = baseRender.phase; diff --git a/src/testing/internal/profile/profile.tsx b/src/testing/internal/profile/profile.tsx index 3a18c9a7000..9a176e2a162 100644 --- a/src/testing/internal/profile/profile.tsx +++ b/src/testing/internal/profile/profile.tsx @@ -175,9 +175,12 @@ export function createTestProfiler({ const domSnapshot = snapshotDOM ? window.document.body.innerHTML : undefined; - const render = new RenderInstance(baseRender, snapshot, domSnapshot, { - ...renderContext, - }); + const render = new RenderInstance( + baseRender, + snapshot, + domSnapshot, + renderContext.renderedComponents + ); renderContext.renderedComponents = []; Profiler.renders.push(render); resolveNextRender?.(render); From 5c562e441d9909619370670484acffdbf95ddf76 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 21 Nov 2023 21:46:56 -0700 Subject: [PATCH 131/199] Remove eslint disable --- src/react/hooks/__tests__/useLoadableQuery.test.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index 2390053936a..4809c642662 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -1,4 +1,3 @@ -/* eslint-disable testing-library/render-result-naming-convention */ import React, { Suspense, useState } from "react"; import { act, From 5e8aadcd1403be314230e9286c796aa1d926a1cc Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 21 Nov 2023 21:56:54 -0700 Subject: [PATCH 132/199] Update another test to use new pattern --- .../hooks/__tests__/useLoadableQuery.test.tsx | 84 ++++++++++++------- 1 file changed, 52 insertions(+), 32 deletions(-) diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index 4809c642662..bb416461e36 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -351,43 +351,60 @@ it("loads a query with variables and suspends by passing variables to the loadQu it("changes variables on a query and resuspends when passing new variables to the loadQuery function", async () => { const { query, mocks } = useVariablesQueryCase(); + const Profiler = createTestProfiler({ + initialSnapshot: { + result: null as UseReadQueryResult | null, + }, + }); + const { SuspenseFallback, ReadQueryHook } = - createDefaultProfiledComponents(); + createDefaultProfiledComponents(Profiler); - const App = createTestProfiler({ - Component: () => { - const [loadQuery, queryRef] = useLoadableQuery(query); + const App = () => { + useTrackRender(); + const [loadQuery, queryRef] = useLoadableQuery(query); - return ( - <> - - - }> - {queryRef && } - - - ); - }, - }); + return ( + <> + + + }> + {queryRef && } + + + ); + }; - const { user } = renderWithMocks(, { mocks }); + const { user } = renderWithMocks( + + + , + { mocks } + ); - expect(SuspenseFallback).not.toHaveRendered(); + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App]); + } await act(() => user.click(screen.getByText("Load 1st character"))); - expect(SuspenseFallback).toHaveRendered(); - expect(ReadQueryHook).not.toHaveRendered(); - expect(App).toHaveRenderedTimes(2); + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } { - const snapshot = await ReadQueryHook.takeSnapshot(); + const { snapshot, renderedComponents } = await Profiler.takeRender(); - expect(snapshot).toEqual({ + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ data: { character: { id: "1", name: "Spider-Man" } }, networkStatus: NetworkStatus.ready, error: undefined, @@ -396,21 +413,24 @@ it("changes variables on a query and resuspends when passing new variables to th await act(() => user.click(screen.getByText("Load 2nd character"))); - expect(SuspenseFallback).toHaveRenderedTimes(2); + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } { - const snapshot = await ReadQueryHook.takeSnapshot(); + const { snapshot, renderedComponents } = await Profiler.takeRender(); - expect(snapshot).toEqual({ + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ data: { character: { id: "2", name: "Black Widow" } }, networkStatus: NetworkStatus.ready, error: undefined, }); } - expect(SuspenseFallback).toHaveRenderedTimes(2); - expect(App).toHaveRenderedTimes(5); - expect(ReadQueryHook).toHaveRenderedTimes(2); + await expect(Profiler).not.toRerender(); }); it("allows the client to be overridden", async () => { From 79e00f5c755b24a5addf2835c14aa582ec679b50 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 21 Nov 2023 22:16:03 -0700 Subject: [PATCH 133/199] Fix usage of profileHook with updates to profiler --- src/testing/internal/profile/profile.tsx | 37 ++++++++++++------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/src/testing/internal/profile/profile.tsx b/src/testing/internal/profile/profile.tsx index 9a176e2a162..9cf80b0a9f2 100644 --- a/src/testing/internal/profile/profile.tsx +++ b/src/testing/internal/profile/profile.tsx @@ -334,44 +334,45 @@ type ProfiledHookFields = export interface ProfiledHook extends React.FC, ProfiledHookFields { - ProfiledComponent: Profiler; + Profiler: Profiler; } /** @internal */ export function profileHook( - renderCallback: (props: Props) => ReturnValue, - { displayName = renderCallback.name || "ProfiledHook" } = {} + renderCallback: (props: Props) => ReturnValue ): ProfiledHook { - let returnValue: ReturnValue; + const Profiler = createTestProfiler(); + const ProfiledHook = (props: Props) => { - ProfiledComponent.replaceSnapshot(renderCallback(props)); + Profiler.replaceSnapshot(renderCallback(props)); return null; }; - ProfiledHook.displayName = displayName; - const ProfiledComponent = createTestProfiler({ - onRender: () => returnValue, - }); + return Object.assign( - function ProfiledHook(props: Props) { - return ; + function App(props: Props) { + return ( + + + + ); }, { - ProfiledComponent, + Profiler, }, { - renders: ProfiledComponent.renders, - totalSnapshotCount: ProfiledComponent.totalRenderCount, + renders: Profiler.renders, + totalSnapshotCount: Profiler.totalRenderCount, async peekSnapshot(options) { - return (await ProfiledComponent.peekRender(options)).snapshot; + return (await Profiler.peekRender(options)).snapshot; }, async takeSnapshot(options) { - return (await ProfiledComponent.takeRender(options)).snapshot; + return (await Profiler.takeRender(options)).snapshot; }, getCurrentSnapshot() { - return ProfiledComponent.getCurrentRender().snapshot; + return Profiler.getCurrentRender().snapshot; }, async waitForNextSnapshot(options) { - return (await ProfiledComponent.waitForNextRender(options)).snapshot; + return (await Profiler.waitForNextRender(options)).snapshot; }, } satisfies ProfiledHookFields ); From 659f884fe5072f639533a59eafc89a632cd7a78a Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 21 Nov 2023 22:16:25 -0700 Subject: [PATCH 134/199] Fix matchers with updates to profiler --- src/testing/matchers/ProfiledComponent.ts | 24 ++++++++++------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/src/testing/matchers/ProfiledComponent.ts b/src/testing/matchers/ProfiledComponent.ts index 66c74480f5d..2ed110bc3a3 100644 --- a/src/testing/matchers/ProfiledComponent.ts +++ b/src/testing/matchers/ProfiledComponent.ts @@ -8,15 +8,12 @@ import type { export const toRerender: MatcherFunction<[options?: NextRenderOptions]> = async function (actual, options) { - const _profiled = actual as Profiler | ProfiledHook; - const profiled = - "ProfiledComponent" in _profiled - ? _profiled.ProfiledComponent - : _profiled; + const _profiler = actual as Profiler | ProfiledHook; + const profiler = "Profiler" in _profiler ? _profiler.Profiler : _profiler; const hint = this.utils.matcherHint("toRerender", "ProfiledComponent", ""); let pass = true; try { - await profiled.peekRender({ timeout: 100, ...options }); + await profiler.peekRender({ timeout: 100, ...options }); } catch (e) { if (e instanceof WaitForRenderTimeoutError) { pass = false; @@ -43,26 +40,25 @@ const failed = {}; export const toRenderExactlyTimes: MatcherFunction< [times: number, options?: NextRenderOptions] > = async function (actual, times, optionsPerRender) { - const _profiled = actual as Profiler | ProfiledHook; - const profiled = - "ProfiledComponent" in _profiled ? _profiled.ProfiledComponent : _profiled; + const _profiler = actual as Profiler | ProfiledHook; + const profiler = "Profiler" in _profiler ? _profiler.Profiler : _profiler; const options = { timeout: 100, ...optionsPerRender }; const hint = this.utils.matcherHint("toRenderExactlyTimes"); let pass = true; try { - if (profiled.totalRenderCount() > times) { + if (profiler.totalRenderCount() > times) { throw failed; } try { - while (profiled.totalRenderCount() < times) { - await profiled.waitForNextRender(options); + while (profiler.totalRenderCount() < times) { + await profiler.waitForNextRender(options); } } catch (e) { // timeouts here should just fail the test, rethrow other errors throw e instanceof WaitForRenderTimeoutError ? failed : e; } try { - await profiled.waitForNextRender(options); + await profiler.waitForNextRender(options); } catch (e) { // we are expecting a timeout here, so swallow that error, rethrow others if (!(e instanceof WaitForRenderTimeoutError)) { @@ -82,7 +78,7 @@ export const toRenderExactlyTimes: MatcherFunction< return ( hint + ` Expected component to${pass ? " not" : ""} render exactly ${times}.` + - ` It rendered ${profiled.totalRenderCount()} times.` + ` It rendered ${profiler.totalRenderCount()} times.` ); }, }; From 2a5a487f1a32ee41cc9b18ac9dfb40ff6f03ff97 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 21 Nov 2023 22:43:33 -0700 Subject: [PATCH 135/199] Update test that checks context to use updated API --- .../hooks/__tests__/useLoadableQuery.test.tsx | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index bb416461e36..c5d7549566c 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -504,8 +504,14 @@ it("passes context to the link", async () => { }), }); + const Profiler = createTestProfiler({ + initialSnapshot: { + result: null as UseReadQueryResult | null, + }, + }); + const { SuspenseFallback, ReadQueryHook } = - createDefaultProfiledComponents(); + createDefaultProfiledComponents(Profiler); function App() { const [loadQuery, queryRef] = useLoadableQuery(query, { @@ -522,13 +528,23 @@ it("passes context to the link", async () => { ); } - const { user } = renderWithClient(, { client }); + const { user } = renderWithClient( + + + , + { client } + ); await act(() => user.click(screen.getByText("Load query"))); - const snapshot = await ReadQueryHook.takeSnapshot(); + // initial render + await Profiler.takeRender(); + // load query + await Profiler.takeRender(); - expect(snapshot).toEqual({ + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ data: { context: { valueA: "A", valueB: "B" } }, networkStatus: NetworkStatus.ready, error: undefined, From 836c1c480fa85dcbe51ed77c9da7f92c65dcd6ca Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 21 Nov 2023 22:45:32 -0700 Subject: [PATCH 136/199] Update another test that checks client overriden to new API --- .../hooks/__tests__/useLoadableQuery.test.tsx | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index c5d7549566c..920ace65569 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -450,8 +450,14 @@ it("allows the client to be overridden", async () => { cache: new InMemoryCache(), }); + const Profiler = createTestProfiler({ + initialSnapshot: { + result: null as UseReadQueryResult | null, + }, + }); + const { SuspenseFallback, ReadQueryHook } = - createDefaultProfiledComponents(); + createDefaultProfiledComponents(Profiler); function App() { const [loadQuery, queryRef] = useLoadableQuery(query, { @@ -468,13 +474,23 @@ it("allows the client to be overridden", async () => { ); } - const { user } = renderWithClient(, { client: globalClient }); + const { user } = renderWithClient( + + + , + { client: globalClient } + ); await act(() => user.click(screen.getByText("Load query"))); - const snapshot = await ReadQueryHook.takeSnapshot(); + // initial + await Profiler.takeRender(); + // load query + await Profiler.takeRender(); - expect(snapshot).toEqual({ + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ data: { greeting: "local hello" }, networkStatus: NetworkStatus.ready, error: undefined, From 4d27a077465e308571207f2b3c6a95833076ebfb Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 21 Nov 2023 22:50:10 -0700 Subject: [PATCH 137/199] Update test that checks for cache update --- .../hooks/__tests__/useLoadableQuery.test.tsx | 33 +++++++++++++++---- 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index 920ace65569..379ce7638cd 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -1286,10 +1286,17 @@ it("reacts to cache updates", async () => { link: new MockLink(mocks), }); + const Profiler = createTestProfiler({ + initialSnapshot: { + result: null as UseReadQueryResult | null, + }, + }); + const { SuspenseFallback, ReadQueryHook } = - createDefaultProfiledComponents(); + createDefaultProfiledComponents(Profiler); function App() { + useTrackRender(); const [loadQuery, queryRef] = useLoadableQuery(query); return ( @@ -1302,14 +1309,25 @@ it("reacts to cache updates", async () => { ); } - const { user } = renderWithClient(, { client }); + const { user } = renderWithClient( + + + , + { client } + ); await act(() => user.click(screen.getByText("Load query"))); + // initial render + await Profiler.takeRender(); + // load query + await Profiler.takeRender(); + { - const snapshot = await ReadQueryHook.takeSnapshot(); + const { snapshot, renderedComponents } = await Profiler.takeRender(); - expect(snapshot).toEqual({ + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ data: { greeting: "Hello" }, error: undefined, networkStatus: NetworkStatus.ready, @@ -1322,14 +1340,17 @@ it("reacts to cache updates", async () => { }); { - const snapshot = await ReadQueryHook.takeSnapshot(); + const { snapshot, renderedComponents } = await Profiler.takeRender(); - expect(snapshot).toEqual({ + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ data: { greeting: "Updated Hello" }, error: undefined, networkStatus: NetworkStatus.ready, }); } + + await expect(Profiler).not.toRerender(); }); it("applies `errorPolicy` on next fetch when it changes between renders", async () => { From 2b6498287372308fb02456bfdcfbda068910e235 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 21 Nov 2023 22:52:31 -0700 Subject: [PATCH 138/199] Rename render context to profiler context --- src/testing/internal/profile/Render.tsx | 1 - src/testing/internal/profile/context.tsx | 18 ++++++++++-------- src/testing/internal/profile/profile.tsx | 16 ++++++++-------- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/src/testing/internal/profile/Render.tsx b/src/testing/internal/profile/Render.tsx index c6534ea8847..4db810c0825 100644 --- a/src/testing/internal/profile/Render.tsx +++ b/src/testing/internal/profile/Render.tsx @@ -12,7 +12,6 @@ As we only use this file in our internal tests, we can safely ignore it. import { within, screen } from "@testing-library/dom"; import { JSDOM, VirtualConsole } from "jsdom"; import { applyStackTrace, captureStackTrace } from "./traces.js"; -import type { RenderContextValue } from "./context.js"; /** @internal */ export interface BaseRender { diff --git a/src/testing/internal/profile/context.tsx b/src/testing/internal/profile/context.tsx index be13d19e17f..8225af13275 100644 --- a/src/testing/internal/profile/context.tsx +++ b/src/testing/internal/profile/context.tsx @@ -1,31 +1,33 @@ import * as React from "react"; -export interface RenderContextValue { +export interface ProfilerContextValue { renderedComponents: React.ComponentType[]; } -const RenderContext = React.createContext( +const ProfilerContext = React.createContext( undefined ); -export function RenderContextProvider({ +export function ProfilerContextProvider({ children, value, }: { children: React.ReactNode; - value: RenderContextValue; + value: ProfilerContextValue; }) { - const parentContext = useRenderContext(); + const parentContext = useProfilerContext(); if (parentContext) { throw new Error("Profilers should not be nested in the same tree"); } return ( - {children} + + {children} + ); } -export function useRenderContext() { - return React.useContext(RenderContext); +export function useProfilerContext() { + return React.useContext(ProfilerContext); } diff --git a/src/testing/internal/profile/profile.tsx b/src/testing/internal/profile/profile.tsx index 9cf80b0a9f2..ede0505ec38 100644 --- a/src/testing/internal/profile/profile.tsx +++ b/src/testing/internal/profile/profile.tsx @@ -8,8 +8,8 @@ global.TextDecoder ??= TextDecoder; import type { Render, BaseRender } from "./Render.js"; import { RenderInstance } from "./Render.js"; import { applyStackTrace, captureStackTrace } from "./traces.js"; -import type { RenderContextValue } from "./context.js"; -import { RenderContextProvider, useRenderContext } from "./context.js"; +import type { ProfilerContextValue } from "./context.js"; +import { ProfilerContextProvider, useProfilerContext } from "./context.js"; type ValidSnapshot = void | (object & { /* not a function */ call?: never }); @@ -135,7 +135,7 @@ export function createTestProfiler({ })); }; - const renderContext: RenderContextValue = { + const profilerContext: ProfilerContextValue = { renderedComponents: [], }; @@ -179,9 +179,9 @@ export function createTestProfiler({ baseRender, snapshot, domSnapshot, - renderContext.renderedComponents + profilerContext.renderedComponents ); - renderContext.renderedComponents = []; + profilerContext.renderedComponents = []; Profiler.renders.push(render); resolveNextRender?.(render); } catch (error) { @@ -200,11 +200,11 @@ export function createTestProfiler({ const Profiler: Profiler = Object.assign( ({ children }: ProfilerProps) => { return ( - + {children} - + ); }, { @@ -387,7 +387,7 @@ export function useTrackRender() { throw new Error("useTrackComponentRender: Unable to determine hook owner"); } - const ctx = useRenderContext(); + const ctx = useProfilerContext(); if (!ctx) { throw new Error( From 5df75f946d0ed3d2e38dd9688ef324807088bc41 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 21 Nov 2023 22:59:51 -0700 Subject: [PATCH 139/199] Extract helper to create default profiler for the tests --- .../hooks/__tests__/useLoadableQuery.test.tsx | 39 +++++++------------ 1 file changed, 14 insertions(+), 25 deletions(-) diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index 379ce7638cd..6c196c90b0c 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -154,6 +154,15 @@ function usePaginatedQueryCase() { return { query, link, client }; } +function createDefaultProfiler() { + return createTestProfiler({ + initialSnapshot: { + error: null as Error | null, + result: null as UseReadQueryResult | null, + }, + }); +} + function createDefaultProfiledComponents< Snapshot extends { result: UseReadQueryResult | null; @@ -230,11 +239,7 @@ function renderWithClient( it("loads a query and suspends when the load query function is called", async () => { const { query, mocks } = useSimpleQueryCase(); - const Profiler = createTestProfiler({ - initialSnapshot: { - result: null as UseReadQueryResult | null, - }, - }); + const Profiler = createDefaultProfiler(); const { SuspenseFallback, ReadQueryHook } = createDefaultProfiledComponents(Profiler); @@ -290,11 +295,7 @@ it("loads a query and suspends when the load query function is called", async () it("loads a query with variables and suspends by passing variables to the loadQuery function", async () => { const { query, mocks } = useVariablesQueryCase(); - const Profiler = createTestProfiler({ - initialSnapshot: { - result: null as UseReadQueryResult | null, - }, - }); + const Profiler = createDefaultProfiler(); const { SuspenseFallback, ReadQueryHook } = createDefaultProfiledComponents(Profiler); @@ -351,11 +352,7 @@ it("loads a query with variables and suspends by passing variables to the loadQu it("changes variables on a query and resuspends when passing new variables to the loadQuery function", async () => { const { query, mocks } = useVariablesQueryCase(); - const Profiler = createTestProfiler({ - initialSnapshot: { - result: null as UseReadQueryResult | null, - }, - }); + const Profiler = createDefaultProfiler(); const { SuspenseFallback, ReadQueryHook } = createDefaultProfiledComponents(Profiler); @@ -450,11 +447,7 @@ it("allows the client to be overridden", async () => { cache: new InMemoryCache(), }); - const Profiler = createTestProfiler({ - initialSnapshot: { - result: null as UseReadQueryResult | null, - }, - }); + const Profiler = createDefaultProfiler(); const { SuspenseFallback, ReadQueryHook } = createDefaultProfiledComponents(Profiler); @@ -520,11 +513,7 @@ it("passes context to the link", async () => { }), }); - const Profiler = createTestProfiler({ - initialSnapshot: { - result: null as UseReadQueryResult | null, - }, - }); + const Profiler = createDefaultProfiler(); const { SuspenseFallback, ReadQueryHook } = createDefaultProfiledComponents(Profiler); From c408bed2dc3a1492e81cdfc3c9ee254da930c16e Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 21 Nov 2023 23:04:39 -0700 Subject: [PATCH 140/199] Convert test that checks for canonical results to new API --- .../hooks/__tests__/useLoadableQuery.test.tsx | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index 6c196c90b0c..8a489717537 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -601,8 +601,10 @@ it('enables canonical results when canonizeResults is "true"', async () => { link: new MockLink([]), }); + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = - createDefaultProfiledComponents(); + createDefaultProfiledComponents(Profiler); function App() { const [loadQuery, queryRef] = useLoadableQuery(query, { @@ -619,15 +621,23 @@ it('enables canonical results when canonizeResults is "true"', async () => { ); } - const { user } = renderWithClient(, { client }); + const { user } = renderWithClient( + + + , + { client } + ); await act(() => user.click(screen.getByText("Load query"))); - const snapshot = await ReadQueryHook.takeSnapshot(); - const resultSet = new Set(snapshot.data.results); + // initial render + await Profiler.takeRender(); + + const { snapshot } = await Profiler.takeRender(); + const resultSet = new Set(snapshot.result?.data.results); const values = Array.from(resultSet).map((item) => item.value); - expect(snapshot).toEqual({ + expect(snapshot.result).toEqual({ data: { results }, networkStatus: NetworkStatus.ready, error: undefined, From 554ecac8fb17b07c43783f10617d7fe9d6f60ce1 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 21 Nov 2023 23:09:49 -0700 Subject: [PATCH 141/199] Update cache-and-network test to new API --- .../hooks/__tests__/useLoadableQuery.test.tsx | 59 +++++++++++-------- 1 file changed, 34 insertions(+), 25 deletions(-) diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index 8a489717537..eb193e74916 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -724,7 +724,8 @@ it("can disable canonical results when the cache's canonizeResults setting is tr }); it("returns initial cache data followed by network data when the fetch policy is `cache-and-network`", async () => { - const query: TypedDocumentNode<{ hello: string }, never> = gql` + type QueryData = { hello: string }; + const query: TypedDocumentNode = gql` query { hello } @@ -742,37 +743,44 @@ it("returns initial cache data followed by network data when the fetch policy is cache.writeQuery({ query, data: { hello: "from cache" } }); - const { SuspenseFallback, ReadQueryHook } = createDefaultProfiledComponents<{ - hello: string; - }>(); + const Profiler = createDefaultProfiler(); - const App = createTestProfiler({ - Component: () => { - const [loadQuery, queryRef] = useLoadableQuery(query, { - fetchPolicy: "cache-and-network", - }); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); - return ( - <> - - }> - {queryRef && } - - - ); - }, - }); + function App() { + useTrackRender(); + const [loadQuery, queryRef] = useLoadableQuery(query, { + fetchPolicy: "cache-and-network", + }); - const { user } = renderWithClient(, { client }); + return ( + <> + + }> + {queryRef && } + + + ); + } + + const { user } = renderWithClient( + + + , + { client } + ); await act(() => user.click(screen.getByText("Load query"))); - expect(SuspenseFallback).not.toHaveRendered(); + // initial render + await Profiler.takeRender(); { - const snapshot = await ReadQueryHook.takeSnapshot(); + const { snapshot, renderedComponents } = await Profiler.takeRender(); - expect(snapshot).toEqual({ + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot.result).toEqual({ data: { hello: "from cache" }, networkStatus: NetworkStatus.loading, error: undefined, @@ -780,9 +788,10 @@ it("returns initial cache data followed by network data when the fetch policy is } { - const snapshot = await ReadQueryHook.takeSnapshot(); + const { snapshot, renderedComponents } = await Profiler.takeRender(); - expect(snapshot).toEqual({ + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ data: { hello: "from link" }, networkStatus: NetworkStatus.ready, error: undefined, From 891183c3d16b52d2190169ed17ceb4791906514f Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 21 Nov 2023 23:28:03 -0700 Subject: [PATCH 142/199] Update test that checks for rendered error boundary when refetch throws --- .../hooks/__tests__/useLoadableQuery.test.tsx | 35 ++++++++++++++++--- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index eb193e74916..e276451a196 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -187,6 +187,7 @@ function createDefaultProfiledComponents< } function ErrorFallback({ error }: { error: Error }) { + useTrackRender(); profiler.mergeSnapshot({ error } as Partial); return
Oops
; @@ -2233,8 +2234,10 @@ it("throws errors when errors are returned after calling `refetch`", async () => }, ]; + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook, ErrorBoundary, ErrorFallback } = - createDefaultProfiledComponents(); + createDefaultProfiledComponents(Profiler); function App() { const [loadQuery, queryRef, { refetch }] = useLoadableQuery(query); @@ -2252,15 +2255,39 @@ it("throws errors when errors are returned after calling `refetch`", async () => ); } - const { user } = renderWithMocks(, { mocks }); + const { user } = renderWithMocks( + + + , + { mocks } + ); await act(() => user.click(screen.getByText("Load query"))); - await ReadQueryHook.waitForNextSnapshot(); + + // initial render + await Profiler.takeRender(); + // load query + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { character: { id: "1", name: "Captain Marvel" } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + await act(() => user.click(screen.getByText("Refetch"))); + // Refetch + await Profiler.takeRender(); + { - const { snapshot } = await ErrorFallback.takeRender(); + const { snapshot, renderedComponents } = await Profiler.takeRender(); + expect(renderedComponents).toStrictEqual([ErrorFallback]); expect(snapshot.error).toEqual( new ApolloError({ graphQLErrors: [new GraphQLError("Something went wrong")], From ce05f8ad63f771bd92dd6f7ebab644a71a3339a6 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 21 Nov 2023 23:34:11 -0700 Subject: [PATCH 143/199] Update multiple refetch test to use new API --- .../hooks/__tests__/useLoadableQuery.test.tsx | 47 ++++++++++++++++--- 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index e276451a196..26442c5ea65 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -2179,10 +2179,13 @@ it("re-suspends multiple times when calling `refetch` multiple times", async () }, ]; + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = - createDefaultProfiledComponents(); + createDefaultProfiledComponents(Profiler); function App() { + useTrackRender(); const [loadQuery, queryRef, { refetch }] = useLoadableQuery(query); return ( @@ -2196,20 +2199,52 @@ it("re-suspends multiple times when calling `refetch` multiple times", async () ); } - const { user } = renderWithMocks(, { mocks }); + const { user } = renderWithMocks( + + + , + { mocks } + ); + + // initial render + await Profiler.takeRender(); await act(() => user.click(screen.getByText("Load query"))); - expect(SuspenseFallback).toHaveRendered(); - await ReadQueryHook.takeSnapshot(); + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + // initial result + await Profiler.takeRender(); const button = screen.getByText("Refetch"); await act(() => user.click(button)); - expect(SuspenseFallback).toHaveRenderedTimes(2); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + // refetch result + await Profiler.takeRender(); await act(() => user.click(button)); - expect(SuspenseFallback).toHaveRenderedTimes(3); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + // refetch result + await Profiler.takeRender(); + + await expect(Profiler).not.toRerender(); }); it("throws errors when errors are returned after calling `refetch`", async () => { From ccde7cb9f901545df34db838a6926563617f594f Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 27 Nov 2023 13:08:30 -0700 Subject: [PATCH 144/199] Rename createTestProfiler to createProfiler --- src/react/components/__tests__/client/Query.test.tsx | 4 ++-- src/react/hoc/__tests__/queries/lifecycle.test.tsx | 4 ++-- src/react/hoc/__tests__/queries/loading.test.tsx | 4 ++-- src/react/hooks/__tests__/useBackgroundQuery.test.tsx | 10 +++++----- src/react/hooks/__tests__/useFragment.test.tsx | 8 ++++---- src/react/hooks/__tests__/useLoadableQuery.test.tsx | 6 +++--- src/react/hooks/__tests__/useSuspenseQuery.test.tsx | 6 +++--- src/testing/internal/profile/index.ts | 2 +- src/testing/internal/profile/profile.tsx | 4 ++-- 9 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/react/components/__tests__/client/Query.test.tsx b/src/react/components/__tests__/client/Query.test.tsx index 02cc1abb222..8efb3d767f1 100644 --- a/src/react/components/__tests__/client/Query.test.tsx +++ b/src/react/components/__tests__/client/Query.test.tsx @@ -11,7 +11,7 @@ import { ApolloProvider } from "../../../context"; import { itAsync, MockedProvider, mockSingleLink } from "../../../../testing"; import { Query } from "../../Query"; import { QueryResult } from "../../../types/types"; -import { createTestProfiler } from "../../../../testing/internal"; +import { createProfiler } from "../../../../testing/internal"; const allPeopleQuery: DocumentNode = gql` query people { @@ -1498,7 +1498,7 @@ describe("Query component", () => { ); } - const ProfiledContainer = createTestProfiler({ + const ProfiledContainer = createProfiler({ Component: Container, }); diff --git a/src/react/hoc/__tests__/queries/lifecycle.test.tsx b/src/react/hoc/__tests__/queries/lifecycle.test.tsx index 34ec5a0cf51..9e15debd3b0 100644 --- a/src/react/hoc/__tests__/queries/lifecycle.test.tsx +++ b/src/react/hoc/__tests__/queries/lifecycle.test.tsx @@ -10,7 +10,7 @@ import { mockSingleLink } from "../../../../testing"; import { Query as QueryComponent } from "../../../components"; import { graphql } from "../../graphql"; import { ChildProps, DataValue } from "../../types"; -import { createTestProfiler } from "../../../../testing/internal"; +import { createProfiler } from "../../../../testing/internal"; describe("[queries] lifecycle", () => { // lifecycle @@ -58,7 +58,7 @@ describe("[queries] lifecycle", () => { } ); - const ProfiledApp = createTestProfiler, Vars>({ + const ProfiledApp = createProfiler, Vars>({ Component: Container, }); diff --git a/src/react/hoc/__tests__/queries/loading.test.tsx b/src/react/hoc/__tests__/queries/loading.test.tsx index bf29abc4834..0149894c873 100644 --- a/src/react/hoc/__tests__/queries/loading.test.tsx +++ b/src/react/hoc/__tests__/queries/loading.test.tsx @@ -13,7 +13,7 @@ import { InMemoryCache as Cache } from "../../../../cache"; import { itAsync, mockSingleLink } from "../../../../testing"; import { graphql } from "../../graphql"; import { ChildProps, DataValue } from "../../types"; -import { createTestProfiler } from "../../../../testing/internal"; +import { createProfiler } from "../../../../testing/internal"; describe("[queries] loading", () => { // networkStatus / loading @@ -413,7 +413,7 @@ describe("[queries] loading", () => { } ); - const ProfiledContainer = createTestProfiler< + const ProfiledContainer = createProfiler< DataValue<{ allPeople: { people: { diff --git a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx index b6176e5be86..eea06adabe0 100644 --- a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx +++ b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx @@ -53,7 +53,7 @@ import { import equal from "@wry/equality"; import { RefetchWritePolicy } from "../../../core/watchQueryOptions"; import { skipToken } from "../constants"; -import { createTestProfiler, spyOnConsole } from "../../../testing/internal"; +import { createProfiler, spyOnConsole } from "../../../testing/internal"; function renderIntegrationTest({ client, @@ -332,7 +332,7 @@ function renderVariablesIntegrationTest({ ); } - const ProfiledApp = createTestProfiler>({ + const ProfiledApp = createProfiler>({ Component: App, snapshotDOM: true, onRender: ({ replaceSnapshot }) => replaceSnapshot(cloneDeep(renders)), @@ -516,7 +516,7 @@ function renderPaginatedIntegrationTest({ ); } - const ProfiledApp = createTestProfiler({ + const ProfiledApp = createProfiler({ Component: App, snapshotDOM: true, initialSnapshot: { @@ -3895,7 +3895,7 @@ describe("useBackgroundQuery", () => { ); } - const ProfiledApp = createTestProfiler({ Component: App, snapshotDOM: true }); + const ProfiledApp = createProfiler({ Component: App, snapshotDOM: true }); render(); @@ -4193,7 +4193,7 @@ describe("useBackgroundQuery", () => { ); } - const ProfiledApp = createTestProfiler({ Component: App, snapshotDOM: true }); + const ProfiledApp = createProfiler({ Component: App, snapshotDOM: true }); render(); { diff --git a/src/react/hooks/__tests__/useFragment.test.tsx b/src/react/hooks/__tests__/useFragment.test.tsx index edac1702d94..b1476c6a053 100644 --- a/src/react/hooks/__tests__/useFragment.test.tsx +++ b/src/react/hooks/__tests__/useFragment.test.tsx @@ -29,7 +29,7 @@ import { concatPagination } from "../../../utilities"; import assert from "assert"; import { expectTypeOf } from "expect-type"; import { SubscriptionObserver } from "zen-observable-ts"; -import { createTestProfiler, spyOnConsole } from "../../../testing/internal"; +import { createProfiler, spyOnConsole } from "../../../testing/internal"; describe("useFragment", () => { it("is importable and callable", () => { @@ -1481,7 +1481,7 @@ describe("has the same timing as `useQuery`", () => { return complete ? JSON.stringify(fragmentData) : "loading"; } - const ProfiledComponent = createTestProfiler({ + const ProfiledComponent = createProfiler({ Component, initialSnapshot: { queryData: undefined as any, @@ -1569,7 +1569,7 @@ describe("has the same timing as `useQuery`", () => { return <>{JSON.stringify({ item: data })}; } - const ProfiledParent = createTestProfiler({ + const ProfiledParent = createProfiler({ Component: Parent, snapshotDOM: true, onRender() { @@ -1664,7 +1664,7 @@ describe("has the same timing as `useQuery`", () => { return <>{JSON.stringify(data)}; } - const ProfiledParent = createTestProfiler({ + const ProfiledParent = createProfiler({ Component: Parent, onRender() { const parent = screen.getByTestId("parent"); diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index 26442c5ea65..51022246b48 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -45,7 +45,7 @@ import { FetchMoreFunction, RefetchFunction } from "../useSuspenseQuery"; import invariant, { InvariantError } from "ts-invariant"; import { Profiler, - createTestProfiler, + createProfiler, spyOnConsole, useTrackRender, } from "../../../testing/internal"; @@ -155,7 +155,7 @@ function usePaginatedQueryCase() { } function createDefaultProfiler() { - return createTestProfiler({ + return createProfiler({ initialSnapshot: { error: null as Error | null, result: null as UseReadQueryResult | null, @@ -1295,7 +1295,7 @@ it("reacts to cache updates", async () => { link: new MockLink(mocks), }); - const Profiler = createTestProfiler({ + const Profiler = createProfiler({ initialSnapshot: { result: null as UseReadQueryResult | null, }, diff --git a/src/react/hooks/__tests__/useSuspenseQuery.test.tsx b/src/react/hooks/__tests__/useSuspenseQuery.test.tsx index 7abfb005cc3..2620e4dee26 100644 --- a/src/react/hooks/__tests__/useSuspenseQuery.test.tsx +++ b/src/react/hooks/__tests__/useSuspenseQuery.test.tsx @@ -51,7 +51,7 @@ import { RefetchWritePolicy, WatchQueryFetchPolicy, } from "../../../core/watchQueryOptions"; -import { createTestProfiler, spyOnConsole } from "../../../testing/internal"; +import { createProfiler, spyOnConsole } from "../../../testing/internal"; type RenderSuspenseHookOptions = Omit< RenderHookOptions, @@ -371,7 +371,7 @@ describe("useSuspenseQuery", () => { ); }; - const ProfiledApp = createTestProfiler< + const ProfiledApp = createProfiler< UseSuspenseQueryResult >({ Component: App, @@ -9613,7 +9613,7 @@ describe("useSuspenseQuery", () => { ); } - const ProfiledApp = createTestProfiler({ + const ProfiledApp = createProfiler({ Component: App, snapshotDOM: true, }); diff --git a/src/testing/internal/profile/index.ts b/src/testing/internal/profile/index.ts index 764a4a33f0b..cecebde1a69 100644 --- a/src/testing/internal/profile/index.ts +++ b/src/testing/internal/profile/index.ts @@ -1,6 +1,6 @@ export type { NextRenderOptions, Profiler, ProfiledHook } from "./profile.js"; export { - createTestProfiler, + createProfiler, profileHook, useTrackRender, WaitForRenderTimeoutError, diff --git a/src/testing/internal/profile/profile.tsx b/src/testing/internal/profile/profile.tsx index ede0505ec38..4c76b24aa9d 100644 --- a/src/testing/internal/profile/profile.tsx +++ b/src/testing/internal/profile/profile.tsx @@ -89,7 +89,7 @@ interface ProfiledComponentFields { } /** @internal */ -export function createTestProfiler({ +export function createProfiler({ onRender, snapshotDOM = false, initialSnapshot, @@ -341,7 +341,7 @@ export interface ProfiledHook export function profileHook( renderCallback: (props: Props) => ReturnValue ): ProfiledHook { - const Profiler = createTestProfiler(); + const Profiler = createProfiler(); const ProfiledHook = (props: Props) => { Profiler.replaceSnapshot(renderCallback(props)); From 0d141df79d93a4384da7013a32c3ef4f3c236ef7 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 27 Nov 2023 13:25:14 -0700 Subject: [PATCH 145/199] Recreate profile helper by using createProfiler --- src/testing/internal/profile/index.ts | 8 +++- src/testing/internal/profile/profile.tsx | 51 +++++++++++++++++++++++- 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/src/testing/internal/profile/index.ts b/src/testing/internal/profile/index.ts index cecebde1a69..9a579cc49e2 100644 --- a/src/testing/internal/profile/index.ts +++ b/src/testing/internal/profile/index.ts @@ -1,6 +1,12 @@ -export type { NextRenderOptions, Profiler, ProfiledHook } from "./profile.js"; +export type { + NextRenderOptions, + Profiler, + ProfiledComponent, + ProfiledHook, +} from "./profile.js"; export { createProfiler, + profile, profileHook, useTrackRender, WaitForRenderTimeoutError, diff --git a/src/testing/internal/profile/profile.tsx b/src/testing/internal/profile/profile.tsx index 4c76b24aa9d..02871e2d875 100644 --- a/src/testing/internal/profile/profile.tsx +++ b/src/testing/internal/profile/profile.tsx @@ -88,8 +88,57 @@ interface ProfiledComponentFields { waitForNextRender(options?: NextRenderOptions): Promise>; } +export interface ProfiledComponent + extends React.FC, + ProfiledComponentFields, + ProfiledComponentOnlyFields {} + +/** @internal */ +export function profile({ + Component, + ...options +}: { + onRender?: ( + info: BaseRender & { + snapshot: Snapshot; + replaceSnapshot: ReplaceSnapshot; + mergeSnapshot: MergeSnapshot; + } + ) => void; + Component: React.ComponentType; + snapshotDOM?: boolean; + initialSnapshot?: Snapshot; +}): ProfiledComponent { + const Profiler = createProfiler(options); + + return Object.assign( + function ProfiledComponent(props: Props) { + return ( + + + + ); + }, + { + mergeSnapshot: Profiler.mergeSnapshot, + replaceSnapshot: Profiler.replaceSnapshot, + getCurrentRender: Profiler.getCurrentRender, + peekRender: Profiler.peekRender, + takeRender: Profiler.takeRender, + totalRenderCount: Profiler.totalRenderCount, + waitForNextRender: Profiler.waitForNextRender, + get renders() { + return Profiler.renders; + }, + } + ); +} + /** @internal */ -export function createProfiler({ +export function createProfiler< + Snapshot extends ValidSnapshot = void, + Props = {}, +>({ onRender, snapshotDOM = false, initialSnapshot, From 4b9a0ec9c028ee9252b2faf17d94d74d80e71151 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 27 Nov 2023 13:31:26 -0700 Subject: [PATCH 146/199] Use profile export in tests that previously used it --- src/react/components/__tests__/client/Query.test.tsx | 4 ++-- src/react/hoc/__tests__/queries/lifecycle.test.tsx | 4 ++-- src/react/hoc/__tests__/queries/loading.test.tsx | 4 ++-- src/react/hooks/__tests__/useBackgroundQuery.test.tsx | 10 +++++----- src/react/hooks/__tests__/useFragment.test.tsx | 8 ++++---- src/react/hooks/__tests__/useSuspenseQuery.test.tsx | 6 +++--- 6 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/react/components/__tests__/client/Query.test.tsx b/src/react/components/__tests__/client/Query.test.tsx index 8efb3d767f1..acdd2015301 100644 --- a/src/react/components/__tests__/client/Query.test.tsx +++ b/src/react/components/__tests__/client/Query.test.tsx @@ -11,7 +11,7 @@ import { ApolloProvider } from "../../../context"; import { itAsync, MockedProvider, mockSingleLink } from "../../../../testing"; import { Query } from "../../Query"; import { QueryResult } from "../../../types/types"; -import { createProfiler } from "../../../../testing/internal"; +import { profile } from "../../../../testing/internal"; const allPeopleQuery: DocumentNode = gql` query people { @@ -1498,7 +1498,7 @@ describe("Query component", () => { ); } - const ProfiledContainer = createProfiler({ + const ProfiledContainer = profile({ Component: Container, }); diff --git a/src/react/hoc/__tests__/queries/lifecycle.test.tsx b/src/react/hoc/__tests__/queries/lifecycle.test.tsx index 9e15debd3b0..cf460af964a 100644 --- a/src/react/hoc/__tests__/queries/lifecycle.test.tsx +++ b/src/react/hoc/__tests__/queries/lifecycle.test.tsx @@ -10,7 +10,7 @@ import { mockSingleLink } from "../../../../testing"; import { Query as QueryComponent } from "../../../components"; import { graphql } from "../../graphql"; import { ChildProps, DataValue } from "../../types"; -import { createProfiler } from "../../../../testing/internal"; +import { profile } from "../../../../testing/internal"; describe("[queries] lifecycle", () => { // lifecycle @@ -58,7 +58,7 @@ describe("[queries] lifecycle", () => { } ); - const ProfiledApp = createProfiler, Vars>({ + const ProfiledApp = profile, Vars>({ Component: Container, }); diff --git a/src/react/hoc/__tests__/queries/loading.test.tsx b/src/react/hoc/__tests__/queries/loading.test.tsx index 0149894c873..387a6803fb5 100644 --- a/src/react/hoc/__tests__/queries/loading.test.tsx +++ b/src/react/hoc/__tests__/queries/loading.test.tsx @@ -13,7 +13,7 @@ import { InMemoryCache as Cache } from "../../../../cache"; import { itAsync, mockSingleLink } from "../../../../testing"; import { graphql } from "../../graphql"; import { ChildProps, DataValue } from "../../types"; -import { createProfiler } from "../../../../testing/internal"; +import { profile } from "../../../../testing/internal"; describe("[queries] loading", () => { // networkStatus / loading @@ -413,7 +413,7 @@ describe("[queries] loading", () => { } ); - const ProfiledContainer = createProfiler< + const ProfiledContainer = profile< DataValue<{ allPeople: { people: { diff --git a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx index eea06adabe0..131364939cd 100644 --- a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx +++ b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx @@ -53,7 +53,7 @@ import { import equal from "@wry/equality"; import { RefetchWritePolicy } from "../../../core/watchQueryOptions"; import { skipToken } from "../constants"; -import { createProfiler, spyOnConsole } from "../../../testing/internal"; +import { profile, spyOnConsole } from "../../../testing/internal"; function renderIntegrationTest({ client, @@ -332,7 +332,7 @@ function renderVariablesIntegrationTest({ ); } - const ProfiledApp = createProfiler>({ + const ProfiledApp = profile>({ Component: App, snapshotDOM: true, onRender: ({ replaceSnapshot }) => replaceSnapshot(cloneDeep(renders)), @@ -516,7 +516,7 @@ function renderPaginatedIntegrationTest({ ); } - const ProfiledApp = createProfiler({ + const ProfiledApp = profile({ Component: App, snapshotDOM: true, initialSnapshot: { @@ -3895,7 +3895,7 @@ describe("useBackgroundQuery", () => { ); } - const ProfiledApp = createProfiler({ Component: App, snapshotDOM: true }); + const ProfiledApp = profile({ Component: App, snapshotDOM: true }); render(); @@ -4193,7 +4193,7 @@ describe("useBackgroundQuery", () => { ); } - const ProfiledApp = createProfiler({ Component: App, snapshotDOM: true }); + const ProfiledApp = profile({ Component: App, snapshotDOM: true }); render(); { diff --git a/src/react/hooks/__tests__/useFragment.test.tsx b/src/react/hooks/__tests__/useFragment.test.tsx index b1476c6a053..21b9e083a03 100644 --- a/src/react/hooks/__tests__/useFragment.test.tsx +++ b/src/react/hooks/__tests__/useFragment.test.tsx @@ -29,7 +29,7 @@ import { concatPagination } from "../../../utilities"; import assert from "assert"; import { expectTypeOf } from "expect-type"; import { SubscriptionObserver } from "zen-observable-ts"; -import { createProfiler, spyOnConsole } from "../../../testing/internal"; +import { profile, spyOnConsole } from "../../../testing/internal"; describe("useFragment", () => { it("is importable and callable", () => { @@ -1481,7 +1481,7 @@ describe("has the same timing as `useQuery`", () => { return complete ? JSON.stringify(fragmentData) : "loading"; } - const ProfiledComponent = createProfiler({ + const ProfiledComponent = profile({ Component, initialSnapshot: { queryData: undefined as any, @@ -1569,7 +1569,7 @@ describe("has the same timing as `useQuery`", () => { return <>{JSON.stringify({ item: data })}; } - const ProfiledParent = createProfiler({ + const ProfiledParent = profile({ Component: Parent, snapshotDOM: true, onRender() { @@ -1664,7 +1664,7 @@ describe("has the same timing as `useQuery`", () => { return <>{JSON.stringify(data)}; } - const ProfiledParent = createProfiler({ + const ProfiledParent = profile({ Component: Parent, onRender() { const parent = screen.getByTestId("parent"); diff --git a/src/react/hooks/__tests__/useSuspenseQuery.test.tsx b/src/react/hooks/__tests__/useSuspenseQuery.test.tsx index 2620e4dee26..642be7d023a 100644 --- a/src/react/hooks/__tests__/useSuspenseQuery.test.tsx +++ b/src/react/hooks/__tests__/useSuspenseQuery.test.tsx @@ -51,7 +51,7 @@ import { RefetchWritePolicy, WatchQueryFetchPolicy, } from "../../../core/watchQueryOptions"; -import { createProfiler, spyOnConsole } from "../../../testing/internal"; +import { profile, spyOnConsole } from "../../../testing/internal"; type RenderSuspenseHookOptions = Omit< RenderHookOptions, @@ -371,7 +371,7 @@ describe("useSuspenseQuery", () => { ); }; - const ProfiledApp = createProfiler< + const ProfiledApp = profile< UseSuspenseQueryResult >({ Component: App, @@ -9613,7 +9613,7 @@ describe("useSuspenseQuery", () => { ); } - const ProfiledApp = createProfiler({ + const ProfiledApp = profile({ Component: App, snapshotDOM: true, }); From f9428fc8fd2e6d814cb3dc965f7ce4c7e178d990 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 27 Nov 2023 13:53:19 -0700 Subject: [PATCH 147/199] Allow useTrackRender to accept `name` option to uniquely identify it --- src/testing/internal/profile/context.tsx | 2 +- src/testing/internal/profile/profile.tsx | 19 ++++++++++++------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/testing/internal/profile/context.tsx b/src/testing/internal/profile/context.tsx index 8225af13275..a8488e73a6c 100644 --- a/src/testing/internal/profile/context.tsx +++ b/src/testing/internal/profile/context.tsx @@ -1,7 +1,7 @@ import * as React from "react"; export interface ProfilerContextValue { - renderedComponents: React.ComponentType[]; + renderedComponents: Array; } const ProfilerContext = React.createContext( diff --git a/src/testing/internal/profile/profile.tsx b/src/testing/internal/profile/profile.tsx index 02871e2d875..1b3da13965a 100644 --- a/src/testing/internal/profile/profile.tsx +++ b/src/testing/internal/profile/profile.tsx @@ -427,13 +427,18 @@ export function profileHook( ); } -export function useTrackRender() { - const owner: React.ComponentType | undefined = (React as any) - .__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED?.ReactCurrentOwner - ?.current?.elementType; +function resolveHookOwner(): React.ComponentType | undefined { + return (React as any).__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED + ?.ReactCurrentOwner?.current?.elementType; +} + +export function useTrackRender({ name }: { name?: string } = {}) { + const component = name || resolveHookOwner(); - if (!owner) { - throw new Error("useTrackComponentRender: Unable to determine hook owner"); + if (!component) { + throw new Error( + "useTrackRender: Unable to determine component. Please ensure the hook is called inside a rendered component or provide a `name` option." + ); } const ctx = useProfilerContext(); @@ -445,6 +450,6 @@ export function useTrackRender() { } React.useLayoutEffect(() => { - ctx.renderedComponents.unshift(owner); + ctx.renderedComponents.unshift(component); }); } From 3671d0ef6678914e89cf2537972f1d45304d6dbe Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 27 Nov 2023 14:02:49 -0700 Subject: [PATCH 148/199] Allow render helpers to take wrapper option --- .../hooks/__tests__/useLoadableQuery.test.tsx | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index 51022246b48..7fe2decf563 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -5,6 +5,7 @@ import { screen, renderHook, waitFor, + RenderOptions, } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { ErrorBoundary as ReactErrorBoundary } from "react-error-boundary"; @@ -209,12 +210,20 @@ function createDefaultProfiledComponents< }; } -function renderWithMocks(ui: React.ReactElement, props: MockedProviderProps) { +function renderWithMocks( + ui: React.ReactElement, + { + wrapper: Wrapper = React.Fragment, + ...props + }: MockedProviderProps & { wrapper?: RenderOptions["wrapper"] } +) { const user = userEvent.setup(); const utils = render(ui, { wrapper: ({ children }) => ( - {children} + + {children} + ), }); @@ -223,21 +232,23 @@ function renderWithMocks(ui: React.ReactElement, props: MockedProviderProps) { function renderWithClient( ui: React.ReactElement, - options: { client: ApolloClient } + options: { client: ApolloClient; wrapper?: RenderOptions["wrapper"] } ) { - const { client } = options; + const { client, wrapper: Wrapper = React.Fragment } = options; const user = userEvent.setup(); const utils = render(ui, { wrapper: ({ children }) => ( - {children} + + {children} + ), }); return { ...utils, user }; } -it("loads a query and suspends when the load query function is called", async () => { +it.only("loads a query and suspends when the load query function is called", async () => { const { query, mocks } = useSimpleQueryCase(); const Profiler = createDefaultProfiler(); From 0d536d4e78fe88e4c081f5c514b00053304e7654 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 27 Nov 2023 14:09:03 -0700 Subject: [PATCH 149/199] Render in the wrapper for each test --- .../hooks/__tests__/useLoadableQuery.test.tsx | 92 ++++++++----------- 1 file changed, 37 insertions(+), 55 deletions(-) diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index 7fe2decf563..1a7cc929e60 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -248,7 +248,7 @@ function renderWithClient( return { ...utils, user }; } -it.only("loads a query and suspends when the load query function is called", async () => { +it("loads a query and suspends when the load query function is called", async () => { const { query, mocks } = useSimpleQueryCase(); const Profiler = createDefaultProfiler(); @@ -270,12 +270,10 @@ it.only("loads a query and suspends when the load query function is called", asy ); } - const { user } = renderWithMocks( - - - , - { mocks } - ); + const { user } = renderWithMocks(, { + mocks, + wrapper: ({ children }) => {children}, + }); { const { renderedComponents } = await Profiler.takeRender(); @@ -326,12 +324,10 @@ it("loads a query with variables and suspends by passing variables to the loadQu ); } - const { user } = renderWithMocks( - - - , - { mocks } - ); + const { user } = renderWithMocks(, { + mocks, + wrapper: ({ children }) => {children}, + }); { const { renderedComponents } = await Profiler.takeRender(); @@ -388,12 +384,10 @@ it("changes variables on a query and resuspends when passing new variables to th ); }; - const { user } = renderWithMocks( - - - , - { mocks } - ); + const { user } = renderWithMocks(, { + mocks, + wrapper: ({ children }) => {children}, + }); { const { renderedComponents } = await Profiler.takeRender(); @@ -479,12 +473,10 @@ it("allows the client to be overridden", async () => { ); } - const { user } = renderWithClient( - - - , - { client: globalClient } - ); + const { user } = renderWithClient(, { + client: globalClient, + wrapper: ({ children }) => {children}, + }); await act(() => user.click(screen.getByText("Load query"))); @@ -545,12 +537,10 @@ it("passes context to the link", async () => { ); } - const { user } = renderWithClient( - - - , - { client } - ); + const { user } = renderWithClient(, { + client, + wrapper: ({ children }) => {children}, + }); await act(() => user.click(screen.getByText("Load query"))); @@ -633,12 +623,10 @@ it('enables canonical results when canonizeResults is "true"', async () => { ); } - const { user } = renderWithClient( - - - , - { client } - ); + const { user } = renderWithClient(, { + client, + wrapper: ({ children }) => {children}, + }); await act(() => user.click(screen.getByText("Load query"))); @@ -776,12 +764,10 @@ it("returns initial cache data followed by network data when the fetch policy is ); } - const { user } = renderWithClient( - - - , - { client } - ); + const { user } = renderWithClient(, { + client, + wrapper: ({ children }) => {children}, + }); await act(() => user.click(screen.getByText("Load query"))); @@ -2210,12 +2196,10 @@ it("re-suspends multiple times when calling `refetch` multiple times", async () ); } - const { user } = renderWithMocks( - - - , - { mocks } - ); + const { user } = renderWithMocks(, { + mocks, + wrapper: ({ children }) => {children}, + }); // initial render await Profiler.takeRender(); @@ -2301,12 +2285,10 @@ it("throws errors when errors are returned after calling `refetch`", async () => ); } - const { user } = renderWithMocks( - - - , - { mocks } - ); + const { user } = renderWithMocks(, { + mocks, + wrapper: ({ children }) => {children}, + }); await act(() => user.click(screen.getByText("Load query"))); From c132c2a4eaa6a40e5581d10da66ee3f83eb4caee Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 27 Nov 2023 14:19:30 -0700 Subject: [PATCH 150/199] Update test that checks ignore errorPolicy to updated profiler --- .../hooks/__tests__/useLoadableQuery.test.tsx | 40 ++++++++++++++++--- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index 1a7cc929e60..4723fd4aaf4 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -2342,8 +2342,10 @@ it('ignores errors returned after calling `refetch` when errorPolicy is set to " }, ]; + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook, ErrorBoundary, ErrorFallback } = - createDefaultProfiledComponents(); + createDefaultProfiledComponents(Profiler); function App() { const [loadQuery, queryRef, { refetch }] = useLoadableQuery(query, { @@ -2363,22 +2365,48 @@ it('ignores errors returned after calling `refetch` when errorPolicy is set to " ); } - const { user } = renderWithMocks(, { mocks }); + const { user } = renderWithMocks(, { + mocks, + wrapper: ({ children }) => {children}, + }); + + // initial render + await Profiler.takeRender(); await act(() => user.click(screen.getByText("Load query"))); - await ReadQueryHook.takeSnapshot(); + + // load query + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toStrictEqual({ + data: { character: { id: "1", name: "Captain Marvel" } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + await act(() => user.click(screen.getByText("Refetch"))); + // refetch + await Profiler.takeRender(); + { - const snapshot = await ReadQueryHook.takeSnapshot(); + const { snapshot, renderedComponents } = await Profiler.takeRender(); - expect(snapshot).toEqual({ + expect(snapshot.error).toBeNull(); + expect(snapshot.result).toEqual({ data: { character: { id: "1", name: "Captain Marvel" } }, error: undefined, networkStatus: NetworkStatus.ready, }); - expect(ErrorFallback).not.toHaveRendered(); + + expect(renderedComponents).not.toContain(ErrorFallback); } + + await expect(Profiler).not.toRerender(); }); it('returns errors after calling `refetch` when errorPolicy is set to "all"', async () => { From 41b4c1142f8812aa695e7a410a250347570ca7d4 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 27 Nov 2023 14:29:36 -0700 Subject: [PATCH 151/199] Update test that checks errorPolicy: all to updated profiler api --- .../hooks/__tests__/useLoadableQuery.test.tsx | 56 ++++++++++++++----- 1 file changed, 41 insertions(+), 15 deletions(-) diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index 4723fd4aaf4..21aff3f1f73 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -2429,8 +2429,10 @@ it('returns errors after calling `refetch` when errorPolicy is set to "all"', as }, ]; - const { SuspenseFallback, ReadQueryHook, ErrorBoundary } = - createDefaultProfiledComponents(); + const Profiler = createDefaultProfiler(); + + const { SuspenseFallback, ReadQueryHook, ErrorBoundary, ErrorFallback } = + createDefaultProfiledComponents(Profiler); function App() { const [loadQuery, queryRef, { refetch }] = useLoadableQuery(query, { @@ -2450,24 +2452,48 @@ it('returns errors after calling `refetch` when errorPolicy is set to "all"', as ); } - const { user } = renderWithMocks(, { mocks }); + const { user } = renderWithMocks(, { + mocks, + wrapper: ({ children }) => {children}, + }); await act(() => user.click(screen.getByText("Load query"))); - await ReadQueryHook.waitForNextSnapshot(); + + // initial render + await Profiler.takeRender(); + // load query + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { character: { id: "1", name: "Captain Marvel" } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + await act(() => user.click(screen.getByText("Refetch"))); - // TODO: Figure out why there is an extra render here. - // Perhaps related? https://github.com/apollographql/apollo-client/issues/11315 - await ReadQueryHook.takeSnapshot(); - const snapshot = await ReadQueryHook.takeSnapshot(); + // refetch + await Profiler.takeRender(); - expect(snapshot).toEqual({ - data: { character: { id: "1", name: "Captain Marvel" } }, - error: new ApolloError({ - graphQLErrors: [new GraphQLError("Something went wrong")], - }), - networkStatus: NetworkStatus.error, - }); + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).not.toContain(ErrorFallback); + expect(snapshot.error).toBeNull(); + expect(snapshot.result).toEqual({ + data: { character: { id: "1", name: "Captain Marvel" } }, + error: new ApolloError({ + graphQLErrors: [new GraphQLError("Something went wrong")], + }), + networkStatus: NetworkStatus.error, + }); + } + + await expect(Profiler).not.toRerender(); }); it('handles partial data results after calling `refetch` when errorPolicy is set to "all"', async () => { From 16b6cb597152a7a54f1b39ddeb455d9ef04a71c4 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 27 Nov 2023 14:35:30 -0700 Subject: [PATCH 152/199] Update partial data test with error policy to use new profiler api --- .../hooks/__tests__/useLoadableQuery.test.tsx | 56 ++++++++++++++----- 1 file changed, 41 insertions(+), 15 deletions(-) diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index 21aff3f1f73..345529d3f62 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -2517,8 +2517,10 @@ it('handles partial data results after calling `refetch` when errorPolicy is set }, ]; - const { SuspenseFallback, ReadQueryHook, ErrorBoundary } = - createDefaultProfiledComponents(); + const Profiler = createDefaultProfiler(); + + const { SuspenseFallback, ReadQueryHook, ErrorBoundary, ErrorFallback } = + createDefaultProfiledComponents(Profiler); function App() { const [loadQuery, queryRef, { refetch }] = useLoadableQuery(query, { @@ -2538,24 +2540,48 @@ it('handles partial data results after calling `refetch` when errorPolicy is set ); } - const { user } = renderWithMocks(, { mocks }); + const { user } = renderWithMocks(, { + mocks, + wrapper: ({ children }) => {children}, + }); await act(() => user.click(screen.getByText("Load query"))); - await ReadQueryHook.waitForNextSnapshot(); + + // initial render + await Profiler.takeRender(); + // load query + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { character: { id: "1", name: "Captain Marvel" } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + await act(() => user.click(screen.getByText("Refetch"))); - // TODO: Figure out why there is an extra render here. - // Perhaps related? https://github.com/apollographql/apollo-client/issues/11315 - await ReadQueryHook.takeSnapshot(); - const snapshot = await ReadQueryHook.takeSnapshot(); + // refetch + await Profiler.takeRender(); - expect(snapshot).toEqual({ - data: { character: { id: "1", name: null } }, - error: new ApolloError({ - graphQLErrors: [new GraphQLError("Something went wrong")], - }), - networkStatus: NetworkStatus.error, - }); + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).not.toContain(ErrorFallback); + expect(snapshot.error).toBeNull(); + expect(snapshot.result).toEqual({ + data: { character: { id: "1", name: null } }, + error: new ApolloError({ + graphQLErrors: [new GraphQLError("Something went wrong")], + }), + networkStatus: NetworkStatus.error, + }); + } + + await expect(Profiler).not.toRerender(); }); it("`refetch` works with startTransition to allow React to show stale UI until finished suspending", async () => { From 2432df90cf3cbc715a8f6f80798badb1de774260 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 27 Nov 2023 14:39:56 -0700 Subject: [PATCH 153/199] Update test that checks fetchMore to updated profiler api --- .../hooks/__tests__/useLoadableQuery.test.tsx | 34 +++++++++++++------ 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index 345529d3f62..d9b8f657708 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -2713,10 +2713,13 @@ it("`refetch` works with startTransition to allow React to show stale UI until f it("re-suspends when calling `fetchMore` with different variables", async () => { const { query, client } = usePaginatedQueryCase(); + + const Profiler = createDefaultProfiler(); const { SuspenseFallback, ReadQueryHook } = - createDefaultProfiledComponents(); + createDefaultProfiledComponents(Profiler); function App() { + useTrackRender(); const [loadQuery, queryRef, { fetchMore }] = useLoadableQuery(query); return ( @@ -2734,16 +2737,22 @@ it("re-suspends when calling `fetchMore` with different variables", async () => ); } - const { user } = renderWithClient(, { client }); + const { user } = renderWithClient(, { + client, + wrapper: ({ children }) => {children}, + }); await act(() => user.click(screen.getByText("Load query"))); - expect(SuspenseFallback).toHaveRendered(); + // initial render + await Profiler.takeRender(); + // load query + await Profiler.takeRender(); { - const snapshot = await ReadQueryHook.waitForNextSnapshot(); + const { snapshot } = await Profiler.takeRender(); - expect(snapshot).toEqual({ + expect(snapshot.result).toEqual({ data: { letters: [ { letter: "A", position: 1 }, @@ -2757,16 +2766,17 @@ it("re-suspends when calling `fetchMore` with different variables", async () => await act(() => user.click(screen.getByText("Fetch more"))); - expect(SuspenseFallback).toHaveRenderedTimes(2); + { + const { renderedComponents } = await Profiler.takeRender(); - // TODO: Figure out why there is an extra render here. - // Perhaps related? https://github.com/apollographql/apollo-client/issues/11315 - await ReadQueryHook.takeSnapshot(); + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } { - const snapshot = await ReadQueryHook.takeSnapshot(); + const { snapshot, renderedComponents } = await Profiler.takeRender(); - expect(snapshot).toEqual({ + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ data: { letters: [ { letter: "C", position: 3 }, @@ -2777,6 +2787,8 @@ it("re-suspends when calling `fetchMore` with different variables", async () => networkStatus: NetworkStatus.ready, }); } + + await expect(Profiler).not.toRerender(); }); it("properly uses `updateQuery` when calling `fetchMore`", async () => { From c2e9dc6377f2ac09ab316a7bcb624441f1589262 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 27 Nov 2023 14:42:52 -0700 Subject: [PATCH 154/199] Update test that checks updateQuery to use updated profiler api --- .../hooks/__tests__/useLoadableQuery.test.tsx | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index d9b8f657708..b368d4d57f6 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -2793,8 +2793,9 @@ it("re-suspends when calling `fetchMore` with different variables", async () => it("properly uses `updateQuery` when calling `fetchMore`", async () => { const { query, client } = usePaginatedQueryCase(); + const Profiler = createDefaultProfiler(); const { SuspenseFallback, ReadQueryHook } = - createDefaultProfiledComponents(); + createDefaultProfiledComponents(Profiler); function App() { const [loadQuery, queryRef, { fetchMore }] = useLoadableQuery(query); @@ -2821,16 +2822,22 @@ it("properly uses `updateQuery` when calling `fetchMore`", async () => { ); } - const { user } = renderWithClient(, { client }); + const { user } = renderWithClient(, { + client, + wrapper: ({ children }) => {children}, + }); await act(() => user.click(screen.getByText("Load query"))); - expect(SuspenseFallback).toHaveRendered(); + // initial render + await Profiler.takeRender(); + // load query + await Profiler.takeRender(); { - const snapshot = await ReadQueryHook.waitForNextSnapshot(); + const { snapshot } = await Profiler.takeRender(); - expect(snapshot).toEqual({ + expect(snapshot.result).toEqual({ data: { letters: [ { letter: "A", position: 1 }, @@ -2844,16 +2851,13 @@ it("properly uses `updateQuery` when calling `fetchMore`", async () => { await act(() => user.click(screen.getByText("Fetch more"))); - expect(SuspenseFallback).toHaveRenderedTimes(2); - - // TODO: Figure out why there is an extra render here. - // Perhaps related? https://github.com/apollographql/apollo-client/issues/11315 - await ReadQueryHook.takeSnapshot(); + // fetch more + await Profiler.takeRender(); { - const snapshot = await ReadQueryHook.takeSnapshot(); + const { snapshot } = await Profiler.takeRender(); - expect(snapshot).toEqual({ + expect(snapshot.result).toEqual({ data: { letters: [ { letter: "A", position: 1 }, @@ -2866,6 +2870,8 @@ it("properly uses `updateQuery` when calling `fetchMore`", async () => { networkStatus: NetworkStatus.ready, }); } + + await expect(Profiler).not.toRerender(); }); it("properly uses cache field policies when calling `fetchMore` without `updateQuery`", async () => { From 29351ad79d7bf0496673b5b0a0c1585bcc2b9f3d Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 27 Nov 2023 14:45:43 -0700 Subject: [PATCH 155/199] Update test that checks field policies with updated profiler api --- .../hooks/__tests__/useLoadableQuery.test.tsx | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index b368d4d57f6..8a0ab5ef949 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -2876,8 +2876,9 @@ it("properly uses `updateQuery` when calling `fetchMore`", async () => { it("properly uses cache field policies when calling `fetchMore` without `updateQuery`", async () => { const { query, link } = usePaginatedQueryCase(); + const Profiler = createDefaultProfiler(); const { SuspenseFallback, ReadQueryHook } = - createDefaultProfiledComponents(); + createDefaultProfiledComponents(Profiler); const client = new ApolloClient({ link, @@ -2910,16 +2911,22 @@ it("properly uses cache field policies when calling `fetchMore` without `updateQ ); } - const { user } = renderWithClient(, { client }); + const { user } = renderWithClient(, { + client, + wrapper: ({ children }) => {children}, + }); await act(() => user.click(screen.getByText("Load query"))); - expect(SuspenseFallback).toHaveRendered(); + // initial render + await Profiler.takeRender(); + // load query + await Profiler.takeRender(); { - const snapshot = await ReadQueryHook.waitForNextSnapshot(); + const { snapshot } = await Profiler.takeRender(); - expect(snapshot).toEqual({ + expect(snapshot.result).toEqual({ data: { letters: [ { letter: "A", position: 1 }, @@ -2933,16 +2940,13 @@ it("properly uses cache field policies when calling `fetchMore` without `updateQ await act(() => user.click(screen.getByText("Fetch more"))); - expect(SuspenseFallback).toHaveRenderedTimes(2); - - // TODO: Figure out why there is an extra render here. - // Perhaps related? https://github.com/apollographql/apollo-client/issues/11315 - await ReadQueryHook.takeSnapshot(); + // fetch more + await Profiler.takeRender(); { - const snapshot = await ReadQueryHook.takeSnapshot(); + const { snapshot } = await Profiler.takeRender(); - expect(snapshot).toEqual({ + expect(snapshot.result).toEqual({ data: { letters: [ { letter: "A", position: 1 }, @@ -2955,6 +2959,8 @@ it("properly uses cache field policies when calling `fetchMore` without `updateQ networkStatus: NetworkStatus.ready, }); } + + await expect(Profiler).not.toRerender(); }); it("`fetchMore` works with startTransition to allow React to show stale UI until finished suspending", async () => { From 6576a59bb1d7e796ffd3ab39a1ec1ee94553f5e0 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 27 Nov 2023 14:48:37 -0700 Subject: [PATCH 156/199] Update test that checks refetchWritePolicy to updated profiler api --- .../hooks/__tests__/useLoadableQuery.test.tsx | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index 8a0ab5ef949..c30a9e89598 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -3171,8 +3171,9 @@ it('honors refetchWritePolicy set to "merge"', async () => { }, }); + const Profiler = createDefaultProfiler(); const { SuspenseFallback, ReadQueryHook } = - createDefaultProfiledComponents(); + createDefaultProfiledComponents(Profiler); const client = new ApolloClient({ link: new MockLink(mocks), @@ -3197,14 +3198,22 @@ it('honors refetchWritePolicy set to "merge"', async () => { ); } - const { user } = renderWithClient(, { client }); + const { user } = renderWithClient(, { + client, + wrapper: ({ children }) => {children}, + }); await act(() => user.click(screen.getByText("Load query"))); + // initial render + await Profiler.takeRender(); + // load query + await Profiler.takeRender(); + { - const snapshot = await ReadQueryHook.takeSnapshot(); + const { snapshot } = await Profiler.takeRender(); - expect(snapshot).toEqual({ + expect(snapshot.result).toEqual({ data: { primes: [2, 3, 5, 7, 11] }, error: undefined, networkStatus: NetworkStatus.ready, @@ -3214,10 +3223,13 @@ it('honors refetchWritePolicy set to "merge"', async () => { await act(() => user.click(screen.getByText("Refetch"))); + // refetch + await Profiler.takeRender(); + { - const snapshot = await ReadQueryHook.takeSnapshot(); + const { snapshot } = await Profiler.takeRender(); - expect(snapshot).toEqual({ + expect(snapshot.result).toEqual({ data: { primes: [2, 3, 5, 7, 11, 13, 17, 19, 23, 29] }, error: undefined, networkStatus: NetworkStatus.ready, @@ -3230,6 +3242,8 @@ it('honors refetchWritePolicy set to "merge"', async () => { ], ]); } + + await expect(Profiler).not.toRerender(); }); it('defaults refetchWritePolicy to "overwrite"', async () => { From 55fc90923d3a431941bc01a528542c878e6b8926 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 27 Nov 2023 14:50:49 -0700 Subject: [PATCH 157/199] Update test that checks refetchWritePolicy default to updated profiler api --- .../hooks/__tests__/useLoadableQuery.test.tsx | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index c30a9e89598..8a54a48586c 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -3289,8 +3289,9 @@ it('defaults refetchWritePolicy to "overwrite"', async () => { }, }); + const Profiler = createDefaultProfiler(); const { SuspenseFallback, ReadQueryHook } = - createDefaultProfiledComponents(); + createDefaultProfiledComponents(Profiler); const client = new ApolloClient({ link: new MockLink(mocks), @@ -3313,14 +3314,22 @@ it('defaults refetchWritePolicy to "overwrite"', async () => { ); } - const { user } = renderWithClient(, { client }); + const { user } = renderWithClient(, { + client, + wrapper: ({ children }) => {children}, + }); await act(() => user.click(screen.getByText("Load query"))); + // initial load + await Profiler.takeRender(); + // load query + await Profiler.takeRender(); + { - const snapshot = await ReadQueryHook.takeSnapshot(); + const { snapshot } = await Profiler.takeRender(); - expect(snapshot).toEqual({ + expect(snapshot.result).toEqual({ data: { primes: [2, 3, 5, 7, 11] }, error: undefined, networkStatus: NetworkStatus.ready, @@ -3330,10 +3339,13 @@ it('defaults refetchWritePolicy to "overwrite"', async () => { await act(() => user.click(screen.getByText("Refetch"))); + // refetch + await Profiler.takeRender(); + { - const snapshot = await ReadQueryHook.takeSnapshot(); + const { snapshot } = await Profiler.takeRender(); - expect(snapshot).toEqual({ + expect(snapshot.result).toEqual({ data: { primes: [13, 17, 19, 23, 29] }, error: undefined, networkStatus: NetworkStatus.ready, From 1f594940f0775929847ddbdad365be49845b1822 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 27 Nov 2023 14:56:38 -0700 Subject: [PATCH 158/199] Update test that checks partial data in cache to updated profiler api --- .../hooks/__tests__/useLoadableQuery.test.tsx | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index 8a54a48586c..4425b7baaac 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -3389,8 +3389,9 @@ it('does not suspend when partial data is in the cache and using a "cache-first" }, ]; + const Profiler = createDefaultProfiler>(); const { SuspenseFallback, ReadQueryHook } = - createDefaultProfiledComponents>(); + createDefaultProfiledComponents(Profiler); const cache = new InMemoryCache(); @@ -3402,6 +3403,7 @@ it('does not suspend when partial data is in the cache and using a "cache-first" const client = new ApolloClient({ link: new MockLink(mocks), cache }); function App() { + useTrackRender(); const [loadQuery, queryRef] = useLoadableQuery(fullQuery, { fetchPolicy: "cache-first", returnPartialData: true, @@ -3417,33 +3419,41 @@ it('does not suspend when partial data is in the cache and using a "cache-first" ); } - const { user } = renderWithClient(, { client }); + const { user } = renderWithClient(, { + client, + wrapper: ({ children }) => {children}, + }); await act(() => user.click(screen.getByText("Load query"))); - expect(SuspenseFallback).not.toHaveRendered(); + // initial load + await Profiler.takeRender(); { - const snapshot = await ReadQueryHook.takeSnapshot(); + const { snapshot, renderedComponents } = await Profiler.takeRender(); - expect(snapshot).toEqual({ + expect(snapshot.result).toEqual({ data: { character: { id: "1" } }, error: undefined, networkStatus: NetworkStatus.loading, }); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); } { - const snapshot = await ReadQueryHook.takeSnapshot(); + const { snapshot, renderedComponents } = await Profiler.takeRender(); - expect(snapshot).toEqual({ + expect(snapshot.result).toEqual({ data: { character: { id: "1", name: "Doctor Strange" } }, error: undefined, networkStatus: NetworkStatus.ready, }); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); } - expect(SuspenseFallback).not.toHaveRendered(); + await expect(Profiler).not.toRerender(); }); it('suspends and does not use partial data from other variables in the cache when changing variables and using a "cache-first" fetch policy with returnPartialData: true', async () => { From 453cbb0133a89ab690d3c5d4eb795bd442341ac8 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 27 Nov 2023 15:03:22 -0700 Subject: [PATCH 159/199] Allow strings in rendered components in render tracker --- src/testing/internal/profile/Render.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/testing/internal/profile/Render.tsx b/src/testing/internal/profile/Render.tsx index 4db810c0825..c077c63fac3 100644 --- a/src/testing/internal/profile/Render.tsx +++ b/src/testing/internal/profile/Render.tsx @@ -63,7 +63,7 @@ export interface Render extends BaseRender { */ withinDOM: () => SyncScreen; - renderedComponents: React.ComponentType[]; + renderedComponents: Array; } /** @internal */ @@ -80,7 +80,7 @@ export class RenderInstance implements Render { baseRender: BaseRender, public snapshot: Snapshot, private stringifiedDOM: string | undefined, - public renderedComponents: React.ComponentType[] + public renderedComponents: Array ) { this.id = baseRender.id; this.phase = baseRender.phase; From 249ebb51065565e19f5ef7d84010fe13c2ef1119 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 27 Nov 2023 15:04:18 -0700 Subject: [PATCH 160/199] Update test that checks partial data and changing vars to updated profiler api --- .../hooks/__tests__/useLoadableQuery.test.tsx | 39 ++++++++++++++----- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index 4425b7baaac..8f1fbd253ff 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -3475,10 +3475,12 @@ it('suspends and does not use partial data from other variables in the cache whe variables: { id: "1" }, }); + const Profiler = createDefaultProfiler>(); const { SuspenseFallback, ReadQueryHook } = - createDefaultProfiledComponents>(); + createDefaultProfiledComponents(Profiler); function App() { + useTrackRender(); const [loadQuery, queryRef] = useLoadableQuery(query, { fetchPolicy: "cache-first", returnPartialData: true, @@ -3495,45 +3497,62 @@ it('suspends and does not use partial data from other variables in the cache whe ); } - const { user } = renderWithMocks(, { mocks, cache }); + const { user } = renderWithMocks(, { + mocks, + cache, + wrapper: ({ children }) => {children}, + }); await act(() => user.click(screen.getByText("Load query"))); - expect(SuspenseFallback).not.toHaveRendered(); + // initial render + await Profiler.takeRender(); { - const snapshot = await ReadQueryHook.takeSnapshot(); + const { snapshot, renderedComponents } = await Profiler.takeRender(); - expect(snapshot).toEqual({ + expect(snapshot.result).toEqual({ data: { character: { id: "1" } }, error: undefined, networkStatus: NetworkStatus.loading, }); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); } { - const snapshot = await ReadQueryHook.takeSnapshot(); + const { snapshot, renderedComponents } = await Profiler.takeRender(); - expect(snapshot).toEqual({ + expect(snapshot.result).toEqual({ data: { character: { id: "1", name: "Spider-Man" } }, error: undefined, networkStatus: NetworkStatus.ready, }); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); } await act(() => user.click(screen.getByText("Change variables"))); - expect(SuspenseFallback).toHaveRendered(); + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } { - const snapshot = await ReadQueryHook.takeSnapshot(); + const { snapshot, renderedComponents } = await Profiler.takeRender(); - expect(snapshot).toEqual({ + expect(snapshot.result).toEqual({ data: { character: { id: "2", name: "Black Widow" } }, error: undefined, networkStatus: NetworkStatus.ready, }); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); } + + await expect(Profiler).not.toRerender(); }); it('suspends when partial data is in the cache and using a "network-only" fetch policy with returnPartialData', async () => { From 7b0caa9c74bc05e51028b877235e5d03cd809146 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 27 Nov 2023 15:08:41 -0700 Subject: [PATCH 161/199] Update test that checks partial data with network-only to updated profiler api --- .../hooks/__tests__/useLoadableQuery.test.tsx | 35 ++++++++++++++----- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index 8f1fbd253ff..02741049185 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -3586,8 +3586,9 @@ it('suspends when partial data is in the cache and using a "network-only" fetch }, ]; + const Profiler = createDefaultProfiler>(); const { SuspenseFallback, ReadQueryHook } = - createDefaultProfiledComponents>(); + createDefaultProfiledComponents(Profiler); const cache = new InMemoryCache(); @@ -3597,6 +3598,7 @@ it('suspends when partial data is in the cache and using a "network-only" fetch }); function App() { + useTrackRender(); const [loadQuery, queryRef] = useLoadableQuery(fullQuery, { fetchPolicy: "network-only", returnPartialData: true, @@ -3612,19 +3614,34 @@ it('suspends when partial data is in the cache and using a "network-only" fetch ); } - const { user } = renderWithMocks(, { mocks, cache }); + const { user } = renderWithMocks(, { + mocks, + cache, + wrapper: ({ children }) => {children}, + }); await act(() => user.click(screen.getByText("Load query"))); - expect(SuspenseFallback).toHaveRendered(); + // initial render + await Profiler.takeRender(); - const snapshot = await ReadQueryHook.takeSnapshot(); + { + const { renderedComponents } = await Profiler.takeRender(); - expect(snapshot).toEqual({ - data: { character: { id: "1", name: "Doctor Strange" } }, - error: undefined, - networkStatus: NetworkStatus.ready, - }); + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { character: { id: "1", name: "Doctor Strange" } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await expect(Profiler).not.toRerender(); }); it('suspends when partial data is in the cache and using a "no-cache" fetch policy with returnPartialData', async () => { From aec18c6a1741b1e88db7d7b6bd2c541910556c48 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 27 Nov 2023 15:10:59 -0700 Subject: [PATCH 162/199] Update test that checks partial data with no-cache to updated profiler api --- .../hooks/__tests__/useLoadableQuery.test.tsx | 33 ++++++++++++++----- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index 02741049185..a1104095fee 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -3684,10 +3684,12 @@ it('suspends when partial data is in the cache and using a "no-cache" fetch poli data: { character: { id: "1" } }, }); + const Profiler = createDefaultProfiler>(); const { SuspenseFallback, ReadQueryHook } = - createDefaultProfiledComponents>(); + createDefaultProfiledComponents(Profiler); function App() { + useTrackRender(); const [loadQuery, queryRef] = useLoadableQuery(fullQuery, { fetchPolicy: "no-cache", returnPartialData: true, @@ -3703,19 +3705,32 @@ it('suspends when partial data is in the cache and using a "no-cache" fetch poli ); } - const { user } = renderWithMocks(, { mocks, cache }); + const { user } = renderWithMocks(, { + mocks, + cache, + wrapper: ({ children }) => {children}, + }); await act(() => user.click(screen.getByText("Load query"))); - expect(SuspenseFallback).toHaveRendered(); + // initial load + await Profiler.takeRender(); - const snapshot = await ReadQueryHook.takeSnapshot(); + { + const { renderedComponents } = await Profiler.takeRender(); - expect(snapshot).toEqual({ - data: { character: { id: "1", name: "Doctor Strange" } }, - error: undefined, - networkStatus: NetworkStatus.ready, - }); + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { character: { id: "1", name: "Doctor Strange" } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } }); it('warns when using returnPartialData with a "no-cache" fetch policy', async () => { From ed51d0ad5bcffda41d75ca49bed52ac2565c3087 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 27 Nov 2023 15:13:56 -0700 Subject: [PATCH 163/199] Update test that checks partial data with cache-and-network to updated profiler api --- .../hooks/__tests__/useLoadableQuery.test.tsx | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index a1104095fee..55f8b211992 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -3800,10 +3800,12 @@ it('does not suspend when partial data is in the cache and using a "cache-and-ne data: { character: { id: "1" } }, }); + const Profiler = createDefaultProfiler>(); const { SuspenseFallback, ReadQueryHook } = - createDefaultProfiledComponents>(); + createDefaultProfiledComponents(Profiler); function App() { + useTrackRender(); const [loadQuery, queryRef] = useLoadableQuery(fullQuery, { fetchPolicy: "cache-and-network", returnPartialData: true, @@ -3819,16 +3821,22 @@ it('does not suspend when partial data is in the cache and using a "cache-and-ne ); } - const { user } = renderWithMocks(, { mocks, cache }); + const { user } = renderWithMocks(, { + mocks, + cache, + wrapper: ({ children }) => {children}, + }); await act(() => user.click(screen.getByText("Load query"))); - expect(SuspenseFallback).not.toHaveRendered(); + // initial render + await Profiler.takeRender(); { - const snapshot = await ReadQueryHook.takeSnapshot(); + const { snapshot, renderedComponents } = await Profiler.takeRender(); - expect(snapshot).toEqual({ + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot.result).toEqual({ data: { character: { id: "1" } }, error: undefined, networkStatus: NetworkStatus.loading, @@ -3836,9 +3844,9 @@ it('does not suspend when partial data is in the cache and using a "cache-and-ne } { - const snapshot = await ReadQueryHook.takeSnapshot(); + const { snapshot } = await Profiler.takeRender(); - expect(snapshot).toEqual({ + expect(snapshot.result).toEqual({ data: { character: { id: "1", name: "Doctor Strange" } }, error: undefined, networkStatus: NetworkStatus.ready, From b1c1b5e984de1a3a9a9bae289b0a78a421d67340 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 27 Nov 2023 15:17:50 -0700 Subject: [PATCH 164/199] Update test that checks changing vars with cache-and-network to updated profiler api --- .../hooks/__tests__/useLoadableQuery.test.tsx | 33 +++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index 55f8b211992..4bf9fc60774 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -3873,10 +3873,12 @@ it('suspends and does not use partial data when changing variables and using a " variables: { id: "1" }, }); + const Profiler = createDefaultProfiler>(); const { SuspenseFallback, ReadQueryHook } = - createDefaultProfiledComponents>(); + createDefaultProfiledComponents(Profiler); function App() { + useTrackRender(); const [loadQuery, queryRef] = useLoadableQuery(query, { fetchPolicy: "cache-and-network", returnPartialData: true, @@ -3893,16 +3895,22 @@ it('suspends and does not use partial data when changing variables and using a " ); } - const { user } = renderWithMocks(, { mocks, cache }); + const { user } = renderWithMocks(, { + mocks, + cache, + wrapper: ({ children }) => {children}, + }); await act(() => user.click(screen.getByText("Load query"))); - expect(SuspenseFallback).not.toHaveRendered(); + // initial render + await Profiler.takeRender(); { - const snapshot = await ReadQueryHook.takeSnapshot(); + const { snapshot, renderedComponents } = await Profiler.takeRender(); - expect(snapshot).toEqual({ + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot.result).toEqual({ data: { character: { id: "1" } }, error: undefined, networkStatus: NetworkStatus.loading, @@ -3910,9 +3918,10 @@ it('suspends and does not use partial data when changing variables and using a " } { - const snapshot = await ReadQueryHook.takeSnapshot(); + const { snapshot, renderedComponents } = await Profiler.takeRender(); - expect(snapshot).toEqual({ + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ data: { character: { id: "1", name: "Spider-Man" } }, error: undefined, networkStatus: NetworkStatus.ready, @@ -3921,12 +3930,16 @@ it('suspends and does not use partial data when changing variables and using a " await act(() => user.click(screen.getByText("Change variables"))); - expect(SuspenseFallback).toHaveRendered(); + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } { - const snapshot = await ReadQueryHook.takeSnapshot(); + const { snapshot } = await Profiler.takeRender(); - expect(snapshot).toEqual({ + expect(snapshot.result).toEqual({ data: { character: { id: "2", name: "Black Widow" } }, error: undefined, networkStatus: NetworkStatus.ready, From 99c0a58ae6666617eec272600f6f38ce8c93e367 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 27 Nov 2023 15:21:49 -0700 Subject: [PATCH 165/199] Update test that checks defer with partial data to updated profiler api --- .../hooks/__tests__/useLoadableQuery.test.tsx | 56 ++++++++++++------- 1 file changed, 35 insertions(+), 21 deletions(-) diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index 4bf9fc60774..4bf9f3a2ae2 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -3993,10 +3993,12 @@ it('does not suspend deferred queries with partial data in the cache and using a const client = new ApolloClient({ link, cache }); + const Profiler = createDefaultProfiler>(); const { SuspenseFallback, ReadQueryHook } = - createDefaultProfiledComponents>(); + createDefaultProfiledComponents(Profiler); function App() { + useTrackRender(); const [loadTodo, queryRef] = useLoadableQuery(query, { fetchPolicy: "cache-first", returnPartialData: true, @@ -4012,16 +4014,21 @@ it('does not suspend deferred queries with partial data in the cache and using a ); } - const { user } = renderWithClient(, { client }); + const { user } = renderWithClient(, { + client, + wrapper: ({ children }) => {children}, + }); await act(() => user.click(screen.getByText("Load todo"))); - expect(SuspenseFallback).not.toHaveRendered(); + // initial render + await Profiler.takeRender(); { - const snapshot = await ReadQueryHook.takeSnapshot(); + const { snapshot, renderedComponents } = await Profiler.takeRender(); - expect(snapshot).toEqual({ + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot.result).toEqual({ data: { greeting: { __typename: "Greeting", @@ -4043,9 +4050,10 @@ it('does not suspend deferred queries with partial data in the cache and using a }); { - const snapshot = await ReadQueryHook.takeSnapshot(); + const { snapshot, renderedComponents } = await Profiler.takeRender(); - expect(snapshot).toEqual({ + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ data: { greeting: { __typename: "Greeting", @@ -4058,25 +4066,29 @@ it('does not suspend deferred queries with partial data in the cache and using a }); } - link.simulateResult({ - result: { - incremental: [ - { - data: { - __typename: "Greeting", - recipient: { name: "Alice", __typename: "Person" }, + link.simulateResult( + { + result: { + incremental: [ + { + data: { + __typename: "Greeting", + recipient: { name: "Alice", __typename: "Person" }, + }, + path: ["greeting"], }, - path: ["greeting"], - }, - ], - hasNext: false, + ], + hasNext: false, + }, }, - }); + true + ); { - const snapshot = await ReadQueryHook.takeSnapshot(); + const { snapshot, renderedComponents } = await Profiler.takeRender(); - expect(snapshot).toEqual({ + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ data: { greeting: { __typename: "Greeting", @@ -4088,6 +4100,8 @@ it('does not suspend deferred queries with partial data in the cache and using a networkStatus: NetworkStatus.ready, }); } + + await expect(Profiler).not.toRerender(); }); it("throws when calling loadQuery on first render", async () => { From a8676bdd764bec06589459a0dfc9cc494fc6a62a Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 27 Nov 2023 15:24:34 -0700 Subject: [PATCH 166/199] Update test that checks canonical results to updated profiler api --- .../hooks/__tests__/useLoadableQuery.test.tsx | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index 4bf9f3a2ae2..3108f520cc2 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -688,8 +688,9 @@ it("can disable canonical results when the cache's canonizeResults setting is tr data: { results }, }); + const Profiler = createDefaultProfiler(); const { SuspenseFallback, ReadQueryHook } = - createDefaultProfiledComponents(); + createDefaultProfiledComponents(Profiler); function App() { const [loadQuery, queryRef] = useLoadableQuery(query, { @@ -706,15 +707,21 @@ it("can disable canonical results when the cache's canonizeResults setting is tr ); } - const { user } = renderWithMocks(, { cache }); + const { user } = renderWithMocks(, { + cache, + wrapper: ({ children }) => {children}, + }); await act(() => user.click(screen.getByText("Load query"))); - const snapshot = await ReadQueryHook.takeSnapshot(); - const resultSet = new Set(snapshot.data.results); + // initial render + await Profiler.takeRender(); + + const { snapshot } = await Profiler.takeRender(); + const resultSet = new Set(snapshot.result!.data.results); const values = Array.from(resultSet).map((item) => item.value); - expect(snapshot).toEqual({ + expect(snapshot.result).toEqual({ data: { results }, networkStatus: NetworkStatus.ready, error: undefined, From 8f9204424a7201f4938ee9486d707e5765e16b89 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 27 Nov 2023 15:26:46 -0700 Subject: [PATCH 167/199] Update test that checks cache data to updated profiler api --- .../hooks/__tests__/useLoadableQuery.test.tsx | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index 3108f520cc2..7e078bd4bc5 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -826,9 +826,12 @@ it("all data is present in the cache, no network request is made", async () => { cache.writeQuery({ query, data: { hello: "from cache" } }); - const { SuspenseFallback, ReadQueryHook } = createDefaultProfiledComponents(); + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); function App() { + useTrackRender(); const [loadQuery, queryRef] = useLoadableQuery(query); return ( @@ -841,21 +844,26 @@ it("all data is present in the cache, no network request is made", async () => { ); } - const { user } = renderWithClient(, { client }); + const { user } = renderWithClient(, { + client, + wrapper: ({ children }) => {children}, + }); await act(() => user.click(screen.getByText("Load query"))); - expect(SuspenseFallback).not.toHaveRendered(); + // initial render + await Profiler.takeRender(); - const snapshot = await ReadQueryHook.takeSnapshot(); + const { snapshot, renderedComponents } = await Profiler.takeRender(); - expect(snapshot).toEqual({ + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot.result).toEqual({ data: { hello: "from cache" }, networkStatus: NetworkStatus.ready, error: undefined, }); - expect(ReadQueryHook).not.toRerender(); + await expect(Profiler).not.toRerender(); }); it("partial data is present in the cache so it is ignored and network request is made", async () => { From 8638291a2a26b458d83fc57f6c010790d90df5b1 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 27 Nov 2023 15:28:59 -0700 Subject: [PATCH 168/199] Update test that checks partial data in cache to updated profiler api --- .../hooks/__tests__/useLoadableQuery.test.tsx | 33 ++++++++++++++----- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index 7e078bd4bc5..6ade9866d31 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -894,9 +894,12 @@ it("partial data is present in the cache so it is ignored and network request is cache.writeQuery({ query, data: { hello: "from cache" } }); } - const { SuspenseFallback, ReadQueryHook } = createDefaultProfiledComponents(); + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); function App() { + useTrackRender(); const [loadQuery, queryRef] = useLoadableQuery(query); return ( @@ -909,19 +912,31 @@ it("partial data is present in the cache so it is ignored and network request is ); } - const { user } = renderWithClient(, { client }); + const { user } = renderWithClient(, { + client, + wrapper: ({ children }) => {children}, + }); await act(() => user.click(screen.getByText("Load query"))); - expect(SuspenseFallback).toHaveRendered(); + // initial render + await Profiler.takeRender(); - const snapshot = await ReadQueryHook.takeSnapshot(); + { + const { renderedComponents } = await Profiler.takeRender(); - expect(snapshot).toEqual({ - data: { foo: "bar", hello: "from link" }, - error: undefined, - networkStatus: NetworkStatus.ready, - }); + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { foo: "bar", hello: "from link" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } }); it("existing data in the cache is ignored when `fetchPolicy` is 'network-only'", async () => { From 1c29a354e491267960bdcc6c6f744ec2db2e846c Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 27 Nov 2023 15:31:35 -0700 Subject: [PATCH 169/199] Update test that checks network-only to updated profiler api --- .../hooks/__tests__/useLoadableQuery.test.tsx | 33 ++++++++++++++----- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index 6ade9866d31..2374b6c1677 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -961,9 +961,12 @@ it("existing data in the cache is ignored when `fetchPolicy` is 'network-only'", cache.writeQuery({ query, data: { hello: "from cache" } }); - const { SuspenseFallback, ReadQueryHook } = createDefaultProfiledComponents(); + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); function App() { + useTrackRender(); const [loadQuery, queryRef] = useLoadableQuery(query, { fetchPolicy: "network-only", }); @@ -978,19 +981,31 @@ it("existing data in the cache is ignored when `fetchPolicy` is 'network-only'", ); } - const { user } = renderWithClient(, { client }); + const { user } = renderWithClient(, { + client, + wrapper: ({ children }) => {children}, + }); await act(() => user.click(screen.getByText("Load query"))); - expect(SuspenseFallback).toHaveRendered(); + // initial render + await Profiler.takeRender(); - const snapshot = await ReadQueryHook.takeSnapshot(); + { + const { renderedComponents } = await Profiler.takeRender(); - expect(snapshot).toEqual({ - data: { hello: "from link" }, - error: undefined, - networkStatus: NetworkStatus.ready, - }); + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { hello: "from link" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } }); it("fetches data from the network but does not update the cache when `fetchPolicy` is 'no-cache'", async () => { From d2b9b5f7ad814e2feba2c9f72a4c3b4db5890b2a Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 27 Nov 2023 15:42:16 -0700 Subject: [PATCH 170/199] Update test that checks no-cache to updated profiler api --- .../hooks/__tests__/useLoadableQuery.test.tsx | 33 ++++++++++++++----- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index 2374b6c1677..778c79b007e 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -1027,9 +1027,12 @@ it("fetches data from the network but does not update the cache when `fetchPolic cache.writeQuery({ query, data: { hello: "from cache" } }); - const { SuspenseFallback, ReadQueryHook } = createDefaultProfiledComponents(); + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); function App() { + useTrackRender(); const [loadQuery, queryRef] = useLoadableQuery(query, { fetchPolicy: "no-cache", }); @@ -1044,19 +1047,31 @@ it("fetches data from the network but does not update the cache when `fetchPolic ); } - const { user } = renderWithClient(, { client }); + const { user } = renderWithClient(, { + client, + wrapper: ({ children }) => {children}, + }); await act(() => user.click(screen.getByText("Load query"))); - expect(SuspenseFallback).toHaveRendered(); + // initial render + await Profiler.takeRender(); - const snapshot = await ReadQueryHook.takeSnapshot(); + { + const { renderedComponents } = await Profiler.takeRender(); - expect(snapshot).toEqual({ - data: { hello: "from link" }, - error: undefined, - networkStatus: NetworkStatus.ready, - }); + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { hello: "from link" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } expect(client.extract()).toEqual({ ROOT_QUERY: { __typename: "Query", hello: "from cache" }, From ae368dd6654980e221a6eaad3dd3ad15396d906b Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 27 Nov 2023 15:45:35 -0700 Subject: [PATCH 171/199] Update test that checks deferred query with cache data to updated profiler api --- .../hooks/__tests__/useLoadableQuery.test.tsx | 30 +++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index 778c79b007e..8b894374c25 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -1246,10 +1246,12 @@ it('does not suspend deferred queries with data in the cache and using a "cache- }); const client = new ApolloClient({ cache, link }); + const Profiler = createDefaultProfiler(); const { SuspenseFallback, ReadQueryHook } = - createDefaultProfiledComponents(); + createDefaultProfiledComponents(Profiler); function App() { + useTrackRender(); const [loadQuery, queryRef] = useLoadableQuery(query, { fetchPolicy: "cache-and-network", }); @@ -1263,16 +1265,22 @@ it('does not suspend deferred queries with data in the cache and using a "cache- ); } - const { user } = renderWithClient(, { client }); + const { user } = renderWithClient(, { + client, + wrapper: ({ children }) => {children}, + }); await act(() => user.click(screen.getByText("Load todo"))); - expect(SuspenseFallback).not.toHaveRendered(); + // initial render + await Profiler.takeRender(); { - const snapshot = await ReadQueryHook.takeSnapshot(); + const { snapshot, renderedComponents } = await Profiler.takeRender(); - expect(snapshot).toEqual({ + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + + expect(snapshot.result).toEqual({ data: { greeting: { __typename: "Greeting", @@ -1295,9 +1303,10 @@ it('does not suspend deferred queries with data in the cache and using a "cache- }); { - const snapshot = await ReadQueryHook.takeSnapshot(); + const { snapshot, renderedComponents } = await Profiler.takeRender(); - expect(snapshot).toEqual({ + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ data: { greeting: { __typename: "Greeting", @@ -1329,9 +1338,10 @@ it('does not suspend deferred queries with data in the cache and using a "cache- ); { - const snapshot = await ReadQueryHook.takeSnapshot(); + const { snapshot, renderedComponents } = await Profiler.takeRender(); - expect(snapshot).toEqual({ + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ data: { greeting: { __typename: "Greeting", @@ -1343,6 +1353,8 @@ it('does not suspend deferred queries with data in the cache and using a "cache- networkStatus: NetworkStatus.ready, }); } + + await expect(Profiler).not.toRerender(); }); it("reacts to cache updates", async () => { From adc9b52452c9b553e90c432b32968c5206e92a36 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 27 Nov 2023 16:58:47 -0700 Subject: [PATCH 172/199] Update test that checks updated error policy to updated profiler api --- .../hooks/__tests__/useLoadableQuery.test.tsx | 43 +++++++++---------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index 8b894374c25..e892ae0d6a4 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -1447,8 +1447,9 @@ it("applies `errorPolicy` on next fetch when it changes between renders", async }, ]; + const Profiler = createDefaultProfiler(); const { SuspenseFallback, ReadQueryHook, ErrorBoundary, ErrorFallback } = - createDefaultProfiledComponents(); + createDefaultProfiledComponents(Profiler); function App() { const [errorPolicy, setErrorPolicy] = useState("none"); @@ -1472,16 +1473,22 @@ it("applies `errorPolicy` on next fetch when it changes between renders", async ); } - const { user } = renderWithMocks(, { mocks }); + const { user } = renderWithMocks(, { + mocks, + wrapper: ({ children }) => {children}, + }); await act(() => user.click(screen.getByText("Load query"))); - expect(SuspenseFallback).toHaveRendered(); + // initial render + await Profiler.takeRender(); + // load query + await Profiler.takeRender(); { - const snapshot = await ReadQueryHook.takeSnapshot(); + const { snapshot } = await Profiler.takeRender(); - expect(snapshot).toEqual({ + expect(snapshot.result).toEqual({ data: { greeting: "Hello" }, error: undefined, networkStatus: NetworkStatus.ready, @@ -1491,31 +1498,23 @@ it("applies `errorPolicy` on next fetch when it changes between renders", async await act(() => user.click(screen.getByText("Change error policy"))); await act(() => user.click(screen.getByText("Refetch greeting"))); - expect(SuspenseFallback).toHaveRenderedTimes(2); - - { - const snapshot = await ReadQueryHook.takeSnapshot(); - - expect(snapshot).toEqual({ - data: { greeting: "Hello" }, - error: undefined, - networkStatus: NetworkStatus.ready, - }); - } + // change error policy + await Profiler.takeRender(); + // refetch + await Profiler.takeRender(); { - const snapshot = await ReadQueryHook.takeSnapshot(); + const { snapshot, renderedComponents } = await Profiler.takeRender(); - expect(snapshot).toEqual({ + // Ensure we aren't rendering the error boundary and instead rendering the + // error message in the hook component. + expect(renderedComponents).not.toContain(ErrorFallback); + expect(snapshot.result).toEqual({ data: { greeting: "Hello" }, error: new ApolloError({ graphQLErrors: [new GraphQLError("oops")] }), networkStatus: NetworkStatus.error, }); } - - // Ensure we aren't rendering the error boundary and instead rendering the - // error message in the hook component. - expect(ErrorFallback).not.toHaveRendered(); }); it("applies `context` on next fetch when it changes between renders", async () => { From f987232000ec32f58704c6c9c12e68548be08eb0 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 27 Nov 2023 17:01:38 -0700 Subject: [PATCH 173/199] Update test that checks updated context to updated profiler api --- .../hooks/__tests__/useLoadableQuery.test.tsx | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index e892ae0d6a4..244a44e08db 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -1541,8 +1541,9 @@ it("applies `context` on next fetch when it changes between renders", async () = cache: new InMemoryCache(), }); + const Profiler = createDefaultProfiler(); const { SuspenseFallback, ReadQueryHook } = - createDefaultProfiledComponents(); + createDefaultProfiledComponents(Profiler); function App() { const [phase, setPhase] = React.useState("initial"); @@ -1562,26 +1563,38 @@ it("applies `context` on next fetch when it changes between renders", async () = ); } - const { user } = renderWithClient(, { client }); + const { user } = renderWithClient(, { + client, + wrapper: ({ children }) => {children}, + }); await act(() => user.click(screen.getByText("Load query"))); + // initial render + await Profiler.takeRender(); + // load query + await Profiler.takeRender(); + { - const snapshot = await ReadQueryHook.takeSnapshot(); + const { snapshot } = await Profiler.takeRender(); - expect(snapshot.data).toEqual({ + expect(snapshot.result!.data).toEqual({ phase: "initial", }); } await act(() => user.click(screen.getByText("Update context"))); await act(() => user.click(screen.getByText("Refetch"))); - await ReadQueryHook.takeSnapshot(); + + // update context + await Profiler.takeRender(); + // refetch + await Profiler.takeRender(); { - const snapshot = await ReadQueryHook.takeSnapshot(); + const { snapshot } = await Profiler.takeRender(); - expect(snapshot.data).toEqual({ + expect(snapshot.result!.data).toEqual({ phase: "rerender", }); } From fbfea1168d48ec43fd8dbdd23cc9787915318d25 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 27 Nov 2023 17:05:24 -0700 Subject: [PATCH 174/199] Update test that checks change to canonizeResults to updated profiler api --- .../hooks/__tests__/useLoadableQuery.test.tsx | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index 244a44e08db..d8dbc13b78f 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -1648,8 +1648,9 @@ it("returns canonical results immediately when `canonizeResults` changes from `f cache, }); + const Profiler = createDefaultProfiler(); const { SuspenseFallback, ReadQueryHook } = - createDefaultProfiledComponents(); + createDefaultProfiledComponents(Profiler); function App() { const [canonizeResults, setCanonizeResults] = React.useState(false); @@ -1670,12 +1671,19 @@ it("returns canonical results immediately when `canonizeResults` changes from `f ); } - const { user } = renderWithClient(, { client }); + const { user } = renderWithClient(, { + client, + wrapper: ({ children }) => {children}, + }); await act(() => user.click(screen.getByText("Load query"))); + // initial render + await Profiler.takeRender(); + { - const { data } = await ReadQueryHook.takeSnapshot(); + const { snapshot } = await Profiler.takeRender(); + const { data } = snapshot.result!; const resultSet = new Set(data.results); const values = Array.from(resultSet).map((item) => item.value); @@ -1687,7 +1695,8 @@ it("returns canonical results immediately when `canonizeResults` changes from `f await act(() => user.click(screen.getByText("Canonize results"))); { - const { data } = await ReadQueryHook.takeSnapshot(); + const { snapshot } = await Profiler.takeRender(); + const { data } = snapshot.result!; const resultSet = new Set(data.results); const values = Array.from(resultSet).map((item) => item.value); From 6a7dc6b56754bfa9f0d3e8a2c1e706ac58a20925 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 27 Nov 2023 17:14:11 -0700 Subject: [PATCH 175/199] Update test that checks change to refetchWritePolicy to updated profiler api --- .../hooks/__tests__/useLoadableQuery.test.tsx | 34 ++++++++++++++----- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index d8dbc13b78f..e35766de880 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -1757,8 +1757,9 @@ it("applies changed `refetchWritePolicy` to next fetch when changing between ren cache, }); + const Profiler = createDefaultProfiler(); const { SuspenseFallback, ReadQueryHook } = - createDefaultProfiledComponents(); + createDefaultProfiledComponents(Profiler); function App() { const [refetchWritePolicy, setRefetchWritePolicy] = @@ -1789,13 +1790,21 @@ it("applies changed `refetchWritePolicy` to next fetch when changing between ren ); } - const { user } = renderWithClient(, { client }); + const { user } = renderWithClient(, { + client, + wrapper: ({ children }) => {children}, + }); await act(() => user.click(screen.getByText("Load query"))); + // initial render + await Profiler.takeRender(); + // load query + await Profiler.takeRender(); + { - const snapshot = await ReadQueryHook.takeSnapshot(); - const { primes } = snapshot.data; + const { snapshot } = await Profiler.takeRender(); + const { primes } = snapshot.result!.data; expect(primes).toEqual([2, 3, 5, 7, 11]); expect(mergeParams).toEqual([[undefined, [2, 3, 5, 7, 11]]]); @@ -1803,9 +1812,12 @@ it("applies changed `refetchWritePolicy` to next fetch when changing between ren await act(() => user.click(screen.getByText("Refetch next"))); + // refetch next + await Profiler.takeRender(); + { - const snapshot = await ReadQueryHook.takeSnapshot(); - const { primes } = snapshot.data; + const { snapshot } = await Profiler.takeRender(); + const { primes } = snapshot.result!.data; expect(primes).toEqual([2, 3, 5, 7, 11, 13, 17, 19, 23, 29]); expect(mergeParams).toEqual([ @@ -1818,12 +1830,16 @@ it("applies changed `refetchWritePolicy` to next fetch when changing between ren } await act(() => user.click(screen.getByText("Change refetch write policy"))); - await ReadQueryHook.takeSnapshot(); await act(() => user.click(screen.getByText("Refetch last"))); + // change refetchWritePolicy + await Profiler.takeRender(); + // refetch last + await Profiler.takeRender(); + { - const snapshot = await ReadQueryHook.takeSnapshot(); - const { primes } = snapshot.data; + const { snapshot } = await Profiler.takeRender(); + const { primes } = snapshot.result!.data; expect(primes).toEqual([31, 37, 41, 43, 47]); expect(mergeParams).toEqual([ From 4f977334d0b6a1c4bafd50a32e2cbd0ebe4a161f Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 27 Nov 2023 17:18:54 -0700 Subject: [PATCH 176/199] Update test that checks change to returnPartialData to updated profiler api --- .../hooks/__tests__/useLoadableQuery.test.tsx | 32 +++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index e35766de880..cb22b21c5f2 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -1928,10 +1928,12 @@ it("applies `returnPartialData` on next fetch when it changes between renders", cache, }); + const Profiler = createDefaultProfiler(); const { SuspenseFallback, ReadQueryHook } = - createDefaultProfiledComponents(); + createDefaultProfiledComponents(Profiler); function App() { + useTrackRender(); const [returnPartialData, setReturnPartialData] = React.useState(false); const [loadQuery, queryRef] = useLoadableQuery(fullQuery, { @@ -1951,14 +1953,22 @@ it("applies `returnPartialData` on next fetch when it changes between renders", ); } - const { user } = renderWithClient(, { client }); + const { user } = renderWithClient(, { + client, + wrapper: ({ children }) => {children}, + }); await act(() => user.click(screen.getByText("Load query"))); + // initial render + await Profiler.takeRender(); + // load query + await Profiler.takeRender(); + { - const snapshot = await ReadQueryHook.takeSnapshot(); + const { snapshot } = await Profiler.takeRender(); - expect(snapshot).toEqual({ + expect(snapshot.result).toEqual({ data: { character: { __typename: "Character", id: "1", name: "Doctor Strange" }, }, @@ -1968,7 +1978,9 @@ it("applies `returnPartialData` on next fetch when it changes between renders", } await act(() => user.click(screen.getByText("Update partial data"))); - await ReadQueryHook.takeSnapshot(); + + // update partial data + await Profiler.takeRender(); cache.modify({ id: cache.identify({ __typename: "Character", id: "1" }), @@ -1978,9 +1990,10 @@ it("applies `returnPartialData` on next fetch when it changes between renders", }); { - const snapshot = await ReadQueryHook.takeSnapshot(); + const { snapshot, renderedComponents } = await Profiler.takeRender(); - expect(snapshot).toEqual({ + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ data: { character: { __typename: "Character", id: "1" }, }, @@ -1990,9 +2003,10 @@ it("applies `returnPartialData` on next fetch when it changes between renders", } { - const snapshot = await ReadQueryHook.takeSnapshot(); + const { snapshot, renderedComponents } = await Profiler.takeRender(); - expect(snapshot).toEqual({ + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ data: { character: { __typename: "Character", From cc586cbe9f7df4cf89363c5b6bc936572bab686c Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 27 Nov 2023 17:22:25 -0700 Subject: [PATCH 177/199] Update test that checks change to fetchPolicy to updated profiler api --- .../hooks/__tests__/useLoadableQuery.test.tsx | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index cb22b21c5f2..4a4b11fa85e 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -2073,8 +2073,9 @@ it("applies updated `fetchPolicy` on next fetch when it changes between renders" cache, }); + const Profiler = createDefaultProfiler(); const { SuspenseFallback, ReadQueryHook } = - createDefaultProfiledComponents(); + createDefaultProfiledComponents(Profiler); function App() { const [fetchPolicy, setFetchPolicy] = @@ -2098,14 +2099,20 @@ it("applies updated `fetchPolicy` on next fetch when it changes between renders" ); } - const { user } = renderWithClient(, { client }); + const { user } = renderWithClient(, { + client, + wrapper: ({ children }) => {children}, + }); await act(() => user.click(screen.getByText("Load query"))); + // initial render + await Profiler.takeRender(); + { - const snapshot = await ReadQueryHook.takeSnapshot(); + const { snapshot } = await Profiler.takeRender(); - expect(snapshot).toEqual({ + expect(snapshot.result).toEqual({ data: { character: { __typename: "Character", @@ -2119,13 +2126,17 @@ it("applies updated `fetchPolicy` on next fetch when it changes between renders" } await act(() => user.click(screen.getByText("Change fetch policy"))); - await ReadQueryHook.takeSnapshot(); await act(() => user.click(screen.getByText("Refetch"))); + // change fetch policy + await Profiler.takeRender(); + // refetch + await Profiler.takeRender(); + { - const snapshot = await ReadQueryHook.takeSnapshot(); + const { snapshot } = await Profiler.takeRender(); - expect(snapshot).toEqual({ + expect(snapshot.result).toEqual({ data: { character: { __typename: "Character", From 84b0ec5f31666a14e84f7930e9f9c8c68b1e9a98 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 27 Nov 2023 17:25:47 -0700 Subject: [PATCH 178/199] Update test that checks resuspending on refetch to updated profiler api --- .../hooks/__tests__/useLoadableQuery.test.tsx | 36 +++++++++++++------ 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index 4a4b11fa85e..2b9aea238bc 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -2181,10 +2181,12 @@ it("re-suspends when calling `refetch`", async () => { }, ]; + const Profiler = createDefaultProfiler(); const { SuspenseFallback, ReadQueryHook } = - createDefaultProfiledComponents(); + createDefaultProfiledComponents(Profiler); function App() { + useTrackRender(); const [loadQuery, queryRef, { refetch }] = useLoadableQuery(query); return ( @@ -2198,18 +2200,26 @@ it("re-suspends when calling `refetch`", async () => { ); } - const { user } = renderWithMocks(, { mocks }); - - expect(SuspenseFallback).not.toHaveRendered(); + const { user } = renderWithMocks(, { + mocks, + wrapper: ({ children }) => {children}, + }); await act(() => user.click(screen.getByText("Load query"))); - expect(SuspenseFallback).toHaveRendered(); + // initial render + await Profiler.takeRender(); { - const snapshot = await ReadQueryHook.takeSnapshot(); + const { renderedComponents } = await Profiler.takeRender(); - expect(snapshot).toEqual({ + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ data: { character: { id: "1", name: "Spider-Man" } }, error: undefined, networkStatus: NetworkStatus.ready, @@ -2218,17 +2228,23 @@ it("re-suspends when calling `refetch`", async () => { await act(() => user.click(screen.getByText("Refetch"))); - expect(SuspenseFallback).toHaveRenderedTimes(2); + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } { - const snapshot = await ReadQueryHook.takeSnapshot(); + const { snapshot } = await Profiler.takeRender(); - expect(snapshot).toEqual({ + expect(snapshot.result).toEqual({ data: { character: { id: "1", name: "Spider-Man (updated)" } }, error: undefined, networkStatus: NetworkStatus.ready, }); } + + await expect(Profiler).not.toRerender(); }); it("re-suspends when calling `refetch` with new variables", async () => { From 10c4e7f22fc554376b9d93c387cbc300198741a2 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 27 Nov 2023 17:28:58 -0700 Subject: [PATCH 179/199] Update test that checks refetch with new vars to updated profiler api --- .../hooks/__tests__/useLoadableQuery.test.tsx | 34 ++++++++++++++----- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index 2b9aea238bc..73eeaabf373 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -2265,10 +2265,12 @@ it("re-suspends when calling `refetch` with new variables", async () => { }, ]; + const Profiler = createDefaultProfiler(); const { SuspenseFallback, ReadQueryHook } = - createDefaultProfiledComponents(); + createDefaultProfiledComponents(Profiler); function App() { + useTrackRender(); const [loadQuery, queryRef, { refetch }] = useLoadableQuery(query); return ( @@ -2282,16 +2284,26 @@ it("re-suspends when calling `refetch` with new variables", async () => { ); } - const { user } = renderWithMocks(, { mocks }); + const { user } = renderWithMocks(, { + mocks, + wrapper: ({ children }) => {children}, + }); await act(() => user.click(screen.getByText("Load query"))); - expect(SuspenseFallback).toHaveRendered(); + // initial render + await Profiler.takeRender(); { - const snapshot = await ReadQueryHook.takeSnapshot(); + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - expect(snapshot).toEqual({ + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ data: { character: { id: "1", name: "Captain Marvel" } }, error: undefined, networkStatus: NetworkStatus.ready, @@ -2300,17 +2312,23 @@ it("re-suspends when calling `refetch` with new variables", async () => { await act(() => user.click(screen.getByText("Refetch with ID 2"))); - expect(SuspenseFallback).toHaveRenderedTimes(2); + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } { - const snapshot = await ReadQueryHook.takeSnapshot(); + const { snapshot } = await Profiler.takeRender(); - expect(snapshot).toEqual({ + expect(snapshot.result).toEqual({ data: { character: { id: "2", name: "Captain America" } }, error: undefined, networkStatus: NetworkStatus.ready, }); } + + await expect(Profiler).not.toRerender(); }); it("re-suspends multiple times when calling `refetch` multiple times", async () => { From f49bec19c96134cf451a9dedf362b5ff6f6e86d2 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 27 Nov 2023 17:34:17 -0700 Subject: [PATCH 180/199] Move OnlyRequiredProperties utility type to src/utilities --- src/react/hooks/useLoadableQuery.ts | 9 ++++----- src/utilities/index.ts | 1 + src/utilities/types/OnlyRequiredProperties.ts | 6 ++++++ 3 files changed, 11 insertions(+), 5 deletions(-) create mode 100644 src/utilities/types/OnlyRequiredProperties.ts diff --git a/src/react/hooks/useLoadableQuery.ts b/src/react/hooks/useLoadableQuery.ts index 0aeb9b051fe..cf5f73fbf8c 100644 --- a/src/react/hooks/useLoadableQuery.ts +++ b/src/react/hooks/useLoadableQuery.ts @@ -18,16 +18,15 @@ import { getSuspenseCache } from "../cache/index.js"; import { useWatchQueryOptions } from "./useSuspenseQuery.js"; import type { FetchMoreFunction, RefetchFunction } from "./useSuspenseQuery.js"; import { canonicalStringify } from "../../cache/index.js"; -import type { DeepPartial } from "../../utilities/index.js"; +import type { + DeepPartial, + OnlyRequiredProperties, +} from "../../utilities/index.js"; import type { CacheKey } from "../cache/types.js"; import { invariant } from "../../utilities/globals/index.js"; let RenderDispatcher: unknown = null; -type OnlyRequiredProperties = { - [K in keyof T as {} extends Pick ? never : K]: T[K]; -}; - type LoadQuery = ( // Use variadic args to handle cases where TVariables is type `never`, in // which case we don't want to allow a variables argument. In other diff --git a/src/utilities/index.ts b/src/utilities/index.ts index da8affb4b5a..ec05d2aa043 100644 --- a/src/utilities/index.ts +++ b/src/utilities/index.ts @@ -129,3 +129,4 @@ export { stripTypename } from "./common/stripTypename.js"; export * from "./types/IsStrictlyAny.js"; export type { DeepOmit } from "./types/DeepOmit.js"; export type { DeepPartial } from "./types/DeepPartial.js"; +export type { OnlyRequiredProperties } from "./types/OnlyRequiredProperties.js"; diff --git a/src/utilities/types/OnlyRequiredProperties.ts b/src/utilities/types/OnlyRequiredProperties.ts new file mode 100644 index 00000000000..5264a0fca69 --- /dev/null +++ b/src/utilities/types/OnlyRequiredProperties.ts @@ -0,0 +1,6 @@ +/** + * Returns a new type that only contains the required properties from `T` + */ +export type OnlyRequiredProperties = { + [K in keyof T as {} extends Pick ? never : K]: T[K]; +}; From 0789e28aa531e01bdac7090c6a6d8a10e87376db Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 27 Nov 2023 17:36:07 -0700 Subject: [PATCH 181/199] Remove toHaveRendered and toHaveRenderedTimes matchers --- src/testing/matchers/index.d.ts | 15 -------- src/testing/matchers/index.ts | 4 --- src/testing/matchers/toHaveRendered.ts | 29 ---------------- src/testing/matchers/toHaveRenderedTimes.ts | 38 --------------------- 4 files changed, 86 deletions(-) delete mode 100644 src/testing/matchers/toHaveRendered.ts delete mode 100644 src/testing/matchers/toHaveRenderedTimes.ts diff --git a/src/testing/matchers/index.d.ts b/src/testing/matchers/index.d.ts index ebcd8e8471e..6f8cb02efd6 100644 --- a/src/testing/matchers/index.d.ts +++ b/src/testing/matchers/index.d.ts @@ -16,21 +16,6 @@ interface ApolloCustomMatchers { */ toMatchDocument(document: DocumentNode): R; - /** - * Used to determine if a profiled component has rendered or not. - */ - toHaveRendered: T extends Profiler | ProfiledHook - ? () => R - : { error: "matcher needs to be called on a ProfiledComponent instance" }; - - /** - * Used to determine if a profiled component has rendered a specific amount - * of times or not. - */ - toHaveRenderedTimes: T extends Profiler | ProfiledHook - ? (count: number) => R - : { error: "matcher needs to be called on a ProfiledComponent instance" }; - /** * Used to determine if the Suspense cache has a cache entry. */ diff --git a/src/testing/matchers/index.ts b/src/testing/matchers/index.ts index 4494ea3814c..d2ebd8ce7c2 100644 --- a/src/testing/matchers/index.ts +++ b/src/testing/matchers/index.ts @@ -1,13 +1,9 @@ import { expect } from "@jest/globals"; -import { toHaveRendered } from "./toHaveRendered.js"; -import { toHaveRenderedTimes } from "./toHaveRenderedTimes.js"; import { toMatchDocument } from "./toMatchDocument.js"; import { toHaveSuspenseCacheEntryUsing } from "./toHaveSuspenseCacheEntryUsing.js"; import { toRerender, toRenderExactlyTimes } from "./ProfiledComponent.js"; expect.extend({ - toHaveRendered, - toHaveRenderedTimes, toHaveSuspenseCacheEntryUsing, toMatchDocument, toRerender, diff --git a/src/testing/matchers/toHaveRendered.ts b/src/testing/matchers/toHaveRendered.ts deleted file mode 100644 index ec469d235d6..00000000000 --- a/src/testing/matchers/toHaveRendered.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { MatcherFunction } from "expect"; -import type { Profiler } from "../internal/index.js"; -import type { ProfiledHook } from "../internal/index.js"; - -export const toHaveRendered: MatcherFunction = function (actual) { - let ProfiledComponent = actual as Profiler | ProfiledHook; - - if ("ProfiledComponent" in ProfiledComponent) { - ProfiledComponent = ProfiledComponent.ProfiledComponent; - } - - const pass = ProfiledComponent.totalRenderCount() > 0; - - const hint = this.utils.matcherHint( - "toHaveRendered", - "ProfiledComponent", - "" - ); - - return { - pass, - message() { - return ( - hint + - `\n\nExpected profiled component to${pass ? " not" : ""} have rendered.` - ); - }, - }; -}; diff --git a/src/testing/matchers/toHaveRenderedTimes.ts b/src/testing/matchers/toHaveRenderedTimes.ts deleted file mode 100644 index f69ee1822b5..00000000000 --- a/src/testing/matchers/toHaveRenderedTimes.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { MatcherFunction } from "expect"; -import type { Profiler, ProfiledHook } from "../internal/index.js"; - -export const toHaveRenderedTimes: MatcherFunction<[count: number]> = function ( - actual, - count -) { - let ProfiledComponent = actual as Profiler | ProfiledHook; - - if ("ProfiledComponent" in ProfiledComponent) { - ProfiledComponent = ProfiledComponent.ProfiledComponent; - } - - const actualRenderCount = ProfiledComponent.totalRenderCount(); - const pass = actualRenderCount === count; - - const hint = this.utils.matcherHint( - "toHaveRenderedTimes", - "ProfiledComponent", - "renderCount" - ); - - return { - pass, - message: () => { - return ( - hint + - `\n\nExpected profiled component to${ - pass ? " not" : "" - } have rendered times ${this.utils.printExpected( - count - )}, but it rendered times ${this.utils.printReceived( - actualRenderCount - )}.` - ); - }, - }; -}; From 40b3678e439a2b1ebf3784d623d2fd0d0abec8eb Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 27 Nov 2023 17:40:12 -0700 Subject: [PATCH 182/199] Rename LoadQuery to LoadQueryFunction and publicly export it --- src/react/hooks/index.ts | 5 ++++- src/react/hooks/useLoadableQuery.ts | 6 +++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/react/hooks/index.ts b/src/react/hooks/index.ts index afc204e0e1b..8a725261f40 100644 --- a/src/react/hooks/index.ts +++ b/src/react/hooks/index.ts @@ -11,7 +11,10 @@ export type { UseSuspenseQueryResult } from "./useSuspenseQuery.js"; export { useSuspenseQuery } from "./useSuspenseQuery.js"; export type { UseBackgroundQueryResult } from "./useBackgroundQuery.js"; export { useBackgroundQuery } from "./useBackgroundQuery.js"; -export type { UseLoadableQueryResult } from "./useLoadableQuery.js"; +export type { + LoadQueryFunction, + UseLoadableQueryResult, +} from "./useLoadableQuery.js"; export { useLoadableQuery } from "./useLoadableQuery.js"; export type { UseReadQueryResult } from "./useReadQuery.js"; export { useReadQuery } from "./useReadQuery.js"; diff --git a/src/react/hooks/useLoadableQuery.ts b/src/react/hooks/useLoadableQuery.ts index cf5f73fbf8c..c88a76244c9 100644 --- a/src/react/hooks/useLoadableQuery.ts +++ b/src/react/hooks/useLoadableQuery.ts @@ -27,7 +27,7 @@ import { invariant } from "../../utilities/globals/index.js"; let RenderDispatcher: unknown = null; -type LoadQuery = ( +export type LoadQueryFunction = ( // Use variadic args to handle cases where TVariables is type `never`, in // which case we don't want to allow a variables argument. In other // words, we don't want to allow variables to be passed as an argument to this @@ -43,7 +43,7 @@ export type UseLoadableQueryResult< TData = unknown, TVariables extends OperationVariables = OperationVariables, > = [ - LoadQuery, + LoadQueryFunction, QueryReference | null, { fetchMore: FetchMoreFunction; @@ -179,7 +179,7 @@ export function useLoadableQuery< [queryRef] ); - const loadQuery: LoadQuery = React.useCallback( + const loadQuery: LoadQueryFunction = React.useCallback( (...args) => { invariant( getRenderDispatcher() !== RenderDispatcher, From 51d6e9225d37b7d0671cdadf043f15255301a2af Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 27 Nov 2023 18:01:38 -0700 Subject: [PATCH 183/199] Extract render dispatcher tracking to internal hook --- src/react/hooks/internal/index.ts | 1 + src/react/hooks/internal/useRenderGuard.ts | 28 +++++++++++++++++++ src/react/hooks/useLoadableQuery.ts | 31 ++++++++++++---------- 3 files changed, 46 insertions(+), 14 deletions(-) create mode 100644 src/react/hooks/internal/useRenderGuard.ts diff --git a/src/react/hooks/internal/index.ts b/src/react/hooks/internal/index.ts index d1c90c41f4a..2a45719986b 100644 --- a/src/react/hooks/internal/index.ts +++ b/src/react/hooks/internal/index.ts @@ -1,4 +1,5 @@ // These hooks are used internally and are not exported publicly by the library export { useDeepMemo } from "./useDeepMemo.js"; export { useIsomorphicLayoutEffect } from "./useIsomorphicLayoutEffect.js"; +export { useRenderGuard } from "./useRenderGuard.js"; export { __use } from "./__use.js"; diff --git a/src/react/hooks/internal/useRenderGuard.ts b/src/react/hooks/internal/useRenderGuard.ts new file mode 100644 index 00000000000..119fa0041a8 --- /dev/null +++ b/src/react/hooks/internal/useRenderGuard.ts @@ -0,0 +1,28 @@ +import * as React from "rehackt"; + +function getRenderDispatcher() { + return (React as any).__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED + ?.ReactCurrentDispatcher?.current; +} + +let RenderDispatcher: unknown = null; + +/* +Relay does this too, so we hope this is safe. +https://github.com/facebook/relay/blob/8651fbca19adbfbb79af7a3bc40834d105fd7747/packages/react-relay/relay-hooks/loadQuery.js#L90-L98 +*/ +export function useRenderGuard() { + RenderDispatcher = getRenderDispatcher(); + + // We use a callback argument here instead of the failure string so that the + // call site can provide a custom failure message while allowing for static + // message extraction on the `invariant` function. + return React.useCallback((onFailure: () => void) => { + if ( + RenderDispatcher !== null && + RenderDispatcher === getRenderDispatcher() + ) { + onFailure(); + } + }, []); +} diff --git a/src/react/hooks/useLoadableQuery.ts b/src/react/hooks/useLoadableQuery.ts index c88a76244c9..e8f492d96da 100644 --- a/src/react/hooks/useLoadableQuery.ts +++ b/src/react/hooks/useLoadableQuery.ts @@ -13,7 +13,7 @@ import type { InternalQueryReference, } from "../cache/QueryReference.js"; import type { LoadableQueryHookOptions } from "../types/types.js"; -import { __use } from "./internal/index.js"; +import { __use, useRenderGuard } from "./internal/index.js"; import { getSuspenseCache } from "../cache/index.js"; import { useWatchQueryOptions } from "./useSuspenseQuery.js"; import type { FetchMoreFunction, RefetchFunction } from "./useSuspenseQuery.js"; @@ -25,8 +25,6 @@ import type { import type { CacheKey } from "../cache/types.js"; import { invariant } from "../../utilities/globals/index.js"; -let RenderDispatcher: unknown = null; - export type LoadQueryFunction = ( // Use variadic args to handle cases where TVariables is type `never`, in // which case we don't want to allow a variables argument. In other @@ -115,7 +113,6 @@ export function useLoadableQuery< query: DocumentNode | TypedDocumentNode, options: LoadableQueryHookOptions = Object.create(null) ): UseLoadableQueryResult { - RenderDispatcher = getRenderDispatcher(); const client = useApolloClient(options.client); const suspenseCache = getSuspenseCache(client); const watchQueryOptions = useWatchQueryOptions({ client, query, options }); @@ -137,6 +134,8 @@ export function useLoadableQuery< queryRef.promiseCache = promiseCache; } + const failDuringRender = useRenderGuard(); + React.useEffect(() => queryRef?.retain(), [queryRef]); const fetchMore: FetchMoreFunction = React.useCallback( @@ -181,10 +180,12 @@ export function useLoadableQuery< const loadQuery: LoadQueryFunction = React.useCallback( (...args) => { - invariant( - getRenderDispatcher() !== RenderDispatcher, - "useLoadableQuery: loadQuery should not be called during render. To load a query during render, use `useBackgroundQuery`." - ); + failDuringRender(() => { + invariant( + false, + "useLoadableQuery: loadQuery should not be called during render. To load a query during render, use `useBackgroundQuery`." + ); + }); const [variables] = args; @@ -204,7 +205,14 @@ export function useLoadableQuery< promiseCache.set(queryRef.key, queryRef.promise); setQueryRef(queryRef); }, - [query, queryKey, suspenseCache, watchQueryOptions, promiseCache] + [ + query, + queryKey, + suspenseCache, + watchQueryOptions, + promiseCache, + failDuringRender, + ] ); return React.useMemo(() => { @@ -215,8 +223,3 @@ export function useLoadableQuery< ]; }, [queryRef, loadQuery, fetchMore, refetch]); } - -function getRenderDispatcher() { - return (React as any).__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED - ?.ReactCurrentDispatcher?.current; -} From fe2cf178458637c2d5b8577c899db40f2e4fbd77 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 27 Nov 2023 18:07:54 -0700 Subject: [PATCH 184/199] Minor tweak to error message for loadQuery function --- src/react/hooks/__tests__/useLoadableQuery.test.tsx | 4 ++-- src/react/hooks/useLoadableQuery.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index 73eeaabf373..d89cb5073d8 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -4286,7 +4286,7 @@ it("throws when calling loadQuery on first render", async () => { expect(() => renderWithMocks(, { mocks })).toThrow( new InvariantError( - "useLoadableQuery: loadQuery should not be called during render. To load a query during render, use `useBackgroundQuery`." + "useLoadableQuery: 'loadQuery' should not be called during render. To start a query during render, use the 'useBackgroundQuery' hook." ) ); }); @@ -4319,7 +4319,7 @@ it("throws when calling loadQuery on subsequent render", async () => { expect(error).toEqual( new InvariantError( - "useLoadableQuery: loadQuery should not be called during render. To load a query during render, use `useBackgroundQuery`." + "useLoadableQuery: 'loadQuery' should not be called during render. To start a query during render, use the 'useBackgroundQuery' hook." ) ); }); diff --git a/src/react/hooks/useLoadableQuery.ts b/src/react/hooks/useLoadableQuery.ts index e8f492d96da..b873a4ba33a 100644 --- a/src/react/hooks/useLoadableQuery.ts +++ b/src/react/hooks/useLoadableQuery.ts @@ -183,7 +183,7 @@ export function useLoadableQuery< failDuringRender(() => { invariant( false, - "useLoadableQuery: loadQuery should not be called during render. To load a query during render, use `useBackgroundQuery`." + "useLoadableQuery: 'loadQuery' should not be called during render. To start a query during render, use the 'useBackgroundQuery' hook." ); }); From 383743d1e8694fc8167d93af1a1025de37ef5418 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 27 Nov 2023 18:36:26 -0700 Subject: [PATCH 185/199] Prevent act warnings when using Profiler.takeRender --- src/testing/internal/profile/profile.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/testing/internal/profile/profile.tsx b/src/testing/internal/profile/profile.tsx index 1b3da13965a..542a6eae940 100644 --- a/src/testing/internal/profile/profile.tsx +++ b/src/testing/internal/profile/profile.tsx @@ -284,6 +284,14 @@ export function createProfiler< }); }, async takeRender(options: NextRenderOptions = {}) { + // In many cases we do not control the resolution of the suspended + // promise which results in noisy tests when using this utility. Instead, + // we disable act warnings when using this utility. + // + // https://github.com/reactwg/react-18/discussions/102 + const prevActEnv = (globalThis as any).IS_REACT_ACT_ENVIRONMENT; + (globalThis as any).IS_REACT_ACT_ENVIRONMENT = false; + let error: unknown = undefined; try { return await Profiler.peekRender({ @@ -297,6 +305,7 @@ export function createProfiler< if (!(error && error instanceof WaitForRenderTimeoutError)) { iteratorPosition++; } + (globalThis as any).IS_REACT_ACT_ENVIRONMENT = prevActEnv; } }, getCurrentRender() { From 1a8de3929a09be8ad94bd1843df5207d9833b53c Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 27 Nov 2023 22:00:28 -0700 Subject: [PATCH 186/199] Add a reset method to loadQuery that sets the queryRef to null and disposes of it --- .../hooks/__tests__/useLoadableQuery.test.tsx | 70 +++++++++++++++++++ src/react/hooks/useLoadableQuery.ts | 11 ++- 2 files changed, 79 insertions(+), 2 deletions(-) diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index d89cb5073d8..c9ec504b420 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -29,6 +29,7 @@ import { MockedResponse, MockLink, MockSubscriptionLink, + wait, } from "../../../testing"; import { concatPagination, @@ -436,6 +437,75 @@ it("changes variables on a query and resuspends when passing new variables to th await expect(Profiler).not.toRerender(); }); +it("resets the `queryRef` to null and disposes of it when calling the `reset` function", async () => { + const { query, mocks } = useSimpleQueryCase(); + + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: new MockLink(mocks), + }); + + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + function App() { + useTrackRender(); + const [loadQuery, queryRef, { reset }] = useLoadableQuery(query); + + // Resetting the result allows us to detect when ReadQueryHook is unmounted + // since it won't render and overwrite the `null` + Profiler.mergeSnapshot({ result: null }); + + return ( + <> + + + }> + {queryRef && } + + + ); + } + + const { user } = renderWithClient(, { + client, + wrapper: ({ children }) => {children}, + }); + + await act(() => user.click(screen.getByText("Load query"))); + + // initial render + await Profiler.takeRender(); + // load query + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await act(() => user.click(screen.getByText("Reset query"))); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App]); + expect(snapshot.result).toBeNull(); + } + + // Since dispose is called in a setTimeout, we need to wait a tick before + // checking to see if the query ref was properly disposed + await wait(0); + + expect(client.getObservableQueries().size).toBe(0); +}); + it("allows the client to be overridden", async () => { const { query } = useSimpleQueryCase(); diff --git a/src/react/hooks/useLoadableQuery.ts b/src/react/hooks/useLoadableQuery.ts index b873a4ba33a..3d002a165c7 100644 --- a/src/react/hooks/useLoadableQuery.ts +++ b/src/react/hooks/useLoadableQuery.ts @@ -37,6 +37,8 @@ export type LoadQueryFunction = ( : [variables: TVariables] ) => void; +type ResetFunction = () => void; + export type UseLoadableQueryResult< TData = unknown, TVariables extends OperationVariables = OperationVariables, @@ -46,6 +48,7 @@ export type UseLoadableQueryResult< { fetchMore: FetchMoreFunction; refetch: RefetchFunction; + reset: ResetFunction; }, ]; @@ -215,11 +218,15 @@ export function useLoadableQuery< ] ); + const reset: ResetFunction = React.useCallback(() => { + setQueryRef(null); + }, [queryRef]); + return React.useMemo(() => { return [ loadQuery, queryRef && wrapQueryRef(queryRef), - { fetchMore, refetch }, + { fetchMore, refetch, reset }, ]; - }, [queryRef, loadQuery, fetchMore, refetch]); + }, [queryRef, loadQuery, fetchMore, refetch, reset]); } From b7c2dfcfd32ab68441dc93999a98b21fc7f80e09 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 27 Nov 2023 22:04:12 -0700 Subject: [PATCH 187/199] Update api report --- .api-reports/api-report-react.md | 11 +++++++---- .api-reports/api-report-react_hooks.md | 11 +++++++---- .api-reports/api-report-utilities.md | 5 +++++ .api-reports/api-report.md | 11 +++++++---- 4 files changed, 26 insertions(+), 12 deletions(-) diff --git a/.api-reports/api-report-react.md b/.api-reports/api-report-react.md index ffb809098ab..938a5329ca7 100644 --- a/.api-reports/api-report-react.md +++ b/.api-reports/api-report-react.md @@ -1048,7 +1048,7 @@ export interface LoadableQueryHookOptions { // Warning: (ae-forgotten-export) The symbol "OnlyRequiredProperties" needs to be exported by the entry point index.d.ts // // @public (undocumented) -type LoadQuery = (...args: [TVariables] extends [never] ? [] : {} extends OnlyRequiredProperties ? [variables?: TVariables] : [variables: TVariables]) => void; +export type LoadQueryFunction = (...args: [TVariables] extends [never] ? [] : {} extends OnlyRequiredProperties ? [variables?: TVariables] : [variables: TVariables]) => void; // @public (undocumented) class LocalState { @@ -1835,6 +1835,9 @@ type RequestHandler = (operation: Operation, forward: NextLink) => Observable void; + // @public (undocumented) type Resolver = (rootValue?: any, args?: any, context?: any, info?: { field: FieldNode; @@ -2149,15 +2152,14 @@ export function useLoadableQuery(query: DocumentNode | TypedDocumentNode, options?: LoadableQueryHookOptions): UseLoadableQueryResult; -// Warning: (ae-forgotten-export) The symbol "LoadQuery" needs to be exported by the entry point index.d.ts -// // @public (undocumented) export type UseLoadableQueryResult = [ -LoadQuery, +LoadQueryFunction, QueryReference | null, { fetchMore: FetchMoreFunction; refetch: RefetchFunction; + reset: ResetFunction; } ]; @@ -2288,6 +2290,7 @@ interface WatchQueryOptions = (...args: [TVariables] extends [never] ? [] : {} extends OnlyRequiredProperties ? [variables?: TVariables] : [variables: TVariables]) => void; +export type LoadQueryFunction = (...args: [TVariables] extends [never] ? [] : {} extends OnlyRequiredProperties ? [variables?: TVariables] : [variables: TVariables]) => void; // @public (undocumented) class LocalState { @@ -1733,6 +1733,9 @@ type RefetchWritePolicy = "merge" | "overwrite"; // @public (undocumented) type RequestHandler = (operation: Operation, forward: NextLink) => Observable | null; +// @public (undocumented) +type ResetFunction = () => void; + // @public (undocumented) type Resolver = (rootValue?: any, args?: any, context?: any, info?: { field: FieldNode; @@ -2040,15 +2043,14 @@ export function useLoadableQuery(query: DocumentNode | TypedDocumentNode, options?: LoadableQueryHookOptions): UseLoadableQueryResult; -// Warning: (ae-forgotten-export) The symbol "LoadQuery" needs to be exported by the entry point index.d.ts -// // @public (undocumented) export type UseLoadableQueryResult = [ -LoadQuery, +LoadQueryFunction, QueryReference | null, { fetchMore: FetchMoreFunction; refetch: RefetchFunction; + reset: ResetFunction; } ]; @@ -2186,6 +2188,7 @@ interface WatchQueryOptions(keyArgs?: KeyArgs): FieldPo // @public (undocumented) export function omitDeep(value: T, key: K): DeepOmit; +// @public (undocumented) +export type OnlyRequiredProperties = { + [K in keyof T as {} extends Pick ? never : K]: T[K]; +}; + // @public (undocumented) type OnQueryUpdated = (observableQuery: ObservableQuery, diff: Cache_2.DiffResult, lastDiff: Cache_2.DiffResult | undefined) => boolean | TResult; diff --git a/.api-reports/api-report.md b/.api-reports/api-report.md index 47f488b186e..4b94b3131a9 100644 --- a/.api-reports/api-report.md +++ b/.api-reports/api-report.md @@ -1450,7 +1450,7 @@ export interface LoadableQueryHookOptions { // Warning: (ae-forgotten-export) The symbol "OnlyRequiredProperties" needs to be exported by the entry point index.d.ts // // @public (undocumented) -type LoadQuery = (...args: [TVariables] extends [never] ? [] : {} extends OnlyRequiredProperties ? [variables?: TVariables] : [variables: TVariables]) => void; +export type LoadQueryFunction = (...args: [TVariables] extends [never] ? [] : {} extends OnlyRequiredProperties ? [variables?: TVariables] : [variables: TVariables]) => void; // @public (undocumented) class LocalState { @@ -2405,6 +2405,9 @@ export const resetApolloContext: typeof getApolloContext; export { resetCaches } +// @public (undocumented) +type ResetFunction = () => void; + // @public (undocumented) export type Resolver = (rootValue?: any, args?: any, context?: any, info?: { field: FieldNode; @@ -2795,15 +2798,14 @@ export function useLoadableQuery(query: DocumentNode | TypedDocumentNode, options?: LoadableQueryHookOptions): UseLoadableQueryResult; -// Warning: (ae-forgotten-export) The symbol "LoadQuery" needs to be exported by the entry point index.d.ts -// // @public (undocumented) export type UseLoadableQueryResult = [ -LoadQuery, +LoadQueryFunction, QueryReference | null, { fetchMore: FetchMoreFunction; refetch: RefetchFunction; + reset: ResetFunction; } ]; @@ -2957,6 +2959,7 @@ interface WriteContext extends ReadMergeModifyContext { // src/link/http/selectHttpOptionsAndBody.ts:128:32 - (ae-forgotten-export) The symbol "HttpQueryOptions" needs to be exported by the entry point index.d.ts // src/react/hooks/useBackgroundQuery.ts:26:3 - (ae-forgotten-export) The symbol "FetchMoreFunction" needs to be exported by the entry point index.d.ts // src/react/hooks/useBackgroundQuery.ts:27:3 - (ae-forgotten-export) The symbol "RefetchFunction" needs to be exported by the entry point index.d.ts +// src/react/hooks/useLoadableQuery.ts:51:5 - (ae-forgotten-export) The symbol "ResetFunction" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) From 2dc9c8e492572f16a28157b8946176ea9ee14a06 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 27 Nov 2023 22:04:23 -0700 Subject: [PATCH 188/199] Update size limits --- .size-limits.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.size-limits.json b/.size-limits.json index 8985dcaa79e..0401326f5b6 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 38569, + "dist/apollo-client.min.cjs": 38606, "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32189 } From 01d6696e33076b1df49fca1fc1f728b54a3e6709 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 27 Nov 2023 22:09:46 -0700 Subject: [PATCH 189/199] Update the changeset to match the updated API --- .changeset/thirty-ties-arrive.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.changeset/thirty-ties-arrive.md b/.changeset/thirty-ties-arrive.md index d9695eb0900..cdab3695aff 100644 --- a/.changeset/thirty-ties-arrive.md +++ b/.changeset/thirty-ties-arrive.md @@ -6,12 +6,14 @@ Introduces a new `useLoadableQuery` hook. This hook works similarly to `useBackg ```tsx function App() { - const [queryRef, loadQuery, { refetch, fetchMore }] = useLoadableQuery(query, options) + const [loadQuery, queryRef, { refetch, fetchMore, reset }] = useLoadableQuery(query, options) return ( <> - {queryRef && } + }> + {queryRef && } + ); } From 5c76e4f0292923df45247dca1a2f75b30afeb2ff Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 27 Nov 2023 22:19:53 -0700 Subject: [PATCH 190/199] Update matcher to allow ProfiledComponent as acceptable value --- src/testing/matchers/ProfiledComponent.ts | 11 +++++++++-- src/testing/matchers/index.d.ts | 11 +++++++++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/testing/matchers/ProfiledComponent.ts b/src/testing/matchers/ProfiledComponent.ts index 2ed110bc3a3..435c24a29a6 100644 --- a/src/testing/matchers/ProfiledComponent.ts +++ b/src/testing/matchers/ProfiledComponent.ts @@ -3,12 +3,16 @@ import { WaitForRenderTimeoutError } from "../internal/index.js"; import type { NextRenderOptions, Profiler, + ProfiledComponent, ProfiledHook, } from "../internal/index.js"; export const toRerender: MatcherFunction<[options?: NextRenderOptions]> = async function (actual, options) { - const _profiler = actual as Profiler | ProfiledHook; + const _profiler = actual as + | Profiler + | ProfiledComponent + | ProfiledHook; const profiler = "Profiler" in _profiler ? _profiler.Profiler : _profiler; const hint = this.utils.matcherHint("toRerender", "ProfiledComponent", ""); let pass = true; @@ -40,7 +44,10 @@ const failed = {}; export const toRenderExactlyTimes: MatcherFunction< [times: number, options?: NextRenderOptions] > = async function (actual, times, optionsPerRender) { - const _profiler = actual as Profiler | ProfiledHook; + const _profiler = actual as + | Profiler + | ProfiledComponent + | ProfiledHook; const profiler = "Profiler" in _profiler ? _profiler.Profiler : _profiler; const options = { timeout: 100, ...optionsPerRender }; const hint = this.utils.matcherHint("toRenderExactlyTimes"); diff --git a/src/testing/matchers/index.d.ts b/src/testing/matchers/index.d.ts index 6f8cb02efd6..b09a823dc25 100644 --- a/src/testing/matchers/index.d.ts +++ b/src/testing/matchers/index.d.ts @@ -6,6 +6,7 @@ import type { import { NextRenderOptions, Profiler, + ProfiledComponent, ProfiledHook, } from "../internal/index.js"; @@ -29,11 +30,17 @@ interface ApolloCustomMatchers { ) => R : { error: "matcher needs to be called on an ApolloClient instance" }; - toRerender: T extends Profiler | ProfiledHook + toRerender: T extends + | Profiler + | ProfiledComponent + | ProfiledHook ? (options?: NextRenderOptions) => Promise : { error: "matcher needs to be called on a ProfiledComponent instance" }; - toRenderExactlyTimes: T extends Profiler | ProfiledHook + toRenderExactlyTimes: T extends + | Profiler + | ProfiledComponent + | ProfiledHook ? (count: number, options?: NextRenderOptions) => Promise : { error: "matcher needs to be called on a ProfiledComponent instance" }; } From 8101e020b984f5089059e1c346c4a0b68a9bf1f6 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 28 Nov 2023 12:30:56 -0700 Subject: [PATCH 191/199] Pass Profiler directly as the wrapper --- .../hooks/__tests__/useLoadableQuery.test.tsx | 177 ++++-------------- 1 file changed, 39 insertions(+), 138 deletions(-) diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index c9ec504b420..cbedf33696e 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -271,10 +271,7 @@ it("loads a query and suspends when the load query function is called", async () ); } - const { user } = renderWithMocks(, { - mocks, - wrapper: ({ children }) => {children}, - }); + const { user } = renderWithMocks(, { mocks, wrapper: Profiler }); { const { renderedComponents } = await Profiler.takeRender(); @@ -325,10 +322,7 @@ it("loads a query with variables and suspends by passing variables to the loadQu ); } - const { user } = renderWithMocks(, { - mocks, - wrapper: ({ children }) => {children}, - }); + const { user } = renderWithMocks(, { mocks, wrapper: Profiler }); { const { renderedComponents } = await Profiler.takeRender(); @@ -385,10 +379,7 @@ it("changes variables on a query and resuspends when passing new variables to th ); }; - const { user } = renderWithMocks(, { - mocks, - wrapper: ({ children }) => {children}, - }); + const { user } = renderWithMocks(, { mocks, wrapper: Profiler }); { const { renderedComponents } = await Profiler.takeRender(); @@ -468,10 +459,7 @@ it("resets the `queryRef` to null and disposes of it when calling the `reset` fu ); } - const { user } = renderWithClient(, { - client, - wrapper: ({ children }) => {children}, - }); + const { user } = renderWithClient(, { client, wrapper: Profiler }); await act(() => user.click(screen.getByText("Load query"))); @@ -545,7 +533,7 @@ it("allows the client to be overridden", async () => { const { user } = renderWithClient(, { client: globalClient, - wrapper: ({ children }) => {children}, + wrapper: Profiler, }); await act(() => user.click(screen.getByText("Load query"))); @@ -607,10 +595,7 @@ it("passes context to the link", async () => { ); } - const { user } = renderWithClient(, { - client, - wrapper: ({ children }) => {children}, - }); + const { user } = renderWithClient(, { client, wrapper: Profiler }); await act(() => user.click(screen.getByText("Load query"))); @@ -693,10 +678,7 @@ it('enables canonical results when canonizeResults is "true"', async () => { ); } - const { user } = renderWithClient(, { - client, - wrapper: ({ children }) => {children}, - }); + const { user } = renderWithClient(, { client, wrapper: Profiler }); await act(() => user.click(screen.getByText("Load query"))); @@ -777,10 +759,7 @@ it("can disable canonical results when the cache's canonizeResults setting is tr ); } - const { user } = renderWithMocks(, { - cache, - wrapper: ({ children }) => {children}, - }); + const { user } = renderWithMocks(, { cache, wrapper: Profiler }); await act(() => user.click(screen.getByText("Load query"))); @@ -841,10 +820,7 @@ it("returns initial cache data followed by network data when the fetch policy is ); } - const { user } = renderWithClient(, { - client, - wrapper: ({ children }) => {children}, - }); + const { user } = renderWithClient(, { client, wrapper: Profiler }); await act(() => user.click(screen.getByText("Load query"))); @@ -914,10 +890,7 @@ it("all data is present in the cache, no network request is made", async () => { ); } - const { user } = renderWithClient(, { - client, - wrapper: ({ children }) => {children}, - }); + const { user } = renderWithClient(, { client, wrapper: Profiler }); await act(() => user.click(screen.getByText("Load query"))); @@ -982,10 +955,7 @@ it("partial data is present in the cache so it is ignored and network request is ); } - const { user } = renderWithClient(, { - client, - wrapper: ({ children }) => {children}, - }); + const { user } = renderWithClient(, { client, wrapper: Profiler }); await act(() => user.click(screen.getByText("Load query"))); @@ -1051,10 +1021,7 @@ it("existing data in the cache is ignored when `fetchPolicy` is 'network-only'", ); } - const { user } = renderWithClient(, { - client, - wrapper: ({ children }) => {children}, - }); + const { user } = renderWithClient(, { client, wrapper: Profiler }); await act(() => user.click(screen.getByText("Load query"))); @@ -1117,10 +1084,7 @@ it("fetches data from the network but does not update the cache when `fetchPolic ); } - const { user } = renderWithClient(, { - client, - wrapper: ({ children }) => {children}, - }); + const { user } = renderWithClient(, { client, wrapper: Profiler }); await act(() => user.click(screen.getByText("Load query"))); @@ -1335,10 +1299,7 @@ it('does not suspend deferred queries with data in the cache and using a "cache- ); } - const { user } = renderWithClient(, { - client, - wrapper: ({ children }) => {children}, - }); + const { user } = renderWithClient(, { client, wrapper: Profiler }); await act(() => user.click(screen.getByText("Load todo"))); @@ -1543,10 +1504,7 @@ it("applies `errorPolicy` on next fetch when it changes between renders", async ); } - const { user } = renderWithMocks(, { - mocks, - wrapper: ({ children }) => {children}, - }); + const { user } = renderWithMocks(, { mocks, wrapper: Profiler }); await act(() => user.click(screen.getByText("Load query"))); @@ -1633,10 +1591,7 @@ it("applies `context` on next fetch when it changes between renders", async () = ); } - const { user } = renderWithClient(, { - client, - wrapper: ({ children }) => {children}, - }); + const { user } = renderWithClient(, { client, wrapper: Profiler }); await act(() => user.click(screen.getByText("Load query"))); @@ -1741,10 +1696,7 @@ it("returns canonical results immediately when `canonizeResults` changes from `f ); } - const { user } = renderWithClient(, { - client, - wrapper: ({ children }) => {children}, - }); + const { user } = renderWithClient(, { client, wrapper: Profiler }); await act(() => user.click(screen.getByText("Load query"))); @@ -1860,10 +1812,7 @@ it("applies changed `refetchWritePolicy` to next fetch when changing between ren ); } - const { user } = renderWithClient(, { - client, - wrapper: ({ children }) => {children}, - }); + const { user } = renderWithClient(, { client, wrapper: Profiler }); await act(() => user.click(screen.getByText("Load query"))); @@ -2023,10 +1972,7 @@ it("applies `returnPartialData` on next fetch when it changes between renders", ); } - const { user } = renderWithClient(, { - client, - wrapper: ({ children }) => {children}, - }); + const { user } = renderWithClient(, { client, wrapper: Profiler }); await act(() => user.click(screen.getByText("Load query"))); @@ -2169,10 +2115,7 @@ it("applies updated `fetchPolicy` on next fetch when it changes between renders" ); } - const { user } = renderWithClient(, { - client, - wrapper: ({ children }) => {children}, - }); + const { user } = renderWithClient(, { client, wrapper: Profiler }); await act(() => user.click(screen.getByText("Load query"))); @@ -2270,10 +2213,7 @@ it("re-suspends when calling `refetch`", async () => { ); } - const { user } = renderWithMocks(, { - mocks, - wrapper: ({ children }) => {children}, - }); + const { user } = renderWithMocks(, { mocks, wrapper: Profiler }); await act(() => user.click(screen.getByText("Load query"))); @@ -2354,10 +2294,7 @@ it("re-suspends when calling `refetch` with new variables", async () => { ); } - const { user } = renderWithMocks(, { - mocks, - wrapper: ({ children }) => {children}, - }); + const { user } = renderWithMocks(, { mocks, wrapper: Profiler }); await act(() => user.click(screen.getByText("Load query"))); @@ -2434,10 +2371,7 @@ it("re-suspends multiple times when calling `refetch` multiple times", async () ); } - const { user } = renderWithMocks(, { - mocks, - wrapper: ({ children }) => {children}, - }); + const { user } = renderWithMocks(, { mocks, wrapper: Profiler }); // initial render await Profiler.takeRender(); @@ -2523,10 +2457,7 @@ it("throws errors when errors are returned after calling `refetch`", async () => ); } - const { user } = renderWithMocks(, { - mocks, - wrapper: ({ children }) => {children}, - }); + const { user } = renderWithMocks(, { mocks, wrapper: Profiler }); await act(() => user.click(screen.getByText("Load query"))); @@ -2603,10 +2534,7 @@ it('ignores errors returned after calling `refetch` when errorPolicy is set to " ); } - const { user } = renderWithMocks(, { - mocks, - wrapper: ({ children }) => {children}, - }); + const { user } = renderWithMocks(, { mocks, wrapper: Profiler }); // initial render await Profiler.takeRender(); @@ -2690,10 +2618,7 @@ it('returns errors after calling `refetch` when errorPolicy is set to "all"', as ); } - const { user } = renderWithMocks(, { - mocks, - wrapper: ({ children }) => {children}, - }); + const { user } = renderWithMocks(, { mocks, wrapper: Profiler }); await act(() => user.click(screen.getByText("Load query"))); @@ -2778,10 +2703,7 @@ it('handles partial data results after calling `refetch` when errorPolicy is set ); } - const { user } = renderWithMocks(, { - mocks, - wrapper: ({ children }) => {children}, - }); + const { user } = renderWithMocks(, { mocks, wrapper: Profiler }); await act(() => user.click(screen.getByText("Load query"))); @@ -2975,10 +2897,7 @@ it("re-suspends when calling `fetchMore` with different variables", async () => ); } - const { user } = renderWithClient(, { - client, - wrapper: ({ children }) => {children}, - }); + const { user } = renderWithClient(, { client, wrapper: Profiler }); await act(() => user.click(screen.getByText("Load query"))); @@ -3060,10 +2979,7 @@ it("properly uses `updateQuery` when calling `fetchMore`", async () => { ); } - const { user } = renderWithClient(, { - client, - wrapper: ({ children }) => {children}, - }); + const { user } = renderWithClient(, { client, wrapper: Profiler }); await act(() => user.click(screen.getByText("Load query"))); @@ -3149,10 +3065,7 @@ it("properly uses cache field policies when calling `fetchMore` without `updateQ ); } - const { user } = renderWithClient(, { - client, - wrapper: ({ children }) => {children}, - }); + const { user } = renderWithClient(, { client, wrapper: Profiler }); await act(() => user.click(screen.getByText("Load query"))); @@ -3436,10 +3349,7 @@ it('honors refetchWritePolicy set to "merge"', async () => { ); } - const { user } = renderWithClient(, { - client, - wrapper: ({ children }) => {children}, - }); + const { user } = renderWithClient(, { client, wrapper: Profiler }); await act(() => user.click(screen.getByText("Load query"))); @@ -3552,10 +3462,7 @@ it('defaults refetchWritePolicy to "overwrite"', async () => { ); } - const { user } = renderWithClient(, { - client, - wrapper: ({ children }) => {children}, - }); + const { user } = renderWithClient(, { client, wrapper: Profiler }); await act(() => user.click(screen.getByText("Load query"))); @@ -3657,10 +3564,7 @@ it('does not suspend when partial data is in the cache and using a "cache-first" ); } - const { user } = renderWithClient(, { - client, - wrapper: ({ children }) => {children}, - }); + const { user } = renderWithClient(, { client, wrapper: Profiler }); await act(() => user.click(screen.getByText("Load query"))); @@ -3738,7 +3642,7 @@ it('suspends and does not use partial data from other variables in the cache whe const { user } = renderWithMocks(, { mocks, cache, - wrapper: ({ children }) => {children}, + wrapper: Profiler, }); await act(() => user.click(screen.getByText("Load query"))); @@ -3855,7 +3759,7 @@ it('suspends when partial data is in the cache and using a "network-only" fetch const { user } = renderWithMocks(, { mocks, cache, - wrapper: ({ children }) => {children}, + wrapper: Profiler, }); await act(() => user.click(screen.getByText("Load query"))); @@ -3946,7 +3850,7 @@ it('suspends when partial data is in the cache and using a "no-cache" fetch poli const { user } = renderWithMocks(, { mocks, cache, - wrapper: ({ children }) => {children}, + wrapper: Profiler, }); await act(() => user.click(screen.getByText("Load query"))); @@ -4062,7 +3966,7 @@ it('does not suspend when partial data is in the cache and using a "cache-and-ne const { user } = renderWithMocks(, { mocks, cache, - wrapper: ({ children }) => {children}, + wrapper: Profiler, }); await act(() => user.click(screen.getByText("Load query"))); @@ -4136,7 +4040,7 @@ it('suspends and does not use partial data when changing variables and using a " const { user } = renderWithMocks(, { mocks, cache, - wrapper: ({ children }) => {children}, + wrapper: Profiler, }); await act(() => user.click(screen.getByText("Load query"))); @@ -4252,10 +4156,7 @@ it('does not suspend deferred queries with partial data in the cache and using a ); } - const { user } = renderWithClient(, { - client, - wrapper: ({ children }) => {children}, - }); + const { user } = renderWithClient(, { client, wrapper: Profiler }); await act(() => user.click(screen.getByText("Load todo"))); From ea821a9c66c156fbbf6ec7770ddf78801fef6258 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 28 Nov 2023 12:39:40 -0700 Subject: [PATCH 192/199] Return function that returns boolean for useRenderGuard --- src/react/hooks/internal/useRenderGuard.ts | 14 ++++---------- src/react/hooks/useLoadableQuery.ts | 14 ++++++-------- 2 files changed, 10 insertions(+), 18 deletions(-) diff --git a/src/react/hooks/internal/useRenderGuard.ts b/src/react/hooks/internal/useRenderGuard.ts index 119fa0041a8..98bb21a8ef1 100644 --- a/src/react/hooks/internal/useRenderGuard.ts +++ b/src/react/hooks/internal/useRenderGuard.ts @@ -14,15 +14,9 @@ https://github.com/facebook/relay/blob/8651fbca19adbfbb79af7a3bc40834d105fd7747/ export function useRenderGuard() { RenderDispatcher = getRenderDispatcher(); - // We use a callback argument here instead of the failure string so that the - // call site can provide a custom failure message while allowing for static - // message extraction on the `invariant` function. - return React.useCallback((onFailure: () => void) => { - if ( - RenderDispatcher !== null && - RenderDispatcher === getRenderDispatcher() - ) { - onFailure(); - } + return React.useCallback(() => { + return ( + RenderDispatcher !== null && RenderDispatcher === getRenderDispatcher() + ); }, []); } diff --git a/src/react/hooks/useLoadableQuery.ts b/src/react/hooks/useLoadableQuery.ts index 3d002a165c7..2ec551bf26e 100644 --- a/src/react/hooks/useLoadableQuery.ts +++ b/src/react/hooks/useLoadableQuery.ts @@ -137,7 +137,7 @@ export function useLoadableQuery< queryRef.promiseCache = promiseCache; } - const failDuringRender = useRenderGuard(); + const calledDuringRender = useRenderGuard(); React.useEffect(() => queryRef?.retain(), [queryRef]); @@ -183,12 +183,10 @@ export function useLoadableQuery< const loadQuery: LoadQueryFunction = React.useCallback( (...args) => { - failDuringRender(() => { - invariant( - false, - "useLoadableQuery: 'loadQuery' should not be called during render. To start a query during render, use the 'useBackgroundQuery' hook." - ); - }); + invariant( + !calledDuringRender(), + "useLoadableQuery: 'loadQuery' should not be called during render. To start a query during render, use the 'useBackgroundQuery' hook." + ); const [variables] = args; @@ -214,7 +212,7 @@ export function useLoadableQuery< suspenseCache, watchQueryOptions, promiseCache, - failDuringRender, + calledDuringRender, ] ); From 2e72d53b801d0dcedf5b6d2a0eb0bb119aaddc31 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 28 Nov 2023 12:59:45 -0700 Subject: [PATCH 193/199] Create reusable helper to temp disable act warnings --- .../internal/disposables/disableActWarnings.ts | 15 +++++++++++++++ src/testing/internal/disposables/index.ts | 1 + src/testing/internal/profile/profile.tsx | 12 +++++------- 3 files changed, 21 insertions(+), 7 deletions(-) create mode 100644 src/testing/internal/disposables/disableActWarnings.ts diff --git a/src/testing/internal/disposables/disableActWarnings.ts b/src/testing/internal/disposables/disableActWarnings.ts new file mode 100644 index 00000000000..c5254c8dc1d --- /dev/null +++ b/src/testing/internal/disposables/disableActWarnings.ts @@ -0,0 +1,15 @@ +import { withCleanup } from "./withCleanup.js"; + +/** + * Temporarily disable act warnings. + * + * https://github.com/reactwg/react-18/discussions/102 + */ +export function disableActWarnings() { + const prev = { prevActEnv: (globalThis as any).IS_REACT_ACT_ENVIRONMENT }; + (globalThis as any).IS_REACT_ACT_ENVIRONMENT = false; + + return withCleanup(prev, ({ prevActEnv }) => { + (globalThis as any).IS_REACT_ACT_ENVIRONMENT = prevActEnv; + }); +} diff --git a/src/testing/internal/disposables/index.ts b/src/testing/internal/disposables/index.ts index 6d232565db4..9895d129589 100644 --- a/src/testing/internal/disposables/index.ts +++ b/src/testing/internal/disposables/index.ts @@ -1,2 +1,3 @@ +export { disableActWarnings } from "./disableActWarnings.js"; export { spyOnConsole } from "./spyOnConsole.js"; export { withCleanup } from "./withCleanup.js"; diff --git a/src/testing/internal/profile/profile.tsx b/src/testing/internal/profile/profile.tsx index 542a6eae940..d74e6cae47f 100644 --- a/src/testing/internal/profile/profile.tsx +++ b/src/testing/internal/profile/profile.tsx @@ -10,6 +10,7 @@ import { RenderInstance } from "./Render.js"; import { applyStackTrace, captureStackTrace } from "./traces.js"; import type { ProfilerContextValue } from "./context.js"; import { ProfilerContextProvider, useProfilerContext } from "./context.js"; +import { disableActWarnings } from "../disposables/index.js"; type ValidSnapshot = void | (object & { /* not a function */ call?: never }); @@ -285,14 +286,12 @@ export function createProfiler< }, async takeRender(options: NextRenderOptions = {}) { // In many cases we do not control the resolution of the suspended - // promise which results in noisy tests when using this utility. Instead, - // we disable act warnings when using this utility. - // - // https://github.com/reactwg/react-18/discussions/102 - const prevActEnv = (globalThis as any).IS_REACT_ACT_ENVIRONMENT; - (globalThis as any).IS_REACT_ACT_ENVIRONMENT = false; + // promise which results in noisy tests when the profiler due to + // repeated act warnings. + using _disabledActWarnings = disableActWarnings(); let error: unknown = undefined; + try { return await Profiler.peekRender({ [_stackTrace]: captureStackTrace(Profiler.takeRender), @@ -305,7 +304,6 @@ export function createProfiler< if (!(error && error instanceof WaitForRenderTimeoutError)) { iteratorPosition++; } - (globalThis as any).IS_REACT_ACT_ENVIRONMENT = prevActEnv; } }, getCurrentRender() { From 3c9d2362dd2263de38a060fcae6b23c8a4b47ac7 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 28 Nov 2023 13:27:00 -0700 Subject: [PATCH 194/199] Rename useTrackRender to useTrackRenders --- .../hooks/__tests__/useLoadableQuery.test.tsx | 54 +++++++++---------- src/testing/internal/profile/index.ts | 2 +- src/testing/internal/profile/profile.tsx | 2 +- 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index cbedf33696e..88e1916210d 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -49,7 +49,7 @@ import { Profiler, createProfiler, spyOnConsole, - useTrackRender, + useTrackRenders, } from "../../../testing/internal"; interface SimpleQueryData { @@ -175,12 +175,12 @@ function createDefaultProfiledComponents< : unknown, >(profiler: Profiler) { function SuspenseFallback() { - useTrackRender(); + useTrackRenders(); return

Loading

; } function ReadQueryHook({ queryRef }: { queryRef: QueryReference }) { - useTrackRender(); + useTrackRenders(); profiler.mergeSnapshot({ result: useReadQuery(queryRef), } as Partial); @@ -189,7 +189,7 @@ function createDefaultProfiledComponents< } function ErrorFallback({ error }: { error: Error }) { - useTrackRender(); + useTrackRenders(); profiler.mergeSnapshot({ error } as Partial); return
Oops
; @@ -258,7 +258,7 @@ it("loads a query and suspends when the load query function is called", async () createDefaultProfiledComponents(Profiler); function App() { - useTrackRender(); + useTrackRenders(); const [loadQuery, queryRef] = useLoadableQuery(query); return ( @@ -309,7 +309,7 @@ it("loads a query with variables and suspends by passing variables to the loadQu createDefaultProfiledComponents(Profiler); function App() { - useTrackRender(); + useTrackRenders(); const [loadQuery, queryRef] = useLoadableQuery(query); return ( @@ -361,7 +361,7 @@ it("changes variables on a query and resuspends when passing new variables to th createDefaultProfiledComponents(Profiler); const App = () => { - useTrackRender(); + useTrackRenders(); const [loadQuery, queryRef] = useLoadableQuery(query); return ( @@ -441,7 +441,7 @@ it("resets the `queryRef` to null and disposes of it when calling the `reset` fu createDefaultProfiledComponents(Profiler); function App() { - useTrackRender(); + useTrackRenders(); const [loadQuery, queryRef, { reset }] = useLoadableQuery(query); // Resetting the result allows us to detect when ReadQueryHook is unmounted @@ -805,7 +805,7 @@ it("returns initial cache data followed by network data when the fetch policy is createDefaultProfiledComponents(Profiler); function App() { - useTrackRender(); + useTrackRenders(); const [loadQuery, queryRef] = useLoadableQuery(query, { fetchPolicy: "cache-and-network", }); @@ -877,7 +877,7 @@ it("all data is present in the cache, no network request is made", async () => { createDefaultProfiledComponents(Profiler); function App() { - useTrackRender(); + useTrackRenders(); const [loadQuery, queryRef] = useLoadableQuery(query); return ( @@ -942,7 +942,7 @@ it("partial data is present in the cache so it is ignored and network request is createDefaultProfiledComponents(Profiler); function App() { - useTrackRender(); + useTrackRenders(); const [loadQuery, queryRef] = useLoadableQuery(query); return ( @@ -1006,7 +1006,7 @@ it("existing data in the cache is ignored when `fetchPolicy` is 'network-only'", createDefaultProfiledComponents(Profiler); function App() { - useTrackRender(); + useTrackRenders(); const [loadQuery, queryRef] = useLoadableQuery(query, { fetchPolicy: "network-only", }); @@ -1069,7 +1069,7 @@ it("fetches data from the network but does not update the cache when `fetchPolic createDefaultProfiledComponents(Profiler); function App() { - useTrackRender(); + useTrackRenders(); const [loadQuery, queryRef] = useLoadableQuery(query, { fetchPolicy: "no-cache", }); @@ -1285,7 +1285,7 @@ it('does not suspend deferred queries with data in the cache and using a "cache- createDefaultProfiledComponents(Profiler); function App() { - useTrackRender(); + useTrackRenders(); const [loadQuery, queryRef] = useLoadableQuery(query, { fetchPolicy: "cache-and-network", }); @@ -1405,7 +1405,7 @@ it("reacts to cache updates", async () => { createDefaultProfiledComponents(Profiler); function App() { - useTrackRender(); + useTrackRenders(); const [loadQuery, queryRef] = useLoadableQuery(query); return ( @@ -1952,7 +1952,7 @@ it("applies `returnPartialData` on next fetch when it changes between renders", createDefaultProfiledComponents(Profiler); function App() { - useTrackRender(); + useTrackRenders(); const [returnPartialData, setReturnPartialData] = React.useState(false); const [loadQuery, queryRef] = useLoadableQuery(fullQuery, { @@ -2199,7 +2199,7 @@ it("re-suspends when calling `refetch`", async () => { createDefaultProfiledComponents(Profiler); function App() { - useTrackRender(); + useTrackRenders(); const [loadQuery, queryRef, { refetch }] = useLoadableQuery(query); return ( @@ -2280,7 +2280,7 @@ it("re-suspends when calling `refetch` with new variables", async () => { createDefaultProfiledComponents(Profiler); function App() { - useTrackRender(); + useTrackRenders(); const [loadQuery, queryRef, { refetch }] = useLoadableQuery(query); return ( @@ -2357,7 +2357,7 @@ it("re-suspends multiple times when calling `refetch` multiple times", async () createDefaultProfiledComponents(Profiler); function App() { - useTrackRender(); + useTrackRenders(); const [loadQuery, queryRef, { refetch }] = useLoadableQuery(query); return ( @@ -2879,7 +2879,7 @@ it("re-suspends when calling `fetchMore` with different variables", async () => createDefaultProfiledComponents(Profiler); function App() { - useTrackRender(); + useTrackRenders(); const [loadQuery, queryRef, { fetchMore }] = useLoadableQuery(query); return ( @@ -3548,7 +3548,7 @@ it('does not suspend when partial data is in the cache and using a "cache-first" const client = new ApolloClient({ link: new MockLink(mocks), cache }); function App() { - useTrackRender(); + useTrackRenders(); const [loadQuery, queryRef] = useLoadableQuery(fullQuery, { fetchPolicy: "cache-first", returnPartialData: true, @@ -3622,7 +3622,7 @@ it('suspends and does not use partial data from other variables in the cache whe createDefaultProfiledComponents(Profiler); function App() { - useTrackRender(); + useTrackRenders(); const [loadQuery, queryRef] = useLoadableQuery(query, { fetchPolicy: "cache-first", returnPartialData: true, @@ -3740,7 +3740,7 @@ it('suspends when partial data is in the cache and using a "network-only" fetch }); function App() { - useTrackRender(); + useTrackRenders(); const [loadQuery, queryRef] = useLoadableQuery(fullQuery, { fetchPolicy: "network-only", returnPartialData: true, @@ -3831,7 +3831,7 @@ it('suspends when partial data is in the cache and using a "no-cache" fetch poli createDefaultProfiledComponents(Profiler); function App() { - useTrackRender(); + useTrackRenders(); const [loadQuery, queryRef] = useLoadableQuery(fullQuery, { fetchPolicy: "no-cache", returnPartialData: true, @@ -3947,7 +3947,7 @@ it('does not suspend when partial data is in the cache and using a "cache-and-ne createDefaultProfiledComponents(Profiler); function App() { - useTrackRender(); + useTrackRenders(); const [loadQuery, queryRef] = useLoadableQuery(fullQuery, { fetchPolicy: "cache-and-network", returnPartialData: true, @@ -4020,7 +4020,7 @@ it('suspends and does not use partial data when changing variables and using a " createDefaultProfiledComponents(Profiler); function App() { - useTrackRender(); + useTrackRenders(); const [loadQuery, queryRef] = useLoadableQuery(query, { fetchPolicy: "cache-and-network", returnPartialData: true, @@ -4140,7 +4140,7 @@ it('does not suspend deferred queries with partial data in the cache and using a createDefaultProfiledComponents(Profiler); function App() { - useTrackRender(); + useTrackRenders(); const [loadTodo, queryRef] = useLoadableQuery(query, { fetchPolicy: "cache-first", returnPartialData: true, diff --git a/src/testing/internal/profile/index.ts b/src/testing/internal/profile/index.ts index 9a579cc49e2..3d9ddd55559 100644 --- a/src/testing/internal/profile/index.ts +++ b/src/testing/internal/profile/index.ts @@ -8,7 +8,7 @@ export { createProfiler, profile, profileHook, - useTrackRender, + useTrackRenders, WaitForRenderTimeoutError, } from "./profile.js"; diff --git a/src/testing/internal/profile/profile.tsx b/src/testing/internal/profile/profile.tsx index d74e6cae47f..1dc3928ade4 100644 --- a/src/testing/internal/profile/profile.tsx +++ b/src/testing/internal/profile/profile.tsx @@ -439,7 +439,7 @@ function resolveHookOwner(): React.ComponentType | undefined { ?.ReactCurrentOwner?.current?.elementType; } -export function useTrackRender({ name }: { name?: string } = {}) { +export function useTrackRenders({ name }: { name?: string } = {}) { const component = name || resolveHookOwner(); if (!component) { From 7226acd3476feb05440c826b0f23d66369f88791 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 28 Nov 2023 16:07:15 -0700 Subject: [PATCH 195/199] Use Profiler as wrapper in missed test --- src/react/hooks/__tests__/useLoadableQuery.test.tsx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index 88e1916210d..19febcc92bf 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -1418,12 +1418,7 @@ it("reacts to cache updates", async () => { ); } - const { user } = renderWithClient( - - - , - { client } - ); + const { user } = renderWithClient(, { client, wrapper: Profiler }); await act(() => user.click(screen.getByText("Load query"))); From 935ff7cf2e5d2ba03285628519f67c1073b659de Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 28 Nov 2023 16:18:51 -0700 Subject: [PATCH 196/199] Order takeRender calls in tests in a more logical order --- .../hooks/__tests__/useLoadableQuery.test.tsx | 237 ++++++++---------- 1 file changed, 106 insertions(+), 131 deletions(-) diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index 19febcc92bf..e82dc181f66 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -461,11 +461,10 @@ it("resets the `queryRef` to null and disposes of it when calling the `reset` fu const { user } = renderWithClient(, { client, wrapper: Profiler }); - await act(() => user.click(screen.getByText("Load query"))); - // initial render await Profiler.takeRender(); - // load query + + await act(() => user.click(screen.getByText("Load query"))); await Profiler.takeRender(); { @@ -536,11 +535,10 @@ it("allows the client to be overridden", async () => { wrapper: Profiler, }); - await act(() => user.click(screen.getByText("Load query"))); - - // initial + // initial render await Profiler.takeRender(); - // load query + + await act(() => user.click(screen.getByText("Load query"))); await Profiler.takeRender(); const { snapshot } = await Profiler.takeRender(); @@ -597,11 +595,10 @@ it("passes context to the link", async () => { const { user } = renderWithClient(, { client, wrapper: Profiler }); - await act(() => user.click(screen.getByText("Load query"))); - // initial render await Profiler.takeRender(); - // load query + + await act(() => user.click(screen.getByText("Load query"))); await Profiler.takeRender(); const { snapshot } = await Profiler.takeRender(); @@ -680,11 +677,11 @@ it('enables canonical results when canonizeResults is "true"', async () => { const { user } = renderWithClient(, { client, wrapper: Profiler }); - await act(() => user.click(screen.getByText("Load query"))); - // initial render await Profiler.takeRender(); + await act(() => user.click(screen.getByText("Load query"))); + const { snapshot } = await Profiler.takeRender(); const resultSet = new Set(snapshot.result?.data.results); const values = Array.from(resultSet).map((item) => item.value); @@ -761,11 +758,11 @@ it("can disable canonical results when the cache's canonizeResults setting is tr const { user } = renderWithMocks(, { cache, wrapper: Profiler }); - await act(() => user.click(screen.getByText("Load query"))); - // initial render await Profiler.takeRender(); + await act(() => user.click(screen.getByText("Load query"))); + const { snapshot } = await Profiler.takeRender(); const resultSet = new Set(snapshot.result!.data.results); const values = Array.from(resultSet).map((item) => item.value); @@ -822,11 +819,11 @@ it("returns initial cache data followed by network data when the fetch policy is const { user } = renderWithClient(, { client, wrapper: Profiler }); - await act(() => user.click(screen.getByText("Load query"))); - // initial render await Profiler.takeRender(); + await act(() => user.click(screen.getByText("Load query"))); + { const { snapshot, renderedComponents } = await Profiler.takeRender(); @@ -892,11 +889,11 @@ it("all data is present in the cache, no network request is made", async () => { const { user } = renderWithClient(, { client, wrapper: Profiler }); - await act(() => user.click(screen.getByText("Load query"))); - // initial render await Profiler.takeRender(); + await act(() => user.click(screen.getByText("Load query"))); + const { snapshot, renderedComponents } = await Profiler.takeRender(); expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); @@ -957,11 +954,11 @@ it("partial data is present in the cache so it is ignored and network request is const { user } = renderWithClient(, { client, wrapper: Profiler }); - await act(() => user.click(screen.getByText("Load query"))); - // initial render await Profiler.takeRender(); + await act(() => user.click(screen.getByText("Load query"))); + { const { renderedComponents } = await Profiler.takeRender(); @@ -1023,11 +1020,11 @@ it("existing data in the cache is ignored when `fetchPolicy` is 'network-only'", const { user } = renderWithClient(, { client, wrapper: Profiler }); - await act(() => user.click(screen.getByText("Load query"))); - // initial render await Profiler.takeRender(); + await act(() => user.click(screen.getByText("Load query"))); + { const { renderedComponents } = await Profiler.takeRender(); @@ -1086,11 +1083,11 @@ it("fetches data from the network but does not update the cache when `fetchPolic const { user } = renderWithClient(, { client, wrapper: Profiler }); - await act(() => user.click(screen.getByText("Load query"))); - // initial render await Profiler.takeRender(); + await act(() => user.click(screen.getByText("Load query"))); + { const { renderedComponents } = await Profiler.takeRender(); @@ -1301,11 +1298,11 @@ it('does not suspend deferred queries with data in the cache and using a "cache- const { user } = renderWithClient(, { client, wrapper: Profiler }); - await act(() => user.click(screen.getByText("Load todo"))); - // initial render await Profiler.takeRender(); + await act(() => user.click(screen.getByText("Load todo"))); + { const { snapshot, renderedComponents } = await Profiler.takeRender(); @@ -1420,11 +1417,10 @@ it("reacts to cache updates", async () => { const { user } = renderWithClient(, { client, wrapper: Profiler }); - await act(() => user.click(screen.getByText("Load query"))); - // initial render await Profiler.takeRender(); - // load query + + await act(() => user.click(screen.getByText("Load query"))); await Profiler.takeRender(); { @@ -1501,11 +1497,10 @@ it("applies `errorPolicy` on next fetch when it changes between renders", async const { user } = renderWithMocks(, { mocks, wrapper: Profiler }); - await act(() => user.click(screen.getByText("Load query"))); - // initial render await Profiler.takeRender(); - // load query + + await act(() => user.click(screen.getByText("Load query"))); await Profiler.takeRender(); { @@ -1519,11 +1514,9 @@ it("applies `errorPolicy` on next fetch when it changes between renders", async } await act(() => user.click(screen.getByText("Change error policy"))); - await act(() => user.click(screen.getByText("Refetch greeting"))); - - // change error policy await Profiler.takeRender(); - // refetch + + await act(() => user.click(screen.getByText("Refetch greeting"))); await Profiler.takeRender(); { @@ -1588,11 +1581,10 @@ it("applies `context` on next fetch when it changes between renders", async () = const { user } = renderWithClient(, { client, wrapper: Profiler }); - await act(() => user.click(screen.getByText("Load query"))); - // initial render await Profiler.takeRender(); - // load query + + await act(() => user.click(screen.getByText("Load query"))); await Profiler.takeRender(); { @@ -1604,11 +1596,9 @@ it("applies `context` on next fetch when it changes between renders", async () = } await act(() => user.click(screen.getByText("Update context"))); - await act(() => user.click(screen.getByText("Refetch"))); - - // update context await Profiler.takeRender(); - // refetch + + await act(() => user.click(screen.getByText("Refetch"))); await Profiler.takeRender(); { @@ -1693,11 +1683,11 @@ it("returns canonical results immediately when `canonizeResults` changes from `f const { user } = renderWithClient(, { client, wrapper: Profiler }); - await act(() => user.click(screen.getByText("Load query"))); - // initial render await Profiler.takeRender(); + await act(() => user.click(screen.getByText("Load query"))); + { const { snapshot } = await Profiler.takeRender(); const { data } = snapshot.result!; @@ -1809,11 +1799,10 @@ it("applies changed `refetchWritePolicy` to next fetch when changing between ren const { user } = renderWithClient(, { client, wrapper: Profiler }); - await act(() => user.click(screen.getByText("Load query"))); - // initial render await Profiler.takeRender(); - // load query + + await act(() => user.click(screen.getByText("Load query"))); await Profiler.takeRender(); { @@ -1825,8 +1814,6 @@ it("applies changed `refetchWritePolicy` to next fetch when changing between ren } await act(() => user.click(screen.getByText("Refetch next"))); - - // refetch next await Profiler.takeRender(); { @@ -1844,11 +1831,9 @@ it("applies changed `refetchWritePolicy` to next fetch when changing between ren } await act(() => user.click(screen.getByText("Change refetch write policy"))); - await act(() => user.click(screen.getByText("Refetch last"))); - - // change refetchWritePolicy await Profiler.takeRender(); - // refetch last + + await act(() => user.click(screen.getByText("Refetch last"))); await Profiler.takeRender(); { @@ -1969,11 +1954,10 @@ it("applies `returnPartialData` on next fetch when it changes between renders", const { user } = renderWithClient(, { client, wrapper: Profiler }); - await act(() => user.click(screen.getByText("Load query"))); - // initial render await Profiler.takeRender(); - // load query + + await act(() => user.click(screen.getByText("Load query"))); await Profiler.takeRender(); { @@ -1989,8 +1973,6 @@ it("applies `returnPartialData` on next fetch when it changes between renders", } await act(() => user.click(screen.getByText("Update partial data"))); - - // update partial data await Profiler.takeRender(); cache.modify({ @@ -2112,11 +2094,11 @@ it("applies updated `fetchPolicy` on next fetch when it changes between renders" const { user } = renderWithClient(, { client, wrapper: Profiler }); - await act(() => user.click(screen.getByText("Load query"))); - // initial render await Profiler.takeRender(); + await act(() => user.click(screen.getByText("Load query"))); + { const { snapshot } = await Profiler.takeRender(); @@ -2134,11 +2116,9 @@ it("applies updated `fetchPolicy` on next fetch when it changes between renders" } await act(() => user.click(screen.getByText("Change fetch policy"))); - await act(() => user.click(screen.getByText("Refetch"))); - - // change fetch policy await Profiler.takeRender(); - // refetch + + await act(() => user.click(screen.getByText("Refetch"))); await Profiler.takeRender(); { @@ -2210,11 +2190,11 @@ it("re-suspends when calling `refetch`", async () => { const { user } = renderWithMocks(, { mocks, wrapper: Profiler }); - await act(() => user.click(screen.getByText("Load query"))); - // initial render await Profiler.takeRender(); + await act(() => user.click(screen.getByText("Load query"))); + { const { renderedComponents } = await Profiler.takeRender(); @@ -2291,11 +2271,11 @@ it("re-suspends when calling `refetch` with new variables", async () => { const { user } = renderWithMocks(, { mocks, wrapper: Profiler }); - await act(() => user.click(screen.getByText("Load query"))); - // initial render await Profiler.takeRender(); + await act(() => user.click(screen.getByText("Load query"))); + { const { renderedComponents } = await Profiler.takeRender(); @@ -2379,8 +2359,15 @@ it("re-suspends multiple times when calling `refetch` multiple times", async () expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); } - // initial result - await Profiler.takeRender(); + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { character: { id: "1", name: "Spider-Man" } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } const button = screen.getByText("Refetch"); @@ -2392,8 +2379,15 @@ it("re-suspends multiple times when calling `refetch` multiple times", async () expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); } - // refetch result - await Profiler.takeRender(); + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { character: { id: "1", name: "Spider-Man" } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } await act(() => user.click(button)); @@ -2403,8 +2397,15 @@ it("re-suspends multiple times when calling `refetch` multiple times", async () expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); } - // refetch result - await Profiler.takeRender(); + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { character: { id: "1", name: "Spider-Man" } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } await expect(Profiler).not.toRerender(); }); @@ -2454,11 +2455,10 @@ it("throws errors when errors are returned after calling `refetch`", async () => const { user } = renderWithMocks(, { mocks, wrapper: Profiler }); - await act(() => user.click(screen.getByText("Load query"))); - // initial render await Profiler.takeRender(); - // load query + + await act(() => user.click(screen.getByText("Load query"))); await Profiler.takeRender(); { @@ -2472,8 +2472,6 @@ it("throws errors when errors are returned after calling `refetch`", async () => } await act(() => user.click(screen.getByText("Refetch"))); - - // Refetch await Profiler.takeRender(); { @@ -2535,8 +2533,6 @@ it('ignores errors returned after calling `refetch` when errorPolicy is set to " await Profiler.takeRender(); await act(() => user.click(screen.getByText("Load query"))); - - // load query await Profiler.takeRender(); { @@ -2550,8 +2546,6 @@ it('ignores errors returned after calling `refetch` when errorPolicy is set to " } await act(() => user.click(screen.getByText("Refetch"))); - - // refetch await Profiler.takeRender(); { @@ -2615,11 +2609,10 @@ it('returns errors after calling `refetch` when errorPolicy is set to "all"', as const { user } = renderWithMocks(, { mocks, wrapper: Profiler }); - await act(() => user.click(screen.getByText("Load query"))); - // initial render await Profiler.takeRender(); - // load query + + await act(() => user.click(screen.getByText("Load query"))); await Profiler.takeRender(); { @@ -2633,8 +2626,6 @@ it('returns errors after calling `refetch` when errorPolicy is set to "all"', as } await act(() => user.click(screen.getByText("Refetch"))); - - // refetch await Profiler.takeRender(); { @@ -2700,11 +2691,10 @@ it('handles partial data results after calling `refetch` when errorPolicy is set const { user } = renderWithMocks(, { mocks, wrapper: Profiler }); - await act(() => user.click(screen.getByText("Load query"))); - // initial render await Profiler.takeRender(); - // load query + + await act(() => user.click(screen.getByText("Load query"))); await Profiler.takeRender(); { @@ -2718,8 +2708,6 @@ it('handles partial data results after calling `refetch` when errorPolicy is set } await act(() => user.click(screen.getByText("Refetch"))); - - // refetch await Profiler.takeRender(); { @@ -2894,11 +2882,10 @@ it("re-suspends when calling `fetchMore` with different variables", async () => const { user } = renderWithClient(, { client, wrapper: Profiler }); - await act(() => user.click(screen.getByText("Load query"))); - // initial render await Profiler.takeRender(); - // load query + + await act(() => user.click(screen.getByText("Load query"))); await Profiler.takeRender(); { @@ -2976,11 +2963,10 @@ it("properly uses `updateQuery` when calling `fetchMore`", async () => { const { user } = renderWithClient(, { client, wrapper: Profiler }); - await act(() => user.click(screen.getByText("Load query"))); - // initial render await Profiler.takeRender(); - // load query + + await act(() => user.click(screen.getByText("Load query"))); await Profiler.takeRender(); { @@ -2999,8 +2985,6 @@ it("properly uses `updateQuery` when calling `fetchMore`", async () => { } await act(() => user.click(screen.getByText("Fetch more"))); - - // fetch more await Profiler.takeRender(); { @@ -3062,11 +3046,10 @@ it("properly uses cache field policies when calling `fetchMore` without `updateQ const { user } = renderWithClient(, { client, wrapper: Profiler }); - await act(() => user.click(screen.getByText("Load query"))); - // initial render await Profiler.takeRender(); - // load query + + await act(() => user.click(screen.getByText("Load query"))); await Profiler.takeRender(); { @@ -3085,8 +3068,6 @@ it("properly uses cache field policies when calling `fetchMore` without `updateQ } await act(() => user.click(screen.getByText("Fetch more"))); - - // fetch more await Profiler.takeRender(); { @@ -3346,11 +3327,10 @@ it('honors refetchWritePolicy set to "merge"', async () => { const { user } = renderWithClient(, { client, wrapper: Profiler }); - await act(() => user.click(screen.getByText("Load query"))); - // initial render await Profiler.takeRender(); - // load query + + await act(() => user.click(screen.getByText("Load query"))); await Profiler.takeRender(); { @@ -3365,8 +3345,6 @@ it('honors refetchWritePolicy set to "merge"', async () => { } await act(() => user.click(screen.getByText("Refetch"))); - - // refetch await Profiler.takeRender(); { @@ -3459,11 +3437,10 @@ it('defaults refetchWritePolicy to "overwrite"', async () => { const { user } = renderWithClient(, { client, wrapper: Profiler }); - await act(() => user.click(screen.getByText("Load query"))); - // initial load await Profiler.takeRender(); - // load query + + await act(() => user.click(screen.getByText("Load query"))); await Profiler.takeRender(); { @@ -3478,8 +3455,6 @@ it('defaults refetchWritePolicy to "overwrite"', async () => { } await act(() => user.click(screen.getByText("Refetch"))); - - // refetch await Profiler.takeRender(); { @@ -3561,11 +3536,11 @@ it('does not suspend when partial data is in the cache and using a "cache-first" const { user } = renderWithClient(, { client, wrapper: Profiler }); - await act(() => user.click(screen.getByText("Load query"))); - // initial load await Profiler.takeRender(); + await act(() => user.click(screen.getByText("Load query"))); + { const { snapshot, renderedComponents } = await Profiler.takeRender(); @@ -3640,11 +3615,11 @@ it('suspends and does not use partial data from other variables in the cache whe wrapper: Profiler, }); - await act(() => user.click(screen.getByText("Load query"))); - // initial render await Profiler.takeRender(); + await act(() => user.click(screen.getByText("Load query"))); + { const { snapshot, renderedComponents } = await Profiler.takeRender(); @@ -3757,11 +3732,11 @@ it('suspends when partial data is in the cache and using a "network-only" fetch wrapper: Profiler, }); - await act(() => user.click(screen.getByText("Load query"))); - // initial render await Profiler.takeRender(); + await act(() => user.click(screen.getByText("Load query"))); + { const { renderedComponents } = await Profiler.takeRender(); @@ -3848,11 +3823,11 @@ it('suspends when partial data is in the cache and using a "no-cache" fetch poli wrapper: Profiler, }); - await act(() => user.click(screen.getByText("Load query"))); - // initial load await Profiler.takeRender(); + await act(() => user.click(screen.getByText("Load query"))); + { const { renderedComponents } = await Profiler.takeRender(); @@ -3964,11 +3939,11 @@ it('does not suspend when partial data is in the cache and using a "cache-and-ne wrapper: Profiler, }); - await act(() => user.click(screen.getByText("Load query"))); - // initial render await Profiler.takeRender(); + await act(() => user.click(screen.getByText("Load query"))); + { const { snapshot, renderedComponents } = await Profiler.takeRender(); @@ -4038,11 +4013,11 @@ it('suspends and does not use partial data when changing variables and using a " wrapper: Profiler, }); - await act(() => user.click(screen.getByText("Load query"))); - // initial render await Profiler.takeRender(); + await act(() => user.click(screen.getByText("Load query"))); + { const { snapshot, renderedComponents } = await Profiler.takeRender(); @@ -4153,11 +4128,11 @@ it('does not suspend deferred queries with partial data in the cache and using a const { user } = renderWithClient(, { client, wrapper: Profiler }); - await act(() => user.click(screen.getByText("Load todo"))); - // initial render await Profiler.takeRender(); + await act(() => user.click(screen.getByText("Load todo"))); + { const { snapshot, renderedComponents } = await Profiler.takeRender(); From 814ae2876a61c8851924dd8014eb3681ee204c24 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 28 Nov 2023 16:21:07 -0700 Subject: [PATCH 197/199] Remove hardcoded size-limit in size-limit.cjs --- .size-limit.cjs | 1 - 1 file changed, 1 deletion(-) diff --git a/.size-limit.cjs b/.size-limit.cjs index e46cfd785cd..7c7b71da42f 100644 --- a/.size-limit.cjs +++ b/.size-limit.cjs @@ -3,7 +3,6 @@ const limits = require("./.size-limits.json"); const checks = [ { path: "dist/apollo-client.min.cjs", - limit: "38410", }, { path: "dist/main.cjs", From 3c8113d113c543d2bc4910e1ee8cd19fe4e1b077 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 28 Nov 2023 16:28:10 -0700 Subject: [PATCH 198/199] Fix syntax error in changeset --- .changeset/thirty-ties-arrive.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/thirty-ties-arrive.md b/.changeset/thirty-ties-arrive.md index cdab3695aff..c8a6fc22c86 100644 --- a/.changeset/thirty-ties-arrive.md +++ b/.changeset/thirty-ties-arrive.md @@ -10,7 +10,7 @@ function App() { return ( <> - + }> {queryRef && } From c20e3736336eae7a9d45811a4e076b905b91057c Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 28 Nov 2023 16:33:08 -0700 Subject: [PATCH 199/199] Remove unused generic arg on createProfiler --- src/testing/internal/profile/profile.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/testing/internal/profile/profile.tsx b/src/testing/internal/profile/profile.tsx index 1dc3928ade4..4b2717dc21d 100644 --- a/src/testing/internal/profile/profile.tsx +++ b/src/testing/internal/profile/profile.tsx @@ -136,10 +136,7 @@ export function profile({ } /** @internal */ -export function createProfiler< - Snapshot extends ValidSnapshot = void, - Props = {}, ->({ +export function createProfiler({ onRender, snapshotDOM = false, initialSnapshot,