| 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 | }); |