main
js 337 lines 11.2 KB
Raw
1 'use strict';
2
3 const {getTestFlags} = require('./TestFlags');
4 const {
5 assertConsoleLogsCleared,
6 resetAllUnexpectedConsoleCalls,
7 patchConsoleMethods,
8 } = require('internal-test-utils/consoleMock');
9 const path = require('path');
10
11 if (process.env.REACT_CLASS_EQUIVALENCE_TEST) {
12 // Inside the class equivalence tester, we have a custom environment, let's
13 // require that instead.
14 require('./spec-equivalence-reporter/setupTests.js');
15 } else {
16 const errorMap = require('../error-codes/codes.json');
17
18 // By default, jest.spyOn also calls the spied method.
19 const spyOn = jest.spyOn;
20 const noop = jest.fn;
21
22 // Can be used to normalize paths in stackframes
23 global.__REACT_ROOT_PATH_TEST__ = path.resolve(__dirname, '../..');
24
25 // Spying on console methods in production builds can mask errors.
26 // This is why we added an explicit spyOnDev() helper.
27 // It's too easy to accidentally use the more familiar spyOn() helper though,
28 // So we disable it entirely.
29 // Spying on both dev and prod will require using both spyOnDev() and spyOnProd().
30 global.spyOn = function () {
31 throw new Error(
32 'Do not use spyOn(). ' +
33 'It can accidentally hide unexpected errors in production builds. ' +
34 'Use spyOnDev(), spyOnProd(), or spyOnDevAndProd() instead.'
35 );
36 };
37
38 if (process.env.NODE_ENV === 'production') {
39 global.spyOnDev = noop;
40 global.spyOnProd = spyOn;
41 global.spyOnDevAndProd = spyOn;
42 } else {
43 global.spyOnDev = spyOn;
44 global.spyOnProd = noop;
45 global.spyOnDevAndProd = spyOn;
46 }
47
48 expect.extend({
49 ...require('./matchers/reactTestMatchers'),
50 });
51
52 // We have a Babel transform that inserts guards against infinite loops.
53 // If a loop runs for too many iterations, we throw an error and set this
54 // global variable. The global lets us detect an infinite loop even if
55 // the actual error object ends up being caught and ignored. An infinite
56 // loop must always fail the test!
57 beforeEach(() => {
58 global.infiniteLoopError = null;
59 });
60 afterEach(() => {
61 const error = global.infiniteLoopError;
62 global.infiniteLoopError = null;
63 if (error) {
64 throw error;
65 }
66 });
67
68 // Patch the console to assert that all console error/warn/log calls assert.
69 patchConsoleMethods({includeLog: !!process.env.CI});
70 beforeEach(resetAllUnexpectedConsoleCalls);
71 afterEach(assertConsoleLogsCleared);
72
73 // TODO: enable this check so we don't forget to reset spyOnX mocks.
74 // afterEach(() => {
75 // if (
76 // console[methodName] !== mockMethod &&
77 // !jest.isMockFunction(console[methodName])
78 // ) {
79 // throw new Error(
80 // `Test did not tear down console.${methodName} mock properly.`
81 // );
82 // }
83 // });
84
85 if (process.env.NODE_ENV === 'production') {
86 // In production, we strip error messages and turn them into codes.
87 // This decodes them back so that the test assertions on them work.
88 // 1. `ErrorProxy` decodes error messages at Error construction time and
89 // also proxies error instances with `proxyErrorInstance`.
90 // 2. `proxyErrorInstance` decodes error messages when the `message`
91 // property is changed.
92 const decodeErrorMessage = function (message) {
93 if (!message) {
94 return message;
95 }
96 const re = /react.dev\/errors\/(\d+)?\??([^\s]*)/;
97 let matches = message.match(re);
98 if (!matches || matches.length !== 3) {
99 // Some tests use React 17, when the URL was different.
100 const re17 = /error-decoder.html\?invariant=(\d+)([^\s]*)/;
101 matches = message.match(re17);
102 if (!matches || matches.length !== 3) {
103 return message;
104 }
105 }
106 const code = parseInt(matches[1], 10);
107 const args = matches[2]
108 .split('&')
109 .filter(s => s.startsWith('args[]='))
110 .map(s => s.slice('args[]='.length))
111 .map(decodeURIComponent);
112 const format = errorMap[code];
113 let argIndex = 0;
114 return format.replace(/%s/g, () => args[argIndex++]);
115 };
116 const OriginalError = global.Error;
117 // Cache Reflect methods so the proxies keep working even if a test
118 // temporarily deletes or overrides them (e.g. to exercise no-Reflect
119 // fallback paths in the React source).
120 const ReflectApply = Reflect.apply;
121 const ReflectConstruct = Reflect.construct;
122 const ReflectGet = Reflect.get;
123 const ReflectSet = Reflect.set;
124 // V8's Error.captureStackTrace (used in Jest) fails if the error object is
125 // a Proxy, so we need to pass it the unproxied instance.
126 const originalErrorInstances = new WeakMap();
127 const captureStackTrace = function (error, ...args) {
128 return OriginalError.captureStackTrace.call(
129 this,
130 originalErrorInstances.get(error) ||
131 // Sometimes this wrapper receives an already-unproxied instance.
132 error,
133 ...args
134 );
135 };
136 const proxyErrorInstance = error => {
137 const proxy = new Proxy(error, {
138 set(target, key, value, receiver) {
139 if (key === 'message') {
140 return ReflectSet(target, key, decodeErrorMessage(value), receiver);
141 }
142 if (key === 'stack') {
143 // https://github.com/nodejs/node/issues/60862
144 return ReflectSet(target, key, value);
145 }
146 return ReflectSet(target, key, value, receiver);
147 },
148 get(target, key, receiver) {
149 if (key === 'stack') {
150 // https://github.com/nodejs/node/issues/60862
151 return ReflectGet(target, key);
152 }
153 return ReflectGet(target, key, receiver);
154 },
155 });
156 originalErrorInstances.set(proxy, error);
157 return proxy;
158 };
159 const ErrorProxy = new Proxy(OriginalError, {
160 apply(target, thisArg, argumentsList) {
161 const error = ReflectApply(target, thisArg, argumentsList);
162 error.message = decodeErrorMessage(error.message);
163 return proxyErrorInstance(error);
164 },
165 construct(target, argumentsList, newTarget) {
166 const error = ReflectConstruct(target, argumentsList, newTarget);
167 error.message = decodeErrorMessage(error.message);
168 return proxyErrorInstance(error);
169 },
170 get(target, key, receiver) {
171 if (key === 'captureStackTrace') {
172 return captureStackTrace;
173 }
174 return ReflectGet(target, key, receiver);
175 },
176 });
177 ErrorProxy.OriginalError = OriginalError;
178 global.Error = ErrorProxy;
179 }
180
181 const expectTestToFail = async (callback, errorToThrowIfTestSucceeds) => {
182 if (callback.length > 0) {
183 throw Error(
184 'Gated test helpers do not support the `done` callback. Return a ' +
185 'promise instead.'
186 );
187 }
188
189 // Install a global error event handler. We treat global error events as
190 // test failures, same as Jest's default behavior.
191 //
192 // Becaused we installed our own error event handler, Jest will not report a
193 // test failure. Conceptually it's as if we wrapped the entire test event in
194 // a try-catch.
195 let didError = false;
196 const errorEventHandler = () => {
197 didError = true;
198 };
199 // eslint-disable-next-line no-restricted-globals
200 if (typeof addEventListener === 'function') {
201 // eslint-disable-next-line no-restricted-globals
202 addEventListener('error', errorEventHandler);
203 }
204
205 try {
206 const maybePromise = callback();
207 if (
208 maybePromise !== undefined &&
209 maybePromise !== null &&
210 typeof maybePromise.then === 'function'
211 ) {
212 await maybePromise;
213 }
214 // Flush unexpected console calls inside the test itself, instead of in
215 // `afterEach` like we normally do. `afterEach` is too late because if it
216 // throws, we won't have captured it.
217 assertConsoleLogsCleared();
218 } catch (testError) {
219 didError = true;
220 }
221 resetAllUnexpectedConsoleCalls();
222 // eslint-disable-next-line no-restricted-globals
223 if (typeof removeEventListener === 'function') {
224 // eslint-disable-next-line no-restricted-globals
225 removeEventListener('error', errorEventHandler);
226 }
227
228 if (!didError) {
229 // The test did not error like we expected it to. Report this to Jest as
230 // a failure.
231 throw errorToThrowIfTestSucceeds;
232 }
233 };
234
235 const coerceGateConditionToFunction = gateFnOrString => {
236 return typeof gateFnOrString === 'string'
237 ? // `gate('foo')` is treated as equivalent to `gate(flags => flags.foo)`
238 flags => flags[gateFnOrString]
239 : // Assume this is already a function
240 gateFnOrString;
241 };
242
243 const gatedErrorMessage = 'Gated test was expected to fail, but it passed.';
244 global._test_gate = (gateFnOrString, testName, callback, timeoutMS) => {
245 const gateFn = coerceGateConditionToFunction(gateFnOrString);
246 let shouldPass;
247 try {
248 const flags = getTestFlags();
249 shouldPass = gateFn(flags);
250 } catch (e) {
251 test(
252 testName,
253 () => {
254 throw e;
255 },
256 timeoutMS
257 );
258 return;
259 }
260 if (shouldPass) {
261 test(testName, callback, timeoutMS);
262 } else {
263 const error = new Error(gatedErrorMessage);
264 Error.captureStackTrace(error, global._test_gate);
265 test(`[GATED, SHOULD FAIL] ${testName}`, () =>
266 expectTestToFail(callback, error, timeoutMS));
267 }
268 };
269 global._test_gate_focus = (gateFnOrString, testName, callback, timeoutMS) => {
270 const gateFn = coerceGateConditionToFunction(gateFnOrString);
271 let shouldPass;
272 try {
273 const flags = getTestFlags();
274 shouldPass = gateFn(flags);
275 } catch (e) {
276 test.only(
277 testName,
278 () => {
279 throw e;
280 },
281 timeoutMS
282 );
283 return;
284 }
285 if (shouldPass) {
286 test.only(testName, callback, timeoutMS);
287 } else {
288 const error = new Error(gatedErrorMessage);
289 Error.captureStackTrace(error, global._test_gate_focus);
290 test.only(
291 `[GATED, SHOULD FAIL] ${testName}`,
292 () => expectTestToFail(callback, error),
293 timeoutMS
294 );
295 }
296 };
297
298 // Dynamic version of @gate pragma
299 global.gate = gateFnOrString => {
300 const gateFn = coerceGateConditionToFunction(gateFnOrString);
301 const flags = getTestFlags();
302 return gateFn(flags);
303 };
304
305 // We augment JSDOM to produce a document that has a loading readyState by default
306 // and can be changed. We mock it here globally so we don't have to import our special
307 // mock in every file.
308 jest.mock('jsdom', () => {
309 return require('internal-test-utils/ReactJSDOM.js');
310 });
311 }
312
313 // We mock createHook so that we can automatically clean it up.
314 let installedHook = null;
315 jest.mock('async_hooks', () => {
316 const actual = jest.requireActual('async_hooks');
317 return {
318 ...actual,
319 createHook(config) {
320 if (installedHook) {
321 installedHook.disable();
322 }
323 return (installedHook = actual.createHook(config));
324 },
325 };
326 });
327
328 // Ensure async hooks are disabled after each test to prevent cross-test pollution.
329 // This is needed because test files that load the Node server (with async debug hooks)
330 // can pollute test files that load the Edge server (which doesn't create new hooks
331 // to trigger the cleanup in the mock above).
332 afterEach(() => {
333 if (installedHook) {
334 installedHook.disable();
335 installedHook = null;
336 }
337 });