Throw if `document` is missing by the time invokeGuardedCallbackDev runs (#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
Dan Abramov committed
Nov 29, 2017 at 15:53 UTC
9491dee79586d21c115cd4d5986c81b7d88d2b3f
2 files changed
+55
-1
packages/react-dom/src/__tests__/ReactDOM-test.js
+39
@@ -372,4 +372,43 @@ describe('ReactDOM', () => {
372
delete global.__REACT_DEVTOOLS_GLOBAL_HOOK__;
373
}
374
});
375
+
376
+ it('throws in DEV if jsdom is destroyed by the time setState() is called', () => {
377
+ spyOnDev(console, 'error');
378
+ class App extends React.Component {
379
+ state = {x: 1};
380
+ render() {
381
+ return <div />;
382
+ }
383
+ }
384
+ const container = document.createElement('div');
385
+ const instance = ReactDOM.render(<App />, container);
386
+ const documentDescriptor = Object.getOwnPropertyDescriptor(
387
+ global,
388
+ 'document',
389
+ );
390
+ try {
391
+ // Emulate jsdom environment cleanup.
392
+ // This is roughly what happens if the test finished and then
393
+ // an asynchronous callback tried to setState() after this.
394
+ delete global.document;
395
+ const fn = () => instance.setState({x: 2});
396
+ if (__DEV__) {
397
+ expect(fn).toThrow(
398
+ 'The `document` global was defined when React was initialized, but is not ' +
399
+ 'defined anymore. This can happen in a test environment if a component ' +
400
+ 'schedules an update from an asynchronous callback, but the test has already ' +
401
+ 'finished running. To solve this, you can either unmount the component at ' +
402
+ 'the end of your test (and ensure that any asynchronous operations get ' +
403
+ 'canceled in `componentWillUnmount`), or you can change the test itself ' +
404
+ 'to be asynchronous.',
405
+ );
406
+ } else {
407
+ expect(fn).not.toThrow();
408
+ }
409
+ } finally {
410
+ // Don't break other tests.
411
+ Object.defineProperty(global, 'document', documentDescriptor);
412
+ }
413
+ });
414
});
packages/shared/ReactErrorUtils.js
+16
-1
@@ -167,6 +167,22 @@ if (__DEV__) {
167
e,
168
f,
169
) {
170
+ // If document doesn't exist we know for sure we will crash in this method
171
+ // when we call document.createEvent(). However this can cause confusing
172
+ // errors: https://github.com/facebookincubator/create-react-app/issues/3482
173
+ // So we preemptively throw with a better message instead.
174
+ invariant(
175
+ typeof document !== 'undefined',
176
+ 'The `document` global was defined when React was initialized, but is not ' +
177
+ 'defined anymore. This can happen in a test environment if a component ' +
178
+ 'schedules an update from an asynchronous callback, but the test has already ' +
179
+ 'finished running. To solve this, you can either unmount the component at ' +
180
+ 'the end of your test (and ensure that any asynchronous operations get ' +
181
+ 'canceled in `componentWillUnmount`), or you can change the test itself ' +
182
+ 'to be asynchronous.',
183
+ );
184
+ const evt = document.createEvent('Event');
185
+
186
// Keeps track of whether the user-provided callback threw an error. We
187
// set this to true at the beginning, then set it to false right after
188
// calling the function. If the function errors, `didError` will never be
@@ -222,7 +238,6 @@ if (__DEV__) {
238
239
// Synchronously dispatch our fake event. If the user-provided function
240
// errors, it will trigger our global error handler.
225
- const evt = document.createEvent('Event');
241
evt.initEvent(evtType, false, false);
242
fakeNode.dispatchEvent(evt);
243