/** * Copyright (c) 2014-present, Facebook, Inc. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @emails react-core */ 'use strict'; var ReactErrorUtils; describe('ReactErrorUtils', () => { beforeEach(() => { // TODO: can we express this test with only public API? ReactErrorUtils = require('shared/ReactErrorUtils').default; }); // Run tests in both DEV and production describe( 'invokeGuardedCallback (development)', invokeGuardedCallbackTests.bind(null, 'development'), ); describe('invokeGuardedCallback (production)', () => { let oldProcess; beforeEach(() => { __DEV__ = false; // Mutating process.env.NODE_ENV would cause our babel plugins to do the // wrong thing. If you change this, make sure to test with jest --no-cache. oldProcess = process; global.process = { ...process, env: {...process.env, NODE_ENV: 'production'}, }; jest.resetModules(); ReactErrorUtils = require('shared/ReactErrorUtils').default; }); afterEach(() => { __DEV__ = true; global.process = oldProcess; }); invokeGuardedCallbackTests('production'); }); function invokeGuardedCallbackTests(environment) { it(`it should rethrow caught errors (${environment})`, () => { var err = new Error('foo'); var callback = function() { throw err; }; ReactErrorUtils.invokeGuardedCallbackAndCatchFirstError( 'foo', callback, null, ); expect(ReactErrorUtils.hasCaughtError()).toBe(false); expect(() => ReactErrorUtils.rethrowCaughtError()).toThrow(err); }); it(`should call the callback the passed arguments (${environment})`, () => { var callback = jest.fn(); ReactErrorUtils.invokeGuardedCallback( 'foo', callback, null, 'arg1', 'arg2', ); expect(callback).toBeCalledWith('arg1', 'arg2'); }); it(`should call the callback with the provided context (${ environment })`, () => { var context = {didCall: false}; ReactErrorUtils.invokeGuardedCallback( 'foo', function() { this.didCall = true; }, context, ); expect(context.didCall).toBe(true); }); it(`should catch errors (${environment})`, () => { const error = new Error(); const returnValue = ReactErrorUtils.invokeGuardedCallback( 'foo', function() { throw error; }, null, 'arg1', 'arg2', ); expect(returnValue).toBe(undefined); expect(ReactErrorUtils.hasCaughtError()).toBe(true); expect(ReactErrorUtils.clearCaughtError()).toBe(error); }); it(`should return false from clearCaughtError if no error was thrown (${ environment })`, () => { var callback = jest.fn(); ReactErrorUtils.invokeGuardedCallback('foo', callback, null); expect(ReactErrorUtils.hasCaughtError()).toBe(false); expect(ReactErrorUtils.clearCaughtError).toThrow( __DEV__ ? 'no error was captured' : 'Minified React error #198', ); }); it(`can nest with same debug name (${environment})`, () => { const err1 = new Error(); let err2; const err3 = new Error(); let err4; ReactErrorUtils.invokeGuardedCallback( 'foo', function() { ReactErrorUtils.invokeGuardedCallback( 'foo', function() { throw err1; }, null, ); err2 = ReactErrorUtils.clearCaughtError(); throw err3; }, null, ); err4 = ReactErrorUtils.clearCaughtError(); expect(err2).toBe(err1); expect(err4).toBe(err3); }); it(`handles nested errors (${environment})`, () => { const err1 = new Error(); let err2; ReactErrorUtils.invokeGuardedCallback( 'foo', function() { 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(err2).toBe(err1); }); it('handles nested errors in separate renderers', () => { const ReactErrorUtils1 = require('shared/ReactErrorUtils').default; jest.resetModules(); const ReactErrorUtils2 = require('shared/ReactErrorUtils').default; expect(ReactErrorUtils1).not.toEqual(ReactErrorUtils2); let ops = []; ReactErrorUtils1.invokeGuardedCallback( null, () => { ReactErrorUtils2.invokeGuardedCallback( null, () => { throw new Error('nested error'); }, null, ); // ReactErrorUtils2 should catch the error ops.push(ReactErrorUtils2.hasCaughtError()); ops.push(ReactErrorUtils2.clearCaughtError().message); }, null, ); // ReactErrorUtils1 should not catch the error ops.push(ReactErrorUtils1.hasCaughtError()); expect(ops).toEqual([true, 'nested error', false]); }); 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; // eslint-disable-line no-throw-literal }, null, ); expect(ReactErrorUtils.hasCaughtError()).toBe(true); expect(ReactErrorUtils.clearCaughtError()).toBe(null); }); } it(`can be shimmed (${environment})`, () => { const ops = []; // Override the original invokeGuardedCallback 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() { throw err; }; ReactErrorUtils.invokeGuardedCallbackAndCatchFirstError( 'foo', callback, null, 'somearg', ); expect(() => ReactErrorUtils.rethrowCaughtError()).toThrow(err); expect(ops).toEqual(['somearg']); }); } });