react-testing-library and MSW - reactjs

I have been trying to implement some tests on my project, but I got some blockers.
This error:
Unable to find an element with the text: C-3PO/i. This could be because the text is broken up by multiple elements. In this case, you can provide a function for your text matcher to make your matcher more flexible.
I don't know how to fix it. Should I use another query or I am missing something?
import { render, screen } from "#testing-library/react";
import CharacterList from "./index";
import { rest } from "msw";
import { setupServer } from "msw/node";
const server = setupServer(
rest.get("https://swapi.dev/api/people/", (req, res, ctx) => {
return res(
ctx.json({ results: [{ name: "Luke Skywalker", gender: "male" }] })
);
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
describe("render characters", () => {
it("should render character C-3PO when get api response", async () => {
render(<CharacterList />);
const character = await screen.findByText("C-3PO/i");
expect(character).toBeInTheDocument();
});
});
and my component:
import { NavLink } from "react-router-dom";
import { useEffect, useState } from "react";
export default function CharactersList() {
const [data, setData] = useState(undefined);
const [home, setHome] = useState(undefined);
useEffect(() => {
fetch("https://swapi.dev/api/people/")
.then((response) => {
if (response) {
return response.json();
} else {
return Promise.reject(response);
}
})
.then((data) => {
setData(data);
});
}, []);
if (data) {
return data.results.map((item) => {
const id = item.url.slice(29);
return (
<>
<NavLink to={`/character/${id}`}>{item.name}</NavLink>
<p>Gender: {item.gender}</p>
<p>Home planet: {item.homeworld}</p>
</>
);
});
} else {
return <p>Loading...</p>;
}
}

Please Add screen.debug() to see your actual screen, from that you can consider which get method will work to you.
I think the problem is you don't have C-3PO/i text in the DOM
describe("render characters", () => {
it("should render character C-3PO when get api response", async () => {
render(<CharacterList />);
screen.debug(); <------- ADD THIS
const character = await screen.findByText("C-3PO/i");
expect(character).toBeInTheDocument();
});
});

Related

Vitest React - component not "re-rendering" after server request

This is my simplifiedReact component:
export const EntryDetail = () => {
const { articleId } = useParams();
const [article, setArticle] = useState({ title: null, body: null, comments: [], likes: [] });
const { title, body, comments, likes } = article;
useEffect(() => {
(async () => {
try {
const response = await getArticleDetail(articleId);
const { title, body, comments, likes } = response.data;
setArticle({ title, body, comments, likes });
} catch (e) {
console.error(e);
}
})();
}, []);
return (
<Container>
{
!article.title
? <div>Loading...</div>
: <>
<h1>{title}</h1>
<p className="body">{body}</p>
</>
}
</Container>
);
};
And this is my test:
import { render, screen } from '#testing-library/react';
import { StateProvider } from '../../config/state';
import { EntryDetail } from './index';
const flushPromises = () => new Promise(resolve => setTimeout(resolve, 0));
vi.mock('react-router-dom', () => ({
useParams: () => ({
articleId: '63d466ca3d00b50db15aed93',
}),
}));
describe("EntryDetail component", () => {
it("should render the EntryDetail component correctly", async () => {
render(
<EntryDetail />
);
await flushPromises();
const element = screen.getByRole("heading");
expect(element).toBeInTheDocument();
});
});
This is what I'm getting in the console:
I was expecting the "await flushPromises()" would actually wait for the response from the call in the useEffect to the "update" the component", but I guess this is kind of "static"? How should this be handled? I actually want to test if the component itself works effectively, I don't want to mock a response, I want to see if the component actually reacts appropriately after the response is back.

Write the test case for useEffect with Fetch API and useState in react

Following are my code which includes the fetch API(getData) call with the useEffect and once get the response it will set the result into the setData using useState
I am trying to write the test case for the useEffect and useState but its failing and when I am seeing into the coverage ,I am getting the red background color with statements not covered for the useEffect block.
import { getData } from '../../api/data';
const [data, setData] = useState({});
useEffect(() => {
getData({ tableName }).then((response) => {
try {
if (response && response.result) {
const result = Array.isArray(response.result)
? response.result[0]
: response.result;
const createDate = result.createdDate;
result.name = result.firstName;
result.submittedDate = `${createDate}`;
result.attribute = Array.isArray(result.attribute)
? result.attribute
: JSON.parse(result.attribute);
setData(result);
}
} catch (error) {
const errorObj = { error: error.message || 'error' };
setData({ errorObj });
}
});
}, []);
And I tried to write the test cases as following for the above code.
import React from "react";
import {
shallowWithIntl,
loadTranslation,
} from "../../../node_modules/enzyme-react-intl/lib/enzyme-react-intl";
import ParentPage from "ParentPage";
import ChildPage from "ChildPage";
import mockResponse from "mockData";
import { shallow, mount } from "enzyme";
import { act } from "react-dom/test-utils";
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve(mockResponse),
})
);
describe("ParentPage", () => {
let useEffect;
let wrapper;
const mockUseEffect = () => {
useEffect.mockImplementationOnce((f) => f());
};
beforeEach(() => {
const defaultProps = {
tableName: "tableName",
};
wrapper = shallowWithIntl(<ParentPage {...defaultProps} />);
useEffect = jest.spyOn(React, "useEffect");
mockUseEffect();
});
it("Should render", () => {
expect(wrapper).toMatchSnapshot();
});
it("Compenent render", async () => {
let wrapper;
await act(async () => {
const setWidgets = jest.fn();
const useStateSpy = jest.spyOn(React, "useState");
useStateSpy.mockImplementation([mockResponse, setWidgets]);
wrapper = await mount(<ChildPage data={mockResponse} />);
await act(async () => {
wrapper.update();
});
console.log(wrapper);
});
});
});
But when I tried using npm run test,And check the coverage I am still getting the statements not covered for the useEffect and useState.
What should I do to achieve the coverage as maximum as possible?

