Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

"Invalid hook call" when mocking React HOC with Jest

I'm using react-speech-recognition to transcribe speech to text in my React app. react-speech-recognition provides the SpeechRecognition higher-order component, which injects additional properties like browserSupportsSpeechRecognition into wrapped components.

My App component looks like this:

// src/App.js
import React, { useEffect } from 'react';
import SpeechRecognition from 'react-speech-recognition';

const App = ({ transcript, browserSupportsSpeechRecognition }) => {
    useEffect(() => {
        console.log(`transcript changed: ${transcript}`);
    }, [transcript]);

    if (! browserSupportsSpeechRecognition) {
        return <span className="error">Speech recognition not supported</span>;
    }

    return <span className="transcript">{transcript}</span>;
};

const options = {
    autoStart: false,
    continuous: false
};

export default SpeechRecognition(options)(App);

I wrote some tests to emulate both browsers that support speech recognition and browsers that don't:

// src/App.spec.js
import React from 'react';
import Enzyme, { mount } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import chai, { expect } from 'chai';
import chaiEnzyme from 'chai-enzyme';

chai.use(chaiEnzyme());

Enzyme.configure({ adapter: new Adapter() });

// Generate a mock SpeechRecognition HOC with the given props
function mockSpeechRecognition(mockProps) {
    return function(options) {
        return function(WrappedComponent) {
            return function(props) {
                return (
                    <WrappedComponent 
                        {...props}
                        {...mockProps}
                        recognition={{}}
                    />
                );
            };
        };
    };
}

describe('App component', () => {

    beforeEach(() => jest.resetModules());

    it('should show an error when speech recognition is not supported', () => {
        jest.mock('react-speech-recognition', () => mockSpeechRecognition({
            browserSupportsSpeechRecognition: false
        }));

        const App = require('./App').default;
        const wrapper = mount(<App />);

        expect(wrapper).to.contain.exactly(1).descendants('.error');
        expect(wrapper.find('.error'))
            .to.have.text('Speech recognition not supported');
    });

    it('should show the transcript when speech recognition is supported', () => {
        jest.mock('react-speech-recognition', () => mockSpeechRecognition({
            browserSupportsSpeechRecognition: true,
            transcript: 'foo'
        }));

        const App = require('./App').default;
        const wrapper = mount(<App />);

        expect(wrapper).to.contain.exactly(1).descendants('.transcript');
        expect(wrapper.find('.transcript')).to.have.text('foo');
    });

});

