Create-react-app: npm test: setState in api callback: Cannot read property 'createEvent' of undefined

Created on 22 Nov 2017  Â·  12Comments  Â·  Source: facebook/create-react-app

Is this a bug report?

Yes

Can you also reproduce the problem with npm 4.x?

I am on 5.5.1 and it’s not npm related

Environment

node -v
v9.2.0
npm ls react-scripts
[email protected] /opt/foxyboy/sw/pri/code-samples/react-test-error
└── [email protected]
echo $(lsb_release --description --codename --short)
Ubuntu 17.10 artful

Root Cause

I used npm t on a component using unmocked fetch, which of course fails.
However, updating state in the catch clause causes a render with the global document value undefined

node_modules/react-dom/cjs/react-dom.development.js:577

var evt = document.createEvent('Event');

is this a bug?

Steps to Reproduce

  1. Fresh create-react-app
  2. modify index/App.js as below
  3. npm t
class App extends Component {
  constructor(...args) {
    super(...args)
    this.state = {}
  }

  componentDidMount() {
    this.myFetch().catch(e => this.setState({e}))
  }

  async myFetch() {
    await fetch('/proxiedApi')
  }

  render() {
    const {e} = this.state
    return (
      <div className="App">
        <header className="App-header">
          {/*<img src={logo} className="App-logo" alt="logo" />*/}
          <h1 className="App-title">Welcome to React</h1>
        </header>
        <p className="App-intro">
          To get started, edit <code>src/App.js</code> and save to reload.
        </p>
        {e && <p>Error: {String(e)}</p>}
      </div>
    );
  }
}

Expected Behavior

  1. The test to succeed
  2. The component to store the “right” error: TypeError: Network request failed properly

Actual Behavior

TypeError: Cannot read property 'createEvent' of undefined

 RUNS  src/App.test.js                    
/opt/foxyboy/sw/pri/code-samples/react-test-error/node_modules/react-scripts/scripts/test.js:20
  throw err;         
  ^                  

TypeError: Cannot read property 'createEvent' of undefined                          
    at Object.invokeGuardedCallbackDev (/opt/foxyboy/sw/pri/code-samples/react-test-error/node_modules/react-dom/cjs/react-dom.development.js:577:26)
    at invokeGuardedCallback (/opt/foxyboy/sw/pri/code-samples/react-test-error/node_modules/react-dom/cjs/react-dom.development.js:436:27)
    at renderRoot (/opt/foxyboy/sw/pri/code-samples/react-test-error/node_modules/react-dom/cjs/react-dom.development.js:10386:7)
    at performWorkOnRoot (/opt/foxyboy/sw/pri/code-samples/react-test-error/node_modules/react-dom/cjs/react-dom.development.js:11000:24)
    at performWork (/opt/foxyboy/sw/pri/code-samples/react-test-error/node_modules/react-dom/cjs/react-dom.development.js:10952:7)
    at requestWork (/opt/foxyboy/sw/pri/code-samples/react-test-error/node_modules/react-dom/cjs/react-dom.development.js:10861:7)
    at scheduleWorkImpl (/opt/foxyboy/sw/pri/code-samples/react-test-error/node_modules/react-dom/cjs/react-dom.development.js:10744:11)
    at scheduleWork (/opt/foxyboy/sw/pri/code-samples/react-test-error/node_modules/react-dom/cjs/react-dom.development.js:10706:12)
    at Object.enqueueSetState (/opt/foxyboy/sw/pri/code-samples/react-test-error/node_modules/react-dom/cjs/react-dom.development.js:6204:7)
    at App.Object.<anonymous>.Component.setState (/opt/foxyboy/sw/pri/code-samples/react-test-error/node_modules/react/cjs/react.development.js:226:16)
    at myFetch.catch.e (/opt/foxyboy/sw/pri/code-samples/react-test-error/src/App.js:12:36)
    at <anonymous>   
    at process._tickCallback (internal/process/next_tick.js:188:7)                  
npm ERR! Test failed.  See above for more details.                                  
question

Most helpful comment

The problem is your test is now asynchronous but you don't wait for it to finish. So by the time the callback runs, the environment is already cleaned up.