How to test a component that fetches data in useEffect and stores it in state with react-testing-library and jest?

I'm fairly new to react-testing-library and generally testing. I want to test a component that fetches the data from an API in useEffect hook. Then it stores it in local state. It renders these array data with array.map, but i'm getting Error: Uncaught [TypeError: Cannot read properties of undefined (reading 'map')] error. I'm probably doing wrong in my test suite, i've researched a lot but couldn't fix it.
import React from 'react';
import { render, screen } from '#testing-library/react';
import '#testing-library/jest-dom'
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { OnePiece } from '.';
const server = setupServer(rest.get('server http address', (req, res, ctx) => {
const totalData = [
{ name: "doffy", price: 100, image: "image url" },
{ name: "lamingo", price: 500, image: "image url" }
];
return res(
ctx.status(200),
ctx.json({
data: { crew: totalData }
})
)
}))
beforeAll(() => server.listen());
afterAll(() => server.close());
beforeEach(() => server.restoreHandlers());
//console.log("mocking axios", axios)
describe('OnePiece', () => {
test('fetches the data from the API and correctly renders it', async () => {
//Here's probably where i fail. Please someone tell me the right way :)
await render(<OnePiece />)
const items = await screen.findAllByAltText('product-image');
expect(items).toHaveLength(2);
// screen.debug()
})
})
And the below is the parts of code useEffect, and totalData.map in the component:
const [totalData, setTotalData] = useState([]);
const [crew, setCrew] = useState('straw-hat-pirates');
useEffect(() => {
let isApiSubscribed = true;
const getOpData = async () => {
const getCrews = await axios.get('http address');
if (isApiSubscribed) {
let data = getCrews.data;
data = data[crew];
// console.log("data", data);
setTotalData(data);
}
}
getOpData();
return () => {
isApiSubscribed=false;
}
}, [crew])
.........
//in the return part
<ProductsWrapper>
{totalData.map((product, index) =>
<ProductCard key={index} name={product.name} price={product.price} imageUrl={product.image} />
)}
</ProductsWrapper>
As i predicted, the problem was the async data fetching. Currently setTimeOut is more than enough for me, but if someone sees this in the future, you can look for the waitFor method of react-testing-library.
Here's the fixed part:
describe('OnePiece', () => {
test('fetches the data from the API and correctly renders it', async () => {
render(<OnePiece />)
setTimeout(async () => {
const items = await screen.findAllByAltText('product-image');
expect(items).toHaveLength(2);
}, 4000)
//screen.debug()
})
})

Jest Mocked data not loaded during test

