Skip to content

Commit b62b438

Browse files
New Render-to-Render testing utilities based on React.Profiler (#11116)
Co-authored-by: Jerel Miller <[email protected]>
1 parent 35e6438 commit b62b438

File tree

8 files changed

+701
-0
lines changed

8 files changed

+701
-0
lines changed

src/testing/internal/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./profile/index.js";
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
/* istanbul ignore file */
2+
3+
/*
4+
Something in this file does not compile correctly while measuring code coverage
5+
and will lead to a
6+
Uncaught [ReferenceError: cov_1zb8w312au is not defined]
7+
if we do not ignore this file in code coverage.
8+
9+
As we only use this file in our internal tests, we can safely ignore it.
10+
*/
11+
12+
import { within, screen } from "@testing-library/dom";
13+
import { JSDOM, VirtualConsole } from "jsdom";
14+
import { applyStackTrace, captureStackTrace } from "./traces.js";
15+
16+
/** @internal */
17+
export interface BaseRender {
18+
id: string;
19+
phase: "mount" | "update" | "nested-update";
20+
actualDuration: number;
21+
baseDuration: number;
22+
startTime: number;
23+
commitTime: number;
24+
/**
25+
* The number of renders that have happened so far (including this render).
26+
*/
27+
count: number;
28+
}
29+
30+
type Screen = typeof screen;
31+
/** @internal */
32+
export type SyncScreen = {
33+
[K in keyof Screen]: K extends `find${string}`
34+
? {
35+
/** @deprecated A snapshot is static, so avoid async queries! */
36+
(...args: Parameters<Screen[K]>): ReturnType<Screen[K]>;
37+
}
38+
: Screen[K];
39+
};
40+
41+
/** @internal */
42+
export interface Render<Snapshot> extends BaseRender {
43+
/**
44+
* The snapshot, as returned by the `takeSnapshot` option of `profile`.
45+
* (If using `profileHook`, this is the return value of the hook.)
46+
*/
47+
snapshot: Snapshot;
48+
/**
49+
* A DOM snapshot of the rendered component, if the `snapshotDOM`
50+
* option of `profile` was enabled.
51+
*/
52+
readonly domSnapshot: HTMLElement;
53+
/**
54+
* Returns a callback to receive a `screen` instance that is scoped to the
55+
* DOM snapshot of this `Render` instance.
56+
* Note: this is used as a callback to prevent linter errors.
57+
* @example
58+
* ```diff
59+
* const { withinDOM } = RenderedComponent.takeRender();
60+
* -expect(screen.getByText("foo")).toBeInTheDocument();
61+
* +expect(withinDOM().getByText("foo")).toBeInTheDocument();
62+
* ```
63+
*/
64+
withinDOM: () => SyncScreen;
65+
}
66+
67+
/** @internal */
68+
export class RenderInstance<Snapshot> implements Render<Snapshot> {
69+
id: string;
70+
phase: "mount" | "update" | "nested-update";
71+
actualDuration: number;
72+
baseDuration: number;
73+
startTime: number;
74+
commitTime: number;
75+
count: number;
76+
77+
constructor(
78+
baseRender: BaseRender,
79+
public snapshot: Snapshot,
80+
private stringifiedDOM: string | undefined
81+
) {
82+
this.id = baseRender.id;
83+
this.phase = baseRender.phase;
84+
this.actualDuration = baseRender.actualDuration;
85+
this.baseDuration = baseRender.baseDuration;
86+
this.startTime = baseRender.startTime;
87+
this.commitTime = baseRender.commitTime;
88+
this.count = baseRender.count;
89+
}
90+
91+
private _domSnapshot: HTMLElement | undefined;
92+
get domSnapshot() {
93+
if (this._domSnapshot) return this._domSnapshot;
94+
if (!this.stringifiedDOM) {
95+
throw new Error(
96+
"DOM snapshot is not available - please set the `snapshotDOM` option"
97+
);
98+
}
99+
100+
const virtualConsole = new VirtualConsole();
101+
const stackTrace = captureStackTrace("RenderInstance.get");
102+
virtualConsole.on("jsdomError", (error) => {
103+
throw applyStackTrace(error, stackTrace);
104+
});
105+
106+
const snapDOM = new JSDOM(this.stringifiedDOM, {
107+
runScripts: "dangerously",
108+
virtualConsole,
109+
});
110+
const document = snapDOM.window.document;
111+
const body = document.body;
112+
const script = document.createElement("script");
113+
script.type = "text/javascript";
114+
script.text = `
115+
${errorOnDomInteraction.toString()};
116+
${errorOnDomInteraction.name}();
117+
`;
118+
body.appendChild(script);
119+
body.removeChild(script);
120+
121+
return (this._domSnapshot = body);
122+
}
123+
124+
get withinDOM() {
125+
const snapScreen = Object.assign(within(this.domSnapshot), {
126+
debug: (
127+
...[dom = this.domSnapshot, ...rest]: Parameters<typeof screen.debug>
128+
) => {
129+
screen.debug(dom, ...rest);
130+
},
131+
logTestingPlaygroundURL: (
132+
...[dom = this.domSnapshot, ...rest]: Parameters<
133+
typeof screen.logTestingPlaygroundURL
134+
>
135+
) => {
136+
screen.logTestingPlaygroundURL(dom, ...rest);
137+
},
138+
});
139+
return () => snapScreen;
140+
}
141+
}
142+
/** @internal */
143+
export function errorOnDomInteraction() {
144+
const events: Array<keyof DocumentEventMap> = [
145+
"auxclick",
146+
"blur",
147+
"change",
148+
"click",
149+
"copy",
150+
"cut",
151+
"dblclick",
152+
"drag",
153+
"dragend",
154+
"dragenter",
155+
"dragleave",
156+
"dragover",
157+
"dragstart",
158+
"drop",
159+
"focus",
160+
"focusin",
161+
"focusout",
162+
"input",
163+
"keydown",
164+
"keypress",
165+
"keyup",
166+
"mousedown",
167+
"mouseenter",
168+
"mouseleave",
169+
"mousemove",
170+
"mouseout",
171+
"mouseover",
172+
"mouseup",
173+
"paste",
174+
"pointercancel",
175+
"pointerdown",
176+
"pointerenter",
177+
"pointerleave",
178+
"pointermove",
179+
"pointerout",
180+
"pointerover",
181+
"pointerup",
182+
"scroll",
183+
"select",
184+
"selectionchange",
185+
"selectstart",
186+
"submit",
187+
"toggle",
188+
"touchcancel",
189+
"touchend",
190+
"touchmove",
191+
"touchstart",
192+
"wheel",
193+
];
194+
function warnOnDomInteraction() {
195+
throw new Error(`
196+
DOM interaction with a snapshot detected in test.
197+
Please don't interact with the DOM you get from \`withinDOM\`,
198+
but still use \`screen\' to get elements for simulating user interaction.
199+
`);
200+
}
201+
events.forEach((event) => {
202+
document.addEventListener(event, warnOnDomInteraction);
203+
});
204+
}

src/testing/internal/profile/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export type {
2+
NextRenderOptions,
3+
ProfiledComponent,
4+
ProfiledHook,
5+
} from "./profile.js";
6+
export { profile, profileHook, WaitForRenderTimeoutError } from "./profile.js";
7+
8+
export type { SyncScreen } from "./Render.js";

0 commit comments

Comments
 (0)