I think the right thing to do for us would be to explicitly unmount the component at the end of the test. For example:

it('renders without crashing', () => {
  const div = document.createElement('div');
  ReactDOM.render(<App />, div);
  ReactDOM.unmountComponentAtNode(div);
});

This would ensure only the initial render is being tested.

You'd still have the problem of a "dead callback" because you never actually cancel it. But you have this problem anyway regardless of tests. If a component starts some work, componentWillUnmount should cancel it. There is no easy way to do cancellation with fetch() API yet as far as I know (at least not until the polyfill updates), so you could set a field yourself:

class App extends Component {
  state = {};
  isMounted = false;

  async componentDidMount() {
    this.isMounted = true;
    try {
      await fetch('/proxiedApi');
    } catch(e) {
      if (this.isMounted) { // <--- don't call setState when unmounted
        this.setState({e});
      }
    }
  }

  componentWillUnmount() {
    this.isMounted = false; // <--- reset it on unmounting
  }

  render() {
    // ...
  }
}

This solution is not ideal but will work. A better one would be to actually cancel the fetch instead of ignoring it. When fetch API supports cancellation you can do this. Or you could use a library like axios that supports it today.

All 12 comments

getaround: check NODE_ENV

  componentDidMount() {
    if (process.env.NODE_ENV === 'test') return // TODO 171121: https://github.com/facebookincubator/create-react-app/issues/3482

if you end up mocking the fetch call, all should be well! I was starting a project from scratch, and ended up here with the exact same error. then I saw I hadn't mocked out my fetch call!

import 'isomorphic-fetch'
import fetchMock from 'fetch-mock'

import App from './App'
import { mockData } from 'data/fixtures'

describe('<App />', () => {

  afterEach(() => {
    fetchMock.reset()
    fetchMock.restore()
  })

  it('renders without crashing', () => {

    fetchMock.getOnce('/initialFetchCall/', mockData)

    mount(
        <App />
    )
  })
})

I am using enzyme 3, react 16

The problem is your test is now asynchronous but you don't wait for it to finish. So by the time the callback runs, the environment is already cleaned up.

I think the right thing to do for us would be to explicitly unmount the component at the end of the test. For example:

it('renders without crashing', () => {
  const div = document.createElement('div');
  ReactDOM.render(<App />, div);
  ReactDOM.unmountComponentAtNode(div);
});

This would ensure only the initial render is being tested.

You'd still have the problem of a "dead callback" because you never actually cancel it. But you have this problem anyway regardless of tests. If a component starts some work, componentWillUnmount should cancel it. There is no easy way to do cancellation with fetch() API yet as far as I know (at least not until the polyfill updates), so you could set a field yourself:

class App extends Component {
  state = {};
  isMounted = false;

  async componentDidMount() {
    this.isMounted = true;
    try {
      await fetch('/proxiedApi');
    } catch(e) {
      if (this.isMounted) { // <--- don't call setState when unmounted
        this.setState({e});
      }
    }
  }

  componentWillUnmount() {
    this.isMounted = false; // <--- reset it on unmounting
  }

  render() {
    // ...
  }
}

This solution is not ideal but will work. A better one would be to actually cancel the fetch instead of ignoring it. When fetch API supports cancellation you can do this. Or you could use a library like axios that supports it today.

I merged https://github.com/facebookincubator/create-react-app/pull/3511. If you make an equivalent change in your test, you can then fix your component to not do anything if it gets unmounted. I'll also look if we can provide a better warning on the React side when this happens.

Here's a PR for React that should provide a better message for this, if merged. https://github.com/facebook/react/pull/11677

For those of us, like @Timer that like to understand root causes, here is that:

When async is used in a React component, the test for that component should be async, too

(async is almost always the answer…)

In this case, the test needs to be kept alive until both render and the promise completes, so the test needs to not only be async, but also have access to the promise. componentDidMount does not have a return value, but we can return the promise, so change the App.js line to:

    return this.myFetch().catch(e => this.setState({e}))

Then make the test asynchronous and instrument componentDidMount: App.test.js:

it('renders without crashing', async () => {

  // instrument
  const {prototype} = App
  const {componentDidMount} = prototype
  let promise
  prototype.componentDidMount = function mock() {
    promise = componentDidMount.apply(this) // this is the App instance
  }

  const div = document.createElement('div');
  await new Promise((resolve, reject) => ReactDOM.render(<App />, div, resolve))

  // wait for the promise to conclude
  expect(promise).toBeInstanceOf(Promise, 'Instrumenting App instance failed')
  await promise

  // remove instrumentation
  Object.assign(prototype, {componentDidMount})
})

As we all know, reactDOM.render has a callback and should be invoked with await

To make these kinds of things less error prone, the great Create React Project could make the test async to begin with

…and for Jestizens to use Jest when mocking a Component method:

it('renders without crashing', async () => {
  const {prototype} = App
  const componentMethodName = 'componentDidMount'
  const spy = jest.spyOn(prototype, componentMethodName)

  let promise
  // 171201: jest.spyOn cannot inspect return values https://github.com/facebook/jest/issues/3821
  // We must therefore mock and intercept componentDidMount invocations to get the return value
  // Jest does not allow direct access to the spiedMethod function value
  // Here’s how to invoke the spied method, though
  const invokeSpiedMethod = spy.getMockImplementation()
  spy.mockImplementation(function mock(...args) {
    // inside the mock implementation, the this value is the App instance
    // the App instance’s method has been mocked and cannot be invoked
    return promise = invokeSpiedMethod.apply(this, args)
  })

  const div = document.createElement('div');
  await new Promise((resolve, reject) => ReactDOM.render(<App />, div, resolve))

  // wait for the promise to conclude
  expect(promise).toBeInstanceOf(Promise, 'Instrumenting App instance failed')
  await promise

  spy.mockReset()
  spy.mockRestore()
})

Just in case:
to debug your Create React App Tests, use:
react-scripts --inspect-brk=0.0.0.0:9229 test --env=jsdom --runInBand -i
and in Chromium, navigate to: chrome://inspect
insert debugger statements at interesting locations

I’m not sure if you read my comments above, but I suggested a way to solve this problem without any mocking or making your component async. Just unmount it in the test, and make sure any async work is canceled on unmounting (which is a good idea anyway).

I saw that: I learned that state should be an ECMAScript instance property. That way you do not have to type out the constructor.

I wanted to leave readers with the root cause that is:
the original test ends early, in fact render is invoked before componentDidMount, so no fixtures, like global document, are available as an async invocation finishes.

Then I wanted to understand what the Jesty and @Facebook’ey way would be to do the async version of the test, ie. mocking and intercepting a ReactJS component method. I discovered there that, for example, Jest cannot presently intercept a component constructor, due to troubles with invocations using the new operator. If that worked, the tested component could be extended which might be useful.

It is more for the future than this particular problem since I use async a lot. Here’s a general learning you will like:
Keep async away from rendering components and instead use redux for resulting UI updates.

I was flattered to have @gaearon look at my ticket.

And here is how to extend a React component during testing in App.test.js:

import React from 'react'
import ReactDOM from 'react-dom'
import App, * as AppImport from './App'

it('renders without crashing', async () => {
  class AppTest extends App {
    static promise
    componentDidMount(...args) {
      return AppTest.promise = super.componentDidMount(...args)
    }
  }

  const App0 = App
  AppImport.default = AppTest

  const div = document.createElement('div');
  await new Promise((resolve, reject) => ReactDOM.render(<App />, div, resolve))

  // wait for the promise to conclude
  const promise = AppTest.promise
  expect(promise).toBeInstanceOf(Promise, 'Instrumenting App instance failed')
  await promise

  AppImport.default = App0
})

There might be additional trickery required to make this work more broadly

I noticed that async tests basically breaks error reporting, errors get much harder to troubleshoot

Create React App is extremely helpful in illustrating solutions with working code.

By the way as far as I can see this doesn't crash the test runner anymore on @next branch so that's good news.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

stopachka picture stopachka  Â·  3Comments

xgqfrms-GitHub picture xgqfrms-GitHub  Â·  3Comments

alleroux picture alleroux  Â·  3Comments

rdamian3 picture rdamian3  Â·  3Comments

wereHamster picture wereHamster  Â·  3Comments