jest + enzyme, using mount(), document.getElementById() returns null on component which appear after _method call

47,309

Solution 1

Found the solution thanks to https://stackoverflow.com/users/853560/lewis-chung and gods of Google:

  1. Attached my component to DOM via attachTo param:

    const result = mount(
        <App />, { attachTo: document.body }
    );
    
  2. Changed buggy string in my method to string which works with element Object

agentToMod.location = locationSelect.options[locationSelect.selectedIndex].text;` : 

_modifyAgentStatus () {

    const { currentAgentProfile, agentsDatabase } = this.state;
    const agentToMod = currentAgentProfile;

    if (agentToMod.status === 'Free') {
        this.setState({
            infoDisplayContent: 'mission'
        });
        agentToMod.status = 'Waiting';
    } else if (agentToMod.status === 'Waiting') {
        const locationSelect = document.getElementById('missionLocationSelect');

        agentToMod.location = agentToMod.location = locationSelect.options[locationSelect.selectedIndex].text;
        agentToMod.status = 'On Mission';
        this.setState({
            infoDisplayContent: 'profile'
            });
        }
    }

Solution 2

attachTo: document.body will generate a warning:

Warning: render(): Rendering components directly into document.body is discouraged, since its children are often manipulated by third-party scripts and browser extensions. This may lead to subtle reconciliation issues. Try rendering into a container element created for your app.

So just attach to a container element instead of document.body, and no need to add it to the global Window object

before(() => {
  // Avoid `attachTo: document.body` Warning
  const div = document.createElement('div');
  div.setAttribute('id', 'container');
  document.body.appendChild(div);
});

after(() => {
  const div = document.getElementById('container');
  if (div) {
    document.body.removeChild(div);
  }
});

it('should display all contents', () => {
  const wrapper = mount(<YourComponent/>,{ attachTo: document.getElementById('container') });
});

Solution 3

Attached your component to DOM via attachTo param.

import { mount} from 'enzyme';

// Avoid Warning: render(): Rendering components directly into document.body is discouraged.
beforeAll(() => {
  const div = document.createElement('div');
  window.domNode = div;
  document.body.appendChild(div);
})

test("Test component with mount + document query selector",()=>{
  const wrapper = mount(<YourComponent/>,{ attachTo: window.domNode });
});

why we need this?

mount only render component to div element not attached it to DOM tree.

// Enzyme code of mount renderer. 

createMountRenderer(options) {
    assertDomAvailable('mount');
    const domNode = options.attachTo || global.document.createElement('div');
    let instance = null;
    return {
      render(el, context, callback) {
        if (instance === null) {
          const ReactWrapperComponent = createMountWrapper(el, options);
          const wrappedEl = React.createElement(ReactWrapperComponent, {
            Component: el.type,
            props: el.props,
            context,
          });
          instance = ReactDOM.render(wrappedEl, domNode);
          if (typeof callback === 'function') {
            callback();
          }
        } else {
          instance.setChildProps(el.props, context, callback);
        }
      },
      unmount() {
        ReactDOM.unmountComponentAtNode(domNode);
        instance = null;
      },
      getNode() {
        return instance ? instanceToTree(instance._reactInternalInstance).rendered : null;
      },
      simulateEvent(node, event, mock) {
        const mappedEvent = mapNativeEventNames(event);
        const eventFn = TestUtils.Simulate[mappedEvent];
        if (!eventFn) {
          throw new TypeError(`ReactWrapper::simulate() event '${event}' does not exist`);
        }
        // eslint-disable-next-line react/no-find-dom-node
        eventFn(ReactDOM.findDOMNode(node.instance), mock);
      },
      batchedUpdates(fn) {
        return ReactDOM.unstable_batchedUpdates(fn);
      },
    };
  }
Share:
47,309
Dmytro Zhytomyrsky
Author by

Dmytro Zhytomyrsky

Updated on February 02, 2020

Comments

  • Dmytro Zhytomyrsky
    Dmytro Zhytomyrsky over 4 years

    I faced a problem with my jest+enzyme mount() testing. I am testing a function, which switches displaying components.

    Switch between components: when state.infoDisplayContent = 'mission' a missionControl component is mounted, when state.infoDisplayContent = 'profile' - other component steps in:

    _modifyAgentStatus () {
        const { currentAgentProfile, agentsDatabase } = this.state;
        const agentToMod = currentAgentProfile;
    
        if (agentToMod.status === 'Free') {
            this.setState({
                infoDisplayContent: 'mission'
            });
            agentToMod.status = 'Waiting';
        } else if (agentToMod.status === 'Waiting') {
            const locationSelect = document.getElementById('missionLocationSelect');
    
            agentToMod.location = locationSelect[locationSelect.selectedIndex].innerText;
            agentToMod.status = 'On Mission';
            this.setState({
                infoDisplayContent: 'profile'
            });
        }
    }
    

    When I trigger this function everything looks Ok, this test runs well and test successfully pass with required component:

    import React from 'react';
    import { mount } from 'enzyme';
    import App from '../containers/App';
    
    const result = mount(
        <App />
    )
    
    test('change mission controls', () => {
        expect(result.state().currentAgentProfile.status).toBe('Free');
        result.find('#statusController').simulate('click');
        expect(result.find('#missionControls')).toHaveLength(1);
        expect(result.find('#missionLocationSelect')).toHaveLength(1);
        expect(result.state().currentAgentProfile.status).toBe('Waiting');
    });
    
    But when I simulate onClick two times: 
    
    test('change mission controls', () => {
        expect(result.state().currentAgentProfile.status).toBe('Free');
        result.find('#statusController').simulate('click');
        expect(result.find('#missionControls')).toHaveLength(1);
        expect(result.find('#missionLocationSelect')).toHaveLength(1);
        expect(result.state().currentAgentProfile.status).toBe('Waiting');
        result.find('#statusController').simulate('click');
        expect(result.state().currentAgentProfile.status).toBe('On Mission');
    });
    

    I get this assert:

        TypeError: Cannot read property 'selectedIndex' of null
    
      at App._modifyAgentStatus (development/containers/App/index.js:251:68)
      at Object.invokeGuardedCallback [as invokeGuardedCallbackWithCatch] (node_modules/react-dom/lib/ReactErrorUtils.js:26:5)
      at executeDispatch (node_modules/react-dom/lib/EventPluginUtils.js:83:21)
      at Object.executeDispatchesInOrder (node_modules/react-dom/lib/EventPluginUtils.js:108:5)
      at executeDispatchesAndRelease (node_modules/react-dom/lib/EventPluginHub.js:43:22)
      at executeDispatchesAndReleaseSimulated (node_modules/react-dom/lib/EventPluginHub.js:51:10)
      at forEachAccumulated (node_modules/react-dom/lib/forEachAccumulated.js:26:8)
      at Object.processEventQueue (node_modules/react-dom/lib/EventPluginHub.js:255:7)
      at node_modules/react-dom/lib/ReactTestUtils.js:350:22
      at ReactDefaultBatchingStrategyTransaction.perform (node_modules/react-dom/lib/Transaction.js:140:20)
      at Object.batchedUpdates (node_modules/react-dom/lib/ReactDefaultBatchingStrategy.js:62:26)
      at Object.batchedUpdates (node_modules/react-dom/lib/ReactUpdates.js:97:27)
      at node_modules/react-dom/lib/ReactTestUtils.js:348:18
      at ReactWrapper.<anonymous> (node_modules/enzyme/build/ReactWrapper.js:776:11)
      at ReactWrapper.single (node_modules/enzyme/build/ReactWrapper.js:1421:25)
      at ReactWrapper.simulate (node_modules/enzyme/build/ReactWrapper.js:769:14)
      at Object.<anonymous> (development/tests/AgentProfile.test.js:26:38)
      at process._tickCallback (internal/process/next_tick.js:109:7)
    

    It is obvious that:

    document.getElementById('missionLocationSelect');
    

    return null, but I can not get why. Element passes tests, as I mention.

    expect(result.find('#missionLocationSelect')).toHaveLength(1);
    

    But it could not be captured with document.getElementById().

    Please, help me to fix this problem and run tests.

  • alechill
    alechill almost 7 years
    The real issue here is that you should never use document.getElementById within a react component. Under the surface ezyme's mount does mount to a real DOM fragment but it just isn't attached to the document. This is a red flag - you should instead use the ref prop of the select component to store a reference to the actual select node itself facebook.github.io/react/docs/refs-and-the-dom.html, or ReactDOM.findDOMNode facebook.github.io/react/docs/react-dom.html#finddomnode . Both can be avoided by using the onChange event of the select component here though.
  • Neurotransmitter
    Neurotransmitter over 6 years
    Warning: render(): Rendering components directly into document.body is discouraged, since its children are often manipulated by third-party scripts and browser extensions. This may lead to subtle reconciliation issues. Try rendering into a container element created for your app.
  • a.barbieri
    a.barbieri about 6 years
    Using refsolved for me. See React Documentation for more info on ref.
  • darul75
    darul75 about 5 years
    using VueJS it helped me to sort out what was wrong in a test by using same kind of option with attachtodocument option vue-test-utils.vuejs.org/api/options.html#attachtodocument
  • user 9191
    user 9191 over 4 years
    I tried to apply the same changes to my test file and still getting the same error