When I run these tests, I get an "Invalid hook call" error that causes the tests to fail:

  ● App component › should show an error when speech recognition is not supported

    Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
    1. You might have mismatching versions of React and the renderer (such as React DOM)
    2. You might be breaking the Rules of Hooks
    3. You might have more than one copy of React in the same app
    See https://reactjs.org/warnings/invalid-hook-call-warning.html for tips about how to debug and fix this problem.

      3 | 
      4 | const App = ({ transcript, browserSupportsSpeechRecognition }) => {
    > 5 |     useEffect(() => {
        |     ^
      6 |         console.log(`transcript changed: ${transcript}`);
      7 |     }, [transcript]);
      8 | 

      at resolveDispatcher (node_modules/react/cjs/react.development.js:1465:13)
      at useEffect (node_modules/react/cjs/react.development.js:1508:20)
      at App (src/App.js:5:5)
      at renderWithHooks (node_modules/react-dom/cjs/react-dom.development.js:14803:18)
      at mountIndeterminateComponent (node_modules/react-dom/cjs/react-dom.development.js:17482:13)
      at beginWork (node_modules/react-dom/cjs/react-dom.development.js:18596:16)
      at HTMLUnknownElement.callCallback (node_modules/react-dom/cjs/react-dom.development.js:188:14)
      at invokeEventListeners (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:193:27)
      at HTMLUnknownElementImpl._dispatch (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:119:9)
      at HTMLUnknownElementImpl.dispatchEvent (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:82:17)
      at HTMLUnknownElementImpl.dispatchEvent (node_modules/jsdom/lib/jsdom/living/nodes/HTMLElement-impl.js:30:27)
      at HTMLUnknownElement.dispatchEvent (node_modules/jsdom/lib/jsdom/living/generated/EventTarget.js:157:21)
      at Object.invokeGuardedCallbackDev (node_modules/react-dom/cjs/react-dom.development.js:237:16)
      at invokeGuardedCallback (node_modules/react-dom/cjs/react-dom.development.js:292:31)
      at beginWork$1 (node_modules/react-dom/cjs/react-dom.development.js:23203:7)
      at performUnitOfWork (node_modules/react-dom/cjs/react-dom.development.js:22157:12)
      at workLoopSync (node_modules/react-dom/cjs/react-dom.development.js:22130:22)
      at performSyncWorkOnRoot (node_modules/react-dom/cjs/react-dom.development.js:21756:9)
      at scheduleUpdateOnFiber (node_modules/react-dom/cjs/react-dom.development.js:21188:7)
      at updateContainer (node_modules/react-dom/cjs/react-dom.development.js:24373:3)
      at node_modules/react-dom/cjs/react-dom.development.js:24758:7
      at unbatchedUpdates (node_modules/react-dom/cjs/react-dom.development.js:21903:12)
      at legacyRenderSubtreeIntoContainer (node_modules/react-dom/cjs/react-dom.development.js:24757:5)
      at Object.render (node_modules/react-dom/cjs/react-dom.development.js:24840:10)
      at fn (node_modules/enzyme-adapter-react-16/src/ReactSixteenAdapter.js:437:26)
      at node_modules/enzyme-adapter-react-16/src/ReactSixteenAdapter.js:354:37
      at batchedUpdates$1 (node_modules/react-dom/cjs/react-dom.development.js:21856:12)
      at Object.act (node_modules/react-dom/cjs/react-dom-test-utils.development.js:929:14)
      at wrapAct (node_modules/enzyme-adapter-react-16/src/ReactSixteenAdapter.js:354:13)
      at Object.render (node_modules/enzyme-adapter-react-16/src/ReactSixteenAdapter.js:423:16)
      at new ReactWrapper (node_modules/enzyme/src/ReactWrapper.js:115:16)
      at mount (node_modules/enzyme/src/mount.js:10:10)
      at Object.<anonymous> (src/App.spec.js:38:25)

  ● App component › should show the transcript when speech recognition is supported

    Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
    1. You might have mismatching versions of React and the renderer (such as React DOM)
    2. You might be breaking the Rules of Hooks
    3. You might have more than one copy of React in the same app
    See https://reactjs.org/warnings/invalid-hook-call-warning.html for tips about how to debug and fix this problem.

      3 | 
      4 | const App = ({ transcript, browserSupportsSpeechRecognition }) => {
    > 5 |     useEffect(() => {
        |     ^
      6 |         console.log(`transcript changed: ${transcript}`);
      7 |     }, [transcript]);
      8 | 

      at resolveDispatcher (node_modules/react/cjs/react.development.js:1465:13)
      at useEffect (node_modules/react/cjs/react.development.js:1508:20)
      at App (src/App.js:5:5)
      at renderWithHooks (node_modules/react-dom/cjs/react-dom.development.js:14803:18)
      at mountIndeterminateComponent (node_modules/react-dom/cjs/react-dom.development.js:17482:13)
      at beginWork (node_modules/react-dom/cjs/react-dom.development.js:18596:16)
      at HTMLUnknownElement.callCallback (node_modules/react-dom/cjs/react-dom.development.js:188:14)
      at invokeEventListeners (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:193:27)
      at HTMLUnknownElementImpl._dispatch (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:119:9)
      at HTMLUnknownElementImpl.dispatchEvent (node_modules/jsdom/lib/jsdom/living/events/EventTarget-impl.js:82:17)
      at HTMLUnknownElementImpl.dispatchEvent (node_modules/jsdom/lib/jsdom/living/nodes/HTMLElement-impl.js:30:27)
      at HTMLUnknownElement.dispatchEvent (node_modules/jsdom/lib/jsdom/living/generated/EventTarget.js:157:21)
      at Object.invokeGuardedCallbackDev (node_modules/react-dom/cjs/react-dom.development.js:237:16)
      at invokeGuardedCallback (node_modules/react-dom/cjs/react-dom.development.js:292:31)
      at beginWork$1 (node_modules/react-dom/cjs/react-dom.development.js:23203:7)
      at performUnitOfWork (node_modules/react-dom/cjs/react-dom.development.js:22157:12)
      at workLoopSync (node_modules/react-dom/cjs/react-dom.development.js:22130:22)
      at performSyncWorkOnRoot (node_modules/react-dom/cjs/react-dom.development.js:21756:9)
      at scheduleUpdateOnFiber (node_modules/react-dom/cjs/react-dom.development.js:21188:7)
      at updateContainer (node_modules/react-dom/cjs/react-dom.development.js:24373:3)
      at node_modules/react-dom/cjs/react-dom.development.js:24758:7
      at unbatchedUpdates (node_modules/react-dom/cjs/react-dom.development.js:21903:12)
      at legacyRenderSubtreeIntoContainer (node_modules/react-dom/cjs/react-dom.development.js:24757:5)
      at Object.render (node_modules/react-dom/cjs/react-dom.development.js:24840:10)
      at fn (node_modules/enzyme-adapter-react-16/src/ReactSixteenAdapter.js:437:26)
      at node_modules/enzyme-adapter-react-16/src/ReactSixteenAdapter.js:354:37
      at batchedUpdates$1 (node_modules/react-dom/cjs/react-dom.development.js:21856:12)
      at Object.act (node_modules/react-dom/cjs/react-dom-test-utils.development.js:929:14)
      at wrapAct (node_modules/enzyme-adapter-react-16/src/ReactSixteenAdapter.js:354:13)
      at Object.render (node_modules/enzyme-adapter-react-16/src/ReactSixteenAdapter.js:423:16)
      at new ReactWrapper (node_modules/enzyme/src/ReactWrapper.js:115:16)
      at mount (node_modules/enzyme/src/mount.js:10:10)
      at Object.<anonymous> (src/App.spec.js:52:25)

However, there are no such errors when I run the dev server and view the page in a browser, and I can see the useEffect hook logging a message to the console. There are also no errors when I create a production build. I think the issue is in how I mocked out the SpeechRecognition HOC. The tests pass if I remove the useEffect hook.

This is a brand new project started with create-react-app. I only have one copy of react and react-dom and the versions match:

$ npm ls react react-dom
[email protected] /Users/NMD/max_programming_projects/react-speech-recognition-invalid-hook-call
├── [email protected] 
└── [email protected]

How can I fix this error in my tests?

like image 524
ThisSuitIsBlackNot Avatar asked Dec 08 '22 10:12

ThisSuitIsBlackNot


1 Answers

It looks like this is a bug in Jest:

Invalid hook call after `jest.resetModules` for dynamic `require`s

The bug happens when you call jest.resetModules or jest.resetModuleRegistry and then require your component inside your tests.

You can work around it by removing jest.resetModules/jest.resetModuleRegistry and wrapping the requires in calls to jest.isolateModules:

describe('App component', () => {

    it('should show an error when speech recognition is not supported', () => {
        jest.mock('react-speech-recognition', () => mockSpeechRecognition({
            browserSupportsSpeechRecognition: false
        }));

        jest.isolateModules(() => {
            const App = require('./App').default;
            const wrapper = mount(<App />);

            expect(wrapper).to.contain.exactly(1).descendants('.error');
            expect(wrapper.find('.error'))
                .to.have.text('Speech recognition not supported');
        });
    });

    it('should show the transcript when speech recognition is supported', () => {
        jest.mock('react-speech-recognition', () => mockSpeechRecognition({
            browserSupportsSpeechRecognition: true,
            transcript: 'foo'
        }));

        jest.isolateModules(() => {
            const App = require('./App').default;
            const wrapper = mount(<App />);

            expect(wrapper).to.contain.exactly(1).descendants('.transcript');
            expect(wrapper.find('.transcript')).to.have.text('foo');
        });
    });

});

When I run this, all tests pass and I can see the output from the useEffect hook:

 PASS  src/App.spec.js
  App component
    ✓ should show an error when speech recognition is not supported (89ms)
    ✓ should show the transcript when speech recognition is supported (6ms)

  console.log src/App.js:6
    transcript changed: undefined

  console.log src/App.js:6
    transcript changed: foo

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        6.577s
Ran all test suites related to changed files.
like image 179
ThisSuitIsBlackNot Avatar answered Dec 31 '22 01:12

ThisSuitIsBlackNot