I have a function that is called inside a useEffect and I'm not able to pass coverage to there. The function changes the value of a state depending of the viewport width, for render html. Basically I do a conditional rendering. This is the code of the function updateMedia:
import { useEffect, useState } from "react";
import { Contact } from "../../features/contacts/models/Contact";
import IndividualContactStyled from "./IndividualContactStyled";
interface ContactProps {
contact: Contact;
}
// eslint-disable-next-line #typescript-eslint/no-redeclare
const IndividualContact = ({ contact }: ContactProps): JSX.Element => {
const initialState = false;
const [isDesktop, setIsDesktop] = useState(initialState);
const updateMedia = () => {
setIsDesktop(window.innerWidth > 799);
};
useEffect(() => {
window.addEventListener("resize", updateMedia);
return () => window.removeEventListener("resize", updateMedia);
});
return (
<IndividualContactStyled className="contact">
{isDesktop && <span className="contact__email">{contact.email}</span>}
{isDesktop && (
<span className="contact__phoneNumber">{contact.phoneNumber}</span>
)}
</div>
</IndividualContactStyled>
);
};
export default IndividualContact;
Now, the coverage don't pass for the updateMedia function. I've made this test, if it helps:
import IndividualContact from "./IndividualContact";
import { render, screen, waitFor } from "#testing-library/react";
describe("Given a IndividualContact component", () => {
describe("When it is instantiated with a contact and in a viewport bigger than 800px", () => {
const contact = {
name: "Dan",
surname: "Abramov",
email: "dan#test.com",
phoneNumber: "888555222",
owner: "owner",
};
test("Then it should render the 'email' and the 'phoneNumber' of the contact", async () => {
global.innerWidth = 1000;
global.dispatchEvent(new Event("resize"));
render(<IndividualContact contact={contact} />);
await waitFor(() => {
expect(screen.getByText("dan#test.com")).toBeInTheDocument();
});
});
});
});
If anyone can help me I would be very grateful. Thanks!
You should render the component and register the resize event on the window first. Then change the value of window.innerWidth and dispatch a resize event on the window.
E.g.
index.tsx:
import React, { useEffect, useState } from 'react';
type Contact = any;
interface ContactProps {
contact: Contact;
}
const initialState = false;
const IndividualContact = ({ contact }: ContactProps) => {
const [isDesktop, setIsDesktop] = useState(initialState);
const updateMedia = () => {
setIsDesktop(window.innerWidth > 799);
};
useEffect(() => {
window.addEventListener('resize', updateMedia);
return () => window.removeEventListener('resize', updateMedia);
});
return (
<div>
{isDesktop && <span className="contact__email">{contact.email}</span>}
{isDesktop && <span className="contact__phoneNumber">{contact.phoneNumber}</span>}
</div>
);
};
export default IndividualContact;
index.test.tsx:
import IndividualContact from './';
import { act, fireEvent, render, screen, waitFor } from '#testing-library/react';
import '#testing-library/jest-dom';
import React from 'react';
describe('Given a IndividualContact component', () => {
describe('When it is instantiated with a contact and in a viewport bigger than 800px', () => {
const contact = {
name: 'Dan',
surname: 'Abramov',
email: 'dan#test.com',
phoneNumber: '888555222',
owner: 'owner',
};
test("Then it should render the 'email' and the 'phoneNumber' of the contact", async () => {
render(<IndividualContact contact={contact} />);
global.innerWidth = 1000;
act(() => {
global.dispatchEvent(new Event('resize'));
});
await waitFor(() => {
expect(screen.queryByText('dan#test.com')).toBeInTheDocument();
});
});
});
});
Test result:
PASS stackoverflow/73652164/index.test.tsx (11.685 s)
Given a IndividualContact component
When it is instantiated with a contact and in a viewport bigger than 800px
✓ Then it should render the 'email' and the 'phoneNumber' of the contact (43 ms)
-----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-----------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
index.tsx | 100 | 100 | 100 | 100 |
-----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 12.385 s
package versions:
"jest": "^26.6.3",
"#testing-library/react": "^11.2.7",
"react": "^16.14.0",
Related
I have a simple TodoList component that uses a custom hook useTodos
import { useState } from 'react'
export const useTodos = (initialTodos = []) => {
const [todos, setTodos] = useState(initialTodos)
const addTodo = (value) => {
const updatedTodos = [...todos, value]
setTodos(updatedTodos)
}
const removeTodo = (index) => {
const updatedTodos = todos.filter((todo, i) => i !== index)
setTodos(updatedTodos)
}
return { todos, addTodo, removeTodo }
}
I would like to test the component with React Testing Library.
In order to do so, I want to mock the initial todos returned by the hook.
jest.mock('hooks/useTodos', () => ({
useTodos: () => ({
todos: ['Wake up', 'Go to work'],
}),
}))
But the methods addTodo and removeTodo are then undefined. On the other hand, when I mock them with jest.fn() they do not work anymore.
Is there any way to mock only todos and keep other methods working?
You can create a mocked useTodos hook with mock todos initial state based on the real useTodos hook.
hooks.js:
import { useState } from 'react';
export const useTodos = (initialTodos = []) => {
const [todos, setTodos] = useState(initialTodos);
const addTodo = (value) => {
const updatedTodos = [...todos, value];
setTodos(updatedTodos);
};
const removeTodo = (index) => {
const updatedTodos = todos.filter((todo, i) => i !== index);
setTodos(updatedTodos);
};
return { todos, addTodo, removeTodo };
};
index.jsx:
import React from 'react';
import { useTodos } from './hooks';
export default function MyComponent() {
const { todos, addTodo, removeTodo } = useTodos();
return (
<div>
{todos.map((todo, i) => (
<p key={i}>
{todo}
<button type="button" onClick={() => removeTodo(i)}>
Remove
</button>
</p>
))}
<button type="button" onClick={() => addTodo('have a drink')}>
Add
</button>
</div>
);
}
index.test.jsx:
import React from 'react';
import { render, screen, fireEvent } from '#testing-library/react';
import '#testing-library/jest-dom/extend-expect';
import MyComponent from '.';
jest.mock('./hooks', () => {
const { useTodos } = jest.requireActual('./hooks');
return {
useTodos: () => useTodos(['Wake up', 'Go to work']),
};
});
describe('69399677', () => {
test('should render todos', () => {
render(<MyComponent />);
expect(screen.getByText(/Wake up/)).toBeInTheDocument();
expect(screen.getByText(/Go to work/)).toBeInTheDocument();
});
test('should add todo', () => {
render(<MyComponent />);
fireEvent.click(screen.getByText(/Add/));
expect(screen.getByText(/have a drink/)).toBeInTheDocument();
});
test('should remove todo', () => {
render(<MyComponent />);
fireEvent.click(screen.getByText(/Go to work/).querySelector('button'));
expect(screen.queryByText(/Go to work/)).not.toBeInTheDocument();
});
});
test result:
PASS examples/69399677/index.test.jsx (8.788 s)
69399677
✓ should render todos (26 ms)
✓ should add todo (10 ms)
✓ should remove todo (4 ms)
-----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-----------|---------|----------|---------|---------|-------------------
All files | 100 | 0 | 100 | 100 |
hooks.js | 100 | 0 | 100 | 100 | 3
index.jsx | 100 | 100 | 100 | 100 |
-----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 3 passed, 3 total
Snapshots: 0 total
Time: 9.406 s
I tried to get a full coverage report and tried to cover all unit testing of my React project,
i know this is just to get a 100% full report and i can do it without it, but i want to know if there are something way to cover all test on a SFC to get 100% coverage report.
This is my component:
import React, { useEffect } from "react";
import { connect } from "react-redux";
import { createStructuredSelector } from "reselect";
///Components
import UserForm from "../../components/user-form/user-form.component";
//Actions
import { getUserStartActionable, updateUserStart } from "../../redux/admin-user/admin-user.actions";
//Selector
import { selectGetUser } from "../../redux/admin-user/admin-user.select";
import { SpinnerLoading } from "../../utils/styles.utils";
import {
WrapContainer,
UserBody,
UserHeader,
UserFooter,
} from "./update-user.styles";
/***
* Componente que renderiza la página para realizar
* el registro de usuarios pacientes
* #param {Object} state
* #param {Function} createUser
* #return {Component} RegisterUsersPage
*/
export const UpdateUser = ({ match, getUser, patientState }) => {
const fetchStart = () => {getUser(match.params.patientId)};
useEffect(fetchStart, []);
return (
<WrapContainer>
<UserHeader>
<h3>Actualizar Usuario</h3>
</UserHeader>
<UserBody>
{patientState.user ? (
<UserForm user={patientState.user} type="UPDATE" />
) : (
<SpinnerLoading text="Cargando información" />
)}
</UserBody>
<UserFooter></UserFooter>
</WrapContainer>
);
};
//Selector de estados
const mapStateToProps = createStructuredSelector({
patientState: selectGetUser,
});
//Despachador de acciones
const mapDispatchToProps = (dispatch) => ({
getUser: (user) => dispatch(getUserStartActionable(user)),
});
export default connect(mapStateToProps, mapDispatchToProps)(UpdateUser);
So when run "yarn test --coverage"
update-user.component.jsx | 66.67 | 0 | 25 | 85.71 | 73 |
My test:
describe("<UpdateUser />", () => {
let wrapper;
beforeEach(() => {
wrapper = shallow(<UpdateUser />);
});
it("should render the RegisterUsers Page", () => {
expect(wrapper).toMatchSnapshot();
});
});
So how i cover the dispatch getUser?
Thanks a lot
That's a way to test a redux action:
import { setResults } from '...';
it('setResults action works as expected', () => {
const results = ['sample result1', 'sample result2'];
const expectedAction = {
type: actions.SET_RESULTS,
data: results
};
expect(setResults(results)).toEqual(expectedAction);
});
That's a way to test a redux reducer:
import { setResults } from '...';
import { setResultsReducer } from '...';
it('returns state with detailedResult value when setDetailedResult(detailedResult) ' +
'action has been called', () => {
const resultsValue = ['sample result1', 'sample result2'];
expect(setResultsReducer([], setResults(resultsValue)))
.toEqual({results: resultsValue});
});
I am working on a React form and have an onSubmit function to it. I only added below lines in onSubmit function.
const id = this.getErrorPositionById();
const errorPosition = document.getElementById(id).offsetTop; //CANNOT READ PROPERTY 'offsetTop' of null
window.scrollTo({
top: errorPosition,
behavior: "smooth"
});
And this is the onSubmit function.
public getErrorPositionById = () => {
const error = this.state.errors;
return Object.keys(error).find(id => error[id] != null);
};
public onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const id = this.getErrorPositionById();
const errorPosition = document.getElementById(id).offsetTop;
window.scrollTo({
top: errorPosition,
behavior: "smooth"
});
if (
!this.state.isValid ||
(!this.props.allowMultipleSubmits && this.state.isSubmitted)
) {
return;
}
Promise.all(this.validateFields())
.then((validationErrors: IValidationErrors[]) => {
this.setError(Object.assign({}, ...validationErrors), () => {
this.isFormValid() ? this.setSubmitted(e) : this.scrollFormToView();
});
})
.then(() => {
const newErrors = this.state.errors;
this.setState({ errors: { ...newErrors, ...this.props.apiErrors } });
});
};
Here is the test case
beforeEach(() => {
jest.clearAllMocks();
formFields = jest.fn();
onSubmit = jest.fn();
onValidate = jest.fn();
validate = jest.fn();
mockPreventDefault = jest.fn();
mockEvent = jest.fn(() => ({ preventDefault: mockPreventDefault }));
mockValidateAllFields = jest.fn(() => Promise);
mockChildFieldComponent = { validate };
instance = formWrapper.instance();
});
it("should not reValidate if form has been submitted already", () => {
instance.validateFields = mockValidateAllFields;
instance.setSubmitted();
expect(instance.state.isSubmitted).toBe(true);
instance.onSubmit(mockEvent());
expect(mockValidateAllFields).toHaveBeenCalledTimes(0);
});
The test case fails with error
TypeError: Cannot read property 'offsetTop' of null
on below line
const errorPosition = document.getElementById(id).offsetTop;
Can someone please help me understand how to eliminate this error.
You should make a stub for document.getElementById(id). For simple, I remove your business logic from the component.
E.g.
index.tsx:
import React, { Component } from 'react';
class SomeComponent extends Component {
state = {
errors: {
'#selector': {},
},
};
public getErrorPositionById = () => {
const error = this.state.errors;
return Object.keys(error).find((id) => error[id] != null);
};
public onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const id = this.getErrorPositionById() as string;
const errorPosition = document.getElementById(id)!.offsetTop;
window.scrollTo({
top: errorPosition,
behavior: 'smooth',
});
};
public render() {
return (
<div>
<form onSubmit={this.onSubmit}></form>
</div>
);
}
}
export default SomeComponent;
index.spec.tsx:
import React from 'react';
import { shallow } from 'enzyme';
import SomeComponent from './';
describe('SomeComponent', () => {
afterEach(() => {
jest.resetAllMocks();
});
it('should handle submit correctly', async () => {
const mElement = { offsetTop: 123 };
document.getElementById = jest.fn().mockReturnValueOnce(mElement);
window.scrollTo = jest.fn();
const wrapper = shallow(<SomeComponent></SomeComponent>);
const mEvent = { preventDefault: jest.fn() };
wrapper.find('form').simulate('submit', mEvent);
expect(mEvent.preventDefault).toHaveBeenCalledTimes(1);
expect(document.getElementById).toBeCalledWith('#selector');
expect(window.scrollTo).toBeCalledWith({ top: 123, behavior: 'smooth' });
});
});
Unit test result with coverage report:
PASS src/stackoverflow/53352420/index.spec.tsx (12.859s)
SomeComponent
✓ should handle submit correctly (20ms)
-----------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
-----------|----------|----------|----------|----------|-------------------|
All files | 100 | 100 | 100 | 100 | |
index.tsx | 100 | 100 | 100 | 100 | |
-----------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 14.751s
Source code: https://github.com/mrdulin/jest-codelab/tree/master/src/stackoverflow/53352420
I'm trying to write unit tests on my component, it looks like this.
export const myComponent = ({text, list, getData = transport.getData}) => {
const [rows, setRows] = React.useState([]);
React.useEffect(() => {
const fetchData = async () => {
const rows = await getData(list);
setRows(rows);
};
fetchData();
}, [list]);
if (rows.length === 0) {
return null;
}
return (
// some JSX
);
};
The problem is that component fetches data via async function, so it will be called after the component check if rows is empty and return null.
if (rows.length === 0) {
return null;
}
I mocked getData so it should return some values. But still, I couldn't understand how I should cover this component with unit testing. I suppose it should be a snapshot, perhaps it is not right.
My test:
import React from 'react';
import {myComponent} from '../components/myComponent';
import renderer from 'react-test-renderer';
describe('myComponent', () => {
test('should renders correctly', async () => {
const mock = {
text: 'text',
list: [],
getData: () =>
Promise.resolve([
{
// ...
},
]),
};
const component = renderer.create(<myComponent text={mock.text}
list={mock.list} getData={mock.getData}/>);
let popup = component.toJSON();
expect(popup).toMatchSnapshot();
});
});
Here is the unit test solution:
index.tsx:
import React from 'react';
const transport = {
async getData(list) {
return [{ id: 1 }];
}
};
export const MyComponent = ({ text, list, getData = transport.getData }) => {
const [rows, setRows] = React.useState<any[]>([]);
React.useEffect(() => {
console.count('useEffect');
const fetchData = async () => {
console.count('fetchData');
const newRows = await getData(list);
setRows(newRows);
};
fetchData();
}, [list]);
if (rows.length === 0) {
return null;
}
return <div>rows count: {rows.length}</div>;
};
index.spec.tsx:
import React from 'react';
import { MyComponent } from './';
import renderer, { act } from 'react-test-renderer';
describe('myComponent', () => {
test('should renders correctly', async () => {
const mProps = {
text: 'text',
list: [],
getData: jest.fn().mockResolvedValueOnce([{ id: 1 }, { id: 2 }, { id: 3 }])
};
let component;
await act(async () => {
component = renderer.create(<MyComponent {...mProps}></MyComponent>);
});
expect(component.toJSON()).toMatchSnapshot();
});
});
Unit test result:
PASS src/stackoverflow/57778786/index.spec.tsx
myComponent
✓ should renders correctly (29ms)
console.count src/stackoverflow/57778786/index.tsx:13
useEffect: 1
console.count src/stackoverflow/57778786/index.tsx:15
fetchData: 1
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 1 passed, 1 total
Time: 3.557s, estimated 8s
index.spec.tsx.snap:
// Jest Snapshot v1
exports[`myComponent should renders correctly 1`] = `
<div>
rows count:
3
</div>
`;
Source code: https://github.com/mrdulin/jest-codelab/tree/master/src/stackoverflow/57778786
I have a thunk like the below
export const goToNewExperience = (request?: sessionRequest): ThunkAction => {
return async (dispatch: Dispatch, getState: GetState) => {
const state = getState();
const isNewExp = state.isNewExp;
if (isNewExp) {
dispatch(updateExperience({
type: UPDATE_EXP,
'NewExperience'
}))
} else if (request && request.isError) {
dispatch(updateExperience({
type: UPDATE_EXP,
'ErrorExperience'
}));
}
};
};
how to test the other action dispatchers in one redux-thunk based on a condition ? any best practices?
I wrote like this but looking for best practices
it('should update exp with New Exp', done => {
const store = createStoreWithState();
const session = {isNewExp:true};
store.dispatch(updateSession(session));
const dispatch = jest.fn();
goToNewExperience()(dispatch, () => store.getState()).then(_ => {
expect((dispatch.mock.calls[0][0]: any).type).toEqual(UPDATE_EXP);
expect((dispatch.mock.calls[0][0]: any).payload).toEqual('NewExperience');
done();
});
});
it('should update exp with Error Exp', done => {
const store = createStoreWithState();
const session = {isNewExp:false};
store.dispatch(updateSession(session));
const dispatch = jest.fn();
goToNewExperience({isError:true})(dispatch, () => store.getState()).then(_ => {
expect((dispatch.mock.calls[0][0]: any).type).toEqual(UPDATE_EXP);
expect((dispatch.mock.calls[0][0]: any).payload).toEqual('ErrorExperience');
done();
});
});
Here is the best practice for write unit testing for actionCreators of redux:
actionCreators.ts:
import { ThunkAction } from 'redux-thunk';
import { Dispatch, AnyAction } from 'redux';
export const UPDATE_EXP = 'UPDATE_EXP';
export const updateExperience = action => ({ type: action.type, payload: { experience: action.experience } });
export interface ISessionRequest {
isError: boolean;
}
type GetState<S = IState> = () => S;
export interface IState {
isNewExp: boolean;
}
export const goToNewExperience = (request?: ISessionRequest): ThunkAction<any, IState, {}, AnyAction> => {
return async (dispatch: Dispatch, getState: GetState<IState>) => {
const state = getState();
const isNewExp = state.isNewExp;
if (isNewExp) {
dispatch(
updateExperience({
type: UPDATE_EXP,
experience: 'NewExperience'
})
);
} else if (request && request.isError) {
dispatch(
updateExperience({
type: UPDATE_EXP,
experience: 'ErrorExperience'
})
);
}
};
};
actionCreators.spec.ts:
import { goToNewExperience, IState, UPDATE_EXP, ISessionRequest } from './actionCreators';
import createMockStore from 'redux-mock-store';
import { AnyAction } from 'redux';
import { ThunkDispatch } from 'redux-thunk';
import thunk from 'redux-thunk';
const middlewares = [thunk];
const mockStore = createMockStore<IState, ThunkDispatch<IState, any, AnyAction>>(middlewares);
describe('goToNewExperience', () => {
it('update new experience', () => {
const initialState = { isNewExp: true };
const store = mockStore(initialState);
return store.dispatch(goToNewExperience()).then(() => {
expect(store.getActions()).toEqual([{ type: UPDATE_EXP, payload: { experience: 'NewExperience' } }]);
});
});
it('update error experience', () => {
const initialState = { isNewExp: false };
const store = mockStore(initialState);
const request: ISessionRequest = { isError: true };
return store.dispatch(goToNewExperience(request)).then(() => {
expect(store.getActions()).toEqual([{ type: UPDATE_EXP, payload: { experience: 'ErrorExperience' } }]);
});
});
it('do nothing', () => {
const initialState = { isNewExp: false };
const store = mockStore(initialState);
const request: ISessionRequest = { isError: false };
return store.dispatch(goToNewExperience(request)).then(() => {
expect(store.getActions()).toEqual([]);
});
});
});
Unit test result with 100% coverage:
PASS src/stackoverflow/49824394/actionCreators.spec.ts (5.028s)
goToNewExperience
✓ update new experience (10ms)
✓ update error experience (1ms)
✓ do nothing (1ms)
-------------------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
-------------------|----------|----------|----------|----------|-------------------|
All files | 100 | 100 | 100 | 100 | |
actionCreators.ts | 100 | 100 | 100 | 100 | |
-------------------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests: 3 passed, 3 total
Snapshots: 0 total
Time: 6.09s, estimated 7s
Here is the completed demo: https://github.com/mrdulin/jest-codelab/tree/master/src/stackoverflow/49824394