Update test and stack frame code to support newer V8 stack formats (#22477)
Brian Vaughn committed
Oct 11, 2021 at 18:40 UTC
c16b005f2d71acde26658f035ff491a5e027da39
4 files changed
+63
-3
packages/shared/ReactComponentStackFrame.js
+9
-1
@@ -168,7 +168,15 @@ export function describeNativeComponentFrame(
168
// The next one that isn't the same should be our match though.
169
if (c < 0 || sampleLines[s] !== controlLines[c]) {
170
// V8 adds a "new" prefix for native classes. Let's remove it to make it prettier.
171
- const frame = '\n' + sampleLines[s].replace(' at new ', ' at ');
171
+ let frame = '\n' + sampleLines[s].replace(' at new ', ' at ');
172
+
173
+ // If our component frame is labeled "<anonymous>"
174
+ // but we have a user-provided "displayName"
175
+ // splice it in to make the stack more readable.
176
+ if (fn.displayName && frame.includes('<anonymous>')) {
177
+ frame = frame.replace('<anonymous>', fn.displayName);
178
+ }
179
+
180
if (__DEV__) {
181
if (typeof fn === 'function') {
182
componentFrameCache.set(fn, frame);
scripts/jest/matchers/toThrow.js
new
+50
@@ -0,0 +1,50 @@
1
+'use strict';
2
+
3
+// V8 uses a different message format when reading properties of null or undefined.
4
+// Older versions use e.g. "Cannot read property 'world' of undefined"
5
+// Newer versions use e.g. "Cannot read properties of undefined (reading 'world')"
6
+// This file overrides the built-in toThrow() matches to handle both cases,
7
+// enabling the React project to support Node 12-16 witout forking tests.
8
+
9
+const toThrowMatchers = require('expect/build/toThrowMatchers').default;
10
+const builtInToThrow = toThrowMatchers.toThrow;
11
+
12
+// Detect the newer stack format:
13
+let newErrorFormat = false;
14
+try {
15
+ null.test();
16
+} catch (error) {
17
+ if (error.message.includes('Cannot read properties of null')) {
18
+ newErrorFormat = true;
19
+ }
20
+}
21
+
22
+// Detect the message pattern we need to rename:
23
+const regex = /Cannot read property '([^']+)' of (.+)/;
24
+
25
+// Massage strings (written in the older format) to match the newer format
26
+// if tests are currently running on Node 16+
27
+function normalizeErrorMessage(message) {
28
+ if (newErrorFormat) {
29
+ const match = message.match(regex);
30
+ if (match) {
31
+ return `Cannot read properties of ${match[2]} (reading '${match[1]}')`;
32
+ }
33
+ }
34
+
35
+ return message;
36
+}
37
+
38
+function toThrow(value, expectedValue) {
39
+ if (typeof expectedValue === 'string') {
40
+ expectedValue = normalizeErrorMessage(expectedValue);
41
+ } else if (expectedValue instanceof Error) {
42
+ expectedValue.message = normalizeErrorMessage(expectedValue.message);
43
+ }
44
+
45
+ return builtInToThrow.call(this, value, expectedValue);
46
+}
47
+
48
+module.exports = {
49
+ toThrow,
50
+};
scripts/jest/setupTests.js
+2
-1
@@ -45,8 +45,9 @@ if (process.env.REACT_CLASS_EQUIVALENCE_TEST) {
45
}
46
47
expect.extend({
48
- ...require('./matchers/toWarnDev'),
48
...require('./matchers/reactTestMatchers'),
49
+ ...require('./matchers/toThrow'),
50
+ ...require('./matchers/toWarnDev'),
51
});
52
53
// We have a Babel transform that inserts guards against infinite loops.
scripts/jest/spec-equivalence-reporter/setupTests.js
+2
-1
@@ -46,8 +46,9 @@ global.spyOnProd = function(...args) {
46
};
47
48
expect.extend({
49
- ...require('../matchers/toWarnDev'),
49
...require('../matchers/reactTestMatchers'),
50
+ ...require('../matchers/toThrow'),
51
+ ...require('../matchers/toWarnDev'),
52
});
53
54
beforeEach(() => (numExpectations = 0));