Skip to content

Commit

Permalink
drop profiler, expose renderTo methods
Browse files Browse the repository at this point in the history
  • Loading branch information
phryneas committed Oct 8, 2024
1 parent a59c1ab commit 9b6fdee
Show file tree
Hide file tree
Showing 11 changed files with 245 additions and 178 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
],
"dependencies": {
"@testing-library/dom": "^10.4.0",
"@testing-library/react": "^16.0.1",
"jsdom": "^25.0.1",
"rehackt": "^0.1.0"
},
Expand Down
11 changes: 11 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export type { NextRenderOptions, RenderStream } from "./profile/profile.js";
export {
createProfiler,
useTrackRenders,
WaitForRenderTimeoutError,
} from "./profile/profile.js";

export type { SyncScreen } from "./profile/Render.js";

export { renderToRenderStream } from "./renderToRenderStream.js";
export { renderHookToSnapshotStream } from "./renderHookToSnapshotStream.js";
24 changes: 11 additions & 13 deletions src/jest/ProfiledComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,16 @@ import type { MatcherFunction } from "expect";
import { WaitForRenderTimeoutError } from "@testing-library/react-render-stream";
import type {
NextRenderOptions,
Profiler,
ProfiledComponent,
ProfiledHook,
RenderStream,
} from "@testing-library/react-render-stream";

