@samitouri / QOS-React / commits / 9491dee795

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