diff --git a/fixtures/dom/src/components/Header.js b/fixtures/dom/src/components/Header.js index 9a4a0a932d..bb19e6247a 100644 --- a/fixtures/dom/src/components/Header.js +++ b/fixtures/dom/src/components/Header.js @@ -59,6 +59,7 @@ class Header extends React.Component { Input change events Buttons + Error Handling diff --git a/fixtures/dom/src/components/fixtures/error-handling/index.js b/fixtures/dom/src/components/fixtures/error-handling/index.js new file mode 100644 index 0000000000..4f8208566f --- /dev/null +++ b/fixtures/dom/src/components/fixtures/error-handling/index.js @@ -0,0 +1,88 @@ +const React = window.React; + +import FixtureSet from '../../FixtureSet'; +import TestCase from '../../TestCase'; + +function BadRender(props) { + throw props.error; +} +class ErrorBoundary extends React.Component { + state = { + shouldThrow: false, + didThrow: false, + error: null, + }; + componentDidCatch(error) { + this.setState({error, didThrow: true}); + } + triggerError = () => { + this.setState({ + shouldThrow: true, + }); + }; + render() { + if (this.state.didThrow) { + if (this.state.error) { + return `Captured an error: ${this.state.error.message}`; + } else { + return `Captured an error: ${this.state.error}`; + } + } + if (this.state.shouldThrow) { + return ; + } + return Trigger error; + } +} +class Example extends React.Component { + state = {key: 0}; + restart = () => { + this.setState(state => ({key: state.key + 1})); + }; + render() { + return ( + + + Reset + + ); + } +} + +export default class ErrorHandlingTestCases extends React.Component { + render() { + return ( + + + + Open the browser DevTools + Make sure "Pause on exceptions" is enabled + Make sure "Pause on caught exceptions" is disabled + Click the "Trigger error" button + Click the reset button + + + The DevTools should pause at the line where the error was thrown, in + the BadRender component. After resuming, the "Trigger error" button + should be replaced with "Captured an error: Oops!" Clicking reset + should reset the test case. + + + + + + Click the "Trigger error" button + Click the reset button + + + The "Trigger error" button should be replaced with "Captured an + error: null". Clicking reset should reset the test case. + + + + + ); + } +} diff --git a/fixtures/dom/src/components/fixtures/index.js b/fixtures/dom/src/components/fixtures/index.js index 295d8c572a..76712994e1 100644 --- a/fixtures/dom/src/components/fixtures/index.js +++ b/fixtures/dom/src/components/fixtures/index.js @@ -7,6 +7,7 @@ import InputChangeEvents from './input-change-events'; import NumberInputFixtures from './number-inputs'; import PasswordInputFixtures from './password-inputs'; import ButtonFixtures from './buttons'; +import ErrorHandling from './error-handling'; /** * A simple routing component that renders the appropriate @@ -30,6 +31,8 @@ function FixturesPage() { return ; case '/buttons': return ; + case '/error-handling': + return ; default: return Please select a test fixture.; } diff --git a/scripts/fiber/tests-passing.txt b/scripts/fiber/tests-passing.txt index eaf497de2c..9269029426 100644 --- a/scripts/fiber/tests-passing.txt +++ b/scripts/fiber/tests-passing.txt @@ -2328,21 +2328,22 @@ src/renderers/shared/stack/reconciler/__tests__/Transaction-test.js * should allow nesting of transactions src/renderers/shared/utils/__tests__/ReactErrorUtils-test.js -* it should rethrow errors caught by invokeGuardedCallbackAndCatchFirstError (development) +* it should rethrow caught errors (development) * should call the callback the passed arguments (development) * should call the callback with the provided context (development) -* should return a caught error (development) -* should return null if no error is thrown (development) +* should catch errors (development) +* should return false from clearCaughtError if no error was thrown (development) * can nest with same debug name (development) * does not return nested errors (development) * can be shimmed (development) -* it should rethrow errors caught by invokeGuardedCallbackAndCatchFirstError (production) +* it should rethrow caught errors (production) * should call the callback the passed arguments (production) * should call the callback with the provided context (production) -* should return a caught error (production) -* should return null if no error is thrown (production) +* should catch errors (production) +* should return false from clearCaughtError if no error was thrown (production) * can nest with same debug name (production) * does not return nested errors (production) +* catches null values * can be shimmed (production) src/renderers/shared/utils/__tests__/accumulateInto-test.js diff --git a/scripts/jest/fiber.setup.js b/scripts/jest/fiber.setup.js index aadd3321bc..02d639057b 100644 --- a/scripts/jest/fiber.setup.js +++ b/scripts/jest/fiber.setup.js @@ -12,6 +12,7 @@ jest.mock('ReactFeatureFlags', () => { const flags = require.requireActual('ReactFeatureFlags'); return Object.assign({}, flags, { disableNewFiberFeatures: true, + forceInvokeGuardedCallbackDev: true, }); }); jest.mock('ReactNativeFeatureFlags', () => { diff --git a/src/renderers/shared/fiber/ReactFiberCommitWork.js b/src/renderers/shared/fiber/ReactFiberCommitWork.js index 23a0230e06..d8c3beb6c8 100644 --- a/src/renderers/shared/fiber/ReactFiberCommitWork.js +++ b/src/renderers/shared/fiber/ReactFiberCommitWork.js @@ -26,7 +26,11 @@ var { } = ReactTypeOfWork; var {commitCallbacks} = require('ReactFiberUpdateQueue'); var {onCommitUnmount} = require('ReactFiberDevToolsHook'); -var {invokeGuardedCallback} = require('ReactErrorUtils'); +var { + invokeGuardedCallback, + hasCaughtError, + clearCaughtError, +} = require('ReactErrorUtils'); var { Placement, @@ -70,14 +74,15 @@ module.exports = function( // Capture errors so they don't interrupt unmounting. function safelyCallComponentWillUnmount(current, instance) { if (__DEV__) { - const unmountError = invokeGuardedCallback( + invokeGuardedCallback( null, callComponentWillUnmountWithTimerInDev, null, current, instance, ); - if (unmountError) { + if (hasCaughtError()) { + const unmountError = clearCaughtError(); captureError(current, unmountError); } } else { @@ -93,8 +98,9 @@ module.exports = function( const ref = current.ref; if (ref !== null) { if (__DEV__) { - const refError = invokeGuardedCallback(null, ref, null, null); - if (refError !== null) { + invokeGuardedCallback(null, ref, null, null); + if (hasCaughtError()) { + const refError = clearCaughtError(); captureError(current, refError); } } else { diff --git a/src/renderers/shared/fiber/ReactFiberErrorLogger.js b/src/renderers/shared/fiber/ReactFiberErrorLogger.js index 4394cc9c05..a35cfa4230 100644 --- a/src/renderers/shared/fiber/ReactFiberErrorLogger.js +++ b/src/renderers/shared/fiber/ReactFiberErrorLogger.js @@ -29,18 +29,38 @@ function logCapturedError(capturedError: CapturedError): void { return; } + const error = (capturedError.error: any); + + // Duck-typing + let message; + let name; + let stack; + + if ( + error !== null && + typeof error.message === 'string' && + typeof error.name === 'string' && + typeof error.stack === 'string' + ) { + message = error.message; + name = error.name; + stack = error.stack; + } else { + // A non-error was thrown. + message = '' + error; + name = 'Error'; + stack = ''; + } + if (__DEV__) { const { componentName, componentStack, - error, errorBoundaryName, errorBoundaryFound, willRetry, } = capturedError; - const {message, name, stack} = error; - const errorSummary = message ? `${name}: ${message}` : name; const componentNameMessage = componentName @@ -85,10 +105,7 @@ function logCapturedError(capturedError: CapturedError): void { `The error is located at: ${componentStack}\n\n` + `The error was thrown at: ${formattedCallStack}`, ); - } - - if (!__DEV__) { - const {error} = capturedError; + } else { console.error( `React caught an error thrown by one of your components.\n\n${error.stack}`, ); diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js index 22f70f9066..c6147347fc 100644 --- a/src/renderers/shared/fiber/ReactFiberScheduler.js +++ b/src/renderers/shared/fiber/ReactFiberScheduler.js @@ -21,7 +21,7 @@ import type {HydrationContext} from 'ReactFiberHydrationContext'; export type CapturedError = { componentName: ?string, componentStack: string, - error: Error, + error: mixed, errorBoundary: ?Object, errorBoundaryFound: boolean, errorBoundaryName: string | null, @@ -38,7 +38,11 @@ var { getStackAddendumByWorkInProgressFiber, } = require('ReactFiberComponentTreeHook'); var {logCapturedError} = require('ReactFiberErrorLogger'); -var {invokeGuardedCallback} = require('ReactErrorUtils'); +var { + invokeGuardedCallback, + hasCaughtError, + clearCaughtError, +} = require('ReactErrorUtils'); var ReactFiberBeginWork = require('ReactFiberBeginWork'); var ReactFiberCompleteWork = require('ReactFiberCompleteWork'); @@ -222,7 +226,7 @@ module.exports = function( let failedBoundaries: Set | null = null; // Error boundaries that captured an error during the current commit. let commitPhaseBoundaries: Set | null = null; - let firstUncaughtError: Error | null = null; + let firstUncaughtError: mixed | null = null; let didFatal: boolean = false; let isCommitting: boolean = false; @@ -468,17 +472,23 @@ module.exports = function( startCommitHostEffectsTimer(); } while (nextEffect !== null) { - let error = null; + let didError = false; + let error; if (__DEV__) { - error = invokeGuardedCallback(null, commitAllHostEffects, null); + invokeGuardedCallback(null, commitAllHostEffects, null); + if (hasCaughtError()) { + didError = true; + error = clearCaughtError(); + } } else { try { commitAllHostEffects(); } catch (e) { + didError = true; error = e; } } - if (error !== null) { + if (didError) { invariant( nextEffect !== null, 'Should have next effect. This error is likely caused by a bug ' + @@ -512,17 +522,23 @@ module.exports = function( startCommitLifeCyclesTimer(); } while (nextEffect !== null) { - let error = null; + let didError = false; + let error; if (__DEV__) { - error = invokeGuardedCallback(null, commitAllLifeCycles, null); + invokeGuardedCallback(null, commitAllLifeCycles, null); + if (hasCaughtError()) { + didError = true; + error = clearCaughtError(); + } } else { try { commitAllLifeCycles(); } catch (e) { + didError = true; error = e; } } - if (error !== null) { + if (didError) { invariant( nextEffect !== null, 'Should have next effect. This error is likely caused by a bug ' + @@ -951,26 +967,25 @@ module.exports = function( // reset it at the end. const previousPriorityContext = priorityContext; - let error; + let didError = false; + let error = null; if (__DEV__) { - error = invokeGuardedCallback( - null, - workLoop, - null, - minPriorityLevel, - deadline, - ); + invokeGuardedCallback(null, workLoop, null, minPriorityLevel, deadline); + if (hasCaughtError()) { + didError = true; + error = clearCaughtError(); + } } else { try { workLoop(minPriorityLevel, deadline); - error = null; } catch (e) { + didError = true; error = e; } } // An error was thrown during the render phase. - while (error !== null) { + while (didError) { if (didFatal) { // This was a fatal error. Don't attempt to recover from it. firstUncaughtError = error; @@ -1000,8 +1015,10 @@ module.exports = function( continue; } + didError = false; + error = null; if (__DEV__) { - error = invokeGuardedCallback( + invokeGuardedCallback( null, performWorkCatchBlock, null, @@ -1010,6 +1027,11 @@ module.exports = function( minPriorityLevel, deadline, ); + if (hasCaughtError()) { + didError = true; + error = clearCaughtError(); + continue; + } } else { try { performWorkCatchBlock( @@ -1020,16 +1042,11 @@ module.exports = function( ); error = null; } catch (e) { + didError = true; error = e; + continue; } } - - if (error !== null) { - // Another error was thrown during the render phase. Continue the - // loop to handle the new error. - continue; - } - // We're finished working. Exit the error loop. break; } @@ -1067,7 +1084,7 @@ module.exports = function( } // Returns the boundary that captured the error, or null if the error is ignored - function captureError(failedWork: Fiber, error: Error): Fiber | null { + function captureError(failedWork: Fiber, error: mixed): Fiber | null { // It is no longer valid because we exited the user code. ReactCurrentOwner.current = null; if (__DEV__) { diff --git a/src/renderers/shared/utils/ReactErrorUtils.js b/src/renderers/shared/utils/ReactErrorUtils.js index a16e885ef7..afea4a7882 100644 --- a/src/renderers/shared/utils/ReactErrorUtils.js +++ b/src/renderers/shared/utils/ReactErrorUtils.js @@ -14,69 +14,15 @@ const invariant = require('fbjs/lib/invariant'); -let caughtError = null; - -let invokeGuardedCallback = function(name, func, context, a, b, c, d, e, f) { - const funcArgs = Array.prototype.slice.call(arguments, 3); - try { - func.apply(context, funcArgs); - } catch (error) { - return error; - } - return null; -}; - -if (__DEV__) { - /** - * To help development we can get better devtools integration by simulating a - * real browser event. - */ - if ( - typeof window !== 'undefined' && - typeof window.dispatchEvent === 'function' && - typeof document !== 'undefined' && - typeof document.createEvent === 'function' - ) { - const fakeNode = document.createElement('react'); - let depth = 0; - - invokeGuardedCallback = function(name, func, context, a, b, c, d, e, f) { - depth++; - const thisDepth = depth; - const funcArgs = Array.prototype.slice.call(arguments, 3); - const boundFunc = function() { - func.apply(context, funcArgs); - }; - let fakeEventError = null; - const onFakeEventError = function(event) { - // Don't capture nested errors - if (depth === thisDepth) { - fakeEventError = event.error; - } - }; - const evtType = `react-${name ? name : 'invokeguardedcallback'}-${depth}`; - window.addEventListener('error', onFakeEventError); - fakeNode.addEventListener(evtType, boundFunc, false); - const evt = document.createEvent('Event'); - evt.initEvent(evtType, false, false); - fakeNode.dispatchEvent(evt); - fakeNode.removeEventListener(evtType, boundFunc, false); - window.removeEventListener('error', onFakeEventError); - depth--; - return fakeEventError; - }; - } -} - -let rethrowCaughtError = function() { - if (caughtError) { - const error = caughtError; - caughtError = null; - throw error; - } -}; - const ReactErrorUtils = { + // Used by Fiber to simulate a try-catch. + _caughtError: null, + _hasCaughtError: false, + + // Used by event system to capture/rethrow the first error. + _rethrowError: null, + _hasRethrowError: false, + injection: { injectErrorUtils(injectedErrorUtils: Object) { invariant( @@ -106,8 +52,8 @@ const ReactErrorUtils = { d: D, e: E, f: F, - ): Error | null { - return invokeGuardedCallback.apply(this, arguments); + ): void { + invokeGuardedCallback.apply(ReactErrorUtils, arguments); }, /** @@ -130,9 +76,13 @@ const ReactErrorUtils = { e: E, f: F, ): void { - const error = ReactErrorUtils.invokeGuardedCallback.apply(this, arguments); - if (error !== null && caughtError === null) { - caughtError = error; + ReactErrorUtils.invokeGuardedCallback.apply(this, arguments); + if (ReactErrorUtils.hasCaughtError()) { + const error = ReactErrorUtils.clearCaughtError(); + if (!ReactErrorUtils._hasRethrowError) { + ReactErrorUtils._hasRethrowError = true; + ReactErrorUtils._rethrowError = error; + } } }, @@ -141,8 +91,151 @@ const ReactErrorUtils = { * we will rethrow to be handled by the top level error handler. */ rethrowCaughtError: function() { - return rethrowCaughtError.apply(this, arguments); + return rethrowCaughtError.apply(ReactErrorUtils, arguments); + }, + + hasCaughtError: function() { + return ReactErrorUtils._hasCaughtError; + }, + + clearCaughtError: function() { + if (ReactErrorUtils._hasCaughtError) { + const error = ReactErrorUtils._caughtError; + ReactErrorUtils._caughtError = null; + ReactErrorUtils._hasCaughtError = false; + return error; + } else { + invariant( + false, + 'clearCaughtError was called but no error was captured. This error ' + + 'is likely caused by a bug in React. Please file an issue.', + ); + } }, }; +let invokeGuardedCallback = function(name, func, context, a, b, c, d, e, f) { + ReactErrorUtils._hasCaughtError = false; + ReactErrorUtils._caughtError = null; + const funcArgs = Array.prototype.slice.call(arguments, 3); + try { + func.apply(context, funcArgs); + } catch (error) { + ReactErrorUtils._caughtError = error; + ReactErrorUtils._hasCaughtError = true; + } +}; + +if (__DEV__) { + const ReactFeatureFlags = require('ReactFeatureFlags'); + + if ( + typeof window !== 'undefined' && + typeof window.dispatchEvent === 'function' && + typeof document !== 'undefined' && + typeof document.createEvent === 'function' + ) { + let preventDefault = true; + + /** + * To help development we can get better devtools integration by simulating a + * real browser event. + */ + const fakeNode = document.createElement('react'); + let depth = 0; + + const invokeGuardedCallbackDev = function( + name, + func, + context, + a, + b, + c, + d, + e, + f, + ) { + ReactErrorUtils._hasCaughtError = false; + ReactErrorUtils._caughtError = null; + + depth++; + const thisDepth = depth; + const funcArgs = Array.prototype.slice.call(arguments, 3); + const boundFunc = function() { + func.apply(context, funcArgs); + }; + const onFakeEventError = function(event) { + // Don't capture nested errors + if (depth === thisDepth) { + ReactErrorUtils._caughtError = event.error; + ReactErrorUtils._hasCaughtError = true; + } + if (preventDefault) { + event.preventDefault(); + } + }; + const evtType = `react-${name ? name : 'invokeguardedcallback'}-${depth}`; + window.addEventListener('error', onFakeEventError); + fakeNode.addEventListener(evtType, boundFunc, false); + const evt = document.createEvent('Event'); + evt.initEvent(evtType, false, false); + fakeNode.dispatchEvent(evt); + fakeNode.removeEventListener(evtType, boundFunc, false); + window.removeEventListener('error', onFakeEventError); + depth--; + }; + + // Feature test the development version of invokeGuardedCallback + // before enabling. + let useInvokeGuardedCallbackDev; + if (ReactFeatureFlags.forceInvokeGuardedCallbackDev) { + // jsdom doesn't handle throwing null correctly (it fails when attempting + // to access the 'message' property) but we need the ability to test it. + // We use a feature flag to override the default feature test. + useInvokeGuardedCallbackDev = true; + } else { + try { + const err = new Error('test'); + invokeGuardedCallbackDev( + null, + () => { + throw err; + }, + null, + ); + const A = ReactErrorUtils.clearCaughtError(); + + invokeGuardedCallbackDev( + null, + () => { + throw null; + }, + null, + ); + const B = ReactErrorUtils.clearCaughtError(); + + if (A === err && B === null) { + useInvokeGuardedCallbackDev = true; + } + } catch (e) { + useInvokeGuardedCallbackDev = false; + } + } + + if (useInvokeGuardedCallbackDev) { + invokeGuardedCallback = invokeGuardedCallbackDev; + preventDefault = false; + } + } +} + +let rethrowCaughtError = function() { + if (ReactErrorUtils._hasRethrowError) { + const error = ReactErrorUtils._rethrowError; + ReactErrorUtils._rethrowError = null; + ReactErrorUtils._hasRethrowError = false; + throw error; + } +}; + module.exports = ReactErrorUtils; diff --git a/src/renderers/shared/utils/ReactFeatureFlags.js b/src/renderers/shared/utils/ReactFeatureFlags.js index ae3a710354..6686c023cd 100644 --- a/src/renderers/shared/utils/ReactFeatureFlags.js +++ b/src/renderers/shared/utils/ReactFeatureFlags.js @@ -15,6 +15,8 @@ var ReactFeatureFlags = { disableNewFiberFeatures: false, enableAsyncSubtreeAPI: false, + // We set this to true when running unit tests + forceInvokeGuardedCallbackDev: false, }; module.exports = ReactFeatureFlags; diff --git a/src/renderers/shared/utils/__tests__/ReactErrorUtils-test.js b/src/renderers/shared/utils/__tests__/ReactErrorUtils-test.js index 327e529011..7384e5c84f 100644 --- a/src/renderers/shared/utils/__tests__/ReactErrorUtils-test.js +++ b/src/renderers/shared/utils/__tests__/ReactErrorUtils-test.js @@ -45,7 +45,7 @@ describe('ReactErrorUtils', () => { }); function invokeGuardedCallbackTests(environment) { - it(`it should rethrow errors caught by invokeGuardedCallbackAndCatchFirstError (${environment})`, () => { + it(`it should rethrow caught errors (${environment})`, () => { var err = new Error('foo'); var callback = function() { throw err; @@ -55,6 +55,7 @@ describe('ReactErrorUtils', () => { callback, null, ); + expect(ReactErrorUtils.hasCaughtError()).toBe(false); expect(() => ReactErrorUtils.rethrowCaughtError()).toThrow(err); }); @@ -82,7 +83,7 @@ describe('ReactErrorUtils', () => { expect(context.didCall).toBe(true); }); - it(`should return a caught error (${environment})`, () => { + it(`should catch errors (${environment})`, () => { const error = new Error(); const returnValue = ReactErrorUtils.invokeGuardedCallback( 'foo', @@ -93,37 +94,39 @@ describe('ReactErrorUtils', () => { 'arg1', 'arg2', ); - expect(returnValue).toBe(error); + expect(returnValue).toBe(undefined); + expect(ReactErrorUtils.hasCaughtError()).toBe(true); + expect(ReactErrorUtils.clearCaughtError()).toBe(error); }); - it(`should return null if no error is thrown (${environment})`, () => { + it(`should return false from clearCaughtError if no error was thrown (${environment})`, () => { var callback = jest.fn(); - const returnValue = ReactErrorUtils.invokeGuardedCallback( - 'foo', - callback, - null, - ); - expect(returnValue).toBe(null); + ReactErrorUtils.invokeGuardedCallback('foo', callback, null); + expect(ReactErrorUtils.hasCaughtError()).toBe(false); + expect(ReactErrorUtils.clearCaughtError).toThrow('no error'); }); it(`can nest with same debug name (${environment})`, () => { const err1 = new Error(); let err2; const err3 = new Error(); - const err4 = ReactErrorUtils.invokeGuardedCallback( + let err4; + ReactErrorUtils.invokeGuardedCallback( 'foo', function() { - err2 = ReactErrorUtils.invokeGuardedCallback( + ReactErrorUtils.invokeGuardedCallback( 'foo', function() { throw err1; }, null, ); + err2 = ReactErrorUtils.clearCaughtError(); throw err3; }, null, ); + err4 = ReactErrorUtils.clearCaughtError(); expect(err2).toBe(err1); expect(err4).toBe(err3); @@ -132,36 +135,56 @@ describe('ReactErrorUtils', () => { it(`does not return nested errors (${environment})`, () => { const err1 = new Error(); let err2; - const err3 = ReactErrorUtils.invokeGuardedCallback( + ReactErrorUtils.invokeGuardedCallback( 'foo', function() { - err2 = ReactErrorUtils.invokeGuardedCallback( + ReactErrorUtils.invokeGuardedCallback( 'foo', function() { throw err1; }, null, ); + err2 = ReactErrorUtils.clearCaughtError(); }, null, ); + // Returns null because inner error was already captured + expect(ReactErrorUtils.hasCaughtError()).toBe(false); - expect(err3).toBe(null); // Returns null because inner error was already captured expect(err2).toBe(err1); }); + if (environment === 'production') { + // jsdom doesn't handle this properly, but Chrome and Firefox should. Test + // this with a fixture. + it('catches null values', () => { + ReactErrorUtils.invokeGuardedCallback( + null, + function() { + throw null; + }, + null, + ); + expect(ReactErrorUtils.hasCaughtError()).toBe(true); + expect(ReactErrorUtils.clearCaughtError()).toBe(null); + }); + } + it(`can be shimmed (${environment})`, () => { const ops = []; // Override the original invokeGuardedCallback - ReactErrorUtils.invokeGuardedCallback = function(name, func, context, a) { - ops.push(a); - try { - func.call(context, a); - } catch (error) { - return error; - } - return null; - }; + ReactErrorUtils.injection.injectErrorUtils({ + invokeGuardedCallback(name, func, context, a) { + ops.push(a); + try { + func.call(context, a); + } catch (error) { + this._hasCaughtError = true; + this._caughtError = error; + } + }, + }); var err = new Error('foo'); var callback = function() { @@ -174,9 +197,6 @@ describe('ReactErrorUtils', () => { 'somearg', ); expect(() => ReactErrorUtils.rethrowCaughtError()).toThrow(err); - // invokeGuardedCallbackAndCatchFirstError and rethrowCaughtError close - // over ReactErrorUtils.invokeGuardedCallback so should use the - // shimmed version. expect(ops).toEqual(['somearg']); }); }
Please select a test fixture.