export const toRerender: MatcherFunction<[options?: NextRenderOptions]> =
async function (actual, options) {
const _profiler = actual as
| Profiler<any>
| ProfiledComponent<any, any>
| ProfiledHook<any, any>;
const profiler = "Profiler" in _profiler ? _profiler.Profiler : _profiler;
const _profiler = actual as RenderStream<any>;
const profiler =
"Profiler" in _profiler
? (_profiler.Profiler as RenderStream<any>)
: _profiler;
const hint = this.utils.matcherHint("toRerender", "ProfiledComponent", "");
let pass = true;
try {
Expand Down Expand Up @@ -44,11 +42,11 @@ const failed = {};
export const toRenderExactlyTimes: MatcherFunction<
[times: number, options?: NextRenderOptions]
> = async function (actual, times, optionsPerRender) {
const _profiler = actual as
| Profiler<any>
| ProfiledComponent<any, any>
| ProfiledHook<any, any>;
const profiler = "Profiler" in _profiler ? _profiler.Profiler : _profiler;
const _profiler = actual as RenderStream<any>;
const profiler =
"Profiler" in _profiler
? (_profiler.Profiler as RenderStream<any>)
: _profiler;
const options = { timeout: 100, ...optionsPerRender };
const hint = this.utils.matcherHint("toRenderExactlyTimes");
let pass = true;
Expand Down
17 changes: 3 additions & 14 deletions src/jest/index.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,17 @@
import { expect } from "@jest/globals";
import { toRerender, toRenderExactlyTimes } from "./ProfiledComponent.js";
import type {
NextRenderOptions,
Profiler,
ProfiledComponent,
ProfiledHook,
} from "../profile/index.js";
import type { NextRenderOptions, RenderStream } from "../index.js";

expect.extend({
toRerender,
toRenderExactlyTimes,
});
interface ApolloCustomMatchers<R = void, T = {}> {
toRerender: T extends
| Profiler<any>
| ProfiledComponent<any, any>
| ProfiledHook<any, any>
toRerender: T extends RenderStream<any> | unknown // TODO
? (options?: NextRenderOptions) => Promise<R>
: { error: "matcher needs to be called on a ProfiledComponent instance" };

toRenderExactlyTimes: T extends
| Profiler<any>
| ProfiledComponent<any, any>
| ProfiledHook<any, any>
toRenderExactlyTimes: T extends RenderStream<any> | unknown // TODO
? (count: number, options?: NextRenderOptions) => Promise<R>
: { error: "matcher needs to be called on a ProfiledComponent instance" };
}
Expand Down
15 changes: 0 additions & 15 deletions src/profile/index.ts

This file was deleted.

168 changes: 34 additions & 134 deletions src/profile/profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ import { applyStackTrace, captureStackTrace } from "./traces.js";
import type { ProfilerContextValue } from "./context.js";
import { ProfilerContextProvider, useProfilerContext } from "./context.js";
import { disableActWarnings } from "./disableActWarnings.js";
import { render } from "@testing-library/react";

type ValidSnapshot = void | (object & { /* not a function */ call?: never });
export type ValidSnapshot =
| void
| (object & { /* not a function */ call?: never });

/** only used for passing around data internally */
const _stackTrace = Symbol();
Expand All @@ -17,17 +20,6 @@ export interface NextRenderOptions {
[_stackTrace]?: string;
}

/** @internal */
interface ProfilerProps {
children: React.ReactNode;
}

/** @internal */
export interface Profiler<Snapshot>
extends React.FC<ProfilerProps>,
ProfiledComponentFields<Snapshot>,
ProfiledComponentOnlyFields<Snapshot> {}

interface ReplaceSnapshot<Snapshot> {
(newSnapshot: Snapshot): void;
(updateSnapshot: (lastSnapshot: Readonly<Snapshot>) => Snapshot): void;
Expand All @@ -42,13 +34,13 @@ interface MergeSnapshot<Snapshot> {
): void;
}

interface ProfiledComponentOnlyFields<Snapshot> {
export interface ProfiledComponentOnlyFields<Snapshot> {
// Allows for partial updating of the snapshot by shallow merging the results
mergeSnapshot: MergeSnapshot<Snapshot>;
// Performs a full replacement of the snapshot
replaceSnapshot: ReplaceSnapshot<Snapshot>;
}
interface ProfiledComponentFields<Snapshot> {
export interface ProfiledComponentFields<Snapshot> {
/**
* An array of all renders that have happened so far.
* Errors thrown during component render will be captured here, too.
Expand Down Expand Up @@ -84,50 +76,16 @@ interface ProfiledComponentFields<Snapshot> {
waitForNextRender(options?: NextRenderOptions): Promise<Render<Snapshot>>;
}

export interface ProfiledComponent<Snapshot extends ValidSnapshot, Props = {}>
extends React.FC<Props>,
ProfiledComponentFields<Snapshot>,
export interface RenderStream<Snapshot extends ValidSnapshot>
extends ProfiledComponentFields<Snapshot>,
ProfiledComponentOnlyFields<Snapshot> {}

/** @internal */
export function profile<Snapshot extends ValidSnapshot = void, Props = {}>({
Component,
...options
}: Parameters<typeof createProfiler<Snapshot>>[0] & {
Component: React.ComponentType<Props>;
}): ProfiledComponent<Snapshot, Props> {
const Profiler = createProfiler(options);

return Object.assign(
function ProfiledComponent(props: Props) {
return (
<Profiler>
<Component {...(props as any)} />
</Profiler>
);
},
{
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;
},
}
);
export interface RenderStreamWithWrapper<Snapshot extends ValidSnapshot>
extends RenderStream<Snapshot> {
Wrapper: React.FC<{ children: React.ReactNode }>;
}

/** @internal */
export function createProfiler<Snapshot extends ValidSnapshot = void>({
onRender,
snapshotDOM = false,
initialSnapshot,
skipNonTrackingRenders,
}: {
export type ProfilerOptions<Snapshot extends ValidSnapshot> = {
onRender?: (
info: BaseRender & {
snapshot: Snapshot;
Expand All @@ -142,7 +100,15 @@ export function createProfiler<Snapshot extends ValidSnapshot = void>({
* `useTrackRenders` occured.
*/
skipNonTrackingRenders?: boolean;
} = {}) {
};

/** @internal */
export function createProfiler<Snapshot extends ValidSnapshot = void>({
onRender,
snapshotDOM = false,
initialSnapshot,
skipNonTrackingRenders,
}: ProfilerOptions<Snapshot> = {}): RenderStreamWithWrapper<Snapshot> {
let nextRender: Promise<Render<Snapshot>> | undefined;
let resolveNextRender: ((render: Render<Snapshot>) => void) | undefined;
let rejectNextRender: ((error: unknown) => void) | undefined;
Expand Down Expand Up @@ -245,16 +211,17 @@ export function createProfiler<Snapshot extends ValidSnapshot = void>({
};

let iteratorPosition = 0;
const Profiler: Profiler<Snapshot> = Object.assign(
({ children }: ProfilerProps) => {
return (
<ProfilerContextProvider value={profilerContext}>
<React.Profiler id="test" onRender={profilerOnRender}>
{children}
</React.Profiler>
</ProfilerContextProvider>
);
},
function Wrapper({ children }: { children: React.ReactNode }) {
return (
<ProfilerContextProvider value={profilerContext}>
<React.Profiler id="test" onRender={profilerOnRender}>
{children}
</React.Profiler>
</ProfilerContextProvider>
);
}

const Profiler: RenderStreamWithWrapper<Snapshot> = Object.assign(
{
replaceSnapshot,
mergeSnapshot,
Expand Down Expand Up @@ -350,7 +317,8 @@ export function createProfiler<Snapshot extends ValidSnapshot = void>({
}
return nextRender;
},
} satisfies ProfiledComponentFields<Snapshot>
} satisfies ProfiledComponentFields<Snapshot>,
{ Wrapper }
);
return Profiler;
}
Expand All @@ -363,74 +331,6 @@ export class WaitForRenderTimeoutError extends Error {
}
}

type StringReplaceRenderWithSnapshot<T extends string> =
T extends `${infer Pre}Render${infer Post}` ? `${Pre}Snapshot${Post}` : T;

type ResultReplaceRenderWithSnapshot<T> = T extends (
...args: infer Args
) => Render<infer Snapshot>
? (...args: Args) => Snapshot
: T extends (...args: infer Args) => Promise<Render<infer Snapshot>>
? (...args: Args) => Promise<Snapshot>
: T;

type ProfiledHookFields<ReturnValue> =
ProfiledComponentFields<ReturnValue> extends infer PC
? {
[K in keyof PC as StringReplaceRenderWithSnapshot<
K & string
>]: ResultReplaceRenderWithSnapshot<PC[K]>;
}
: never;

/** @internal */
export interface ProfiledHook<Props, ReturnValue>
extends React.FC<Props>,
ProfiledHookFields<ReturnValue> {
Profiler: Profiler<ReturnValue>;
}

/** @internal */
export function profileHook<ReturnValue extends ValidSnapshot, Props>(
renderCallback: (props: Props) => ReturnValue
): ProfiledHook<Props, ReturnValue> {
const Profiler = createProfiler<ReturnValue>();

const ProfiledHook = (props: Props) => {
Profiler.replaceSnapshot(renderCallback(props));
return null;
};

return Object.assign(
function App(props: Props) {
return (
<Profiler>
<ProfiledHook {...(props as any)} />
</Profiler>
);
},
{
Profiler,
},
{
renders: Profiler.renders,
totalSnapshotCount: Profiler.totalRenderCount,
async peekSnapshot(options) {
return (await Profiler.peekRender(options)).snapshot;
},
async takeSnapshot(options) {
return (await Profiler.takeRender(options)).snapshot;
},
getCurrentSnapshot() {
return Profiler.getCurrentRender().snapshot;
},
async waitForNextSnapshot(options) {
return (await Profiler.waitForNextRender(options)).snapshot;
},
} satisfies ProfiledHookFields<ReturnValue>
);
}

function resolveR18HookOwner(): React.ComponentType | undefined {
return (React as any).__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
?.ReactCurrentOwner?.current?.elementType;
Expand Down
Loading

0 comments on commit 9b6fdee

Please sign in to comment.