mirror of
https://github.com/zebrajr/react.git
synced 2025-12-06 12:20:20 +01:00
This is the first step to experimenting with a new type of stack traces behind the `enableOwnerStacks` flag - in DEV only. The idea is to generate stacks that are more like if the JSX was a direct call even though it's actually a lazy call. Not only can you see which exact JSX call line number generated the erroring component but if that's inside an abstraction function, which function called that function and if it's a component, which component generated that component. For this to make sense it really need to be the "owner" stack rather than the parent stack like we do for other component stacks. On one hand it has more precise information but on the other hand it also loses context. For most types of problems the owner stack is the most useful though since it tells you which component rendered this component. The problem with the platform in its current state is that there's two ways to deal with stacks: 1) `new Error().stack` 2) `console.createTask()` The nice thing about `new Error().stack` is that we can extract the frames and piece them together in whatever way we want. That is great for constructing custom UIs like error dialogs. Unfortunately, we can't take custom stacks and set them in the native UIs like Chrome DevTools. The nice thing about `console.createTask()` is that the resulting stacks are natively integrated into the Chrome DevTools in the console and the breakpoint debugger. They also automatically follow source mapping and ignoreLists. The downside is that there's no way to extract the async stack outside the native UI itself so this information cannot be used for custom UIs like errors dialogs. It also means we can't collect this on the server and then pass it to the client for server components. The solution here is that we use both techniques and collect both an `Error` object and a `Task` object for every JSX call. The main concern about this approach is the performance so that's the main thing to test. It's certainly too slow for production but it might also be too slow even for DEV. This first PR doesn't actually use the stacks yet. It just collects them as the first step. The next step is to start utilizing this information in error printing etc. For RSC we pass the stack along across over the wire. This can be concatenated on the client following the owner path to create an owner stack leading back into the server. We'll later use this information to restore fake frames on the client for native integration. Since this information quickly gets pretty heavy if we include all frames, we strip out the top frame. We also strip out everything below the functions that call into user space in the Flight runtime. To do this we need to figure out the frames that represents calling out into user space. The resulting stack is typically just the one frame inside the owner component's JSX callsite. I also eagerly strip out things we expect to be ignoreList:ed anyway - such as `node_modules` and Node.js internals.
310 lines
10 KiB
JavaScript
310 lines
10 KiB
JavaScript
'use strict';
|
|
|
|
const {getTestFlags} = require('./TestFlags');
|
|
const {
|
|
flushAllUnexpectedConsoleCalls,
|
|
resetAllUnexpectedConsoleCalls,
|
|
patchConsoleMethods,
|
|
} = require('internal-test-utils/consoleMock');
|
|
|
|
if (process.env.REACT_CLASS_EQUIVALENCE_TEST) {
|
|
// Inside the class equivalence tester, we have a custom environment, let's
|
|
// require that instead.
|
|
require('./spec-equivalence-reporter/setupTests.js');
|
|
} else {
|
|
const errorMap = require('../error-codes/codes.json');
|
|
|
|
// 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;
|
|
}
|
|
|
|
expect.extend({
|
|
...require('./matchers/reactTestMatchers'),
|
|
...require('./matchers/toThrow'),
|
|
...require('./matchers/toWarnDev'),
|
|
});
|
|
|
|
// 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!
|
|
beforeEach(() => {
|
|
global.infiniteLoopError = null;
|
|
});
|
|
afterEach(() => {
|
|
const error = global.infiniteLoopError;
|
|
global.infiniteLoopError = null;
|
|
if (error) {
|
|
throw error;
|
|
}
|
|
});
|
|
|
|
// Patch the console to assert that all console error/warn/log calls assert.
|
|
patchConsoleMethods({includeLog: !!process.env.CI});
|
|
beforeEach(resetAllUnexpectedConsoleCalls);
|
|
afterEach(flushAllUnexpectedConsoleCalls);
|
|
|
|
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 = message.match(re);
|
|
if (!matches || matches.length !== 3) {
|
|
// Some tests use React 17, when the URL was different.
|
|
const re17 = /error-decoder.html\?invariant=(\d+)([^\s]*)/;
|
|
matches = message.match(re17);
|
|
if (!matches || matches.length !== 3) {
|
|
return message;
|
|
}
|
|
}
|
|
const code = parseInt(matches[1], 10);
|
|
const args = matches[2]
|
|
.split('&')
|
|
.filter(s => s.startsWith('args[]='))
|
|
.map(s => s.slice('args[]='.length))
|
|
.map(decodeURIComponent);
|
|
const format = errorMap[code];
|
|
let argIndex = 0;
|
|
return format.replace(/%s/g, () => 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,
|
|
originalErrorInstances.get(error) ||
|
|
// 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,
|
|
decodeErrorMessage(value),
|
|
receiver
|
|
);
|
|
}
|
|
return Reflect.set(target, key, value, receiver);
|
|
},
|
|
});
|
|
originalErrorInstances.set(proxy, error);
|
|
return proxy;
|
|
};
|
|
const ErrorProxy = new Proxy(OriginalError, {
|
|
apply(target, thisArg, argumentsList) {
|
|
const error = Reflect.apply(target, thisArg, argumentsList);
|
|
error.message = decodeErrorMessage(error.message);
|
|
return proxyErrorInstance(error);
|
|
},
|
|
construct(target, argumentsList, newTarget) {
|
|
const error = Reflect.construct(target, argumentsList, newTarget);
|
|
error.message = decodeErrorMessage(error.message);
|
|
return proxyErrorInstance(error);
|
|
},
|
|
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 Error(
|
|
'Gated test helpers do not support the `done` callback. Return a ' +
|
|
'promise instead.'
|
|
);
|
|
}
|
|
|
|
// 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
|
|
addEventListener('error', 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
|
|
removeEventListener('error', 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 = gateFn(flags);
|
|
} catch (e) {
|
|
test(
|
|
testName,
|
|
() => {
|
|
throw e;
|
|
},
|
|
timeoutMS
|
|
);
|
|
return;
|
|
}
|
|
if (shouldPass) {
|
|
test(testName, callback, timeoutMS);
|
|
} else {
|
|
const error = new Error(gatedErrorMessage);
|
|
Error.captureStackTrace(error, global._test_gate);
|
|
test(`[GATED, SHOULD FAIL] ${testName}`, () =>
|
|
expectTestToFail(callback, error, timeoutMS));
|
|
}
|
|
};
|
|
global._test_gate_focus = (gateFn, testName, callback, timeoutMS) => {
|
|
let shouldPass;
|
|
try {
|
|
const flags = getTestFlags();
|
|
shouldPass = gateFn(flags);
|
|
} catch (e) {
|
|
test.only(
|
|
testName,
|
|
() => {
|
|
throw e;
|
|
},
|
|
timeoutMS
|
|
);
|
|
return;
|
|
}
|
|
if (shouldPass) {
|
|
test.only(testName, callback, timeoutMS);
|
|
} else {
|
|
const error = new Error(gatedErrorMessage);
|
|
Error.captureStackTrace(error, global._test_gate_focus);
|
|
test.only(
|
|
`[GATED, SHOULD FAIL] ${testName}`,
|
|
() => expectTestToFail(callback, error),
|
|
timeoutMS
|
|
);
|
|
}
|
|
};
|
|
|
|
// Dynamic version of @gate pragma
|
|
global.gate = fn => {
|
|
const flags = getTestFlags();
|
|
return fn(flags);
|
|
};
|
|
}
|
|
|
|
// 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.
|
|
lazyRequireFunctionExports('react/jsx-dev-runtime');
|
|
|
|
// 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.
|
|
lazyRequireFunctionExports('react/jsx-runtime');
|
|
|
|
function lazyRequireFunctionExports(moduleName) {
|
|
jest.mock(moduleName, () => {
|
|
return new Proxy(jest.requireActual(moduleName), {
|
|
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 jest.requireActual(moduleName)[prop].apply(this, 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];
|
|
}
|
|
},
|
|
});
|
|
});
|
|
}
|