How to test function inside react context - javascript

iam having trouble with covering function inside react context
Before that, let me show you the snippet of the code
import {
useCallback,
useEffect,
useState,
createContext,
ReactNode,
FC
} from 'react';
import { useHistory, useLocation } from 'react-router-dom';
import PropTypes from 'prop-types';
import { pushBackRoute } from '../libs/helpers/backRouteHelper';
export interface BottomBarProviderProps {
children: ReactNode;
}
export interface IBottomBarProvider {
selectedMenu: string;
handleClickMenu: (item: { name: string; route: string }) => void;
}
export const defaultValueBottomBarContext = {
selectedMenu: '',
handleClickMenu: () => {}
};
export const BottomBarContext = createContext<IBottomBarProvider>(
defaultValueBottomBarContext
);
export const BottomBarProvider: FC<BottomBarProviderProps> = ({
children,
...props
}) => {
const history = useHistory();
const location = useLocation();
const [selectedMenu, setSelectedMenu] = useState('Belanja');
const handleClickMenu = useCallback(
(item) => {
setSelectedMenu(item.name);
pushBackRoute(location.pathname);
history.push(item.route);
},
[selectedMenu, location?.pathname]
);
useEffect(() => {
Iif (location.pathname === '/marketplace/history') {
setSelectedMenu('Riwayat');
}
}, [location.pathname]);
const value: IBottomBarProvider = {
selectedMenu,
handleClickMenu
};
return (
<BottomBarContext.Provider
value={value}
{...props}
data-testid="bottom-bar-context"
>
{children}
</BottomBarContext.Provider>
);
};
BottomBarProvider.propTypes = {
children: PropTypes.node
};
BottomBarProvider.defaultProps = {
children: null
};
So i managed to cover most of the hooks and render, but the function/handle function inside this context is really hard
And this is my current test code, below :
/* eslint-disable jest/prefer-called-with */
import { render, screen } from '#testing-library/react';
import { BottomBarProvider } from './BottomBarContext';
import '#testing-library/jest-dom/extend-expect';
const mockHistoryPush = jest.fn();
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useLocation: () => ({
pathname: '/marketplace'
}),
useHistory: () => ({
push: mockHistoryPush
})
}));
describe('BottomBarProvider', () => {
it('should render the children and update the selected menu when handleClickMenu is called', () => {
//Arrange
const children = <div data-testid="children">Hello, World!</div>;
//Act
render(<BottomBarProvider>{children}</BottomBarProvider>);
//Assert
expect(screen.getByTestId('children')).toHaveTextContent('Hello, World!');
});
});
Do you guys have any idea how to cover the function inside this context ?

