Reputation: 24063
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?
Upvotes: 8
Views: 17455
Reputation: 24063
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.
Upvotes: 15
Reputation: 13682
You have few options but in general you need to mock your speech recognition hoc properly by passing browserSupportsSpeechRecognition
as shown in the below code.
Option One
You can mock useEffect
. Just write code to suit your needs in useEffect mock.
describe("App component", () => {
beforeEach(() => jest.resetModules());
it("should show an error when speech recognition is not supported", () => {
jest.mock("react", () => ({
...jest.requireActual("React"),
useEffect: (f) => f(),
}));
jest.mock("react-speech-recognition", () => {
return mockSpeechRecognition({ browserSupportsSpeechRecognition: false });
});
const App = require("./App").default;
const wrapper = mount(<App transcript={"hi"} />);
expect(wrapper).to.contain.exactly(1).descendants(".error");
expect(wrapper.find(".error")).to.have.text(
"Speech recognition not supported"
);
});
Option Two
You do not actually need to mock speech recognition hoc at all. It is an overhead. The library guys will do their testing themselves. You can do a named export of App
and import it and write regular tests.
describe("App component - no mock", () => {
beforeEach(() => jest.resetModules());
it("should show an error when speech recognition is not supported", () => {
const wrapper = mount(
<App browserSupportsSpeechRecognition={false} transcript={"hi"} />
);
expect(wrapper).to.contain.exactly(1).descendants(".error");
expect(wrapper.find(".error")).to.have.text(
"Speech recognition not supported"
);
});
it("should NOT show an error when speech recognition is not supported", () => {
const wrapper = mount(
<App browserSupportsSpeechRecognition={true} transcript={"hi"} />
);
expect(wrapper).does.not.contain.descendants(".error");
// expect(wrapper.find(".error")).to.have.text(
// "Speech recognition not supported"
// );
});
});
Option Three
Use react testing library instead of enzyme.
Above tests are run locally and they pass
References:
Upvotes: 3
Reputation: 11283
Could you try mock SpeechRecognition
following way?
jest.mock('react-speech-recognition', () => ({
__esModule: true,
default: mockSpeechRecognition({
browserSupportsSpeechRecognition: false
})
}));
Upvotes: 0