Skip to content

Commit

Permalink
Throw if document is missing by the time invokeGuardedCallbackDev r…
Browse files Browse the repository at this point in the history
…uns (facebook#11677)

* Warn if `document` is missing by the time invokeGuardedCallback runs in DEV

* Typo

* Add a comment

* Use invariant() instead

* Create event immediately for clarity
  • Loading branch information
gaearon authored and madeinfree committed Nov 30, 2017
1 parent bd61655 commit 10f9f9e
Show file tree
Hide file tree
Showing 2 changed files with 55 additions and 1 deletion.
39 changes: 39 additions & 0 deletions packages/react-dom/src/__tests__/ReactDOM-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 <div />;
}
}
const container = document.createElement('div');
const instance = ReactDOM.render(<App />, 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);
}
});
});
17 changes: 16 additions & 1 deletion packages/shared/ReactErrorUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);

Expand Down

0 comments on commit 10f9f9e

Please sign in to comment.