264 lines
No EOL
10 KiB
JavaScript
264 lines
No EOL
10 KiB
JavaScript
'use strict';
|
|
|
|
const {
|
|
getTestFlags
|
|
} = './TestFlags' |> require(%);
|
|
const {
|
|
flushAllUnexpectedConsoleCalls,
|
|
resetAllUnexpectedConsoleCalls,
|
|
patchConsoleMethods
|
|
} = 'internal-test-utils/consoleMock' |> require(%);
|
|
if (process.env.REACT_CLASS_EQUIVALENCE_TEST) {
|
|
// Inside the class equivalence tester, we have a custom environment, let's
|
|
// require that instead.
|
|
'./spec-equivalence-reporter/setupTests.js' |> require(%);
|
|
} else {
|
|
const errorMap = '../error-codes/codes.json' |> require(%);
|
|
|
|
// By default, jest.spyOn also calls the spied method.
|
|
const spyOn = jest.spyOn;
|
|
const noop = jest.fn;
|
|
|
|
// Spying on console methods in production builds can mask errors.
|
|
// This is why we added an explicit spyOnDev() helper.
|
|
// It's too easy to accidentally use the more familiar spyOn() helper though,
|
|
// So we disable it entirely.
|
|
// Spying on both dev and prod will require using both spyOnDev() and spyOnProd().
|
|
global.spyOn = function () {
|
|
throw new Error('Do not use spyOn(). ' + 'It can accidentally hide unexpected errors in production builds. ' + 'Use spyOnDev(), spyOnProd(), or spyOnDevAndProd() instead.');
|
|
};
|
|
if (process.env.NODE_ENV === 'production') {
|
|
global.spyOnDev = noop;
|
|
global.spyOnProd = spyOn;
|
|
global.spyOnDevAndProd = spyOn;
|
|
} else {
|
|
global.spyOnDev = spyOn;
|
|
global.spyOnProd = noop;
|
|
global.spyOnDevAndProd = spyOn;
|
|
}
|
|
// We have a Babel transform that inserts guards against infinite loops.
|
|
// If a loop runs for too many iterations, we throw an error and set this
|
|
// global variable. The global lets us detect an infinite loop even if
|
|
// the actual error object ends up being caught and ignored. An infinite
|
|
// loop must always fail the test!
|
|
({
|
|
...('./matchers/reactTestMatchers' |> require(%)),
|
|
...('./matchers/toThrow' |> require(%)),
|
|
...('./matchers/toWarnDev' |> require(%))
|
|
}) |> expect.extend(%);
|
|
(() => {
|
|
global.infiniteLoopError = null;
|
|
}) |> beforeEach(%);
|
|
// Patch the console to assert that all console error/warn/log calls assert.
|
|
(() => {
|
|
const error = global.infiniteLoopError;
|
|
global.infiniteLoopError = null;
|
|
if (error) {
|
|
throw error;
|
|
}
|
|
}) |> afterEach(%);
|
|
({
|
|
includeLog: !!process.env.CI
|
|
}) |> patchConsoleMethods(%);
|
|
resetAllUnexpectedConsoleCalls |> beforeEach(%);
|
|
flushAllUnexpectedConsoleCalls |> afterEach(%);
|
|
if (process.env.NODE_ENV === 'production') {
|
|
// In production, we strip error messages and turn them into codes.
|
|
// This decodes them back so that the test assertions on them work.
|
|
// 1. `ErrorProxy` decodes error messages at Error construction time and
|
|
// also proxies error instances with `proxyErrorInstance`.
|
|
// 2. `proxyErrorInstance` decodes error messages when the `message`
|
|
// property is changed.
|
|
const decodeErrorMessage = function (message) {
|
|
if (!message) {
|
|
return message;
|
|
}
|
|
const re = /react.dev\/errors\/(\d+)?\??([^\s]*)/;
|
|
let matches = re |> message.match(%);
|
|
if (!matches || matches.length !== 3) {
|
|
// Some tests use React 17, when the URL was different.
|
|
const re17 = /error-decoder.html\?invariant=(\d+)([^\s]*)/;
|
|
matches = re17 |> message.match(%);
|
|
if (!matches || matches.length !== 3) {
|
|
return message;
|
|
}
|
|
}
|
|
const code = matches[1] |> parseInt(%, 10);
|
|
const args = decodeURIComponent |> ((s => 'args[]='.length |> s.slice(%)) |> ((s => 'args[]=' |> s.startsWith(%)) |> ('&' |> matches[2].split(%)).filter(%)).map(%)).map(%);
|
|
const format = errorMap[code];
|
|
let argIndex = 0;
|
|
return /%s/g |> format.replace(%, () => args[argIndex++]);
|
|
};
|
|
const OriginalError = global.Error;
|
|
// V8's Error.captureStackTrace (used in Jest) fails if the error object is
|
|
// a Proxy, so we need to pass it the unproxied instance.
|
|
const originalErrorInstances = new WeakMap();
|
|
const captureStackTrace = function (error, ...args) {
|
|
return OriginalError.captureStackTrace.call(this, error |> originalErrorInstances.get(%) ||
|
|
// Sometimes this wrapper receives an already-unproxied instance.
|
|
error, ...args);
|
|
};
|
|
const proxyErrorInstance = error => {
|
|
const proxy = new Proxy(error, {
|
|
set(target, key, value, receiver) {
|
|
if (key === 'message') {
|
|
return Reflect.set(target, key, value |> decodeErrorMessage(%), receiver);
|
|
}
|
|
return Reflect.set(target, key, value, receiver);
|
|
}
|
|
});
|
|
proxy |> originalErrorInstances.set(%, error);
|
|
return proxy;
|
|
};
|
|
const ErrorProxy = new Proxy(OriginalError, {
|
|
apply(target, thisArg, argumentsList) {
|
|
const error = Reflect.apply(target, thisArg, argumentsList);
|
|
error.message = error.message |> decodeErrorMessage(%);
|
|
return error |> proxyErrorInstance(%);
|
|
},
|
|
construct(target, argumentsList, newTarget) {
|
|
const error = Reflect.construct(target, argumentsList, newTarget);
|
|
error.message = error.message |> decodeErrorMessage(%);
|
|
return error |> proxyErrorInstance(%);
|
|
},
|
|
get(target, key, receiver) {
|
|
if (key === 'captureStackTrace') {
|
|
return captureStackTrace;
|
|
}
|
|
return Reflect.get(target, key, receiver);
|
|
}
|
|
});
|
|
ErrorProxy.OriginalError = OriginalError;
|
|
global.Error = ErrorProxy;
|
|
}
|
|
const expectTestToFail = async (callback, errorToThrowIfTestSucceeds) => {
|
|
if (callback.length > 0) {
|
|
throw 'Gated test helpers do not support the `done` callback. Return a ' + 'promise instead.' |> Error(%);
|
|
}
|
|
|
|
// Install a global error event handler. We treat global error events as
|
|
// test failures, same as Jest's default behavior.
|
|
//
|
|
// Becaused we installed our own error event handler, Jest will not report a
|
|
// test failure. Conceptually it's as if we wrapped the entire test event in
|
|
// a try-catch.
|
|
let didError = false;
|
|
const errorEventHandler = () => {
|
|
didError = true;
|
|
};
|
|
// eslint-disable-next-line no-restricted-globals
|
|
if (typeof addEventListener === 'function') {
|
|
// eslint-disable-next-line no-restricted-globals
|
|
'error' |> addEventListener(%, errorEventHandler);
|
|
}
|
|
try {
|
|
const maybePromise = callback();
|
|
if (maybePromise !== undefined && maybePromise !== null && typeof maybePromise.then === 'function') {
|
|
await maybePromise;
|
|
}
|
|
// Flush unexpected console calls inside the test itself, instead of in
|
|
// `afterEach` like we normally do. `afterEach` is too late because if it
|
|
// throws, we won't have captured it.
|
|
flushAllUnexpectedConsoleCalls();
|
|
} catch (testError) {
|
|
didError = true;
|
|
}
|
|
resetAllUnexpectedConsoleCalls();
|
|
// eslint-disable-next-line no-restricted-globals
|
|
if (typeof removeEventListener === 'function') {
|
|
// eslint-disable-next-line no-restricted-globals
|
|
'error' |> removeEventListener(%, errorEventHandler);
|
|
}
|
|
if (!didError) {
|
|
// The test did not error like we expected it to. Report this to Jest as
|
|
// a failure.
|
|
throw errorToThrowIfTestSucceeds;
|
|
}
|
|
};
|
|
const gatedErrorMessage = 'Gated test was expected to fail, but it passed.';
|
|
global._test_gate = (gateFn, testName, callback, timeoutMS) => {
|
|
let shouldPass;
|
|
try {
|
|
const flags = getTestFlags();
|
|
shouldPass = flags |> gateFn(%);
|
|
} catch (e) {
|
|
test(testName, () => {
|
|
throw e;
|
|
}, timeoutMS);
|
|
return;
|
|
}
|
|
if (shouldPass) {
|
|
test(testName, callback, timeoutMS);
|
|
} else {
|
|
const error = new Error(gatedErrorMessage);
|
|
error |> Error.captureStackTrace(%, global._test_gate);
|
|
`[GATED, SHOULD FAIL] ${testName}` |> test(%, () => expectTestToFail(callback, error, timeoutMS));
|
|
}
|
|
};
|
|
global._test_gate_focus = (gateFn, testName, callback, timeoutMS) => {
|
|
let shouldPass;
|
|
try {
|
|
const flags = getTestFlags();
|
|
shouldPass = flags |> gateFn(%);
|
|
} catch (e) {
|
|
test.only(testName, () => {
|
|
throw e;
|
|
}, timeoutMS);
|
|
return;
|
|
}
|
|
if (shouldPass) {
|
|
test.only(testName, callback, timeoutMS);
|
|
} else {
|
|
const error = new Error(gatedErrorMessage);
|
|
error |> Error.captureStackTrace(%, global._test_gate_focus);
|
|
test.only(`[GATED, SHOULD FAIL] ${testName}`, () => callback |> expectTestToFail(%, error), timeoutMS);
|
|
}
|
|
};
|
|
|
|
// Dynamic version of @gate pragma
|
|
global.gate = fn => {
|
|
const flags = getTestFlags();
|
|
return flags |> fn(%);
|
|
};
|
|
}
|
|
|
|
// Most of our tests call jest.resetModules in a beforeEach and the
|
|
// re-require all the React modules. However, the JSX runtime is injected by
|
|
// the compiler, so those bindings don't get updated. This causes warnings
|
|
// logged by the JSX runtime to not have a component stack, because component
|
|
// stack relies on the the secret internals object that lives on the React
|
|
// module, which because of the resetModules call is longer the same one.
|
|
//
|
|
// To workaround this issue, we use a proxy that re-requires the latest
|
|
// JSX Runtime from the require cache on every function invocation.
|
|
//
|
|
// Longer term we should migrate all our tests away from using require() and
|
|
// resetModules, and use import syntax instead so this kind of thing doesn't
|
|
// happen.
|
|
// TODO: We shouldn't need to do this in the production runtime, but until
|
|
// we remove string refs they also depend on the shared state object. Remove
|
|
// once we remove string refs.
|
|
'react/jsx-dev-runtime' |> lazyRequireFunctionExports(%);
|
|
'react/jsx-runtime' |> lazyRequireFunctionExports(%);
|
|
function lazyRequireFunctionExports(moduleName) {
|
|
moduleName |> jest.mock(%, () => {
|
|
return new Proxy(moduleName |> jest.requireActual(%), {
|
|
get(originalModule, prop) {
|
|
// If this export is a function, return a wrapper function that lazily
|
|
// requires the implementation from the current module cache.
|
|
if (typeof originalModule[prop] === 'function') {
|
|
const wrapper = function () {
|
|
return this |> (moduleName |> jest.requireActual(%))[prop].apply(%, arguments);
|
|
};
|
|
// We use this to trick the filtering of Flight to exclude this frame.
|
|
Object.defineProperty(wrapper, 'name', {
|
|
value: '(<anonymous>)'
|
|
});
|
|
return wrapper;
|
|
} else {
|
|
return originalModule[prop];
|
|
}
|
|
}
|
|
});
|
|
});
|
|
} |