I am trying my hand at unit testing and here is where I am stuck. The refs for inputs are saved in a global context and am trying to mock the context so I can test the components properly.
Issue is once of my components calls the focus() property from the ref and I am not sure how to mock the ref properly so even my test can use the focus() property.
This is my testing code:
import { render as rtlRender, screen } from "#testing-library/react";
import "#testing-library/jest-dom";
import { Provider } from "react-redux";
import store from "../redux/store";
import { GlobalContextProvider } from "../common/GlobalContext";
import { SalesContextProvider } from "../Sales/SalesContext";
import PatientAutocomplete from "../Sales/PatientAutocomplete";
const dummySalesContext = {
patientRef: null,
};
const dummyGlobalContext = {
currentComponent: "demo",
setEnableGlobalNavigation: jest.fn(),
};
const render = (component) =>
rtlRender(<Provider store={store()}>{component}</Provider>);
test("renders learn react link", () => {
render(
<GlobalContextProvider value={dummyGlobalContext}>
<SalesContextProvider value={dummySalesContext}>
<PatientAutocomplete
searchTerm="Demo"
patientSelected={null}
close={null}
/>
</SalesContextProvider>
</GlobalContextProvider>
);
const linkElement = screen.getByText(/Add patient/i);
expect(linkElement).toBeInTheDocument();
});
This is the error that is thrown:
TypeError: Cannot read property 'focus' of undefined
}, [searchTerm, debounceFetchPatients]);
useEffect(() => {
if (!showEditPatient) patientRef.current.focus({ preventScroll: true });
^
}, [showEditPatient]);
Related
here is the basic setup
component.js
import {functionIWantToMock, val} from "somewhere/on/my/hdd"
export function Component() {
return (<>
<div>{val}</div>
<div>{functionIWantToMock()}</div>
</>)
}
component.test.js
import { Component } from "./component";
import { render, screen } from "#testing-library/react";
it("should change the result of the function", () => {
// Change behaviour of functionIWantToMock
functionIWantToMock = () => {
return "empty"
}
render(<Component/>);
// This is not a proper assertion
expect(screen.getByRole("img")).toBeVisible()
})
I've looked at manual mocking, spying and jest.fn() but none of those will work, i think, because the call is happening within the component and so I can't mock it globally.
What is the correct way to mock functionIWantToMock so that i can change its behaviour in the component for the test?
You can either use module mocking or dependency injection for this.
Module mocking
The Jest docs have an example of mocking a module, you would need to do this:
component.test.js
import { Component } from "./component";
import { render, screen } from "#testing-library/react";
// This gives you a handle to the function so you can mock
// the return value, make expectations on it, etc
import { functionIWantToMock } from "somewhere/on/my/hdd";
// This mocks all exports from the module
jest.mock("somewhere/on/my/hdd");
it("should change the result of the function", () => {
// Change behaviour of functionIWantToMock
functionIWantToMock.mockReturnValue("empty");
render(<Component/>);
expect(functionIWantToMock).toBeCalled()
})
I recommend adding clearMocks: true to your Jest config to avoid leaking state between tests.
Dependency Injection
Your other option is to pass the function into the component:
component.js
import {functionIWantToMock as defaultFn, val} from "somewhere/on/my/hdd"
// Using the import as the default function helps you avoid
// passing the function where this component is used.
export function Component({functionIWantToMock = defaultFn}) {
return (<>
<div>{val}</div>
<div>{functionIWantToMock()}</div>
</>)
}
component.test.js
import { Component } from "./component";
import { render, screen } from "#testing-library/react";
it("should change the result of the function", () => {
const mockFn = jest.fn(() => {
return "empty"
})
render(<Component functionIWantToMock={mockFn} />);
expect(mockFn).toBeCalled()
})
Scroll.js
import React from "react";
export const ScrollToTop = ({ children, location }) => {
React.useEffect(() => window.scrollTo(0, 0), [location.pathname]);
return children;
};
Scroll.test.js
import React from "react";
import { ScrollToTop } from "./ScrollToTop";
describe("ScrollToTop", () => {
it("", () => {
expect(
ScrollToTop({
children: "some children",
location: { pathname: "the path" }
})
).toEqual();
});
});
and the result I'm getting is
enter image description here
You should not call ScrollToTop as a function directly, this is what error message is complaining about.
React docs recommend the Testing Library for writing tests.
Here is an example of how you can write Scroll.test.js using the library above:
import React from "react";
import { render } from '#testing-library/react';
import { ScrollToTop } from "./ScrollToTop";
describe("ScrollToTop", () => {
it('calls window.scrollTo()', () => {
window.scrollTo = jest.fn(); // create a moack function and record all calls
render(<ScrollToTop location={{ pathname: 'pathname' }}>Text</ScrollToTop>); // render a component
expect(window.scrollTo).toHaveBeenCalledWith(0, 0); // check that scrollTo mock was called
});
});
I have the following component in React
import React from 'react'
import { Row, Col, Form, Input, Button, Card } from 'antd';
import { FormComponentProps } from 'antd/lib/form/Form';
import Particles from 'react-particles-js';
import { useTranslation } from "react-i18next";
import { connect } from 'react-redux';
import { RootState } from '../../services/store/rootReducer';
import { UsersActions } from '../../services/store';
interface LoginProps extends FormComponentProps {
rootState: RootState
}
class Login extends React.Component<LoginProps> {
state = { email: '', password: ''};
changeHandler = (e: any, name: any) => {
var value = e.target.value;
this.setState({[name]: value})
}
loginUser = () => {
try {
UsersActions.loginRequestAsync(this.state, (token: any) => {
console.log(token);
});
} catch(exception)
{
console.log(exception)
}
}
render() {
const { t } = useTranslation();
const { getFieldDecorator } = this.props.form;
return (
<div>
///blabla
</div>
)
}
}
const mapStateToProps = (state: RootState) => ({
rootState: state
});
const mapDispatchToProps = {}
const createdForm = Form.create()(Login);
export default connect(
mapStateToProps,
mapDispatchToProps
)(createdForm);
When I add the line
const { t } = useTranslation();
The app do not compile with
×
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
fix this problem.
Now, I tried to understand the rule, hooks must be called only on top level of a component in order for react to always load the component the same way. But where is my top level ?
I tried to put outside of render and as a property of the component, I still have the same loading error.
You broke the rules of Hooks, namely: No Hooks in classes.
That should really be the trick here. Try to rewrite it to something like the following:
function Login(props: LoginProps) {
const [data, setData] = useState({ email: '', password: '' });
const { t } = useTranslation();
const loginUser = () => { /* ... */ };
render() {
return <div>
{/*...*/ }
</div>
}
}
On the document pages, there is even a page only on Hook Errors/Warnings: Invalid Hook Call Warning
In Breaking the Rules of Hooks it states:
🔴 Do not call Hooks in class components.
🔴 Do not call in event handlers.
🔴 Do not call Hooks inside functions passed to useMemo, useReducer, or useEffect.
Hooks are used in functional components, here you have a class component that's why it's throwing an error here, error is saying it
Hooks can only be called inside of the body of a function component.
Hope it helps
I have a simple connected component that, when loaded, builds the user details by calling an async action and then displays those details from state.
import PropTypes from 'prop-types';
import React from 'react';
import { connect } from 'react-redux';
import { loadUserDetails } from '../actions/user';
class ShowUserDetailsLite extends React.Component {
componentDidMount() {
this.props.loadUserDetails();
}
render() {
const { userDetails } = this.props;
const handleClickEdit = () => {
history.push(editProfilePath());
};
return (
<div>
<div className="email">
{userDetails.email_address}
</div>
<div className="name">
{userDetails.first_name} {userDetails.last_name}
</div>
</div>
);
}
}
ShowUserDetailsLite.propTypes = {
loadUserDetails: PropTypes.func.isRequired,
userDetails: PropTypes.shape({}).isRequired,
};
const mapDispatchToProps = {
loadUserDetails,
};
function mapStateToProps(state) {
const userDetails = state.user.details;
return { userDetails };
}
export default connect(mapStateToProps, mapDispatchToProps)(ShowUserDetailsLite);
To start, I'd like to test that my component is displaying the correct information from state, so I have the following test.
import configureStore from 'redux-mock-store'
import { shallow } from 'enzyme';
import { expect } from 'chai';
import React from 'react'
import Provider from 'react-redux';
import ShowUserDetailsLite from '../../components/ShowUserDetailsLite'
describe('ShowUserDetailsLite component', () => {
it('displays the current user', () => {
const mockStore = configureStore();
let store = mockStore({
user: {
details: {
email_address: 'test#test.com',
first_name: 'First',
last_name: 'Last',
}
},
});
let wrapper = shallow(<ShowUserDetailsLite store={store} loadUserDetails={jest.fn()}/>).dive()
expect(wrapper.find('.email').text()).to.eql('test#test.com')
})
})
When I run this test with the componentDidMount function commented out it works great, the state is read and the correct information is displayed, but when I run the test including the componentDidMount function the test attempts to call the function and I get the following error:
FAIL app/tests/components/ShowUserDetailsLite.test.js
ShowUserDetailsLite component
✕ displays the current user (32ms)
● ShowUserDetailsLite component › displays the current user
ReferenceError: regeneratorRuntime is not defined
16 |
17 | export function loadUserDetails() {
> 18 | return async dispatch => {
| ^
19 | try {
20 | const res = await axios.get(`/${window.realm}/api/userDetails`);
21 | dispatch({ type: SET_USER_DETAILS, data: res.data });
At this stage I don't care about testing the loadUserDetails function so I simply want to stub it. I understood that to do that you simply need to pass the function in as a property, which I've attempted to do by passing in a jest function:
let wrapper = shallow(<ShowUserDetailsLite store={store} loadUserDetails={jest.fn()}/>).dive()
But still I'm getting the error. How do I properly stub async actions called in componentdidmount for connected component tests?
I come probably to late but i ended up here struggeling too with the same problem ,after digging for 2 days i founded that :
Since you wrap your component with Connect you need to pass your stubbed props function to the child component
Your can export your component without the HOC connect
export ShowUserDetailsLite
then import it
import {ShowUserDetailsLite} from ...
And now you have a shallowed component without connect you can directly pass the props like the users and the loadUserDetails mocked
To be more complete with the original question :
How to stub componentDidMount
When you mount or shallow your component you will fire componentDidMount .
With shallow
for shallow rendering your can use the option {disableLifecycleMethods:true}
wrapper = shallow(
<Stats {...props} />, {disableLifecycleMethods:true}
)
With mount
If you want to use mount for rendering you need to stub componentDidMount doing this way :
jest.spyOn(YOUCOMPONENT.prototype, 'componentDidMount').mockImplementation();
then you mount your component
wrapper = mount(
<Stats {...props} />
)
And now you can have access to the methods if you want to stub a method called in the componentDidMount:
const spy = jest.spyOn(wrapper.instance(), 'myMethod').mockImplementation(() => {//do stuff})
I have a Login component as below and I am writing some test cases for this component. When I tried to run the test I got the following error:
Test
import renderer from 'react-test-renderer'
import Login from '../Login'
let props, wrapper
beforeEach(() => {
props = {
loginAttempt: jest.fn(),
recoverAttempt: jest.fn(),
reset: jest.fn()
}
wrapper = shallow(<Login {...props} />)
})
describe('tests for <Login />', () => {
test('should have a formProvider with handlesubmit atribute', () => {
const value = wrapper.find('FormProvider')
expect(value.length).toBe(1)
})
})
//Snapshot test
test('Snapshot test for the Contact form', () => {
const tree = renderer.create(<Login {...props} />).toJSON()
expect(tree).toMatchSnapshot()
})
Component
import React, { Component } from 'react'
import KeyboardAvoidingWrapper from 'components/Wrappers/KeyboardAvoidingWrapper'
export default class AuthScreen extends Component {
state = {
}
toggleRecovery = e => {
)
}
loginAttempt = data => {
}
recoverAttempt = data => {
}
componentWillUnmount() {
}
render() {
let { loginAttempt, toggleRecovery, recoverAttempt, state, props } = this
let { recovery } = state
let { error, fetching } = props
return (
<KeyboardAvoidingWrapper enabled={false} behavior="padding" fluid>
UI GOES HERE..
</KeyboardAvoidingWrapper>
)
}
}
Error
● Test suite failed to run
Invariant Violation: Native module cannot be null.
at invariant (node_modules/react-native/node_modules/fbjs/lib/invariant.js:40:15)
at new NativeEventEmitter (node_modules/react-native/Libraries/EventEmitter/NativeEventEmitter.js:36:36)
at Object.<anonymous> (node_modules/react-native-safari-view/SafariViewManager.ios.js:12:20)
at Object.<anonymous> (node_modules/react-native-safari-view/index.js:1:238)
Why I am getting this error? Is it because the component does not get imported correctly? I could not figure out the why this happening. How can I solve this issue?
This problem happens when you import a native component in the render tree, as the test renderer do not have them. To fix this, either you need to mock the component (https://jestjs.io/docs/en/manual-mocks), or use shallow rendering (https://reactjs.org/docs/shallow-renderer.html)
For your particular case, this is the github issue to help you: https://github.com/naoufal/react-native-safari-view/issues/99
Another solution could be using react-native-mock-render module (the most active fork of react-native-mock)