main
js 378 lines 10.7 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8 import * as SchedulerMock from 'scheduler/unstable_mock';
9 import {diff} from 'jest-diff';
10 import {equals} from '@jest/expect-utils';
11 import enqueueTask from './enqueueTask';
12 import simulateBrowserEventDispatch from './simulateBrowserEventDispatch';
13 import {
14 clearLogs,
15 clearWarnings,
16 clearErrors,
17 createLogAssertion,
18 } from './consoleMock';
19 export {getDebugInfo} from './debugInfo';
20 export {act, serverAct} from './internalAct';
21 const {assertConsoleLogsCleared} = require('internal-test-utils/consoleMock');
22
23 import {thrownErrors, actingUpdatesScopeDepth} from './internalAct';
24
25 function assertYieldsWereCleared(caller) {
26 const actualYields = SchedulerMock.unstable_clearLog();
27 if (actualYields.length !== 0) {
28 const error = Error(
29 'The event log is not empty. Call assertLog(...) first.',
30 );
31 Error.captureStackTrace(error, caller);
32 throw error;
33 }
34 assertConsoleLogsCleared();
35 }
36
37 export async function waitForMicrotasks() {
38 return new Promise(resolve => {
39 enqueueTask(() => resolve());
40 });
41 }
42
43 export async function waitFor(expectedLog, options) {
44 assertYieldsWereCleared(waitFor);
45
46 // Create the error object before doing any async work, to get a better
47 // stack trace.
48 const error = new Error();
49 Error.captureStackTrace(error, waitFor);
50
51 const stopAfter = expectedLog.length;
52 const actualLog = [];
53 do {
54 // Wait until end of current task/microtask.
55 await waitForMicrotasks();
56 if (SchedulerMock.unstable_hasPendingWork()) {
57 SchedulerMock.unstable_flushNumberOfYields(stopAfter - actualLog.length);
58 actualLog.push(...SchedulerMock.unstable_clearLog());
59 if (stopAfter > actualLog.length) {
60 // Continue flushing until we've logged the expected number of items.
61 } else {
62 // Once we've reached the expected sequence, wait one more microtask to
63 // flush any remaining synchronous work.
64 await waitForMicrotasks();
65 actualLog.push(...SchedulerMock.unstable_clearLog());
66 break;
67 }
68 } else {
69 // There's no pending work, even after a microtask.
70 break;
71 }
72 } while (true);
73
74 if (options && options.additionalLogsAfterAttemptingToYield) {
75 expectedLog = expectedLog.concat(
76 options.additionalLogsAfterAttemptingToYield,
77 );
78 }
79
80 if (equals(actualLog, expectedLog)) {
81 return;
82 }
83
84 error.message = `
85 Expected sequence of events did not occur.
86
87 ${diff(expectedLog, actualLog)}
88 `;
89 throw error;
90 }
91
92 export async function waitForAll(expectedLog) {
93 assertYieldsWereCleared(waitForAll);
94
95 // Create the error object before doing any async work, to get a better
96 // stack trace.
97 const error = new Error();
98 Error.captureStackTrace(error, waitForAll);
99
100 do {
101 // Wait until end of current task/microtask.
102 await waitForMicrotasks();
103 if (!SchedulerMock.unstable_hasPendingWork()) {
104 // There's no pending work, even after a microtask. Stop flushing.
105 break;
106 }
107 SchedulerMock.unstable_flushAllWithoutAsserting();
108 } while (true);
109
110 const actualLog = SchedulerMock.unstable_clearLog();
111 if (equals(actualLog, expectedLog)) {
112 return;
113 }
114
115 error.message = `
116 Expected sequence of events did not occur.
117
118 ${diff(expectedLog, actualLog)}
119 `;
120 throw error;
121 }
122
123 function aggregateErrors(errors: Array<mixed>): mixed {
124 if (errors.length > 1 && typeof AggregateError === 'function') {
125 return new AggregateError(errors);
126 }
127 return errors[0];
128 }
129
130 export async function waitForThrow(expectedError: mixed): mixed {
131 assertYieldsWereCleared(waitForThrow);
132
133 // Create the error object before doing any async work, to get a better
134 // stack trace.
135 const error = new Error();
136 Error.captureStackTrace(error, waitForThrow);
137
138 do {
139 // Wait until end of current task/microtask.
140 await waitForMicrotasks();
141 if (!SchedulerMock.unstable_hasPendingWork()) {
142 // There's no pending work, even after a microtask. Stop flushing.
143 error.message = 'Expected something to throw, but nothing did.';
144 throw error;
145 }
146
147 const errorHandlerDOM = function (event: ErrorEvent) {
148 // Prevent logs from reprinting this error.
149 event.preventDefault();
150 thrownErrors.push(event.error);
151 };
152 const errorHandlerNode = function (err: mixed) {
153 thrownErrors.push(err);
154 };
155 // We track errors that were logged globally as if they occurred in this scope and then rethrow them.
156 if (actingUpdatesScopeDepth === 0) {
157 if (
158 typeof window === 'object' &&
159 typeof window.addEventListener === 'function'
160 ) {
161 // We're in a JS DOM environment.
162 window.addEventListener('error', errorHandlerDOM);
163 } else if (typeof process === 'object') {
164 // Node environment
165 process.on('uncaughtException', errorHandlerNode);
166 }
167 }
168 try {
169 SchedulerMock.unstable_flushAllWithoutAsserting();
170 } catch (x) {
171 thrownErrors.push(x);
172 } finally {
173 if (actingUpdatesScopeDepth === 0) {
174 if (
175 typeof window === 'object' &&
176 typeof window.addEventListener === 'function'
177 ) {
178 // We're in a JS DOM environment.
179 window.removeEventListener('error', errorHandlerDOM);
180 } else if (typeof process === 'object') {
181 // Node environment
182 process.off('uncaughtException', errorHandlerNode);
183 }
184 }
185 }
186 if (thrownErrors.length > 0) {
187 const thrownError = aggregateErrors(thrownErrors);
188 thrownErrors.length = 0;
189
190 if (expectedError === undefined) {
191 // If no expected error was provided, then assume the caller is OK with
192 // any error being thrown. We're returning the error so they can do
193 // their own checks, if they wish.
194 return thrownError;
195 }
196 if (equals(thrownError, expectedError)) {
197 return thrownError;
198 }
199 if (
200 typeof expectedError === 'string' &&
201 typeof thrownError === 'object' &&
202 thrownError !== null &&
203 typeof thrownError.message === 'string'
204 ) {
205 if (thrownError.message.includes(expectedError)) {
206 return thrownError;
207 } else {
208 error.message = `
209 Expected error was not thrown.
210
211 ${diff(expectedError, thrownError.message)}
212 `;
213 throw error;
214 }
215 }
216 error.message = `
217 Expected error was not thrown.
218
219 ${diff(expectedError, thrownError)}
220 `;
221 throw error;
222 }
223 } while (true);
224 }
225
226 // This is prefixed with `unstable_` because you should almost always try to
227 // avoid using it in tests. It's really only for testing a particular
228 // implementation detail (update starvation prevention).
229 export async function unstable_waitForExpired(expectedLog): mixed {
230 assertYieldsWereCleared(unstable_waitForExpired);
231
232 // Create the error object before doing any async work, to get a better
233 // stack trace.
234 const error = new Error();
235 Error.captureStackTrace(error, unstable_waitForExpired);
236
237 // Wait until end of current task/microtask.
238 await waitForMicrotasks();
239 SchedulerMock.unstable_flushExpired();
240
241 const actualLog = SchedulerMock.unstable_clearLog();
242 if (equals(actualLog, expectedLog)) {
243 return;
244 }
245
246 error.message = `
247 Expected sequence of events did not occur.
248
249 ${diff(expectedLog, actualLog)}
250 `;
251 throw error;
252 }
253
254 // TODO: This name is a bit misleading currently because it will stop as soon as
255 // React yields for any reason, not just for a paint. I've left it this way for
256 // now because that's how untable_flushUntilNextPaint already worked, but maybe
257 // we should split these use cases into separate APIs.
258 export async function waitForPaint(expectedLog) {
259 assertYieldsWereCleared(waitForPaint);
260
261 // Create the error object before doing any async work, to get a better
262 // stack trace.
263 const error = new Error();
264 Error.captureStackTrace(error, waitForPaint);
265
266 // Wait until end of current task/microtask.
267 await waitForMicrotasks();
268 if (SchedulerMock.unstable_hasPendingWork()) {
269 // Flush until React yields.
270 SchedulerMock.unstable_flushUntilNextPaint();
271 // Wait one more microtask to flush any remaining synchronous work.
272 await waitForMicrotasks();
273 }
274
275 const actualLog = SchedulerMock.unstable_clearLog();
276 if (equals(actualLog, expectedLog)) {
277 return;
278 }
279
280 error.message = `
281 Expected sequence of events did not occur.
282
283 ${diff(expectedLog, actualLog)}
284 `;
285 throw error;
286 }
287
288 export async function waitForDiscrete(expectedLog) {
289 assertYieldsWereCleared(waitForDiscrete);
290
291 // Create the error object before doing any async work, to get a better
292 // stack trace.
293 const error = new Error();
294 Error.captureStackTrace(error, waitForDiscrete);
295
296 // Wait until end of current task/microtask.
297 await waitForMicrotasks();
298
299 const actualLog = SchedulerMock.unstable_clearLog();
300 if (equals(actualLog, expectedLog)) {
301 return;
302 }
303
304 error.message = `
305 Expected sequence of events did not occur.
306
307 ${diff(expectedLog, actualLog)}
308 `;
309 throw error;
310 }
311
312 export function assertLog(expectedLog) {
313 const actualLog = SchedulerMock.unstable_clearLog();
314 if (equals(actualLog, expectedLog)) {
315 return;
316 }
317
318 const error = new Error(`
319 Expected sequence of events did not occur.
320
321 ${diff(expectedLog, actualLog)}
322 `);
323 Error.captureStackTrace(error, assertLog);
324 throw error;
325 }
326
327 export const assertConsoleLogDev = createLogAssertion(
328 'log',
329 'assertConsoleLogDev',
330 clearLogs,
331 );
332 export const assertConsoleWarnDev = createLogAssertion(
333 'warn',
334 'assertConsoleWarnDev',
335 clearWarnings,
336 );
337 export const assertConsoleErrorDev = createLogAssertion(
338 'error',
339 'assertConsoleErrorDev',
340 clearErrors,
341 );
342
343 // Simulates dispatching events, waiting for microtasks in between.
344 // This matches the browser behavior, which will flush microtasks
345 // between each event handler. This will allow discrete events to
346 // flush between events across different event handlers.
347 export async function simulateEventDispatch(
348 node: Node,
349 eventType: string,
350 ): Promise<void> {
351 // Ensure the node is in the document.
352 for (let current = node; current; current = current.parentNode) {
353 if (current === document) {
354 break;
355 } else if (current.parentNode == null) {
356 return;
357 }
358 }
359
360 const customEvent = new Event(eventType, {
361 bubbles: true,
362 });
363
364 Object.defineProperty(customEvent, 'target', {
365 // Override the target to the node on which we dispatched the event.
366 value: node,
367 });
368
369 const impl = Object.getOwnPropertySymbols(node)[0];
370 const oldDispatch = node[impl].dispatchEvent;
371 try {
372 node[impl].dispatchEvent = simulateBrowserEventDispatch;
373
374 await node.dispatchEvent(customEvent);
375 } finally {
376 node[impl].dispatchEvent = oldDispatch;
377 }
378 }