I have a component that loads data from an API which I mocked for my test but it is not loaded as the test cannot find the element which contain the data.
component:
import { useDispatch, useSelector } from "react-redux";
import { useState, useEffect, useCallback } from "react";
import { businessDataActions } from "../store/business-data";
import { fetchBusinessListing } from "../services/business-listing";
import styles from "../styles/BizCard.module.css";
import BizCardItem from "./BizCardItem";
const BizCard = (props) => {
const dispatch = useDispatch();
const [listing, setListing] = useState([]);
//load all listing
const fetchListing = useCallback(async () => {
dispatch(businessDataActions.setIsLoading({ isLoading: true }));
const ListingService = await fetchBusinessListing();
if (ListingService.success) {
setListing(ListingService.data);
} else {
dispatch(
businessDataActions.setNotify({
severity: "error",
message: "Problem when fetching listing.",
state: true,
})
);
}
dispatch(businessDataActions.setIsLoading({ isLoading: false }));
}, []);
useEffect(() => {
fetchListing();
}, []);
const businessList = listing.map((item) => (
<BizCardItem
key={item.key}
id={item.id}
name={item.name}
shortDescription={item.shortDescription}
imageUrl={item.imageUrl}
/>
));
return (
<div className={styles.grid} role="grid">
{businessList}
</div>
);
};
test file:
const bizListing = [
...some fake data
];
jest.mock("../../services/business-listing", () => {
return function fakeListing() {
return { success: true, data: bizListing };
}
});
afterEach(cleanup);
describe('BizCard', () => {
test("loading listing", async () => {
useSession.mockReturnValueOnce([null, false]);
await act(async () => {render(
<BizCard />
)});
const itemGrid = await screen.findAllByRole("gridcell");
expect(itemGrid).not.toHaveLength(0);
});
});
services/business-listing:
export const fetchBusinessListing = async() => {
try {
const response = await fetch(
"/api/business"
);
if (!response.ok) {
throw new Error('Something went wrong!');
}
const data = await response.json();
const loadedBusiness = [];
for (const key in data) {
let imgUrl =
data[key].imageUrl !== "undefined" && data[key].imageUrl !== ""
? data[key].imageUrl
: '/no-image.png';
loadedBusiness.push({
key: data[key]._id,
id: data[key]._id,
name: data[key].businessName,
shortDescription: data[key].shortDescription,
imageUrl: imgUrl,
});
}
return { success: true, data: loadedBusiness };
} catch (error) {
return ({success: false, message: error.message});
}
}
The test executed with these returned:
TypeError: (0 , _businessListing.fetchBusinessListing) is not a function
48 | // }
49 |
> 50 | const ListingService = await fetchBusinessListing();
Unable to find role="gridcell"
I can confirm gridcell is rendered when I am using browser.
Can anyone please shed some light on my problem
Manage to solve the problem myself, problem is with the mock:
jest.mock("../../services/business-listing", () => {
return {
fetchBusinessListing: jest.fn(() => { return { success: true, data: bizListing }}),
}
});

debounce async/await and update component state

I have question about debounce async function. Why my response is undefined? validatePrice is ajax call and I receive response from server and return it (it is defined for sure).
I would like to make ajax call after user stops writing and update state after I get reponse. Am I doing it right way?
handleTargetPriceDayChange = ({ target }) => {
const { value } = target;
this.setState(state => ({
selected: {
...state.selected,
Price: {
...state.selected.Price,
Day: parseInt(value)
}
}
}), () => this.doPriceValidation());
}
doPriceValidation = debounce(async () => {
const response = await this.props.validatePrice(this.state.selected);
console.log(response);
//this.setState({ selected: res.TOE });
}, 400);
actions.js
export function validatePrice(product) {
const actionUrl = new Localization().getURL(baseUrl, 'ValidateTargetPrice');
return function (dispatch) {
dispatch({ type: types.VALIDATE_TARGET_PRICE_REQUEST });
dispatch(showLoader());
return axios.post(actionUrl, { argModel: product }, { headers })
.then((res) => {
dispatch({ type: types.VALIDATE_TARGET_PRICE_REQUEST_FULFILLED, payload: res.data });
console.log(res.data); // here response is OK (defined)
return res;
})
.catch((err) => {
dispatch({ type: types.VALIDATE_TARGET_PRICE_REQUEST_REJECTED, payload: err.message });
})
.then((res) => {
dispatch(hideLoader());
return res.data;
});
};
}
Please find below the working code with lodash debounce function.
Also here is the codesandbox link to play with.
Some changes:-
1) I have defined validatePrice in same component instead of taking from prop.
2) Defined the debounce function in componentDidMount.
import React from "react";
import ReactDOM from "react-dom";
import _ from "lodash";
import "./styles.css";
class App extends React.Component {
state = {
selected: { Price: 10 }
};
componentDidMount() {
this.search = _.debounce(async () => {
const response = await this.validatePrice(this.state.selected);
console.log(response);
}, 2000);
}
handleTargetPriceDayChange = ({ target }) => {
const { value } = target;
console.log(value);
this.setState(
state => ({
selected: {
...state.selected,
Price: {
...state.selected.Price,
Day: parseInt(value)
}
}
}),
() => this.doPriceValidation()
);
};
doPriceValidation = () => {
this.search();
};
validatePrice = selected => {
return new Promise(resolve => resolve(`response sent ${selected}`));
};
render() {
return (
<div className="App">
<input type="text" onChange={this.handleTargetPriceDayChange} />
</div>
);
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Hope that helps!!!
You can use the throttle-debounce library to achieve your goal.
Import code in top
import { debounce } from 'throttle-debounce';
Define below code in constructor
// Here I have consider 'doPriceValidationFunc' is the async function
this.doPriceValidation = debounce(400, this.doPriceValidationFunc);
That's it.

Resources