From ac9e3640dea4c2681e2c7844d237f32480e80df2 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Thu, 30 Sep 2021 14:12:28 -0400 Subject: [PATCH 1/2] Updated tests to support newer V8 stack formats --- scripts/jest/matchers/toThrow.js | 50 +++++++++++++++++++ scripts/jest/setupTests.js | 3 +- .../spec-equivalence-reporter/setupTests.js | 3 +- 3 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 scripts/jest/matchers/toThrow.js diff --git a/scripts/jest/matchers/toThrow.js b/scripts/jest/matchers/toThrow.js new file mode 100644 index 0000000000000..148fd49fa099d --- /dev/null +++ b/scripts/jest/matchers/toThrow.js @@ -0,0 +1,50 @@ +'use strict'; + +// V8 uses a different message format when reading properties of null or undefined. +// Older versions use e.g. "Cannot read property 'world' of undefined" +// Newer versions use e.g. "Cannot read properties of undefined (reading 'world')" +// This file overrides the built-in toThrow() matches to handle both cases, +// enabling the React project to support Node 12-16 witout forking tests. + +const toThrowMatchers = require('expect/build/toThrowMatchers').default; +const builtInToThrow = toThrowMatchers.toThrow; + +// Detect the newer stack format: +let newErrorFormat = false; +try { + null.test(); +} catch (error) { + if (error.message.includes('Cannot read properties of null')) { + newErrorFormat = true; + } +} + +// Detect the message pattern we need to rename: +const regex = /Cannot read property '([^']+)' of (.+)/; + +// Massage strings (written in the older format) to match the newer format +// if tests are currently running on Node 16+ +function normalizeErrorMessage(message) { + if (newErrorFormat) { + const match = message.match(regex); + if (match) { + return `Cannot read properties of ${match[2]} (reading '${match[1]}')`; + } + } + + return message; +} + +function toThrow(value, expectedValue) { + if (typeof expectedValue === 'string') { + expectedValue = normalizeErrorMessage(expectedValue); + } else if (expectedValue instanceof Error) { + expectedValue.message = normalizeErrorMessage(expectedValue.message); + } + + return builtInToThrow.call(this, value, expectedValue); +} + +module.exports = { + toThrow, +}; diff --git a/scripts/jest/setupTests.js b/scripts/jest/setupTests.js index abb358f817aa2..63dbcf27ebcfe 100644 --- a/scripts/jest/setupTests.js +++ b/scripts/jest/setupTests.js @@ -45,8 +45,9 @@ if (process.env.REACT_CLASS_EQUIVALENCE_TEST) { } expect.extend({ - ...require('./matchers/toWarnDev'), ...require('./matchers/reactTestMatchers'), + ...require('./matchers/toThrow'), + ...require('./matchers/toWarnDev'), }); // We have a Babel transform that inserts guards against infinite loops. diff --git a/scripts/jest/spec-equivalence-reporter/setupTests.js b/scripts/jest/spec-equivalence-reporter/setupTests.js index 31814bb754d95..7d6989d146359 100644 --- a/scripts/jest/spec-equivalence-reporter/setupTests.js +++ b/scripts/jest/spec-equivalence-reporter/setupTests.js @@ -46,8 +46,9 @@ global.spyOnProd = function(...args) { }; expect.extend({ - ...require('../matchers/toWarnDev'), ...require('../matchers/reactTestMatchers'), + ...require('../matchers/toThrow'), + ...require('../matchers/toWarnDev'), }); beforeEach(() => (numExpectations = 0)); From dabc7fde6f00603326989d44e78186a9043830f5 Mon Sep 17 00:00:00 2001 From: Brian Vaughn Date: Thu, 30 Sep 2021 14:12:52 -0400 Subject: [PATCH 2/2] Updated describeNativeComponentFrame() to support displayName in stack frames for newer versions of v8 --- packages/shared/ReactComponentStackFrame.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/shared/ReactComponentStackFrame.js b/packages/shared/ReactComponentStackFrame.js index 0e6b8a03c1258..c101003e569e1 100644 --- a/packages/shared/ReactComponentStackFrame.js +++ b/packages/shared/ReactComponentStackFrame.js @@ -168,7 +168,15 @@ export function describeNativeComponentFrame( // The next one that isn't the same should be our match though. if (c < 0 || sampleLines[s] !== controlLines[c]) { // V8 adds a "new" prefix for native classes. Let's remove it to make it prettier. - const frame = '\n' + sampleLines[s].replace(' at new ', ' at '); + let frame = '\n' + sampleLines[s].replace(' at new ', ' at '); + + // If our component frame is labeled "" + // but we have a user-provided "displayName" + // splice it in to make the stack more readable. + if (fn.displayName && frame.includes('')) { + frame = frame.replace('', fn.displayName); + } + if (__DEV__) { if (typeof fn === 'function') { componentFrameCache.set(fn, frame);