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?
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 require
s 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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With