Skip to content

Commit 83661c3

Browse files
authored
Improvements to profiler testing utility (#11376)
1 parent 5db567e commit 83661c3

File tree

2 files changed

+62
-40
lines changed

2 files changed

+62
-40
lines changed

src/testing/internal/profile/profile.tsx

Lines changed: 59 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,27 @@ export interface NextRenderOptions {
2323
export interface ProfiledComponent<Props, Snapshot>
2424
extends React.FC<Props>,
2525
ProfiledComponentFields<Props, Snapshot>,
26-
ProfiledComponenOnlyFields<Props, Snapshot> {}
26+
ProfiledComponentOnlyFields<Props, Snapshot> {}
2727

28-
interface UpdateSnapshot<Snapshot> {
28+
interface ReplaceSnapshot<Snapshot> {
2929
(newSnapshot: Snapshot): void;
3030
(updateSnapshot: (lastSnapshot: Readonly<Snapshot>) => Snapshot): void;
3131
}
3232

33-
interface ProfiledComponenOnlyFields<Props, Snapshot> {
34-
updateSnapshot: UpdateSnapshot<Snapshot>;
33+
interface MergeSnapshot<Snapshot> {
34+
(partialSnapshot: Partial<Snapshot>): void;
35+
(
36+
updatePartialSnapshot: (
37+
lastSnapshot: Readonly<Snapshot>
38+
) => Partial<Snapshot>
39+
): void;
40+
}
41+
42+
interface ProfiledComponentOnlyFields<Props, Snapshot> {
43+
// Allows for partial updating of the snapshot by shallow merging the results
44+
mergeSnapshot: MergeSnapshot<Snapshot>;
45+
// Performs a full replacement of the snapshot
46+
replaceSnapshot: ReplaceSnapshot<Snapshot>;
3547
}
3648
interface ProfiledComponentFields<Props, Snapshot> {
3749
/**
@@ -54,21 +66,14 @@ interface ProfiledComponentFields<Props, Snapshot> {
5466
*/
5567
takeRender(options?: NextRenderOptions): Promise<Render<Snapshot>>;
5668
/**
57-
* Returns the current render count.
69+
* Returns the total number of renders.
5870
*/
59-
currentRenderCount(): number;
71+
totalRenderCount(): number;
6072
/**
6173
* Returns the current render.
6274
* @throws {Error} if no render has happened yet
6375
*/
6476
getCurrentRender(): Render<Snapshot>;
65-
/**
66-
* Iterates the renders until the render count is reached.
67-
*/
68-
takeUntilRenderCount(
69-
count: number,
70-
optionsPerRender?: NextRenderOptions
71-
): Promise<void>;
7277
/**
7378
* Waits for the next render to happen.
7479
* Does not advance the render iterator.
@@ -90,18 +95,18 @@ export function profile<
9095
onRender?: (
9196
info: BaseRender & {
9297
snapshot: Snapshot;
93-
updateSnapshot: UpdateSnapshot<Snapshot>;
98+
replaceSnapshot: ReplaceSnapshot<Snapshot>;
99+
mergeSnapshot: MergeSnapshot<Snapshot>;
94100
}
95101
) => void;
96102
snapshotDOM?: boolean;
97103
initialSnapshot?: Snapshot;
98104
}) {
99-
let currentRender: Render<Snapshot> | undefined;
100105
let nextRender: Promise<Render<Snapshot>> | undefined;
101106
let resolveNextRender: ((render: Render<Snapshot>) => void) | undefined;
102107
let rejectNextRender: ((error: unknown) => void) | undefined;
103108
const snapshotRef = { current: initialSnapshot };
104-
const updateSnapshot: UpdateSnapshot<Snapshot> = (snap) => {
109+
const replaceSnapshot: ReplaceSnapshot<Snapshot> = (snap) => {
105110
if (typeof snap === "function") {
106111
if (!initialSnapshot) {
107112
throw new Error(
@@ -118,6 +123,16 @@ export function profile<
118123
snapshotRef.current = snap;
119124
}
120125
};
126+
127+
const mergeSnapshot: MergeSnapshot<Snapshot> = (partialSnapshot) => {
128+
replaceSnapshot((snapshot) => ({
129+
...snapshot,
130+
...(typeof partialSnapshot === "function"
131+
? partialSnapshot(snapshot)
132+
: partialSnapshot),
133+
}));
134+
};
135+
121136
const profilerOnRender: React.ProfilerOnRenderCallback = (
122137
id,
123138
phase,
@@ -145,7 +160,8 @@ export function profile<
145160
*/
146161
onRender?.({
147162
...baseRender,
148-
updateSnapshot,
163+
replaceSnapshot,
164+
mergeSnapshot,
149165
snapshot: snapshotRef.current!,
150166
});
151167

@@ -154,8 +170,6 @@ export function profile<
154170
? window.document.body.innerHTML
155171
: undefined;
156172
const render = new RenderInstance(baseRender, snapshot, domSnapshot);
157-
// eslint-disable-next-line testing-library/render-result-naming-convention
158-
currentRender = render;
159173
Profiled.renders.push(render);
160174
resolveNextRender?.(render);
161175
} catch (error) {
@@ -178,29 +192,31 @@ export function profile<
178192
</React.Profiler>
179193
),
180194
{
181-
updateSnapshot,
182-
} satisfies ProfiledComponenOnlyFields<Props, Snapshot>,
195+
replaceSnapshot,
196+
mergeSnapshot,
197+
} satisfies ProfiledComponentOnlyFields<Props, Snapshot>,
183198
{
184199
renders: new Array<
185200
| Render<Snapshot>
186201
| { phase: "snapshotError"; count: number; error: unknown }
187202
>(),
188-
currentRenderCount() {
203+
totalRenderCount() {
189204
return Profiled.renders.length;
190205
},
191206
async peekRender(options: NextRenderOptions = {}) {
192207
if (iteratorPosition < Profiled.renders.length) {
193208
const render = Profiled.renders[iteratorPosition];
209+
194210
if (render.phase === "snapshotError") {
195211
throw render.error;
196212
}
213+
197214
return render;
198215
}
199-
const render = Profiled.waitForNextRender({
216+
return Profiled.waitForNextRender({
200217
[_stackTrace]: captureStackTrace(Profiled.peekRender),
201218
...options,
202219
});
203-
return render;
204220
},
205221
async takeRender(options: NextRenderOptions = {}) {
206222
let error: unknown = undefined;
@@ -219,18 +235,25 @@ export function profile<
219235
}
220236
},
221237
getCurrentRender() {
222-
if (!currentRender) {
223-
throw new Error("Has not been rendered yet!");
238+
// The "current" render should point at the same render that the most
239+
// recent `takeRender` call returned, so we need to get the "previous"
240+
// iterator position, otherwise `takeRender` advances the iterator
241+
// to the next render. This means we need to call `takeRender` at least
242+
// once before we can get a current render.
243+
const currentPosition = iteratorPosition - 1;
244+
245+
if (currentPosition < 0) {
246+
throw new Error(
247+
"No current render available. You need to call `takeRender` before you can get the current render."
248+
);
224249
}
225-
return currentRender;
226-
},
227-
async takeUntilRenderCount(
228-
count: number,
229-
optionsPerRender?: NextRenderOptions
230-
) {
231-
while (Profiled.renders.length < count) {
232-
await Profiled.takeRender(optionsPerRender);
250+
251+
const render = Profiled.renders[currentPosition];
252+
253+
if (render.phase === "snapshotError") {
254+
throw render.error;
233255
}
256+
return render;
234257
},
235258
waitForNextRender({
236259
timeout = 1000,
@@ -306,7 +329,7 @@ export function profileHook<ReturnValue extends ValidSnapshot, Props>(
306329
): ProfiledHook<Props, ReturnValue> {
307330
let returnValue: ReturnValue;
308331
const Component = (props: Props) => {
309-
ProfiledComponent.updateSnapshot(renderCallback(props));
332+
ProfiledComponent.replaceSnapshot(renderCallback(props));
310333
return null;
311334
};
312335
const ProfiledComponent = profile<ReturnValue, Props>({
@@ -322,7 +345,7 @@ export function profileHook<ReturnValue extends ValidSnapshot, Props>(
322345
},
323346
{
324347
renders: ProfiledComponent.renders,
325-
currentSnapshotCount: ProfiledComponent.currentRenderCount,
348+
totalSnapshotCount: ProfiledComponent.totalRenderCount,
326349
async peekSnapshot(options) {
327350
return (await ProfiledComponent.peekRender(options)).snapshot;
328351
},
@@ -332,7 +355,6 @@ export function profileHook<ReturnValue extends ValidSnapshot, Props>(
332355
getCurrentSnapshot() {
333356
return ProfiledComponent.getCurrentRender().snapshot;
334357
},
335-
takeUntilSnapshotCount: ProfiledComponent.takeUntilRenderCount,
336358
async waitForNextSnapshot(options) {
337359
return (await ProfiledComponent.waitForNextRender(options)).snapshot;
338360
},

src/testing/matchers/ProfiledComponent.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,11 @@ export const toRenderExactlyTimes: MatcherFunction<
5252
const hint = this.utils.matcherHint("toRenderExactlyTimes");
5353
let pass = true;
5454
try {
55-
if (profiled.currentRenderCount() > times) {
55+
if (profiled.totalRenderCount() > times) {
5656
throw failed;
5757
}
5858
try {
59-
while (profiled.currentRenderCount() < times) {
59+
while (profiled.totalRenderCount() < times) {
6060
await profiled.waitForNextRender(options);
6161
}
6262
} catch (e) {
@@ -84,7 +84,7 @@ export const toRenderExactlyTimes: MatcherFunction<
8484
return (
8585
hint +
8686
` Expected component to${pass ? " not" : ""} render exactly ${times}.` +
87-
` It rendered ${profiled.currentRenderCount()} times.`
87+
` It rendered ${profiled.totalRenderCount()} times.`
8888
);
8989
},
9090
};

0 commit comments

Comments
 (0)