I have a sticky bar which appears at the bottom of the screen, but when the user scrolls to a particular element, in this case Apply Now button, then the sticky bar disappears (in scss I've set it to display: none;). I need to write test case for such behaviour, can someone please help me with this, been stuck to this since a long time.
My React component:
/* eslint-disable #typescript-eslint/no-unsafe-assignment,#typescript-eslint/no-unsafe-member-access */
import { ButtonActionEmphasis } from '#honeycomb-npm/honeycomb-react';
import { useEffect, useState } from 'react';;
import './ApplyNowStickyBar.scss';
const ApplyNowStickyBar = (): JSX.Element => {
const [isElementVisible, setIsElementVisible] = useState(false);
const applyNowButton = document.getElementById('calc-applyNowBtn');
useEffect(() => {
const observer = new IntersectionObserver((entries) => {
const entry = entries[0];
setIsElementVisible(entry.isIntersecting);
})
if(applyNowButton !== null){
observer.observe(applyNowButton);
}
}, [applyNowButton]);
return (
<div data-testid='applyNowStickySection' className={isElementVisible ? 'apply-now-bar' : 'apply-now-sticky-bar'}>
<div className="section">
<ButtonActionEmphasis>
Apply Now
</ButtonActionEmphasis>
</div>
</div>
);
};
export default ApplyNowStickyBar;
I'm trying to do this in my test case but it always keeps failing and says that the element is visible
/* eslint-disable #typescript-eslint/no-unsafe-call */
/* eslint-disable #typescript-eslint/no-unsafe-member-access */
/* eslint-disable #typescript-eslint/no-unsafe-assignment */
import { fireEvent, render, RenderResult, screen, waitFor } from '#testing-library/react';
import { Provider } from 'react-redux';
import { store } from '../../store/store';
import ApplyNowStickyBar from './ApplyNowStickyBar';
const setup = (): RenderResult => {
const applyNowStickyBar = document.getElementById('calc-applyNowBtn');
const intersectionObserverMock = () => ({
observe: () => applyNowStickyBar
})
window.IntersectionObserver = jest.fn().mockImplementation(intersectionObserverMock);
return render(<Provider store={store}><ApplyNowStickyBar/></Provider>);
};
describe('ApplyNowStickyBar', () => {
it('should render', async () => {
const { baseElement } = setup();
await waitFor(() => expect(baseElement).toBeInTheDocument());
});
it('should not show apply now sticky bar when ', async () => {
Object.defineProperty(window, 'innerWidth', {writable: true, configurable: true, value: 300})
setup();
const applyNowBtn = document.getElementById('calc-applyNowBtn');
await fireEvent.scroll(window, { target: { scrollY: applyNowBtn ?.getBoundingClientRect().y } });
expect(screen.getByTestId('applyNowStickySection')).not.toBeVisible();
});
});
Related
I want to render overlay on the long running operations.
Consider I have the following code
let spinnerState = useRecoilValue(overlayState);
return <BrowserRouter>
<Spin indicator={<LoadingOutlined />} spinning={spinnerState.shown} tip={spinnerState.content}>.........</BrowserRouter>
What I do in different components
const [, setOverlayState] = useRecoilState(overlayState);
const onButtonWithLongRunningOpClick = async () => {
Modal.destroyAll();
setOverlayState({
shown: true,
content: text
});
try {
await myApi.post({something});
} finally {
setOverlayState(overlayStateDefault);
}
}
How can I refactor this to use such construction that I have in this onbuttonclick callback? I tried to move it to the separate function, but you cannot use hooks outside of react component. It's frustrating for me to write these try ... finally every time. What I basically want is something like
await withOverlay(async () => await myApi.post({something}), 'Text to show during overlay');
Solution
Write a custom hook that includes both UI and API. This pattern is widely used in a large app but I couldn't find the name yet.
// lib/buttonUi.js
const useOverlay = () => {
const [loading, setLoading] = useState(false);
return {loading, setLoading, spinnerShow: loading };
}
export const useButton = () => {
const overlay = useOverlay();
const someOp = async () => {
overlay.setLoading(true);
await doSomeOp();
/* ... */
overlay.setLoading(false);
}
return {someOp, ...overlay}
}
// components/ButtonComponent.jsx
import { useButton } from 'lib/buttonUi';
const ButtonComponent = () => {
const {spinnerShow, someOp} = useButton();
return <button onClick={someOp}>
<Spinner show={spinnerShow} />
</button>
}
export default ButtonComponent;
create a custom hook that handles the logic for showing and hiding the overlay.
import { useRecoilState } from 'recoil';
const useOverlay = () => {
const [, setOverlayState] = useRecoilState(overlayState);
const withOverlay = async (fn: () => Promise<void>, content: string) => {
setOverlayState({ shown: true, content });
try {
await fn();
} finally {
setOverlayState(overlayStateDefault);
}
};
return withOverlay;
};
You can then use the useOverlay hook in your components
import { useOverlay } from './useOverlay';
const Component = () => {
const withOverlay = useOverlay();
const onButtonWithLongRunningOpClick = async () => {
await withOverlay(async () => await myApi.post({ something }), 'Text to show during overlay');
};
return <button onClick={onButtonWithLongRunningOpClick}>Click me</button>;
};
I'm working with React Testing Library. The problem I have is that fireEvent doesn't trigger, and the error throws, is following:
Expected number of calls: >= 1
Received number of calls: 0
For this particular test do I need to work with #testing-library/react-hooks? Do I need to render my Product component wrapped on context provider where the function is imported from?
Product.tsx component
import React, { FunctionComponent } from "react";
import { useProducts } from "./ContextWrapper";
import { Button } from "#mui/material";
import { articleProps } from "./types";
const Product: FunctionComponent<{ article: articleProps }> = ({
article,
}) => {
const { handleAddToCart } = useProducts(); // this is from global context
return (
<Button
aria-label="AddToCart"
onClick={() => handleAddToCart(article)}
>
Add to cart
</Button>
);
};
export default Product;
Product.test.tsx
import React from "react";
import { fireEvent, render, screen } from "#testing-library/react";
import Product from "./Product";
import { act, renderHook } from "#testing-library/react-hooks";
const article = {
name: "samsung",
variantName: "phone",
categories: [],
};
const renderProduct = () => {
return render(
<ContextProvider>
<Product article={article} />
</ContextProvider>
);
};
//first test just tests if button exists with provided content and works fine
test("renders product", () => {
render(<Product article={article} />);
const AddToCartbutton = screen.getByRole("button", { name: /AddToCart/i });
expect(AddToCartbutton).toHaveTextContent("Add to cart");
});
// this test throws the error described above
test("test addToCart button", () => {
renderProduct();
const onAddToCart = jest.fn();
const AddToCartbutton = screen.getByRole("button", { name: /AddToCart/i });
fireEvent.click(AddToCartbutton);
expect(onAddToCart).toHaveBeenCalled();
});
ContextWrapper.tsx
import React, {createContext, useContext, ReactNode, useState} from "react";
import { prodProps } from "../types";
type ProductContextProps = {
productData: prodProps;
handleAddToCart: (clickedItem: productPropsWithAmount) => void;
};
const ProductContextDefaultValues: ProductContextProps = {
productData: null as any,
handleAddToCart:null as any;
};
type Props = {
children: ReactNode;
};
const ProductContext = createContext<ProductContextProps>(
ProductContextDefaultValues
);
export function useProducts() {
return useContext(ProductContext);
}
const ContextWrapper = ({ children }: Props) => {
const { data } = useGraphQlData();
const [productData, setProductData] = useState<Category>(data);
const [cartItems, setCartItems] = useState<prodProps[]>([]);
useEffect(() => {
if (data) {
setProductData(data);
}
}, [data]);
const handleAddToCart = (clickedItem: prodProps) => {
setCartItems((prev: prodProps[]) => {
return [...prev, { ...clickedItem }];
});
};
return (
<ProductContext.Provider
value={{
productData,
handleAddToCart,
}}
>
{children}
</ProductContext.Provider>
);
};
export default ContextWrapper;
I advise you to create Button Component and test it separately like this:
const onClickButton = jest.fn();
await render(<Button onClick={onClickButton} />);
const AddToCartbutton = screen.getByRole("button", { name: /AddToCart/i });
await fireEvent.click(AddToCartbutton);
expect(onClickButton).toHaveBeenCalled();
One way to accomplish this is to mock ContextWrapper, as your test is specifically referring to Product component.
So, you could do something like this into your test:
import * as ContextWrapper from '--- PATH TO ContextWrapper ---';
test('test addToCart button', () => {
/// mockFunction
const onAddToCart = jest.fn();
jest.spyOn(ContextWrapper, 'useProducts').mockImplementationOnce(() => ({
productData: {
example_prodProp1: 'initialValue1',
example_prodProp2: 'initialValue2',
},
// Set mockFunction to handleAddToCart of useProducts
handleAddToCart: onAddToCart,
}));
render(<Product article={article} />);
const AddToCartbutton = screen.getByRole('button', { name: /AddToCart/i });
fireEvent.click(AddToCartbutton);
expect(onAddToCart).toHaveBeenCalledWith({
prodProp1: 'samsung',
prodProp2: 'phone',
});
});
In this line jest.spyOn(ContextWrapper, 'useProducts').mockImplementation we are mocking useProducts return value and setting the handleAddToCart function to your mockFunction and that's how you can check if it has been called.
* This test is strictly focused on Product component and you just want to garantee that the component calls the handleAddToCart function from ContextWrapper.
For test how handleAddToCart should work, you can create a specific test for the ContextWrapper.
I want to test the modal component, but there is an error with defining the cancel button,
it renders only if it's not mobile.
isMobile is a variable that is a boolean value from hook - useBreakpoint (ant design library hook).
I don't know how to mock that value, or how to click that button.
Note: if I remove the isMobile check, the button clicks well:)
import React from 'react'
import {Grid, Typography} from 'antd'
import {Button} from '#/components/Button'
import {Modal} from '#/components/Modal'
import translations from './translations'
import {ConfirmationModalProps} from './props'
const {Text} = Typography
const {useBreakpoint} = Grid
export const ConfirmationModal = ({visible, onClose, children}: ConfirmationModalProps) => {
const screens = useBreakpoint()
const isMobile = screens.xs
return (
<Modal
title={translations().chargeConfirmation}
visible={visible}
onOk={onClose}
onCancel={onClose}
footer={[
!isMobile && (
<Button role={'cancel-button'} type={'ghost'} key={'cancel'} onClick={onClose}>
{ translations().cancel }
</Button>
),
<Button type={'primary'} key={'charge'} onClick={onClose}>
{ translations().confirm }
</Button>
]}
>
<Text>{translations().confirmationText(children)}</Text>
</Modal>
)
}
describe('ConfirmationModal', () => {
it('should should the children and close button', async () => {
const onClose = jest.fn()
jest.mock('antd/es/grid/hooks/useBreakpoint', () => ({
xs: false
}))
render(<ConfirmationModal onClose={onClose} visible={true}>100</ConfirmationModal>)
const child = screen.getByText('Are you sure you want to charge 100')
expect(child).toBeTruthy()
expect(screen.queryByTestId('cancel')).toBeDefined()
await waitFor(() => screen.queryByTestId('cancel'))
fireEvent.click(screen.queryByRole('cancel-button'))
expect(onClose).toHaveBeenCalledTimes(1)
})
})
Errors are:
Error: Unable to fire a "click" event - please provide a DOM element.
Unable to find an accessible element with the role "cancel-button"
Depending on queryByRole or getByRole selector.
What is wrong?
Let's take a look at the source code of the useBreakpoint hook.
import { useEffect, useRef } from 'react';
import useForceUpdate from '../../_util/hooks/useForceUpdate';
import type { ScreenMap } from '../../_util/responsiveObserve';
import ResponsiveObserve from '../../_util/responsiveObserve';
function useBreakpoint(refreshOnChange: boolean = true): ScreenMap {
const screensRef = useRef<ScreenMap>({});
const forceUpdate = useForceUpdate();
useEffect(() => {
const token = ResponsiveObserve.subscribe(supportScreens => {
screensRef.current = supportScreens;
if (refreshOnChange) {
forceUpdate();
}
});
return () => ResponsiveObserve.unsubscribe(token);
}, []);
return screensRef.current;
}
export default useBreakpoint;
It uses ResponsiveObserve.subscribe() to get the supportScreens, it calls ResponsiveObserve.register(), the .register() method use window.matchMedia() underly. jestjs use JSDOM(a DOM implementation) as its test environment, but JSDOM does not implement window.matchMedia() yet. So we need to mock it, see Mocking methods which are not implemented in JSDOM
E.g.
import { render } from '#testing-library/react';
import React from 'react';
import { Grid } from 'antd';
const { useBreakpoint } = Grid;
describe('72021761', () => {
test('should pass', () => {
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: jest.fn().mockImplementation(
(query) =>
({
addListener: (cb: (e: { matches: boolean }) => void) => {
cb({ matches: query === '(max-width: 575px)' });
},
removeListener: jest.fn(),
matches: query === '(max-width: 575px)',
} as any)
),
});
let screensVar;
function Demo() {
const screens = useBreakpoint();
screensVar = screens;
return <div />;
}
render(<Demo />);
expect(screensVar).toEqual({
xs: true,
sm: false,
md: false,
lg: false,
xl: false,
xxl: false,
});
});
});
I'm using react-router-dom: "^6.2.2" in my project for a long time, but I don't know before that this version is not included useBlocker() and usePrompt(). So I'm found this solution and followed them. Then implemented into React Hook createContext() and useContext(). The dialog is displayed when changing route or refresh the page as expected.
But it has an error that useLocation() get the previous location despite the fact that I'm at the current page.
The NavigationBlocker code.
import React, { useState, useEffect, useContext, useCallback, createContext } from "react"
import { useLocation, useNavigate, UNSAFE_NavigationContext } from "react-router-dom"
import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle } from "#mui/material"
const navigationBlockerContext = createContext()
function NavigationBlocker(navigationBlockerHandler,canShowDialogPrompt) {
const navigator = useContext(UNSAFE_NavigationContext).navigator
useEffect(()=>{
console.log("useEffect() in NavigationBlocker")
if (!canShowDialogPrompt) return
// For me, this is the dark part of the code
// maybe because I didn't work with React Router 5,
// and it emulates that
const unblock = navigator.block((tx)=>{
const autoUnblockingTx = {
...tx,
retry() {
unblock()
tx.retry()
}
}
navigationBlockerHandler(autoUnblockingTx)
})
return unblock
})
}
function NavigationBlockerController(canShowDialogPrompt) {
// It's look like this function is being re-rendered before routes done that cause the useLocation() get the previous route page.
const navigate = useNavigate();
const currentLocation = useLocation();
const [showDialogPrompt, setShowDialogPrompt] = useState(false);
const [wantToNavigateTo, setWantToNavigateTo] = useState(null);
const [isNavigationConfirmed, setIsNavigationConfirmed] = useState(false);
const handleNavigationBlocking = useCallback(
(locationToNavigateTo) => {
// currentLocation.pathname is the previous route but locationToNavigateTo.location.pathname is the current route
if (!isNavigationConfirmed && locationToNavigateTo.location.pathname !== currentLocation.pathname) // {
setShowDialogPrompt(true);
setWantToNavigateTo(locationToNavigateTo);
return false;
}
return true;
},
[isNavigationConfirmed]
);
const cancelNavigation = useCallback(() => {
setIsNavigationConfirmed(false);
setShowDialogPrompt(false);
}, []);
const confirmNavigation = useCallback(() => {
setIsNavigationConfirmed(true);
setShowDialogPrompt(false);
}, []);
useEffect(() => {
if (isNavigationConfirmed && wantToNavigateTo) {
navigate(wantToNavigateTo.location.pathname);
setIsNavigationConfirmed(false)
setWantToNavigateTo(null)
}
}, [isNavigationConfirmed, wantToNavigateTo]);
NavigationBlocker(handleNavigationBlocking, canShowDialogPrompt);
return [showDialogPrompt, confirmNavigation, cancelNavigation];
}
function LeavingPageDialog({showDialog,setShowDialog,cancelNavigation,confirmNavigation}) {
const preventDialogClose = (event,reason) => {
if (reason) {
return
}
}
const handleConfirmNavigation = () => {
setShowDialog(false)
confirmNavigation()
}
const handleCancelNavigation = () => {
setShowDialog(true)
cancelNavigation()
}
return (
<Dialog fullWidth open={showDialog} onClose={preventDialogClose}>
<DialogTitle>ต้องการบันทึกการเปลี่ยนแปลงหรือไม่</DialogTitle>
<DialogContent>
<DialogContentText>
ดูเหมือนว่ามีการแก้ไขข้อมูลเกิดขึ้น
ถ้าออกจากหน้านี้โดยที่ไม่มีการบันทึกข้อมูล
การเปลี่ยนแปลงทั้งหมดจะสูญหาย
</DialogContentText>
</DialogContent>
<DialogActions>
<Button variant="outlined" color="error" onClick={handleConfirmNavigation}>
ละทิ้งการเปลี่ยนแปลง
</Button>
<Button variant="contained" onClick={handleCancelNavigation}>
กลับไปบันทึกข้อมูล
</Button>
</DialogActions>
</Dialog>
)
}
export function NavigationBlockerProvider({children}) {
const [showDialogLeavingPage,setShowDialogLeavingPage] = useState(false)
const [showDialogPrompt,confirmNavigation,cancelNavigation] = NavigationBlockerController(showDialogLeavingPage)
return (
<navigationBlockerContext.Provider value={{showDialog:setShowDialogLeavingPage}}>
<LeavingPageDialog showDialog={showDialogPrompt} setShowDialog={setShowDialogLeavingPage} cancelNavigation={cancelNavigation} confirmNavigation={confirmNavigation}/>
{children}
</navigationBlockerContext.Provider>
)
}
export const useNavigationBlocker = () => {
return useContext(navigationBlockerContext)
}
Expected comparison.
"/user_profile" === "/user_profile"
Error in comparison code.
"/user_profile" === "/home"
// locationToNavigateTo and currentLocation variable
The NavigationBlocker consumer code usage example.
function UserProfile() {
const prompt = useNavigatorBlocker()
const enablePrompt = () => {
prompt.showDialog(true)
}
const disablePrompt = () => {
prompt.showDialog(false)
}
}
The dialog image if it works correctly and if I click discard change, then route to the page that I clicked before. (Not pop-up when clicked anything except changing route.)
There is a bug that the dialog is poped-up when clicked at the menu bar button. When I clicked discard change the page is not changed.
Thank you, any help is appreciated.
From what I can see your useNavigationBlockerController hook handleNavigationBlocking memoized callback is missing a dependency on the location.pathname value. In other words, it is closing over and referencing a stale value.
Add the missing dependencies:
const navigationBlockerContext = createContext();
...
function useNavigationBlockerHandler(
navigationBlockerHandler,
canShowDialogPrompt
) {
const navigator = useContext(UNSAFE_NavigationContext).navigator;
useEffect(() => {
if (!canShowDialogPrompt) return;
// For me, this is the dark part of the code
// maybe because I didn't work with React Router 5,
// and it emulates that
const unblock = navigator.block((tx) => {
const autoUnblockingTx = {
...tx,
retry() {
unblock();
tx.retry();
}
};
navigationBlockerHandler(autoUnblockingTx);
});
return unblock;
});
}
...
function useNavigationBlockerController(canShowDialogPrompt) {
// It's look like this function is being re-rendered before routes done that cause the useLocation() get the previous route page.
const navigate = useNavigate();
const currentLocation = useLocation();
const [showDialogPrompt, setShowDialogPrompt] = useState(false);
const [wantToNavigateTo, setWantToNavigateTo] = useState(null);
const [isNavigationConfirmed, setIsNavigationConfirmed] = useState(false);
const handleNavigationBlocking = useCallback(
(locationToNavigateTo) => {
// currentLocation.pathname is the previous route but locationToNavigateTo.location.pathname is the current route
if (
!isNavigationConfirmed &&
locationToNavigateTo.location.pathname !== currentLocation.pathname
) {
setShowDialogPrompt(true);
setWantToNavigateTo(locationToNavigateTo);
return false;
}
return true;
},
[isNavigationConfirmed, currentLocation.pathname] // <-- add current pathname
);
const cancelNavigation = useCallback(() => {
setIsNavigationConfirmed(false);
setShowDialogPrompt(false);
}, []);
const confirmNavigation = useCallback(() => {
setIsNavigationConfirmed(true);
setShowDialogPrompt(false);
}, []);
useEffect(() => {
if (isNavigationConfirmed && wantToNavigateTo) {
navigate(wantToNavigateTo.location.pathname);
setIsNavigationConfirmed(false);
setWantToNavigateTo(null);
}
}, [isNavigationConfirmed, navigate, wantToNavigateTo]); // <-- add navigate
useNavigationBlockerHandler(handleNavigationBlocking, canShowDialogPrompt);
return [showDialogPrompt, confirmNavigation, cancelNavigation];
}
...
export function NavigationBlockerProvider({ children }) {
const [showDialogLeavingPage, setShowDialogLeavingPage] = useState(false);
const [
showDialogPrompt,
confirmNavigation,
cancelNavigation
] = useNavigationBlockerController(showDialogLeavingPage);
return (
<navigationBlockerContext.Provider
value={{ showDialog: setShowDialogLeavingPage }}
>
<LeavingPageDialog
showDialog={showDialogPrompt}
setShowDialog={setShowDialogLeavingPage}
cancelNavigation={cancelNavigation}
confirmNavigation={confirmNavigation}
/>
{children}
</navigationBlockerContext.Provider>
);
}
...
export const useNavigationBlocker = () => {
return useContext(navigationBlockerContext);
};
Hllo Guys, I'm having a bit trouble with testing my component
The problem is that I would like to test my React Native Component that uses saga to fetch data from server.
The Problem is that I do know what I'm supposed to do, I think I should mock my API calls in my test file but I do not know how :/
The component file is really simple, when mounted it dispatches action to fetch list on vehicles, and then it shows them in UI. And until that is fetched it shows loading text
Bellow are my current setup of components & test file.
Here is a screen component that fetches initial data on screen load
Screen Component
import React, { useContext, useEffect, useState } from 'react';
import { Platform, FlatList, View, ActivityIndicator, Text } from 'react-native';
import PropTypes from 'prop-types';
import { useDispatch, useSelector } from 'react-redux';
import { vehiclesActions } from '_store/vehicles';
export const MainScreen = ({ navigation }) => {
/**
* Redux selectors and dispatch
*/
const {
loading = true,
vehicles = [],
loadMore = false
} = useSelector((state) => state.vehicles);
/**
* Initial effect, fetches all vehicles
*/
useEffect(() => {
dispatch(
vehiclesActions.vehicleGet({
page: 1,
})
);
}, []);
const renderCard = () => {
return (<View><Text>Test</Text></View>)
}
if (loading) {
return (<View><Text>App Loading </Text></View>
}
return (
<View style={styles.wrapper}>
<View
style={
Platform.OS === 'ios' ? { marginTop: 30 } : { marginTop: 0, flex: 1 }
}
>
{!loading && (
<View style={Platform.OS === 'ios' ? {} : { flex: 1 }}>
<FlatList
testID={'flat-list'}
data={vehicles}
renderItem={renderCard}
/>
</View>
)}
</View>
</View>
);
};
MainScreen.propTypes = {
navigation: PropTypes.object
};
export default MainScreen;
My Vehicles Saga:
const api = {
vehicles: {
getVehicles: (page) => {
return api.get(`/vehicles/list?page=${page}`, {});
},
}
function* getVehicles(action) {
try {
const { page } = action.payload;
const { data } = yield call(api.vehicles.getVehicles, page);
yield put({ type: vehiclesConstants.VEHICLE_GET_SUCCESS, payload: data });
} catch (err) {
yield call(errorHandler, err);
yield put({ type: vehiclesConstants.VEHICLE_GET_FAIL });
}
}
export function* vehiclesSaga() {
yield takeLatest(vehiclesConstants.VEHICLE_GET_REQUEST, getVehicles);
}
Actions:
export const vehiclesActions = {
vehicleGet: payload => ({ type: vehiclesConstants.VEHICLE_GET_REQUEST, payload }),
vehicleGetSuccess: payload => ({ type: vehiclesConstants.VEHICLE_GET_SUCCESS, payload }),
vehicleGetFail: error => ({ type: vehiclesConstants.VEHICLE_GET_FAIL, error }),
}
Reducer
import { vehiclesConstants } from "./constants";
const initialState = {
vehicles: [],
loading: true,
};
export const vehiclesReducer = (state = initialState, action) => {
switch (action.type) {
case vehiclesConstants.VEHICLE_GET_REQUEST:
return {
...state,
loading: true,
};
case vehiclesConstants.VEHICLE_GET_SUCCESS:
return {
...state,
loading: false,
vehicles: action.payload,
};
}
}
My Test File
import 'react-native';
import React from 'react';
import {cleanup, render, fireEvent} from '#testing-library/react-native';
import AppScreen from '../../../../src/screens/App/index';
import {Provider} from 'react-redux';
import {store} from '../../../../src/store/configureStore';
describe('App List Component', () => {
beforeEach(() => jest.useFakeTimers());
afterEach(cleanup);
it('should render vehicle list page title', async () => {
const navigation = {
setParams: () => {},
navigate: jest.fn(),
};
const route = {
}
const component = (
<Provider store={store}>
<AppScreen route={route} navigation={navigation} />
</Provider>);
const {getByText, getByTestId} = render(component);
const pageTitle = await getByText('App Loading'); // this works fine
expect(pageTitle).toBeDefined();
});
it('should navigate to add vehicle', async () => {
const navigation = {
setParams: () => {},
navigate: jest.fn(),
};
const route = {
}
const component = (
<Provider store={store}>
<AppScreen route={route} navigation={navigation} />
</Provider>);
const {getByText, getByTestId} = render(component);
const flatList = await getByTestId('flat-list');// this throws error since flat list is still not shown, and loading is showing instead
});
Like I see above I cannot find element with testId flat-list, since component AppScreen it always show loading text, is there any way I could mock that API call and make this to work ?
Jest allows you to mock any module using jest.mock.
You have to write an alternative to axios.get like this
const vehiclesData = [
// ... put default data here
]
const delay = (ms, value) =>
new Promise(res => setTimeout(() => res(value), ms))
const mockAxiosGet = async (path) => {
let result = null
if (path.includes('vehicles/list') {
const query = new URLSearchParams(path.replace(/^[^?]+\?/, ''))
const page = + query.get('page')
const pageSize = 10
const offset = (page - 1)*pageSize
result = vehiclesData.slice(offset, offset + pageSize)
}
return delay(
// simulate 100-500ms latency
Math.floor(100 + Math.random()*400),
{ data: result }
)
}
Then modify the test file as
import 'react-native';
import React from 'react';
import {cleanup, render, fireEvent} from '#testing-library/react-native';
import axios from 'axios'
// enable jest mock on 'axios' module
jest.mock('axios')
import AppScreen from '../../../../src/screens/App/index';
import {Provider} from 'react-redux';
import {store} from '../../../../src/store/configureStore';
describe('App List Component', () => {
before(() => {
// mock axios implementation
axios.get.mockImplementation(mockAxiosGet)
})
beforeEach(() => jest.useFakeTimers());
afterEach(cleanup);
it('should render vehicle list page title', async () => {
const navigation = {
setParams: () => {},
navigate: jest.fn(),
};
const route = {
}
const component = (
<Provider store={store}>
<AppScreen route={route} navigation={navigation} />
</Provider>);
const {getByText, getByTestId} = render(component);
const pageTitle = await getByText('App Loading'); // this works fine
expect(pageTitle).toBeDefined();
});
it('should navigate to add vehicle', async () => {
const navigation = {
setParams: () => {},
navigate: jest.fn(),
};
const route = {
}
const component = (
<Provider store={store}>
<AppScreen route={route} navigation={navigation} />
</Provider>);
const {getByText, getByTestId} = render(component);
const flatList = await getByTestId('flat-list');// this throws error since flat list is still not shown, and loading is showing instead
});
For your use case, read more at Mocking Implementations