Test ClientPortal in Next.JS using Jest - javascript

I'm using NextJS and I'm trying to test the ClientPortal component. I'm using Jest and React Testing Library for testing.
Here's the ClientPortal component:
import { useEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
export default function ClientPortal({ children, selector }) {
const ref = useRef();
const [mounted, setMounted] = useState(false);
useEffect(() => {
ref.current = document.querySelector(selector);
setMounted(true);
}, [selector]);
return mounted ? createPortal(children, ref.current) : null;
}
How can this be tested using Jest?

First, make sure the testEnvironment configuration is jsdom. For jestjs v26, it's jsdom by default. For jestjs v27, follow this guide to setup the testEnvironment configuration.
The test method is very straightforward. Create a DOM container to store the portal. Query the DOM element and assert whether it exists.
index.jsx:
import { useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
export default function ClientPortal({ children, selector }) {
const ref = useRef();
const [mounted, setMounted] = useState(false);
useEffect(() => {
ref.current = document.querySelector(selector);
setMounted(true);
}, [selector]);
return mounted ? createPortal(children, ref.current) : null;
}
index.test.jsx:
import React from 'react';
import { render, screen } from '#testing-library/react';
import '#testing-library/jest-dom/extend-expect';
import ClientPortal from './';
function TestChild() {
return <div>child</div>;
}
describe('69550058', () => {
test('should pass', () => {
const main = document.createElement('main');
const portalContainer = document.createElement('div');
portalContainer.id = 'portal-container';
document.body.appendChild(portalContainer);
const { container } = render(
<ClientPortal selector={'#portal-container'}>
<TestChild />
</ClientPortal>,
{ container: document.body.appendChild(main) }
);
expect(screen.getByText(/child/)).toBeInTheDocument();
expect(portalContainer.innerHTML).toEqual('<div>child</div>');
expect(container).toMatchInlineSnapshot(`<main />`);
});
});
test result:
PASS examples/69550058/index.test.jsx (8.941 s)
69550058
✓ should pass (33 ms)
-----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-----------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
index.jsx | 100 | 100 | 100 | 100 |
-----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 1 passed, 1 total
Time: 9.624 s, estimated 11 s
package versions:
"jest": "^26.6.3",
"react": "^16.14.0",
"react-dom": "^16.14.0",
"#testing-library/react": "^11.2.2",

Related

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"

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"

How can I test React Router with Jest

I'm new to testing with jest and I want to test the following code.
import React from "react";
import "./ButtonLogin.css";
import { Link } from 'react-router-dom';
function ButtonLogin() {
return (
<Link to="/login"> <button className="button-login">Iniciar sesión</button></Link>
)
}
export default ButtonLogin;
import { MemoryRouter } from 'react-router-dom';
import { render, fireEvent, Link } from '#testing-library/react';
import { ButtonLogin } from './ButtonLogin';
it('routes to a new route', async () => {
ButtonLogin = jest.fn();
const { getByText } = render(
<MemoryRouter ButtonLogin={ButtonLogin}>
<Link to="/login">Iniciar sesión</Link>
</MemoryRouter>
);
fireEvent.click(getByText('Iniciar sesión'));
expect(ButtonLogin).toHaveBeenCalledWith('/login');
});
I have performed the following test but it fails and I get the following error in line 9.
routes to a new route
"ButtonLogin" is read-only.
You can use the createMemoryHistory function and Router component to test it. Create a memory history with initial entries to simulate the current location, this way we don't rely on the real browser environment. After firing the click event, assert the pathname is changed correctly or not.
ButtonLogin.tsx:
import React from 'react';
import { Link } from 'react-router-dom';
function ButtonLogin() {
return (
<Link to="/login">
<button className="button-login">Iniciar sesión</button>
</Link>
);
}
export default ButtonLogin;
ButtonLogin.test.tsx:
import { fireEvent, render } from '#testing-library/react';
import React from 'react';
import { Router } from 'react-router-dom';
import ButtonLogin from './ButtonLogin';
import { createMemoryHistory } from 'history';
describe('ButtonLogin', () => {
test('should pass', () => {
const history = createMemoryHistory({ initialEntries: ['/home'] });
const { getByText } = render(
<Router history={history}>
<ButtonLogin />
</Router>
);
expect(history.location.pathname).toBe('/home');
fireEvent.click(getByText('Iniciar sesión'));
expect(history.location.pathname).toBe('/login');
});
});
test result:
PASS examples/69878146/ButtonLogin.test.tsx (10.675 s)
ButtonLogin
✓ should pass (41 ms)
-----------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-----------------|---------|----------|---------|---------|-------------------
All files | 100 | 100 | 100 | 100 |
ButtonLogin.tsx | 100 | 100 | 100 | 100 |
-----------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 11.722 s, estimated 12 s
package version: "react-router-dom": "^5.2.0"

How to jest mock a function living within another react component?

I have been trying to write tests for the following react component which returns different components depending on my props:
const Choice: React.FC<States> = props => {
function getChoiceComponent(): JSX.Element {
if (props.choices) {
return <FirstComponent {...props} />;
} else {
return <SecondComponent {...props} />;
}
}
return <>{getChoiceComponent()}</>;
};
How can I mock getChoiceComponent function and test it?
We should test the react component by changing the props and state rather than test the getChoiceComponent method directly. Here is the unit test solution,
index.tsx:
import React from 'react';
import FirstComponent from './first';
import SecondComponent from './second';
type States = any;
const Choice: React.FC<States> = (props) => {
function getChoiceComponent(): JSX.Element {
if (props.choices) {
return <FirstComponent {...props} />;
} else {
return <SecondComponent {...props} />;
}
}
return <>{getChoiceComponent()}</>;
};
export default Choice;
first.tsx:
import React from 'react';
const FirstComponent = () => <div>first component</div>;
export default FirstComponent;
second.tsx:
import React from 'react';
const SecondComponent = () => <div>second component</div>;
export default SecondComponent;
index.test.tsx:
import Choice from './';
import FirstComponent from './first';
import SecondComponent from './second';
import React from 'react';
import { shallow } from 'enzyme';
describe('60152774', () => {
it('should render first component', () => {
const props = { choices: [] };
const wrapper = shallow(<Choice {...props}></Choice>);
expect(wrapper.find(FirstComponent)).toBeTruthy();
});
it('should render second component', () => {
const props = {};
const wrapper = shallow(<Choice {...props}></Choice>);
expect(wrapper.find(SecondComponent)).toBeTruthy();
});
});
Unit test results with coverage report:
PASS stackoverflow/60152774/index.test.tsx
60152774
✓ should render first component (20ms)
✓ should render second component (5ms)
------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
------------|---------|----------|---------|---------|-------------------
All files | 88.24 | 100 | 50 | 100 |
first.tsx | 75 | 100 | 0 | 100 |
index.tsx | 100 | 100 | 100 | 100 |
second.tsx | 75 | 100 | 0 | 100 |
------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 3.065s
Source code: https://github.com/mrdulin/react-apollo-graphql-starter-kit/tree/master/stackoverflow/60152774

How to test function that we get from action, has been called?

So I was trying to create a unit test using jest in ReactJS. The Unit test itself just to verify if the function (from action) has been called
I already tried to mock the function, but the result tells that I must mock the function
Here the code of the function that I want to create a unit test
import { testfunction } from '../../actions/auth';
handleSubmit(userParams) {
this.setState({ form: { ...this.state.form, isFetching: true } });
this.props.dispatch(testfunction(userParams,
this.successCallback.bind(this), this.errorCallback.bind(this)));
}
and for the unit test
import React from 'react';
import { shallow } from 'enzyme';
import toJson from 'enzyme-to-json';
import thunk from 'redux-thunk';
import configureStore from 'redux-mock-store';
import Login from '../../../components/auth/Login';
const mockStore = configureStore([thunk]);
const initialState = {
history: { },
};
const store = mockStore(initialState);
let wrapper;
let history;
let testfunction;
beforeEach(() => {
testfunction= jest.fn();
history = { push: jest.fn() };
wrapper = shallow(
<Login
history={history}
store={store}
testfunction={testfunction}
/>
);
});
describe('handleSubmit()', () => {
test('should call testfunction props', () => {
const component = wrapper.dive();
const instance = component.instance();
const sampleUserParams = {
email: 'test#test.com',
password: 'samplePassword123',
};
instance.handleSubmit(sampleUserParams);
expect(testfunction).toHaveBeenCalled();
});
});
I just want to check if the "testfunction" is called when I called handleSubmit function. But the error message is:
"Expected mock function to have been called."
it feels my way to mock the function is wrong. Does anyone know how to correct way to test that function?
Here is the solution:
index.tsx:
import React, { Component } from 'react';
import { testfunction } from './testfunction';
class Login extends Component<any, any> {
constructor(props) {
super(props);
this.state = {
form: {}
};
}
public render() {
const userParams = {};
return (
<div className="login">
<form onSubmit={() => this.handleSubmit(userParams)}>some form</form>
</div>
);
}
private handleSubmit(userParams) {
this.setState({ form: { ...this.state.form, isFetching: true } });
this.props.dispatch(testfunction(userParams, this.successCallback.bind(this), this.errorCallback.bind(this)));
}
private successCallback() {
console.log('successCallback');
}
private errorCallback() {
console.log('errorCallback');
}
}
export { Login };
testFunction.ts:
async function testfunction(userParams, successCallback, errorCallback) {
return {
type: 'ACTION_TYPE',
payload: {}
};
}
export { testfunction };
Unit test:
import React from 'react';
import { shallow } from 'enzyme';
import { Login } from './';
import { testfunction } from './testfunction';
jest.mock('./testfunction.ts');
describe('Login', () => {
const dispatch = jest.fn();
const sampleUserParams = {
email: 'test#test.com',
password: 'samplePassword123'
};
it('handleSubmit', () => {
const wrapper = shallow(<Login dispatch={dispatch} />);
expect(wrapper.is('.login')).toBeTruthy();
expect(wrapper.find('form')).toHaveLength(1);
wrapper.find('form').simulate('submit');
const cmpInstance = wrapper.instance();
expect(dispatch).toBeCalledWith(
// tslint:disable-next-line: no-string-literal
testfunction(sampleUserParams, cmpInstance['successCallback'], cmpInstance['errorCallback'])
);
// tslint:disable-next-line: no-string-literal
expect(testfunction).toBeCalledWith(sampleUserParams, cmpInstance['successCallback'], cmpInstance['errorCallback']);
});
});
Unit test with coverage report:
PASS src/stackoverflow/57847401/index.spec.tsx
Login
✓ handleSubmit (22ms)
-----------------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
-----------------|----------|----------|----------|----------|-------------------|
All files | 86.36 | 100 | 62.5 | 85 | |
index.tsx | 90 | 100 | 71.43 | 88.89 | 27,30 |
testfunction.ts | 50 | 100 | 0 | 50 | 2 |
-----------------|----------|----------|----------|----------|-------------------|
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 3.201s, estimated 4s
Here is the completed demo: https://github.com/mrdulin/jest-codelab/tree/master/src/stackoverflow/57847401

Categories

Resources