Mock functions, hooks, and components of the module you don't own are not recommended. The useLocation and useHistory hooks in your case. Incorrect mock will break their functions which causes your tests to pass based on incorrect mock implementation.
Use <MemoryRouter/> component for testing history change. See official example of RTL about how to test react router
You should also create a test component to consume the React Context for testing the <BottomBarProvider/> component and BottomBarContext. The key is to trigger the handleClickMenu event handler by firing a click event on some element of the test component.
E.g.
BottomBarContext.tsx:
import React from 'react';
import {
useCallback,
useEffect,
useState,
createContext,
ReactNode,
FC
} from 'react';
import { useHistory, useLocation } from 'react-router-dom';
// import { pushBackRoute } from '../libs/helpers/backRouteHelper';
export interface BottomBarProviderProps {
children: ReactNode;
}
export interface IBottomBarProvider {
selectedMenu: string;
handleClickMenu: (item: { name: string; route: string }) => void;
}
export const defaultValueBottomBarContext = {
selectedMenu: '',
handleClickMenu: () => { }
};
export const BottomBarContext = createContext<IBottomBarProvider>(
defaultValueBottomBarContext
);
export const BottomBarProvider: FC<BottomBarProviderProps> = ({
children,
...props
}) => {
const history = useHistory();
const location = useLocation();
const [selectedMenu, setSelectedMenu] = useState('Belanja');
const handleClickMenu = useCallback(
(item) => {
setSelectedMenu(item.name);
// pushBackRoute(location.pathname);
history.push(item.route);
},
[selectedMenu, location?.pathname]
);
useEffect(() => {
if (location.pathname === '/marketplace/history') {
setSelectedMenu('Riwayat');
}
}, [location.pathname]);
const value: IBottomBarProvider = {
selectedMenu,
handleClickMenu
};
return (
<BottomBarContext.Provider value={value} {...props} data-testid="bottom-bar-context">
{children}
</BottomBarContext.Provider>
);
};
BottomBarContext.test.tsx:
import { fireEvent, render, screen } from '#testing-library/react';
import '#testing-library/jest-dom';
import React, { useContext } from 'react';
import { MemoryRouter, Route, Switch } from 'react-router-dom';
import { BottomBarContext, BottomBarProvider } from './BottomBarContext';
describe('BottomBarProvider', () => {
test('should pass', async () => {
const TestComp = () => {
const bottomBarContext = useContext(BottomBarContext);
const menuItems = [
{ name: 'a', route: '/a' },
{ name: 'b', route: '/b' },
];
return (
<>
<ul>
{menuItems.map((item) => (
<li key={item.route} onClick={() => bottomBarContext.handleClickMenu(item)}>
{item.name}
</li>
))}
</ul>
<p>selected menu: {bottomBarContext.selectedMenu}</p>
<Switch>
{menuItems.map((item) => (
<Route key={item.route} path={item.route} component={() => <div>{item.name} component</div>} />
))}
</Switch>
</>
);
};
render(
<MemoryRouter initialEntries={['/']}>
<BottomBarProvider>
<TestComp />
</BottomBarProvider>
</MemoryRouter>
);
const firstListItem = screen.getAllByRole('listitem')[0];
fireEvent.click(firstListItem);
expect(screen.getByText('selected menu: a')).toBeInTheDocument();
expect(screen.getByText('a component')).toBeInTheDocument();
});
});
Test result:
PASS stackoverflow/74931928/BottomBarContext.test.tsx (16.574 s)
BottomBarProvider
✓ should pass (116 ms)
----------------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------------------|---------|----------|---------|---------|-------------------
All files | 95 | 66.67 | 75 | 94.74 |
BottomBarContext.tsx | 95 | 66.67 | 75 | 94.74 | 50
----------------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 18.184 s, estimated 23 s
Coverage HTML reporter:

Related

How to use actual module instead of using mock

