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
Related
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",
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
My component is
const _onBoldClick = (e) => {
e.preventDefault();
onEnterText(RichUtils.toggleInlineStyle(editorState, 'BOLD'));
}
<button className = "text-button-style" onMouseDown = { e => { _onBoldClick(e)} }>B</button>
I am getting error of,
TypeError: e.preventDefault is not a function
74 |
75 | const _onUnderlineClick = (e) => {
> 76 | e.preventDefault();
| ^
77 | onEnterText(RichUtils.toggleInlineStyle(editorState, 'UNDERLINE'));
78 |
79 | }
at _onUnderlineClick (src/components/L3_Admin/L4_AnnouncementsTab/L4_AnnouncementsPage.js:76:11)
Here my tried test code is,
it('onmousedown U button', async () => {
let responseData = [
{
}]
const spy = jest.spyOn(axios, 'get');
const preventDefault = jest.fn();
const _onUnderlineClick = jest.fn();
spy.mockImplementation(async() => await act(() => Promise.resolve({ data: responseData })));
const component = mount( <Provider store= {store}>
<AnnouncementsTab /> </Provider>);
component.update();
// expect(component.find('button')).toHaveLength(6);
// expect(component.find('.text-button-style')).toHaveLength(3);
await component.find('.text-button-style').at(0).invoke('onMouseDown',{ preventDefault })(
{
nativeEvent: {
preventDefault,
e: jest.fn(),
_onUnderlineClick: (e)=>{}
}
},
1000
)
});
The function signature is .invoke(invokePropName)(...args) => Any. There is NO second parameter for .invoke() method. You should pass the mocked event object to the returned function of .invoke().
E.g.
MyComponent.jsx:
import React from 'react';
export function MyComponent() {
const _onBoldClick = (e) => {
e.preventDefault();
};
return (
<button
className="text-button-style"
onMouseDown={(e) => {
_onBoldClick(e);
}}
>
B
</button>
);
}
MyComponent.test.jsx:
import { mount } from 'enzyme';
import React from 'react';
import { MyComponent } from './MyComponent';
describe('67817812', () => {
it('onmousedown U button', async () => {
const preventDefault = jest.fn();
const component = mount(<MyComponent />);
component.find('.text-button-style').at(0).invoke('onMouseDown')({ preventDefault });
expect(preventDefault).toBeCalledTimes(1);
});
});
test result:
PASS examples/67817812/MyComponent.test.jsx (7.766 s)
67817812
✓ onmousedown U button (36 ms)
-----------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-----------------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
MyComponent.jsx | 100 | 100 | 100 | 100 |
-----------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 8.754 s, estimated 9 s
I've written a custom hook that makes an api call the returns the result and an error if present. When trying to right a Jest Test for the component that is using the custom hooks I get 'arr[Symbol.iterator] is not a function' or 'Cannot read property 'Symbol(Symbol.iterator)' of undefined' before the hook when using Jest shallow()
I felt this was an issue with the api response working with a promise so I tried various different approaches around .then or 'wait' for aysnc. This doesn't resolve the issue
My Custom api hook
/* Packages */
import { useEffect, useState } from 'react';
export const useApiCall = (endPoint) => {
const [hasError, setHasErrors] = useState(false);
const [fetchData, setFetchData] = useState(null);
const getData = async () => {
try {
const response = await fetch(endPoint);
if (!response.ok) {
throw Error(response.statusText);
}
const responseJSON = await response.json();
setFetchData(responseJSON.data);
}
catch (error) {
setHasErrors(true);
}
}
useEffect(() => {
getData()
},[])
return [fetchData, hasError]
}
My component using the hook
/* Packages */
import React, { useState } from 'react';
/* Utils */
import { useApiCall } from '../../utils/useApiCall';
/* Components */
import FooterStructure from './FooterStructure'
const Footer = () => {
const [footerLinks, error] = useApiCall('http://www.mocky.io/v2/5d7678f93200000362297bae');
const [visible, setVisible] = useState();
const showContent = () => {
setVisible(!visible);
}
const componentProps = {
footerLinks : footerLinks,
error : error,
visible : visible,
showContent : showContent
};
return (
<FooterStructure {...componentProps} />
)
};
export default Footer;
My Jest test without/with promises
import { shallow, mount } from 'enzyme';
import Footer from '.';
import mock from './__mocks__/FooterLinks';
import { useApiCall } from '../../utils/useApiCall';
import { testHook } from '../../utils/TestUtils';
jest.mock('../../utils/useApiCall');
let footerData,
wrapper;
beforeEach(() => {
wrapper = shallow(<Footer />);
testHook(async () => {
useApiCall.mockImplementation(() => Promise.resolve({ data: mock }));
useApiCall().then((resolve)=>{
footerData = resolve;
})
// This promise gives the same error
// .then(()=>{
// wrapper = shallow(<Footer />);
// })
// .catch((error)=>{
// console.log(error);
// });
});
});
describe('Footer links', () => {
it('should have 3 links', () => {
expect(footerData.data.length).toEqual(3);
});
});
The error
Footer links
✕ should have 3 links (46ms)
● Footer links › should have 3 links
TypeError: Cannot read property 'Symbol(Symbol.iterator)' of undefined
8 | import FooterStructure from './FooterStructure'
9 |
> 10 | const Footer = () => {
| ^
11 | const [footerLinks, error] = useApiCall('http://www.mocky.io/v2/5d7678f93200000362297bae');
12 | const [visible, setVisible] = useState();
13 |
at _iterableToArrayLimit (node_modules/babel-preset-react-app/node_modules/#babel/runtime/helpers/iterableToArrayLimit.js:8:22)
at _slicedToArray (node_modules/babel-preset-react-app/node_modules/#babel/runtime/helpers/slicedToArray.js:8:33)
at Footer (src/global/Footer/index.js:10:22)
at ReactShallowRenderer.render (node_modules/react-test-renderer/cjs/react-test-renderer-shallow.development.js:794:32)
at render (node_modules/enzyme-adapter-react-16/src/ReactSixteenAdapter.js:636:55)
at fn (node_modules/enzyme-adapter-utils/src/Utils.js:99:18)
at Object.render (node_modules/enzyme-adapter-react-16/src/ReactSixteenAdapter.js:636:20)
at new render (node_modules/enzyme/src/ShallowWrapper.js:397:22)
at shallow (node_modules/enzyme/src/shallow.js:10:10)
at Object.<anonymous>.beforeEach (src/global/Footer/Footer.test.js:14:15)
● Footer links › should have 3 links
TypeError: Cannot read property 'data' of undefined
24 | describe('Footer links', () => {
25 | it('should have 3 links', () => {
> 26 | expect(footerData.data.length).toEqual(3);
| ^
27 | });
28 | });
29 |
at Object.data (src/global/Footer/Footer.test.js:26:27)
Test Suites: 1 failed, 1 total
Tests: 1 failed, 1 total
Snapshots: 0 total
Time: 0.66s, estimated 1s
Ran all test suites related to changed files.
Watch Usage: Press w to show more.```
My jest test with promise
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