Writing unit test for server error scenario in enzyme - javascript

I was working on a functionality which involved fetching the data from an API and rendering the components accordingly.
index.js
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import StarRating from './components/StarRating';
import {
API_URL,
API_TIMEOUT,
} from '../../../../someLoc';
export async function fetchWithTimeout(resource, options = {}) {
const { timeout } = options;
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeout);
const response = await fetch(resource, {
...options,
signal: controller.signal,
});
clearTimeout(id);
return response;
}
const UserRating = ({ productId }) => {
const [loading, setLoading] = useState(true);
const [data, setData] = useState({});
const [error, setError] = useState(false);
useEffect(
() => {
const fetchReviewsNRatings = async (bvUrl, bvTimeout) => {
const url = `${bvUrl}:${productId}`;
const response = await fetchWithTimeout(url, { timeout: bvTimeout });
if (response.ok) {
const ratingData = await response.json();
setData(ratingData);
setLoading(false);
} else {
setError(true);
setLoading(false);
}
};
fetchReviewsNRatings(API_TIMEOUT);
},
[productId],
);
// I didn't know how to test this branch if I simply returned null. so wrapped null inside of div.
// MY INTENT HERE: if(loading) return null;
if (loading) return <div className="test-class">{null}</div>;
// destructuring the response to get the data once loading is complete.
const {
Results: [
{
ABC: {
DEF: { GHI, JKL },
},
},
],
} = data;
const numReviews = +GHI;
const value = JKL.toFixed(1);
return !error && <StarRating value={value} numReviews={numReviews} />;
};
UserRating.propTypes = {
productId: PropTypes.string,
};
export default UserRating;
index.test.js
import React from 'react';
import { mount } from 'enzyme';
import { act } from 'react-dom/test-utils';
import UserRating from '..';
import { PRODUCT_ID } from '../../../../../someLoc';
describe('Testing for <UserRating /> component', () => {
const data = {
Results: [
{
ABC: {
DEF: {
GHI: 4.567,
JKL: 1103,
},
},
},
],
};
const productId = PRODUCT_ID;
let component = null;
beforeEach(() => {
global.fetchWithTimeout = jest.fn(() =>
Promise.resolve({
ok: true,
status: 200,
data,
json: () => data,
}),
);
global.fetch = jest.fn(() =>
Promise.resolve({
ok: true,
status: 200,
data,
json: () => data,
}),
);
component = act(() => mount(<UserRating productId={productId} />));
});
it('should call fetch', async () => {
expect(component.exists()).toBeTruthy();
// Testting for loading state.
expect(component.find('.test-class').exists()).toBe(true);
});
What's working as expected:
The functionality that is expected.
The above test which I managed to write is passing for win-win scenario i.e. when the data is loaded and fetched successfully.
MY QUESTION:
a. I want to write a unit test in such a way so as to test what happens if there is some error from server side(5xx) or from client side(4xx).
b. What else can I improve in my passing test scenario?
What I have tried:
describe('Testing for <UserRating /> component for failed response', () => {
const data = { message: '500: Internal Server Error' };
const error = true;
const productId = PRODUCT_ID;
let component = null;
beforeEach(() => {
global.fetchWithTimeout = jest.fn(() =>
Promise.reject(
new Error({
ok: false,
status: 500,
data,
json: () => data,
}),
),
);
global.fetch = jest.fn(() =>
Promise.reject(
new Error({
ok: false,
status: 500,
data,
json: () => data,
}),
),
);
component = act(() => mount(<UserRating productId={productId} />));
});
it('should not fetch', async () => {
expect(component.exists()).toBeFalsy();
expect(component.find('.test-class').exists()).toBe(true);
console.debug(component);
});
});
When I tried the above code it is breaking the win-win scenario as well with the error message cannot read property exists for null. I am relatively new to enzyme and facing issues while using enzyme for writing tests for hooks. I know that React testing library is preferrable but my organization is yet to shift onto that. I've to write fail case as well since the coverage for branches is poor. I'll be highly obliged for any help. Thanks
REFFERED LINKS and Questions:
how-to-test-async-data-fetching-react-component-using-jest-and-enzyme
Testing hooks with mount
Should a Promise.reject message be wrapped in Error?

Related

Closing server sent event in Nextjs

My component is listening to firestore through server sent events
'use client';
import styles from './Messages.module.scss';
import React, { useState, useContext, useEffect } from 'react';
import Message from '../Message/Message';
import { ChatContext } from '#/context/ChatContext';
import { useMessages } from '#/hooks/useMessages';
import { MessageData } from '#/typedef';
export default function Messages() {
const [messages, setMessages] = useState<Array<MessageData>>([]);
const { data: chatContextData } = useContext(ChatContext);
const slug = chatContextData.chatId ? chatContextData.chatId : '';
const encodedSlug = encodeURIComponent(slug);
const { isLoading } = useMessages(chatContextData.chatId || '', Boolean(chatContextData.chatId));
useEffect(() => {
if (!encodedSlug) {
return;
}
const eventSource = new EventSource(`/api/messages?chatId=${encodedSlug}`);
eventSource.onmessage = (event) => {
const message = JSON.parse(event.data);
setMessages(message?.chats?.messages);
};
return () => {
eventSource.close();
};
}, [encodedSlug]);
return (
<div className={styles.messages}>
{isLoading && <p>Loading...</p>}
{messages?.map((message: MessageData) => (
<Message message={message} key={message.id} />
))}
</div>
);
}
I am using swr to handle my api
import useSWR from 'swr';
const fetcher = async (url: string) => {
const res = await fetch(url);
return res.json();
};
export const useMessages = (slug: string, shouldFetch: boolean) => {
const encodedSlug = encodeURIComponent(slug);
const { data, isLoading, error, mutate } = useSWR(
shouldFetch ? `/api/messages/?chatId=${encodedSlug}` : null,
fetcher
);
return {
data,
isLoading,
error,
mutate
};
};
Below is my api
import { database } from '#/utils/firebase';
import type { NextApiRequest, NextApiResponse } from 'next';
const handler = async (req: NextApiRequest, res: NextApiResponse<FirebaseFirestore.DocumentData>) => {
res.setHeader('Content-Type', 'text/event-stream');
if (req.method === 'GET') {
const chatId = req.query.chatId;
try {
const chatRef = database.collection('chats').doc(chatId as string);
const chatData = await chatRef.get();
if (!chatData.exists) {
return res.status(404).json({ error: 'Chat not found' });
}
// send the initial data
res.write(`data: ${JSON.stringify({ chats: chatData.data() })}\n\n`);
// listen to updates and send to the connected client
let unsubscribe: () => void;
const sendMessage = (chatData: FirebaseFirestore.DocumentData) => {
res.write(`data: ${JSON.stringify({ chats: chatData })}\n\n`);
};
unsubscribe = chatRef.onSnapshot((doc) => {
if (doc.exists) {
const chatData = doc.data() as FirebaseFirestore.DocumentData;
sendMessage(chatData);
}
});
// remove the listener when the client disconnects
req.on('close', () => {
unsubscribe();
});
res.statusCode = 200;
res.end();
} catch (error) {
console.error(error);
return res.status(500).json({ error: 'Error retrieving Chat' });
}
}
};
export default handler;
However, when I switch tabs or even close my laptop, I can see that a request to my api is being sent. It never stops. I even killed the dev server and the server sent events still seem to be going. It's been 10 minutes and I've sent around 1000 read requests to firestore.
How do I close my event source connection? Why is my firestore still being sent read requests? My application is not public so I have no idea where all these read requests are coming from.

Context returns undefined functions on any refresh in React Native App

I have an issue on any app refresh, ONE of my Contexts is returning undefined functions.
I have the following structure:
export default function AppWrapper() {
return (
<GlobalProvider>
<ChildProvider>
<App />
</ChildProvider>
</GlobalProvider>
);
}
I am getting undefined on functions from ChildProvider, which looks like this:
const INITIAL_STATE = {
orders: [],
error: null,
};
const ChildContext = React.createContext({ ...INITIAL_STATE });
const ChildConsumer = ChildContext.Consumer;
const OrderProvider = (props) => {
const { children } = props;
const { store, userId } = useContext(GlobalContext);
const [state, setState] = React.useReducer((state, newState) => {
const combinedState = { ...state, ...newState };
return combinedState;
}, INITIAL_STATE);
const getDataFromStore = async () => {
if (store?.id) {
try {
const { error, data } = await getOrdersByStore({store.id});
if (data) {
const data1 = <iterating through data>;
await setState({ orders: data1 });
} else {
await setState({ error: error });
}
} catch (e) {
await setState({ error: e });
}
}
};
const actions = { getDataFromStore};
return <ChildContext.Provider value={{ ...state, ...actions }}>{children}</ChildContext.Provider>;
};
export { ChildContext, ChildConsumer, ChildProvider };
And in Home.js, I call these functions as such:
const { getDataFromStore } = useContext(ChildContext)
useEffect(() => {
const getOrders = async () => {
store?.name && (await getDataFromStore());
};
getOrders();
}, [store]);
And in Home.js is where that function (and any function defined in ChildContext) is undefined on any refresh.
Some Details:
It is not a spelling error, or 'can't find file' error.
The only thing I am currently storing in session is the auth token.
Everything from state is defined (even if in its INITIAL_STATE form)
Wherever I call the undefined function, I use async/await
I do not actually use the Consumer anywhere
The AppWrapper, GlobalContext and ChildContext are all nested inside a context folder. App.js is in the root directory.

Have a javascript function pass a reference to itself in to another function

I found myself continuously writing the same shape of code for asynchronous calls so I tried to wrap it up in something that would abstract some of the details. What I was hoping was that in my onError callback I could pass a reference of the async function being executed so that some middleware could implement retry logic if it was necessary. Maybe this is a code smell that I'm tackling this the wrong way but I'm curious if it's possible or if there are other suggestions for handling this.
const runAsync = (asyncFunc) => {
let _onBegin = null;
let _onCompleted = null;
let _onError = null;
let self = this;
return {
onBegin(f) {
_onBegin = f;
return this;
},
onCompleted(f) {
_onCompleted = f;
return this;
},
onError(f) {
_onError = f;
return this;
},
async execute() {
if (_onBegin) {
_onBegin();
}
try {
let data = await asyncFunc();
if (_onCompleted) {
_onCompleted(data);
}
} catch (e) {
if (_onError) {
_onError(e ** /*i'd like to pass a function reference here as well*/ ** );
}
return Promise.resolve();
}
},
};
};
await runAsync(someAsyncCall())
.onBegin((d) => dispatch(something(d)))
.onCompleted((d) => dispatch(something(d)))
.onError((d, func) => dispatch(something(d, func)))
.execute()
I'm thinking you could use a custom hook. Something like -
import { useState, useEffect } from 'react'
const useAsync = (f) => {
const [state, setState] =
useState({ loading: true, result: null, error: null })
const runAsync = async () => {
try {
setState({ ...state, loading: false, result: await f })
}
catch (err) {
setState({ ...state, loading: false, error: err })
}
}
useEffect(_ => { runAsync() }, [])
return state
}
Now we can use it in a component -
const FriendList = ({ userId }) => {
const response =
useAsync(UserApi.fetchFriends(userId)) // <-- some promise-returning call
if (response.loading)
return <Loading />
else if (response.error)
return <Error ... />
else
return <ul>{response.result.map(Friend)}</ul>
}
The custom hook api is quite flexible. The above approach is naive, but we can think it through a bit more and make it more usable -
import { useState, useEffect } from 'react'
const identity = x => x
const useAsync = (runAsync = identity, deps = []) => {
const [loading, setLoading] = useState(true)
const [result, setResult] = useState(null)
const [error, setError] = useState(null)
useEffect(_ => {
Promise.resolve(runAsync(...deps))
.then(setResult, setError)
.finally(_ => setLoading(false))
}, deps)
return { loading, error, result }
}
Custom hooks are dope. We can make custom hooks using other custom hooks -
const fetchJson = (url = "") =>
fetch(url).then(r => r.json()) // <-- stop repeating yourself
const useJson = (url = "") => // <-- another hook
useAsync(fetchJson, [url]) // <-- useAsync
const FriendList = ({ userId }) => {
const { loading, error, result } =
useJson("some.server/friends.json") // <-- dead simple
if (loading)
return <Loading .../>
if (error)
return <Error .../>
return <ul>{result.map(Friend)}</ul>
}

Rendering a React component in a Jest test returns undefined

I have been trying to create a test with React, with the component TodoItems. It is just a regular Todolist with some basic features. When the test runs, it gives an error saying todos is undefined. The code in the test itself doesn't work as a result.
Test:
it("Tasks length increased by one when task is added.", () => {
const { todos } = render(<TodoItems />);
const todoLen = getByTestId(todos, "tasks");
console.log(todos);
expect(todoLen.toBe(1));
});
Component tested:
export default function Tasks() {
let [tasks, setTasks] = useState([
{
content: "(Dette er en eksempelopppgave) "
}
]);
useEffect(() => {
fetch("http://localhost:8080/all", {
crossDomain: true,
method: "GET",
mode: "no-cors",
credentials: "include"
})
.then(res => res.json())
.then(function(response) {
console.log("TodoItems is fetching.. ", response);
if (response.status !== 200) {
console.log("Fetching failed, response status: " + response.status);
return;
}
response.json().then(function(data) {
//was data instead of tasks, testing
console.log(data, " is a response");
});
})
.catch(function(err) {
console.log("Fetch error: ", err);
});
}, []);
// });
let handleAddTask = task => {
setTasks(tasks.concat(task));
console.log("handleAddTask content: " + JSON.stringify(task));
};
let handleDeleteTask = id => {
setTasks(tasks.filter(task => task.id !== id));
};
return (
<div className="Tasks">
<h2>Tasks</h2>
<TaskList deleteTask={handleDeleteTask} tasks={tasks} />
<TaskForm addTask={handleAddTask} />
<Button onClick={reqListener}>Load</Button>
</div>
);
}
Edit(complete test code):
import React from "react";
import renderer from "react-test-renderer";
import TodoItems from "../components/TodoItems.js";
import { render, fireEvent, getByTestId } from "#testing-library/react";
import App from "../App.js";
//Test for the tasks array.
it("Tasks length increased by one when task is added.", () => {
const { container, getByTestId } = render(<TodoItems />);
const todos = getByTestId("tasks");
expect(container).toBeDefined();
expect(todos).toHaveLength(1);
});
When you're testing with the #testing-library/react, the render method returns container with a bunch of helpful getters, so you can do as follows:
it("Tasks length increased by one when task is added.", () => {
const { container, getByTestId } = render(<TodoItems />);
const todos = getByTestId("tasks");
expect(container).toBeDefined();
expect(todos).toHaveLength(1);
});
For the line const todos = getByTestId("tasks"); to work, you need to add data-testid attribute to your component:
<TaskList deleteTask={handleDeleteTask} tasks={tasks} data-testid="tasks"/>

Getting .then of undefined when trying to test a dispatch action function in React/Redux/Jest

component › toggleNotification actions › creates NOTIFICATION_REMOVE after successfull NOTIFICATION_ADD (error)
TypeError: Cannot read property 'then' of undefined
In my changePassword component
3 dispatch functions, and focusing on testing the toggleNotification.
const mapDispatchToProps = dispatch => ({
verifyEmail: (...args) => { dispatch(verifyEmail(...args)); },
toggleNotification: (flag, msg, type) => { dispatch(toggleNotification(flag, msg, type)); },
displayError: (error, errMsg) => { dispatch(displayError(error, errMsg)); }
});
The toggleNotification dispatch function in /actions
export const toggleNotification = (notification, message, type) => (dispatch) => {
if (notification === true) {
dispatch({
type: NOTIFICATION_ADD,
payload: {
message,
type
}
});
setTimeout(() => {
dispatch({
type: NOTIFICATION_REMOVE
});
}, 7000);
}
};
Finally my changePassword.test.js
import React from 'react';
import { shallow } from 'enzyme';
import toJson from 'enzyme-to-json';
// Testing packages
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import expect from 'expect';
// Actions
import { verifyEmail, toggleNotification } from 'actions';
// Action Types
import { NOTIFICATION_ADD, NOTIFICATION_REMOVE } from 'actionTypes';
// String Constants
import { CHANGE_PASS_NOT_MATCH } from 'copy/Components/login';
// Component to test
import { ChangePasswordJest } from './changePassword';
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);
describe('<ChangePassword /> component', () => {
const wrapper = shallow(<ChangePasswordJest />);
describe('when rendering', () => {
it('should render', () => {
const tree = toJson(wrapper);
expect(tree).toMatchSnapshot();
expect(wrapper).toHaveLength(1);
});
});
describe('toggleNotification actions', () => {
it('creates NOTIFICATION_REMOVE after successfull NOTIFICATION_ADD (error)', () => {
const expectedActions = [
{
type: NOTIFICATION_ADD,
payload: {
notification: true,
message: CHANGE_PASS_NOT_MATCH,
type: 'error'
}
},
{ type: NOTIFICATION_REMOVE }
];
const store = mockStore();
console.log('store -->', store);
console.log('toggleNotification -->', toggleNotification);
return store.dispatch(toggleNotification(true, CHANGE_PASS_NOT_MATCH, 'error')).then(() => {
// return of async actions
expect(store.getActions()).toEqual(expectedActions);
});
});
});
});
In the terminal my console logs print out store and toggleNotification as expected:
Any thoughts or ideas why I'm getting
TypeError: Cannot read property 'then' of undefined
On
return store.dispatch(toggleNotification(true, CHANGE_PASS_NOT_MATCH, 'error'))
.then(() => {
// return of async actions
expect(store.getActions()).toEqual(expectedActions);
});
The problem was that my toggleNotifcations function wasn't actually a Promise.
export const toggleNotification = (notification, message, type) => (dispatch) => {
if (notification === true) {
dispatch({
type: NOTIFICATION_ADD,
payload: {
message,
type
}
});
setTimeout(() => {
dispatch({
type: NOTIFICATION_REMOVE
});
}, 7000);
}
};
Instead all I needed to do in my test was just call that dispatch function:
describe('toggleNotification actions', () => {
let store;
beforeEach(() => {
store = mockStore();
});
it('dispatches a NOTIFICATION_ADD (passwords don\'t match error)', () => {
const expectedActions = [
{
type: NOTIFICATION_ADD,
payload: {
message: CHANGE_PASS_NOT_MATCH,
type: 'error'
}
}
];
store.dispatch(toggleNotification(true, CHANGE_PASS_NOT_MATCH, 'error'));
console.log('store.getActions()', store.getActions());
expect(store.getActions()).toEqual(expectedActions);
});
});
New problem now is that even though I have a passing test for that dispatch action. It did nothing for the coverage!
Will be posting a new question related to this new issue:

Categories