2016-11-29 00:54:57 +08:00
|
|
|
/**
|
2017-09-25 04:48:13 +08:00
|
|
|
* Copyright (c) 2014-present, Facebook, Inc.
|
2016-11-29 00:54:57 +08:00
|
|
|
*
|
2017-09-25 04:48:13 +08:00
|
|
|
* This source code is licensed under the MIT license found in the
|
|
|
|
|
* LICENSE file in the root directory of this source tree.
|
2016-11-29 00:54:57 +08:00
|
|
|
*
|
|
|
|
|
* @emails react-core
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
'use strict';
|
|
|
|
|
|
|
|
|
|
var ReactErrorUtils;
|
|
|
|
|
|
|
|
|
|
describe('ReactErrorUtils', () => {
|
|
|
|
|
beforeEach(() => {
|
2017-06-14 07:58:35 +08:00
|
|
|
// TODO: can we express this test with only public API?
|
2017-11-03 03:50:03 +08:00
|
|
|
ReactErrorUtils = require('shared/ReactErrorUtils').default;
|
2016-11-29 00:54:57 +08:00
|
|
|
});
|
|
|
|
|
|
2017-02-25 08:19:29 +08:00
|
|
|
// Run tests in both DEV and production
|
2017-03-14 08:05:18 +08:00
|
|
|
describe(
|
|
|
|
|
'invokeGuardedCallback (development)',
|
|
|
|
|
invokeGuardedCallbackTests.bind(null, 'development'),
|
|
|
|
|
);
|
2017-02-25 08:19:29 +08:00
|
|
|
describe('invokeGuardedCallback (production)', () => {
|
|
|
|
|
let oldProcess;
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
__DEV__ = false;
|
2017-07-27 03:40:09 +08:00
|
|
|
|
|
|
|
|
// 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.
|
2017-02-25 08:19:29 +08:00
|
|
|
oldProcess = process;
|
|
|
|
|
global.process = {
|
2017-07-27 03:40:09 +08:00
|
|
|
...process,
|
|
|
|
|
env: {...process.env, NODE_ENV: 'production'},
|
2017-02-25 08:19:29 +08:00
|
|
|
};
|
2017-07-27 03:40:09 +08:00
|
|
|
|
2017-02-25 08:19:29 +08:00
|
|
|
jest.resetModules();
|
2017-11-03 03:50:03 +08:00
|
|
|
ReactErrorUtils = require('shared/ReactErrorUtils').default;
|
2017-02-09 06:20:23 +08:00
|
|
|
});
|
|
|
|
|
|
2017-02-25 08:19:29 +08:00
|
|
|
afterEach(() => {
|
|
|
|
|
__DEV__ = true;
|
|
|
|
|
global.process = oldProcess;
|
2017-02-09 06:20:23 +08:00
|
|
|
});
|
|
|
|
|
|
2017-02-25 08:19:29 +08:00
|
|
|
invokeGuardedCallbackTests('production');
|
2016-11-29 00:54:57 +08:00
|
|
|
});
|
|
|
|
|
|
2017-02-25 08:19:29 +08:00
|
|
|
function invokeGuardedCallbackTests(environment) {
|
2017-07-22 06:34:41 +08:00
|
|
|
it(`it should rethrow caught errors (${environment})`, () => {
|
2016-11-29 00:54:57 +08:00
|
|
|
var err = new Error('foo');
|
|
|
|
|
var callback = function() {
|
|
|
|
|
throw err;
|
|
|
|
|
};
|
2017-03-14 08:05:18 +08:00
|
|
|
ReactErrorUtils.invokeGuardedCallbackAndCatchFirstError(
|
|
|
|
|
'foo',
|
|
|
|
|
callback,
|
|
|
|
|
null,
|
|
|
|
|
);
|
2017-07-22 06:34:41 +08:00
|
|
|
expect(ReactErrorUtils.hasCaughtError()).toBe(false);
|
2016-11-29 00:54:57 +08:00
|
|
|
expect(() => ReactErrorUtils.rethrowCaughtError()).toThrow(err);
|
|
|
|
|
});
|
|
|
|
|
|
2017-02-25 08:19:29 +08:00
|
|
|
it(`should call the callback the passed arguments (${environment})`, () => {
|
2017-02-09 06:20:23 +08:00
|
|
|
var callback = jest.fn();
|
2017-03-14 08:05:18 +08:00
|
|
|
ReactErrorUtils.invokeGuardedCallback(
|
|
|
|
|
'foo',
|
|
|
|
|
callback,
|
|
|
|
|
null,
|
|
|
|
|
'arg1',
|
|
|
|
|
'arg2',
|
|
|
|
|
);
|
2017-02-09 06:20:23 +08:00
|
|
|
expect(callback).toBeCalledWith('arg1', 'arg2');
|
|
|
|
|
});
|
|
|
|
|
|
2017-11-08 02:09:33 +08:00
|
|
|
it(`should call the callback with the provided context (${
|
|
|
|
|
environment
|
|
|
|
|
})`, () => {
|
2017-03-14 08:05:18 +08:00
|
|
|
var context = {didCall: false};
|
|
|
|
|
ReactErrorUtils.invokeGuardedCallback(
|
|
|
|
|
'foo',
|
|
|
|
|
function() {
|
|
|
|
|
this.didCall = true;
|
|
|
|
|
},
|
|
|
|
|
context,
|
|
|
|
|
);
|
2017-02-09 06:20:23 +08:00
|
|
|
expect(context.didCall).toBe(true);
|
|
|
|
|
});
|
|
|
|
|
|
2017-07-22 06:34:41 +08:00
|
|
|
it(`should catch errors (${environment})`, () => {
|
2017-02-09 06:20:23 +08:00
|
|
|
const error = new Error();
|
2017-03-14 08:05:18 +08:00
|
|
|
const returnValue = ReactErrorUtils.invokeGuardedCallback(
|
|
|
|
|
'foo',
|
|
|
|
|
function() {
|
|
|
|
|
throw error;
|
|
|
|
|
},
|
|
|
|
|
null,
|
|
|
|
|
'arg1',
|
|
|
|
|
'arg2',
|
|
|
|
|
);
|
2017-07-22 06:34:41 +08:00
|
|
|
expect(returnValue).toBe(undefined);
|
|
|
|
|
expect(ReactErrorUtils.hasCaughtError()).toBe(true);
|
|
|
|
|
expect(ReactErrorUtils.clearCaughtError()).toBe(error);
|
2017-02-09 06:20:23 +08:00
|
|
|
});
|
|
|
|
|
|
2017-11-08 02:09:33 +08:00
|
|
|
it(`should return false from clearCaughtError if no error was thrown (${
|
|
|
|
|
environment
|
|
|
|
|
})`, () => {
|
2016-11-29 00:54:57 +08:00
|
|
|
var callback = jest.fn();
|
2017-07-22 06:34:41 +08:00
|
|
|
ReactErrorUtils.invokeGuardedCallback('foo', callback, null);
|
|
|
|
|
expect(ReactErrorUtils.hasCaughtError()).toBe(false);
|
2017-07-27 03:39:58 +08:00
|
|
|
expect(ReactErrorUtils.clearCaughtError).toThrow(
|
|
|
|
|
__DEV__ ? 'no error was captured' : 'Minified React error #198',
|
|
|
|
|
);
|
2016-11-29 00:54:57 +08:00
|
|
|
});
|
|
|
|
|
|
2017-02-25 08:19:29 +08:00
|
|
|
it(`can nest with same debug name (${environment})`, () => {
|
2017-02-09 09:11:34 +08:00
|
|
|
const err1 = new Error();
|
|
|
|
|
let err2;
|
|
|
|
|
const err3 = new Error();
|
2017-07-22 06:34:41 +08:00
|
|
|
let err4;
|
|
|
|
|
ReactErrorUtils.invokeGuardedCallback(
|
2017-03-14 08:05:18 +08:00
|
|
|
'foo',
|
|
|
|
|
function() {
|
2017-07-22 06:34:41 +08:00
|
|
|
ReactErrorUtils.invokeGuardedCallback(
|
2017-03-14 08:05:18 +08:00
|
|
|
'foo',
|
|
|
|
|
function() {
|
|
|
|
|
throw err1;
|
|
|
|
|
},
|
|
|
|
|
null,
|
|
|
|
|
);
|
2017-07-22 06:34:41 +08:00
|
|
|
err2 = ReactErrorUtils.clearCaughtError();
|
2017-03-14 08:05:18 +08:00
|
|
|
throw err3;
|
|
|
|
|
},
|
|
|
|
|
null,
|
|
|
|
|
);
|
2017-07-22 06:34:41 +08:00
|
|
|
err4 = ReactErrorUtils.clearCaughtError();
|
2017-02-09 09:11:34 +08:00
|
|
|
|
|
|
|
|
expect(err2).toBe(err1);
|
|
|
|
|
expect(err4).toBe(err3);
|
|
|
|
|
});
|
|
|
|
|
|
[invokeGuardedCallback] Handle nested errors across separate renderers (#10270)
invokeGuardedCallback is a function we use in place of try-catch
statement. It accepts a function, and if the function throws, it
captures the error. In production, the implementation is a normal try-
catch. In development, we swap out the prod implementation for a special
version designed to preserve "Pause on all exceptions" behavior of the
browser DevTools.
invokeGuardedCallbackDev works by dispatching an event to a dummy DOM
node and calling the provided function inside a handler for that event.
We also attach an error event handler to the window object. If the
function throws, the global event handler is called and we can access
the error.
The global event handler is added and removed right before and after the
fake event is dispatched. But if invokeGuardedCallbackDev is nested --
that is, if it's invoked inside the body of another
invokeGuardedCallbackDev -- multiple error event handlers will attached
simultaneously. We only want the handler that corresponds to the deepest
level to handle the error. So we keep track of a depth counter, and
within the event handler, we only handle the error if the current depth
matches the depth at the time the function was invoked.
The problem that we discovered, and that this PR fixes, is that the
depth counter is local to each renderer. So if you nest separate copies
of invokeGuardedCallback from separate renderers, each renderer will
have its own depth counter, and multiple error handlers will fire for a
single, nested error.
2017-07-26 00:44:43 +08:00
|
|
|
it(`handles nested errors (${environment})`, () => {
|
2017-02-09 09:11:34 +08:00
|
|
|
const err1 = new Error();
|
|
|
|
|
let err2;
|
2017-07-22 06:34:41 +08:00
|
|
|
ReactErrorUtils.invokeGuardedCallback(
|
2017-03-14 08:05:18 +08:00
|
|
|
'foo',
|
|
|
|
|
function() {
|
2017-07-22 06:34:41 +08:00
|
|
|
ReactErrorUtils.invokeGuardedCallback(
|
2017-03-14 08:05:18 +08:00
|
|
|
'foo',
|
|
|
|
|
function() {
|
|
|
|
|
throw err1;
|
|
|
|
|
},
|
|
|
|
|
null,
|
|
|
|
|
);
|
2017-07-22 06:34:41 +08:00
|
|
|
err2 = ReactErrorUtils.clearCaughtError();
|
2017-03-14 08:05:18 +08:00
|
|
|
},
|
|
|
|
|
null,
|
|
|
|
|
);
|
2017-07-22 06:34:41 +08:00
|
|
|
// Returns null because inner error was already captured
|
|
|
|
|
expect(ReactErrorUtils.hasCaughtError()).toBe(false);
|
2017-02-09 09:11:34 +08:00
|
|
|
|
|
|
|
|
expect(err2).toBe(err1);
|
|
|
|
|
});
|
2017-03-02 04:48:02 +08:00
|
|
|
|
[invokeGuardedCallback] Handle nested errors across separate renderers (#10270)
invokeGuardedCallback is a function we use in place of try-catch
statement. It accepts a function, and if the function throws, it
captures the error. In production, the implementation is a normal try-
catch. In development, we swap out the prod implementation for a special
version designed to preserve "Pause on all exceptions" behavior of the
browser DevTools.
invokeGuardedCallbackDev works by dispatching an event to a dummy DOM
node and calling the provided function inside a handler for that event.
We also attach an error event handler to the window object. If the
function throws, the global event handler is called and we can access
the error.
The global event handler is added and removed right before and after the
fake event is dispatched. But if invokeGuardedCallbackDev is nested --
that is, if it's invoked inside the body of another
invokeGuardedCallbackDev -- multiple error event handlers will attached
simultaneously. We only want the handler that corresponds to the deepest
level to handle the error. So we keep track of a depth counter, and
within the event handler, we only handle the error if the current depth
matches the depth at the time the function was invoked.
The problem that we discovered, and that this PR fixes, is that the
depth counter is local to each renderer. So if you nest separate copies
of invokeGuardedCallback from separate renderers, each renderer will
have its own depth counter, and multiple error handlers will fire for a
single, nested error.
2017-07-26 00:44:43 +08:00
|
|
|
it('handles nested errors in separate renderers', () => {
|
2017-11-03 03:50:03 +08:00
|
|
|
const ReactErrorUtils1 = require('shared/ReactErrorUtils').default;
|
[invokeGuardedCallback] Handle nested errors across separate renderers (#10270)
invokeGuardedCallback is a function we use in place of try-catch
statement. It accepts a function, and if the function throws, it
captures the error. In production, the implementation is a normal try-
catch. In development, we swap out the prod implementation for a special
version designed to preserve "Pause on all exceptions" behavior of the
browser DevTools.
invokeGuardedCallbackDev works by dispatching an event to a dummy DOM
node and calling the provided function inside a handler for that event.
We also attach an error event handler to the window object. If the
function throws, the global event handler is called and we can access
the error.
The global event handler is added and removed right before and after the
fake event is dispatched. But if invokeGuardedCallbackDev is nested --
that is, if it's invoked inside the body of another
invokeGuardedCallbackDev -- multiple error event handlers will attached
simultaneously. We only want the handler that corresponds to the deepest
level to handle the error. So we keep track of a depth counter, and
within the event handler, we only handle the error if the current depth
matches the depth at the time the function was invoked.
The problem that we discovered, and that this PR fixes, is that the
depth counter is local to each renderer. So if you nest separate copies
of invokeGuardedCallback from separate renderers, each renderer will
have its own depth counter, and multiple error handlers will fire for a
single, nested error.
2017-07-26 00:44:43 +08:00
|
|
|
jest.resetModules();
|
2017-11-03 03:50:03 +08:00
|
|
|
const ReactErrorUtils2 = require('shared/ReactErrorUtils').default;
|
[invokeGuardedCallback] Handle nested errors across separate renderers (#10270)
invokeGuardedCallback is a function we use in place of try-catch
statement. It accepts a function, and if the function throws, it
captures the error. In production, the implementation is a normal try-
catch. In development, we swap out the prod implementation for a special
version designed to preserve "Pause on all exceptions" behavior of the
browser DevTools.
invokeGuardedCallbackDev works by dispatching an event to a dummy DOM
node and calling the provided function inside a handler for that event.
We also attach an error event handler to the window object. If the
function throws, the global event handler is called and we can access
the error.
The global event handler is added and removed right before and after the
fake event is dispatched. But if invokeGuardedCallbackDev is nested --
that is, if it's invoked inside the body of another
invokeGuardedCallbackDev -- multiple error event handlers will attached
simultaneously. We only want the handler that corresponds to the deepest
level to handle the error. So we keep track of a depth counter, and
within the event handler, we only handle the error if the current depth
matches the depth at the time the function was invoked.
The problem that we discovered, and that this PR fixes, is that the
depth counter is local to each renderer. So if you nest separate copies
of invokeGuardedCallback from separate renderers, each renderer will
have its own depth counter, and multiple error handlers will fire for a
single, nested error.
2017-07-26 00:44:43 +08:00
|
|
|
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]);
|
|
|
|
|
});
|
|
|
|
|
|
2017-07-22 06:34:41 +08:00
|
|
|
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() {
|
2017-08-30 08:54:36 +08:00
|
|
|
throw null; // eslint-disable-line no-throw-literal
|
2017-07-22 06:34:41 +08:00
|
|
|
},
|
|
|
|
|
null,
|
|
|
|
|
);
|
|
|
|
|
expect(ReactErrorUtils.hasCaughtError()).toBe(true);
|
|
|
|
|
expect(ReactErrorUtils.clearCaughtError()).toBe(null);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2017-03-02 04:48:02 +08:00
|
|
|
it(`can be shimmed (${environment})`, () => {
|
|
|
|
|
const ops = [];
|
|
|
|
|
// Override the original invokeGuardedCallback
|
2017-07-22 06:34:41 +08:00
|
|
|
ReactErrorUtils.injection.injectErrorUtils({
|
|
|
|
|
invokeGuardedCallback(name, func, context, a) {
|
|
|
|
|
ops.push(a);
|
|
|
|
|
try {
|
|
|
|
|
func.call(context, a);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
this._hasCaughtError = true;
|
|
|
|
|
this._caughtError = error;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
});
|
2017-03-02 04:48:02 +08:00
|
|
|
|
|
|
|
|
var err = new Error('foo');
|
|
|
|
|
var callback = function() {
|
|
|
|
|
throw err;
|
|
|
|
|
};
|
2017-03-14 08:05:18 +08:00
|
|
|
ReactErrorUtils.invokeGuardedCallbackAndCatchFirstError(
|
|
|
|
|
'foo',
|
|
|
|
|
callback,
|
|
|
|
|
null,
|
|
|
|
|
'somearg',
|
|
|
|
|
);
|
2017-03-02 04:48:02 +08:00
|
|
|
expect(() => ReactErrorUtils.rethrowCaughtError()).toThrow(err);
|
|
|
|
|
expect(ops).toEqual(['somearg']);
|
|
|
|
|
});
|
2017-02-25 08:19:29 +08:00
|
|
|
}
|
2016-11-29 00:54:57 +08:00
|
|
|
});
|