I have below Jest config
jest: {
configure: {
testEnvironment: 'jsdom',
preset: 'ts-jest',
transform: {...},
moduleNameMapper: {
antd: '<rootDir>/__mocks__/antd/index.tsx'
},
testMatch: ['<rootDir>/src/**/*.test.(ts|tsx)$'],
},
},
In <rootDir>/__mocks__/antd/index.tsx
class Select extends Component<SelectProps> {
static Option: FC = (props) => <option {...props} data-testid="mock-selectOption" />;
render() {
const {
...props
} = this.props;
// #ts-ignore
return <select {...props} data-testid="mock-select" />;
}
}
I have a Select Component
import React from 'react';
import { Select as MySelect } from 'antd';
const { Option } = MySelect;
export interface SelectOption {
id: string;
label: string;
}
interface SelectProp {
options: SelectOption[];
selectedOption: string;
onChange: (key: string) => void;
}
function Select({ options, selectedOption, onChange }: SelectProp) {
return (
<MySelect value={selectedOption} onChange={onChange} >
{options.map((opt) => (
<Option key={opt.id} value={opt.id}>
{opt.label}
</Option>
))}
</MySelect>
);
}
export default Select;
I have a test case
import React from 'react';
import { fireEvent } from '#testing-library/react';
import { render } from 'setupTests';
import Select from './Select';
jest.mock('antd', () => {
const originalModule = jest.requireActual('antd');
return { ...originalModule };
});
describe('Select', () => {
const handleChange = jest.fn();
const mockProps = {
options: [],
onChange: handleChange,
};
it('render successfully', () => {
const { getByTestId } = render(<Select {...mockProps} />);
getByTestId('asca'); // use for debug
});
});
getByTestId('asca') will make the test case fails, then I see below DOM modal
TestingLibraryElementError: Unable to find an element by: [data-testid="asca"]
<body>
<div>
<select
data-testid="mock-select"
/>
</div>
</body>
which turns out still using the mock but not the actual antd component.
I've tried to add
beforeEach(() => {
jest.unmock('antd');
});
but still got the same result.
How can I use the actual module instead of mock?
Use jest.unmock(moduleName) API is correct,
Indicates that the module system should never return a mocked version of the specified module from require() (e.g. that it should always return the real module).
But you need to know:
When using babel-jest, calls to unmock will automatically be hoisted to the top of the code block.
If you import the Select component and call jest.unmock() in beforeEach hook. When running the test case, the mock Select component has been imported, then the beforeEach hook execute. it's too late for jest to unmock the Select component.
Now, you have two options:
__mocks__/antd/index.tsx:
import { SelectProps } from "antd/lib/select";
import React, { Component, FC } from "react";
class Select extends Component<SelectProps<any>> {
static Option: FC = (props) => <option {...props} data-testid="mock-selectOption" />;
render() {
const { ...props } = this.props;
// #ts-ignore
return <select {...props} data-testid="mock-select" />;
}
}
Select.tsx:
import React from 'react';
import { Select as MySelect } from 'antd';
const { Option } = MySelect;
export interface SelectOption {
id: string;
label: string;
}
interface SelectProp {
options: SelectOption[];
selectedOption: string;
onChange: (key: string) => void;
}
function Select({ options, selectedOption, onChange }: SelectProp) {
return (
<MySelect data-testid='asca' value={selectedOption} onChange={onChange}>
{options.map((opt) => (
<Option key={opt.id} value={opt.id}>
{opt.label}
</Option>
))}
</MySelect>
);
}
export default Select;
1. Call jest.unmock() API in module scope of test file(Put it after the import statement is ok, it will be hoisted to the top of the code block)
import React from 'react';
import { render } from '#testing-library/react';
import '#testing-library/jest-dom/extend-expect';
import Select from './Select';
jest.unmock('antd');
describe('Select', () => {
it('render successfully', async () => {
const handleChange = jest.fn();
const mockProps = {
options: [],
onChange: handleChange,
selectedOption: '',
};
const { getByTestId } = render(<Select {...mockProps} />);
expect(getByTestId('asca')).toBeInTheDocument();
});
});
2. Call jest.unmock() API in beforeEach hook or it functional scope, then dynamic import the Select component.(Dynamic import statement must put after the jest.unmock() API)
import React from 'react';
import { render } from '#testing-library/react';
import '#testing-library/jest-dom/extend-expect';
describe('Select', () => {
it('render successfully', async () => {
jest.unmock('antd');
const Select = (await import('./Select')).default
const handleChange = jest.fn();
const mockProps = {
options: [],
onChange: handleChange,
selectedOption: '',
};
const { getByTestId } = render(<Select {...mockProps} />);
expect(getByTestId('asca')).toBeInTheDocument();
});
});
Test result:
PASS stackoverflow/73274190/Select.test.tsx (11.55 s)
Select
✓ render successfully (1156 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 12.049 s, estimated 13 s
jest.config.js:
module.exports = {
preset: 'ts-jest/presets/js-with-ts',
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['jest-extended'],
setupFiles: ['./jest.setup.js'],
};
package version:
"antd": "^4.16.12",
"jest": "^26.6.3",

Testing state updations in an async method via React Testing Library

I have made a dummy component for the purpose of illustration. Please find the code below-
Fetch.js
import React from "react";
import { useFetch } from "./useFetch";
const FetchPost = () => {
const { toShow, dummyAPICall } = useFetch();
return toShow ? (
<>
<button onClick={dummyAPICall} data-testid="fetch-result">
Fetch
</button>
</>
) : null;
};
export { FetchPost };
useFetch.js
import { useState } from "react";
const useFetch = () => {
const [toShow, setToShow] = useState(true);
const dummyAPICall = () => {
fetch("https://jsonplaceholder.typicode.com/todos/1")
.then((response) => response.json())
.then((json) => {
setToShow(false);
})
.catch(() => {
setToShow(true);
});
};
return {
toShow,
setToShow,
dummyAPICall,
};
};
export { useFetch };
I want to make an assertion here, that on click of the Fetch button my Fetch component shouldn't render on screen so, using React Testing Library, I am writing the test case like this:
import { fireEvent, render, screen } from "#testing-library/react";
import { FetchPost } from "../Fetch";
import { useFetch } from "../useFetch";
jest.mock("../useCounter");
describe("Use Fetch tests", () => {
it("Should fetch results and show/hide component", async () => {
useFetch.mockImplementation(() => {
return { toShow: true, dummyAPICall: jest.fn() };
});
render(<FetchPost></FetchPost>);
expect(screen.getByText(/fetch/i)).toBeInTheDocument();
fireEvent.click(screen.getByTestId("fetch-result"));
await waitFor(() =>
expect(screen.queryByText(/fetch/i)).not.toBeInTheDocument()
);
});
});
My assertion:
expect(screen.getByText(/fetch/i)).not.toBeInTheDocument();
is failing as the component is still present. How can I modify my test case to handle this?
Mock network IO side effect is better than mock useFetch hook. It seems you forget to import waitFor helper function from RTL package.
There are two options to mock network IO side effects.
msw, here is example
global.fetch = jest.fn(), don't need to install additional package and set up.
I am going to use option 2 to solve your question.
E.g.
fetch.jsx:
import React from 'react';
import { useFetch } from './useFetch';
const FetchPost = () => {
const { toShow, dummyAPICall } = useFetch();
return toShow ? (
<>
<button onClick={dummyAPICall} data-testid="fetch-result">
Fetch
</button>
</>
) : null;
};
export { FetchPost };
useFetch.js:
import { useState } from 'react';
const useFetch = () => {
const [toShow, setToShow] = useState(true);
const dummyAPICall = () => {
fetch('https://jsonplaceholder.typicode.com/todos/1')
.then((response) => response.json())
.then((json) => {
setToShow(false);
})
.catch(() => {
setToShow(true);
});
};
return { toShow, setToShow, dummyAPICall };
};
export { useFetch };
fetch.test.jsx:
import React from 'react';
import { FetchPost } from './fetch';
import { fireEvent, render, screen, waitFor } from '#testing-library/react';
import '#testing-library/jest-dom/extend-expect';
describe('Use Fetch tests', () => {
it('Should fetch results and show/hide component', async () => {
const mResponse = { json: jest.fn() };
global.fetch = jest.fn().mockResolvedValueOnce(mResponse);
render(<FetchPost />);
expect(screen.getByText(/fetch/i)).toBeInTheDocument();
fireEvent.click(screen.getByTestId('fetch-result'));
await waitFor(() => expect(screen.queryByText(/fetch/i)).not.toBeInTheDocument());
});
});
Test result:
PASS examples/70666232/fetch.test.jsx (12.699 s)
Use Fetch tests
✓ Should fetch results and show/hide component (51 ms)
-------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-------------|---------|----------|---------|---------|-------------------
All files | 93.75 | 100 | 83.33 | 93.75 |
fetch.jsx | 100 | 100 | 100 | 100 |
useFetch.js | 90 | 100 | 80 | 90 | 12
-------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 14.38 s
package version:
"#testing-library/react": "^11.2.2",
"#testing-library/jest-dom": "^5.11.6",
"jest": "^26.6.3",
"react": "^16.14.0"

Reactjs props.match.params.id do not work in react-router-dom 6 [duplicate]

How can i access url parameter in my react component ?
App.js
<Route path="/question/:id" element={<QuestionView />} />
QuestionView.js
class QuestionView extends React.Component {
render() {
const { questions, users } = this.props;
const {id} = ???
Issue
In react-router-dom v6 the Route components no longer have route props (history, location, and match), and the current solution is to use the React hooks "versions" of these to use within the components being rendered. React hooks can't be used in class components though.
To access the match params with a class component you must either convert to a function component, or roll your own custom withRouter Higher Order Component to inject the "route props" like the withRouter HOC from react-router-dom v5.x did.
Solution
I won't cover converting a class component to function component. Here's an example custom withRouter HOC:
const withRouter = WrappedComponent => props => {
const params = useParams();
// etc... other react-router-dom v6 hooks
return (
<WrappedComponent
{...props}
params={params}
// etc...
/>
);
};
And decorate the component with the new HOC.
export default withRouter(Post);
This will inject a params prop for the class component.
this.props.params.id
HOC withRouter TypeScript version with generic Params
withRouter.tsx
import { ComponentType } from 'react';
import { useLocation, useNavigate, useParams } from 'react-router-dom';
export interface WithRouterProps<T = ReturnType<typeof useParams>> {
history: {
back: () => void;
goBack: () => void;
location: ReturnType<typeof useLocation>;
push: (url: string, state?: any) => void;
}
location: ReturnType<typeof useLocation>;
match: {
params: T;
};
navigate: ReturnType<typeof useNavigate>;
}
export const withRouter = <P extends object>(Component: ComponentType<P>) => {
return (props: Omit<P, keyof WithRouterProps>) => {
const location = useLocation();
const match = { params: useParams() };
const navigate = useNavigate();
const history = {
back: () => navigate(-1),
goBack: () => navigate(-1),
location,
push: (url: string, state?: any) => navigate(url, { state }),
replace: (url: string, state?: any) => navigate(url, {
replace: true,
state
})
};
return (
<Component
history={history}
location={location}
match={match}
navigate={navigate}
{...props as P}
/>
);
};
};
MyClass.tsx
import { Component } from 'react';
import { withRouter, WithRouterProps } from './withRouter';
interface Params {
id: string;
}
type Props = WithRouterProps<Params>;
class MyClass extends Component<Props> {
render() {
const { match } = this.props;
console.log(match.params.id); // with autocomplete
return <div>MyClass</div>;
}
}
export default withRouter(MyClass);
Here's the code example I'm using in my project to get the id from the URL:
import React from 'react'
import {Button} from 'antd'
import {useParams} from 'react-router-dom'
const DeleteUser = () => {
const {id} = useParams()
const handleDelete = async () => {
// handle delete function
}
return (
<Button onClick={handleDelete}>Delete User</Button>
)
}
export default DeleteUser
If you would like to use a class, then you will need to wrap it with the withRouter. I provide an example below:
This is my class for the movie form:
class MovieForm extends Form {
state = {
data: {
title: "",
genreId: "",
numberInStock: "",
dailyRentalRate: ""
},
genres: [],
errors: {}
};
schema = {
_id: Joi.string(),
title: Joi.string()
.required()
.label("Title"),
genreId: Joi.string()
.required()
.label("Genre"),
numberInStock: Joi.number()
.required()
.min(0)
.max(100)
.label("Number in Stock"),
dailyRentalRate: Joi.number()
.required()
.min(0)
.max(10)
.label("Daily Rental Rate")
};
componentDidMount() {
const genres = getGenres();
this.setState({ genres });
// const movieId = this.props.match.params.id;
const movieId = this.props.params.id;
if (movieId === "new") return;
const movie = getMovie(movieId);
if (!movie) return this.props.history.replace("/not-found");
this.setState({ data: this.mapToViewModel(movie) });
}
mapToViewModel(movie) {
return {
_id: movie._id,
title: movie.title,
genreId: movie.genre._id,
numberInStock: movie.numberInStock,
dailyRentalRate: movie.dailyRentalRate
};
}
doSubmit = () => {
saveMovie(this.state.data);
this.props.navigate("/movies");
};
render() {
return (
<div>
<h1>Movie Form</h1>
<form onSubmit={this.handleSubmit}>
{this.renderInput("title", "Title")}
{this.renderSelect("genreId", "Genre", this.state.genres)}
{this.renderInput("numberInStock", "Number in Stock", "number")}
{this.renderInput("dailyRentalRate", "Rate")}
{this.renderButton("Save")}
</form>
</div>
);
}
}
I write a wrapper outside of the class:
const withRouter = WrappedComponent => props => {
const params = useParams();
const navigate = useNavigate();
return (
<WrappedComponent
{...props}
params={params}
navigate={navigate}
/>
);
};
Now, at the end of the file I will export it like below:
export default withRouter(MovieForm);
Insdie the withRouter, I get all the functions that I will use later inside the class:
const params = useParams();
const navigate = useNavigate();
TypeScript version
withRouter.tsx
import React from 'react';
import {
useLocation,
useNavigate,
useParams,
NavigateFunction,
Params,
Location,
} from 'react-router-dom';
export interface RouterProps {
router: {
navigate: NavigateFunction;
readonly params: Params<string>;
location: Location;
}
}
function withRouter(Component: React.ComponentType<RouterProps>) {
const ComponentWithRouterProp: React.FC = () => {
const location = useLocation();
const navigate = useNavigate();
const params = useParams();
return (
<Component
router={{ location, navigate, params }}
/>
);
};
return ComponentWithRouterProp;
}
export default withRouter;
MyComponent.tsx
import React from 'react';
import { connect } from 'react-redux';
import { RootState } from '##/redux/store';
import {
addSettings,
updateSettings,
} from '##/redux/mySlice';
import withRouter, { RouterProps } from '##/withRouter';
const mapState = (state: RootState) => ({
myStore: state.variation.myStore,
});
const mapDispatch = {
addSettings,
updateSettings,
};
type IProps = ReturnType<typeof mapState> & typeof mapDispatch & RouterProps;
class MyComponent extends React.Component<IProps> {
constructor(props: IProps) {
super(props);
}
onNavigateHome = () => {
this.props.router.navigate('/');
}
render(): React.ReactNode {
return (
<div className="test" onClick={this.onNavigateHome}>test</div>
);
}
}
export default withRouter(connect(mapState, mapDispatch)(MyComponent));
I had a similar issue, described here:
Params from React Router with class components and typescript
I created a new functional component, so I can use useParams():
import React from 'react';
import { useParams } from 'react-router';
type Props = {
children: JSX.Element
};
export const WithParams: React.FC<Props> = (props) => {
const params = useParams();
return React.cloneElement(props.children, {...props.children.props, ...params });
};
and added it to my Route.element
<Route path="/Contacts/VerifyEmailAddress/:id"
element={
<WithParams>
<VerifyEmail />
</WithParams>
}>
</Route>
and added the parameters I need to the props of my child component.
export class VerifyEmailProps {
public id?: string;
}

Unit test for React component containing useRouteMatch

Having the following component:
import React, { useState, useEffect } from 'react';
import { createPortal } from 'react-dom';
import { useRouteMatch, Link } from 'react-router-dom';
interface MyComponentProps {
myId?: string;
link?: string;
}
export const MyComponent: React.FunctionComponent<MyComponentProps> = ({
myId = 'default-id',
link,
children
}) => {
const [myOutlet, setMyOutlet] = useState<HTMLOListElement>();
const match = useRouteMatch();
useEffect(() => {
const outletElement = document.getElementById(myId) as HTMLOListElement;
if (outletElement) {
setMyOutlet(outletElement);
}
}, [myId]);
if (!myOutlet) {
return null;
}
return createPortal(
<li>
<Link to={link || match.url}>{children}</Link>
</li>,
myOutlet
);
};
export default MyComponent;
I want to write unit tests using React Testing Library for it, the problem is that it keeps throwing an error because of useRouteMatch.
Here is my code:
import { render, screen } from '#testing-library/react';
import { MyComponent } from './my-component';
describe('MyComponent', () => {
const testId = 'default-id';
const link = '/route';
it('should render MyComponent successfully', () => {
const element = render(<MyComponent myId={testId} link={link} />);
expect(element).toBeTruthy();
});
});
The error appears at the line with const match = useRouteMatch();, is there a way to include this part in the test?
You should use <MemoryRouter>:
A <Router> that keeps the history of your “URL” in memory (does not read or write to the address bar)
Provide mock locations in the history stack by using the initialEntries props.
Then, use <Route> component to render some UI when its path matches the current URL.
The following example, assuming that the location pathname in the browser current history stack is /one, <Route>'s path prop is also /one, The two matching, rendering MyComponent.
E.g.
my-component.tsx:
import React, { useState, useEffect } from 'react';
import { createPortal } from 'react-dom';
import { useRouteMatch, Link } from 'react-router-dom';
interface MyComponentProps {
myId?: string;
link?: string;
}
export const MyComponent: React.FunctionComponent<MyComponentProps> = ({ myId = 'default-id', link, children }) => {
const [myOutlet, setMyOutlet] = useState<HTMLOListElement>();
const match = useRouteMatch();
console.log('match: ', match);
useEffect(() => {
const outletElement = document.getElementById(myId) as HTMLOListElement;
if (outletElement) {
setMyOutlet(outletElement);
}
}, [myId]);
if (!myOutlet) {
return null;
}
return createPortal(
<li>
<Link to={link || match.url}>{children}</Link>
</li>,
myOutlet
);
};
export default MyComponent;
my-component.test.tsx:
import React from 'react';
import { render, screen } from '#testing-library/react';
import { MyComponent } from './my-component';
import { MemoryRouter, Route } from 'react-router-dom';
describe('MyComponent', () => {
const testId = 'default-id';
const link = '/route';
it('should render MyComponent successfully', () => {
const element = render(
<MemoryRouter initialEntries={[{ pathname: '/one' }]}>
<Route path="/one">
<MyComponent myId={testId} link={link} />
</Route>
</MemoryRouter>
);
expect(element).toBeTruthy();
});
});
test result:
PASS examples/70077434/my-component.test.tsx (8.433 s)
MyComponent
✓ should render MyComponent successfully (46 ms)
console.log
match: { path: '/one', url: '/one', isExact: true, params: {} }
at MyComponent (examples/70077434/my-component.tsx:13:11)
------------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
------------------|---------|----------|---------|---------|-------------------
All files | 87.5 | 28.57 | 100 | 86.67 |
my-component.tsx | 87.5 | 28.57 | 100 | 86.67 | 18,26
------------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 8.951 s, estimated 9 s
package versions:
"react": "^16.14.0",
"react-router-dom": "^5.2.0"

Enzyme Shallow not working but mount works

I have the following a.jsx file which I am trying to test:
import React, { Component } from "react";
import { SideNavigation } from "#xxx/react-components-xxx";
import { withTranslation } from "react-i18next";
class Navigation extends Component {
constructor(props) {
super(props);
}
render() {
const { t } = this.props;
const ITEMS = [
{
type: "section",
text: t("navigation.abc"),
items: [
{
type: "link",
text: t("navigation.abc"),
href: "#/abc"
},
{ type: "link", text: t("navigation.def"), href: "#/def" }
]
}
];
const HEADER = {
href: "#/",
text: t("navigation.title")
};
return (
<SideNavigation
header={HEADER}
items={ITEMS}
/>
);
}
}
export default withTranslation()(Navigation);
And following is the test case I have written:
import React from "react";
import { shallow } from 'enzyme';
import Navigation from '../src/a';
import { SideNavigation } from '#xxx/react-components-xxx';
describe('Navigation component', () => {
it('should render consistently', () => {
const wrapper = shallow(
<Navigation />
);
console.log(wrapper.render());
expect(wrapper).not.toBeNull();
const sideNav = wrapper.find(SideNavigation);
console.log(sideNav.render());
const sideNavProps = sideNav.props();
console.log(sideNavProps);
});
});
And with this, I get following eror:
Method “type” is meant to be run on 1 node. 0 found instead.
14 |
15 | const sideNav = wrapper.find(SideNavigation);
> 16 | console.log(sideNav.render());
| ^
17 | const sideNavProps = sideNav.props();
18 | console.log(sideNavProps);
19 | });
at ShallowWrapper.single (node_modules/enzyme/src/ShallowWrapper.js:1636:13)
at ShallowWrapper.type (node_modules/enzyme/src/ShallowWrapper.js:1372:17)
at ShallowWrapper.render (node_modules/enzyme/src/ShallowWrapper.js:1106:17)
at Object.<anonymous> (tst/components/Navigation.test.js:16:29)
But if I use mount instead of shallow, everything works fine here. Can someone tell what is the issue here?
It's because of the WithTranslation.
They mention it in their doc
For testing purpose of your component you should export the pure component without extending with the withTranslation hoc and test that:
export MyComponent;
export default withTranslation('ns')(MyComponent);
In the test, test the myComponent export passing a t function mock:
import { MyComponent } from './myComponent';
<MyComponent t={key => key} />

Categories

Resources