diff --git a/packages/react-dom/src/__tests__/ReactDOM-test.js b/packages/react-dom/src/__tests__/ReactDOM-test.js index dde2608bfa3bd..f10ebfb6dbcdb 100644 --- a/packages/react-dom/src/__tests__/ReactDOM-test.js +++ b/packages/react-dom/src/__tests__/ReactDOM-test.js @@ -372,4 +372,43 @@ describe('ReactDOM', () => { delete global.__REACT_DEVTOOLS_GLOBAL_HOOK__; } }); + + it('throws in DEV if jsdom is destroyed by the time setState() is called', () => { + spyOnDev(console, 'error'); + class App extends React.Component { + state = {x: 1}; + render() { + return
; + } + } + const container = document.createElement('div'); + const instance = ReactDOM.render(, container); + const documentDescriptor = Object.getOwnPropertyDescriptor( + global, + 'document', + ); + try { + // Emulate jsdom environment cleanup. + // This is roughly what happens if the test finished and then + // an asynchronous callback tried to setState() after this. + delete global.document; + const fn = () => instance.setState({x: 2}); + if (__DEV__) { + expect(fn).toThrow( + 'The `document` global was defined when React was initialized, but is not ' + + 'defined anymore. This can happen in a test environment if a component ' + + 'schedules an update from an asynchronous callback, but the test has already ' + + 'finished running. To solve this, you can either unmount the component at ' + + 'the end of your test (and ensure that any asynchronous operations get ' + + 'canceled in `componentWillUnmount`), or you can change the test itself ' + + 'to be asynchronous.', + ); + } else { + expect(fn).not.toThrow(); + } + } finally { + // Don't break other tests. + Object.defineProperty(global, 'document', documentDescriptor); + } + }); }); diff --git a/packages/shared/ReactErrorUtils.js b/packages/shared/ReactErrorUtils.js index cc59ee6fb4bc5..cbbfc81652bec 100644 --- a/packages/shared/ReactErrorUtils.js +++ b/packages/shared/ReactErrorUtils.js @@ -167,6 +167,22 @@ if (__DEV__) { e, f, ) { + // If document doesn't exist we know for sure we will crash in this method + // when we call document.createEvent(). However this can cause confusing + // errors: https://github.com/facebookincubator/create-react-app/issues/3482 + // So we preemptively throw with a better message instead. + invariant( + typeof document !== 'undefined', + 'The `document` global was defined when React was initialized, but is not ' + + 'defined anymore. This can happen in a test environment if a component ' + + 'schedules an update from an asynchronous callback, but the test has already ' + + 'finished running. To solve this, you can either unmount the component at ' + + 'the end of your test (and ensure that any asynchronous operations get ' + + 'canceled in `componentWillUnmount`), or you can change the test itself ' + + 'to be asynchronous.', + ); + const evt = document.createEvent('Event'); + // Keeps track of whether the user-provided callback threw an error. We // set this to true at the beginning, then set it to false right after // calling the function. If the function errors, `didError` will never be @@ -222,7 +238,6 @@ if (__DEV__) { // Synchronously dispatch our fake event. If the user-provided function // errors, it will trigger our global error handler. - const evt = document.createEvent('Event'); evt.initEvent(evtType, false, false); fakeNode.dispatchEvent(evt);