how can i test the child component has mounted. Please see the below implmentation.
i have an api call, until the data resolves it will show a loader then it will show the actual component.
Using mount i need to simulate a click on the child component also. What am i doing wrong here.
Please see the below snippet.
// App.js
import React, {Component, Fragment} from 'react'
import Child from './child'
class App extends Component{
state = {
data: null,
enable: false
}
componentDidMount(){
this.getData()
}
getData = async () => {
const response = await fetch('http://www.example.com');
const data = await response.json();
this.setState({
data
})
}
_handleChildClick = () => {
this.setState({
enable: true
})
}
render(){
const {data, enable} = this.state
if(!data){
return (
<div>
Loading
</div>
)
}else{
<Fragment>
<Child
handleChildClick={this._handleChildClick}
/>
</Fragment>
}
}
}
export default App
import React from 'react';
const child = () => {
return(
<div>
<button
className="toggle"
onClick={props.handleChildClick}
>
Toggle
</button>
</div>
)
}
export default child
// App.test.js
import React from 'react';
import {enzyme} from 'enzyme';
import App from './App';
describe("App test cases", () => {
it('should trigger _handleChildClick', async () => {
window.fetch = jest.fn().mockImplementation(() => ({
status: 200,
json: () => new Promise((resolve, reject) => {
resolve(
{
name: "some data"
}
)
})
}))
const mountWrapper = await mount(<App />)
mountWrapper.update()
console.log("mountWrapper", mountWrapper.debug()) // showing the loader one
setTimeout(() => {
console.log("mountWrapper", mountWrapper.debug()) // nothing showing
// expect(mountWrapper.find('.toggle').length).toEqual(1)
},0)
})
})
You have to update your enzyme wrapper inside the timeout.
import React from 'react';
import {enzyme} from 'enzyme';
import App from './App';
describe("App test cases", () => {
it('should trigger _handleChildClick', async () => {
window.fetch = jest.fn().mockImplementation(() => ({
status: 200,
json: () => new Promise((resolve, reject) => {
resolve(
{
name: "some data"
}
)
})
}))
const mountWrapper = await mount(<App />)
mountWrapper.update()
console.log("mountWrapper", mountWrapper.debug()) // showing the loader one
setTimeout(() => {
//**An update required here
mountWrapper.update();
console.log("mountWrapper", mountWrapper.debug()) // nothing showing
// expect(mountWrapper.find('.toggle').length).toEqual(1)
},0)
})
})
Live example created here: https://codesandbox.io/s/5083l6vmjk
Related
I am working through a react app using v17. I have a component that adds an expense. The functionality in the component works as expected in the GUI but when I try to test it using jest/enzyme it throws an error of TypeError: Cannot read properties of undefined (reading 'find'). In the GUI it finds the expense I am trying to edit without issue. Am I not testing it correctly when trying to match a snapshot?
Edit Expense Component
import React from "react";
import { useParams } from "react-router-dom";
import { connect } from "react-redux";
import { ExpenseForm } from "./ExpenseForm";
import { editExpense, removeExpense } from "../actions/expenses";
//React router
import { useNavigate } from "react-router-dom";
export const EditExpensePage = (props) => {
const navigate = useNavigate();
const { expenseID } = useParams();
const foundExpense = props.expenses.find(
(expense) => expense.id === expenseID
);
return (
<div>
<h1>Edit Expense</h1>
<ExpenseForm
expense={foundExpense}
onSubmit={(expense) => {
// //Edit expense action expects 2 params (id, updates)
props.editExpense(expenseID, expense);
// //Redirect to dashboard
navigate("/");
}}
/>
<button
onClick={(e) => {
e.preventDefault();
props.removeExpense(expenseID);
navigate("/");
}}
>
Remove Expense
</button>
</div>
);
};
const mapStateToProps = (state, props) => ({
expenses: state.expenses
});
const mapDispatchToProps = (dispatch) => ({
editExpense: (id, expense) => dispatch(editExpense(id, expense)),
removeExpense: (id) => dispatch(removeExpense(id))
});
export default connect(mapStateToProps, mapDispatchToProps)(EditExpensePage);
Current Test
import React from "react";
import { shallow } from "enzyme";
import { EditExpensePage } from "../../components/EditExpensePage";
import { testExpenses } from "../fixtures/expenses";
let history, editExpense, removeExpense, wrapper;
//Mock for use navigate
const mockedUsedNavigate = jest.fn();
jest.mock("react-router-dom", () => ({
...jest.requireActual("react-router-dom"),
useNavigate: () => mockedUsedNavigate
}));
const setup = (props) => {
const component = shallow(
<EditExpensePage
{...props}
expense={editExpense}
history={history}
removeExpense={removeExpense}
/>
);
return {
component: component
};
};
describe("EditForm component", () => {
beforeEach(() => {
setup();
});
test("should render EditExpensePage", () => {
expect(wrapper).toMatchSnapshot();
});
});
Updated editExpense value in test
const setup = (props) => {
//editExpense = testExpenses[1]; //Same error
editExpense = jest.fn(); //Same error
let removeExpense = jest.fn();
let history = jest.fn();
const component = shallow(
<EditExpensePage
{...props}
expense={editExpense}
history={history}
removeExpense={removeExpense}
/>
);
return {
component: component
};
};
Updated Test File
import React from "react";
import { shallow } from "enzyme";
import { EditExpensePage } from "../../components/EditExpensePage";
import { testExpenses } from "../fixtures/expenses";
let editExpense, expenseID, removeExpense, wrapper;
//Mock for use navigate
const mockedUsedNavigate = jest.fn();
jest.mock("react-router-dom", () => ({
...jest.requireActual("react-router-dom"),
useNavigate: () => mockedUsedNavigate
}));
const setup = (props) => {
expenseID = 1;
editExpense = [testExpenses.find((expense) => expense.id === expenseID)];
console.log(editExpense);
//Output from this console log
// [
// {
// id: 1,
// description: 'Wifi payment',
// note: 'Paid wifi',
// amount: 10400,
// createdAt: 13046400000
// }
// ]
const component = shallow(
<EditExpensePage
{...props}
expense={editExpense}
removeExpense={removeExpense}
/>
);
return {
component: component
};
};
describe("EditForm component", () => {
beforeEach(() => {
setup();
});
test("should render EditExpensePage", () => {
expect(wrapper).toMatchSnapshot();
});
});
Updated your code
import React from "react";
import { shallow } from "enzyme";
import { EditExpensePage } from "../../components/EditExpensePage";
import { testExpenses } from "../fixtures/expenses";
let history, editExpense, removeExpense, wrapper;
//Mock for use navigate
const mockedUsedNavigate = jest.fn();
jest.mock("react-router-dom", () => ({
...jest.requireActual("react-router-dom"),
useNavigate: () => mockedUsedNavigate
}));
const setup = (props) => {
editExpense = testExpenses; //it should be an array like this [{ id: 1 }]
const component = shallow(
<EditExpensePage
{...props}
expenses={editExpense}
history={history}
removeExpense={removeExpense}
/>
);
return {
component: component
};
};
describe("EditForm component", () => {
beforeEach(() => {
setup();
});
test("should render EditExpensePage", () => {
expect(wrapper).toMatchSnapshot();
});
});
Your EditExpensePage is calling props.expenses, but in your test cases, you never set it up.
You only introduce it here
let history, editExpense, removeExpense, wrapper;
but you haven't set the value for editExpense which means it's undefined.
That's why undefined.find throws an error.
I'd suggest you set a mocked value for editExpense.
I want to test the child component render after click a display detail button. When click the button, it will call api getPokemonDetail to get the Pokemon detail and then change the PokemonDetail state.
The app is running well. But on my unit test, I can only get the Loading text after button click. I used axios-mock-adapter to mock axios request. If I disable the axios mock, it's running correct. Looks like the axios mock is not working after button click. How should I fix it? The below is my code.
Parent Component:
const getPokemonDetail = async (pokemonName) => {
const queryName = pokemonName ?? "ditto"
try {
const result = await axios.get(`https://pokeapi.co/api/v2/pokemon/${queryName}`)
console.log(result.data)
const { name, id, weight } = result.data
return Promise.resolve({ name, id, weight })
} catch (error) {
return undefined
}
}
const ParentComponent = (props) => {
const { pokemonName } = props
const [pokemonDetail, setPokemonDetail] = useState({
detail: null,
loading: true,
})
const retrieveDisplayStatus = async () => {
const getPokemonDetailResult = await getPokemonDetail(pokemonName)
if (getPokemonDetailResult) {
setPokemonDetail({
detail: getPokemonDetailResult,
loading: false,
})
}
}
const { showDetails, toggleDetails } = useToggleDetails(retrieveDisplayStatus)
return (
<>
<h2>Display Pokemon Detail</h2>
<button onClick={toggleDetails}>Display Detail</button>
<>
{showDetails ? (
<>{
pokemonDetail.loading ?
<div>Loading...</div> :
<ChildComponent pokemonDetail={pokemonDetail.detail} />
}</>
) : null}
</>
</>
)
}
Child Component:
const ChildComponent = (props) => {
const { pokemonDetail } = props
const { id, name, weight } = pokemonDetail
return (
<>
<div>ChildComponent</div>
<div data-testid="testId">id: {id}</div>
<div>name: {name}</div>
<div>weight: {weight}</div>
</>
)
}
export default ChildComponent
On the Parent component test:
import { render, screen } from "#testing-library/react"
import ParentComponent from "../ParentComponent"
import userEvent from "#testing-library/user-event"
import axios from "axios"
import MockAdapter from "axios-mock-adapter"
describe("test", () => {
let mock
beforeAll(() => {
mock = new MockAdapter(axios)
})
afterEach(() => {
mock.reset()
})
test("Render child component after api call", async () => {
render(<ParentComponent />)
const heading = screen.getByText("Display Pokemon Detail")
expect(heading).toBeInTheDocument()
const toggleButton = screen.getByRole("button", { name: /display/i })
expect(toggleButton).toBeInTheDocument()
userEvent.click(toggleButton)
const loadingText = await screen.findByText(new RegExp("Loading", "i"))
expect(loadingText).toBeInTheDocument()
// the below is not working, looks like the axios mock not working
mock.onGet(`https://pokeapi.co/api/v2/pokemon/ditto`).reply(200, { name: "ditto", id: 132, weight: 40 })
const childHeading = await screen.findByText("ChildComponent")
expect(childHeading).toBeInTheDocument()
const id = await screen.findByTestId("testId")
expect(id).toHaveTextContent("132")
})
})
My component has a function which is triggered when a save button is clicked. Then based on that a fetch is done in the wrapper and the fetch response is then again passed down as a prop. So the putFn property accepts a function, the putResponse accepts a Promise.
I would like to mock the wrapper and focus in this test just on the component, in this example "myComponent".
Given the following test setup:
./MyComponent.test.js
function setup() {
let mockPutResponse;
const putMockFn = jest.fn(() => {
mockPutResponse = Promise.resolve(
JSON.stringify({ success: true, loading: false })
);
});
render(<MyComponent putFn={putMockFn} putResponse={mockPutResponse} />);
return { putMockFn };
}
test("MyComponent saves the stuff", async () => {
const { putMockFn } = setup();
const button = screen.getByRole("button", { name: /save changes/i });
userEvent.click(button);
// this passes
expect(putMockFn).toHaveBeenCalled();
// this does not pass since the component shows this message
// based on the putResponse property
expect(await screen.findByText(/saved successfully/i)).toBeInTheDocument();
});
How can I mock the return value passed into the putResponse property?
The component I want to test is something in the line of this:
./MyComponent.js
import React from "react";
const MyComponent = ({ putFn, putResponse }) => {
return (
<form onSubmit={putFn}>
{putResponse?.loading && <p>Loading...</p>}
{putResponse?.success && <p>saved succesfully</p>}
<label htmlFor="myInput">My input</label>
<input name="myInput" id="myInput" type="text" />
<button>Save changes</button>
</form>
);
};
export default MyComponent;
Which is used by a kind of wrapper, something similar to:
./App.js (arbitrary code)
import React, { useState } from "react";
import MyComponent from "./MyComponent";
export default function App() {
const [wrapperPutResponse, setWrapperPutResponse] = useState();
const handlePut = e => {
e.preventDefault();
setWrapperPutResponse({ loading: true });
// timeout, in the actual app this is a fetch
setTimeout(function() {
setWrapperPutResponse({ success: true, loading: false });
}, 3000);
};
return <MyComponent putFn={handlePut} putResponse={wrapperPutResponse} />;
}
Created a sandbox: codesandbox.io/s/bold-cloud-2ule8?file=/src/MyComponent.test.js
You can create a Wrapper component to render and control MyComponent
import React, { useState, useEffect } from "react";
import { screen, render } from "#testing-library/react";
import userEvent from "#testing-library/user-event";
import MyComponent from "./MyComponent";
const mockPutResponse = jest.fn()
function setup() {
const Wrapper = () => {
const [clicked, setClicked] = useState(false)
const response = clicked ? { success: true, loading: false} : null
useEffect(() => {
mockPutResponse.mockImplementation(() => {
setClicked(true)
})
}, [])
return <MyComponent putFn={mockPutResponse} putResponse={response} />
}
render(<Wrapper />);
}
test("MyComponent saves the stuff", async () => {
setup()
// expect(await screen.findByText(/loading.../i)).toBeInTheDocument();
const button = screen.getByRole("button", { name: /save changes/i });
userEvent.click(button);
// this passes
expect(mockPutResponse).toHaveBeenCalled();
// had some issue with expect acting up when using the toBeInDocument assertion
// so I used this one instead
const text = await screen.findByText(/saved succesfully/i)
expect(text).toBeTruthy()
});
Codesandbox
how can i test the child component onclick.
Please see the below snippet.
// App.js
import React, {Component, Fragment} from 'react'
import Child from './child'
class App extends Component{
state = {
data: null,
enable: false
}
componentDidMount(){
this.getData()
}
getData = async () => {
const response = await fetch('http://www.example.com');
const data = await response.json();
this.setState({
data
})
}
_handleChildClick = () => {
this.setState({
enable: true
})
}
render(){
const {data, enable} = this.state
if(!data){
return (
<div>
Loading
</div>
)
}else{
<Fragment>
<Child
handleChildClick={this._handleChildClick}
/>
</Fragment>
}
}
}
export default App
import React from 'react';
const child = () => {
return(
<div>
<button
className="toggle"
onClick={props.handleChildClick}
>
Toggle
</button>
</div>
)
}
export default child
// App.test.js
import React from 'react';
import {enzyme} from 'enzyme';
import App from './App';
describe("App test cases", () => {
it('should trigger _handleChildClick', async () => {
window.fetch = jest.fn().mockImplementation(() => ({
status: 200,
json: () => new Promise((resolve, reject) => {
resolve(
{
name: "some data"
}
)
})
}))
const mountWrapper = await mount(<App />)
setTimeout(() => {
mountWrapper.update()
const SpyhandleChildClick = jest.spyOn(mountWrapper.instance(),'_handleChildClick')
mountWrapper.find('.toggle').simulate('click')
expect(SpyhandleChildClick).toHaveBeenCalled() // not called
},0)
})
})
Some important points to consider.
Asynchronous code in your tests
If you have to do asynchronous tasks in your tests you always have to await until the asynchronous stuff is completed.
setTimeout(() => {
mountWrapper.update()
const SpyhandleChildClick = jest.spyOn(mountWrapper.instance(),'_handleChildClick')
mountWrapper.find('.toggle').simulate('click')
expect(SpyhandleChildClick).toHaveBeenCalled() // not called
},0)
Above in your code you have a timeout segment. Any test condition inside this code block will not be evaluated since by the time they are evaluated you 'test session' will already be over due to the aync nature.
Testing arrow functions in React with enzyme - forceUpdate()
There seem to be a problem with the enzyme library where you have to force update the react component after spying for it to latch on to the method.
Please follow the github issue for more information : https://github.com/airbnb/enzyme/issues/365
I also cleaned up your test code a bit to make it more understandable!
// App.test.js
import React from 'react';
import {enzyme} from 'enzyme';
import App from './App';
describe("App test cases", () => {
it("should trigger _handleChildClick", async () => {
window.fetch = jest.fn().mockImplementation(() => ({
status: 200,
json: () =>
new Promise((resolve, reject) => {
resolve({
name: "some data"
});
})
}));
const mountWrapper = mount(<App />);
mountWrapper.update();
console.log("mountWrapper", mountWrapper.debug()); // showing the loader one
//[FIX]This code will block and wait for your asynchronous tasks to be completed
await new Promise(res => setTimeout(() => res(), 0));
mountWrapper.update();
console.log("mountWrapper", mountWrapper.debug()); // nothing showing
expect(mountWrapper.find(".toggle").length).toEqual(1);
//[FIX]Get a reference from the wrapper and force update after the spyOn call
const instance = mountWrapper.instance();
const spy = jest.spyOn(instance, "_handleChildClick");
instance.forceUpdate();
mountWrapper.find(".toggle").simulate("click");
expect(spy).toHaveBeenCalled();
});
});
Live Demo Link: Click on the 'Tests' tab on the browser to see the test results
https://codesandbox.io/s/mz21kpm37j
I wrote a simple unit test for the following. I am new to React JS testing - Trying to run a test using jest and enzyme.
render() {
return (
<div>
<div className="not-found">
<div className='_2'>WAS NOT FOUND</div>
<div onClick={() => {window.history.back()}} className='not-found-
btn' href='/'>GO BACK</div>
)
}
}
The file looks simple, there are no props and the only thing not being covered when the test is running is onClick . How could I test onClick and make sure the test is 100 % covered. Thanks
<div onClick={() => {window.history.back()}} className='not-found-
btn' href='/'>GO BACK</div>
file.test.js
// jest mock functions (mocks this.props.func)
const onClick = jest.fn();
// defining this.props
const baseProps = {
onClick,
}
describe(' Test', () => {
let wrapper;
let tree;
beforeEach(() => wrapper = shallow(<Component{...baseProps } />));
// before each test, shallow mount the Component
it('should render correctly', () => {
tree = renderer.create(<NotFound {...baseProps} />)
let treeJson = tree.toJSON()
expect(treeJson).toMatchSnapshot();
tree.unmount()
});
it('calls onClick event ', () => {
const mockOnClick = jest.fn();
const wrapper = shallow(
<NotFound onClick={mockOnClick} className='not-found-btn' />
);
const component = wrapper.shallow();
component.find('GO BACK').simulate('click');
expect(mockOnClick.mock.calls.length).toEqual(1);
I'd avoid using window history and instead use react-router-dom for MPAs. In addition, instead of using an anonymous function, you can use a PureComponent class (it's similar to a Component class, but it doesn't update state) with a method class function.
Working example: https://codesandbox.io/s/j3qo6ppxqy (this example uses react-router-dom and has a mix of integration and unit testing -- see the tests tab at the bottom of the page to run the tests and look for __test__ folders to see the code)
components/NotFound/notfound.js
import React, { PureComponent } from "react";
import { Button } from "antd";
export default class NotFound extends PureComponent {
handlePageBack = () => this.props.history.push("/");
render = () => (
<div className="notfound">
<h1>404 - Not Found!</h1>
<Button type="default" onClick={this.handlePageBack}>
Go Back
</Button>
</div>
);
}
components/NotFound/__tests__/notfound.test.js (as mentioned here, you can also test the class method, if desired)
import React from "react";
import { shallowComponent } from "../../../tests/utils";
import NotFound from "../notfound";
const mockGoBack = jest.fn();
const initialProps = {
history: {
goBack: mockGoBack
}
};
/*
the shallowComponent function below is a custom function in "tests/utils/index.js" that
simplifies shallow mounting a component with props and state
*/
const wrapper = shallowComponent(<NotFound {...initialProps} />);
describe("Not Found", () => {
it("renders without errors", () => {
const notfoundComponent = wrapper.find("div.notfound");
expect(notfoundComponent).toHaveLength(1);
});
it("pushes back a page when Go Back button is clicked", () => {
wrapper.find("Button").simulate("click");
expect(mockGoBack).toHaveBeenCalled();
});
});
window.history.back is being called, but it has a delay time. I can make it work using a Promise:
const Component = ()=> (<div>
<button onClick={()=> window.history.back()} className="btn btn-back">
Back
</button>
</div>)
Component.test.js
import React from "react";
import { render, unmountComponentAtNode } from "react-dom";
import { act } from "react-dom/test-utils";
const delayAction = (fn, time = 1000) =>
new Promise((resolve) => {
fn();
setTimeout(() => {
resolve();
}, time);
});
let container = null;
describe("App tests", () => {
afterEach(() => {
//unmount Component...
});
beforeEach(() => {
//mount Component
});
it("should call history.back()", async (done) => {
const btnBack = container.querySelector(".btn-back");
await act(() =>
delayAction(() => btnBack.dispatchEvent(new MouseEvent("click", { bubbles: true })))
);
// asserts..
done();
});
});