From 562c3bb6e17da001c958b0e3ae3cc644a2550888 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 21 Nov 2023 18:31:19 -0700 Subject: [PATCH 01/31] 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 02/31] 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 03/31] 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 04/31] 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 05/31] 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 06/31] 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 07/31] 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 08/31] 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 09/31] 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 10/31] 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 11/31] 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 12/31] 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 13/31] 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 14/31] 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 15/31] 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 16/31] 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 17/31] 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 18/31] 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 19/31] 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 20/31] 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 21/31] 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 22/31] 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 23/31] 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 24/31] 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 25/31] 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 26/31] 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 27/31] 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 28/31] 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 29/31] 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 30/31] 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 31/31] 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, });