I have a simple pop-up that I want to test if the clicked button closes the popup using testing-library/react. The way shown in docs doesn't work for me for some reason. I'm able to console log the button, so the query works but fireEvent doesn't trigger the button.
test("close the popup", () => {
render(
<Popup>
<div id="content"></div>
</Popup>
);
const button = screen.getByText("Got It");
fireEvent.click(button);
expect(button).not.toBeInTheDocument();
});
import React, { useEffect, useState } from "react";
import PropTypes from "prop-types";
const Popup = (props) => {
const id = "popup-active"
const [popupActive, setPopupActive] = useState(() => {
const value = window.localStorage.getItem(id);
return value !== null ? JSON.parse(value) : true;
});
useEffect(() => {
window.localStorage.setItem(id, popupActive);
}, [popupActive]);
const handleX = () => {
setPopupActive(!popupActive);
};
return (
<div
style={{
visibility: popupActive ? "visible" : "hidden",
opacity: popupActive ? "1" : "0",
}}
className={popupStyles.overlay}
>
<div className={popupStyles.popup}>
<div className={popupStyles.content}>{props.children}</div>
<button
data-testid="button-popup"
className={popupStyles.button}
onClick={handleX}
>
Got It
</button>
</div>
</div>
);
};
export default Popup;
You should use toBeVisible matcher.
This allows you to check if an element is currently visible to the user.
An element is visible if all the following conditions are met:
it is present in the document
it does not have its css property display set to none
it does not have its css property visibility set to either hidden or collapse
it does not have its css property opacity set to 0
its parent element is also visible (and so on up to the top of the DOM tree)
it does not have the hidden attribute
if <details /> it has the open attribute
E.g.
index.tsx:
import React from 'react';
import { useEffect, useState } from 'react';
const id = 'popup-active';
const Popup = (props) => {
const [popupActive, setPopupActive] = useState(() => {
const value = window.localStorage.getItem(id);
return value !== null ? JSON.parse(value) : true;
});
useEffect(() => {
window.localStorage.setItem(id, popupActive);
}, [popupActive]);
const handleX = () => {
setPopupActive(!popupActive);
};
return (
<div
style={{
visibility: popupActive ? 'visible' : 'hidden',
opacity: popupActive ? '1' : '0',
}}
>
<div>
<div>{props.children}</div>
<button data-testid="button-popup" onClick={handleX}>
Got It
</button>
</div>
</div>
);
};
export default Popup;
index.test.tsx:
import { fireEvent, render, screen } from '#testing-library/react';
import '#testing-library/jest-dom/extend-expect';
import React from 'react';
import Popup from './';
describe('73407176', () => {
test('should pass', () => {
render(
<Popup>
<div id="content"></div>
</Popup>
);
const button = screen.getByText('Got It');
fireEvent.click(button);
expect(button).not.toBeVisible();
});
});
Test result:
PASS stackoverflow/73407176/index.test.tsx (11.549 s)
73407176
✓ should pass (73 ms)
-----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-----------|---------|----------|---------|---------|-------------------
All files | 100 | 83.33 | 100 | 100 |
index.tsx | 100 | 83.33 | 100 | 100 | 8
-----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 12.167 s
Also check this question: What is the difference between toBeInTheDocument() and toBeVisible()?
Related
I have a React app, and I'm trying to build out some unit tests.
I got most of this test working, but I still can't figure out how to test one line using jest. I can't tell if it's because of the package I imported or if I'm doing something else incorrectly.
LogOutForDisagreement.js
import React from 'react';
import Form from 'react-bootstrap/Form';
import Button from 'react-bootstrap/Button';
import ClipboardClip from './../ClipboardClip';
const store = require('store2');
function LogOutForDisagreement(props) {
const handleClick = (e) => {
e.preventDefault();
props.onChange(
props.props.user = {
loggedIn: false,
email: '',
password: '',
firstName: ''
},
);
store.clearAll();
//sessionStorage.clear();
};
return (
<div id="logOutForDisagreementClipboard" className={props.className}>
<Form className="form">
<ClipboardClip />
<div className="centeredPaper">
<p>Sorry, you must agree to our privacy policy to access the site. You must now log out and log back in to agree to the terms.</p>
<Button id="logOutForDisagreement" type="" onClick={handleClick}>
Log Out
</Button>
</div>
</Form>
</div>
);
}
export default LogOutForDisagreement;
LogOutForDisagreement.test.js
import React from 'react';
import { render, fireEvent, cleanup } from '#testing-library/react';
import toJson from 'enzyme-to-json';
import LogOutForDisagreement from './LogOutForDisagreement';
const store = require('store2');
let props = {
props:
{
user: {
loggedIn: false,
email: 'email#example.com',
password: '0cc3dd4ad221f83270f876908064fa67d43407aa4bd580731b30c58c4fe2d8de00464107e5a143594068461af21ece9fb542e2f254b6f895eea6a6627f4f7c438a42fb18af7f539c4be2456661a5beec4a561f9443988182445b3952a5d9321a2d725b24b151f79a2806432c848b0b0c02576676e3a5c6f0661b4e318ca4f3e134d066808fab8fdd5322ed5cf5ad68aae43254a8fefdb69809c1bfdc07fe0365f38baa424d0c059c3a9fbff1f3525dae410740b9719929ef3f34235da519591f0410a08438132600fa802079b8d6f372f6dc439eb1b100aed28bb55cc3c6dc8982644940bd506278943fa8e430836cb874283e9f4438aac04a817f86bd1606036f03d196a211bdd91ac683d4ec63fcd503aa97b53d5c2571d39855b2be5f77be80a7f767271c8789aec26c66530c22387007c704f96b1a76a47e8e13fb263a0a6b24b2959495d34b47e03bcb95e8af13b555c5c403ec01427182afa1bae35ff81224b051fde7a61bd9044ed74042444a05e06186eedacc38c0128ff7d70c',
firstName: 'FirstName'
},
className: "logOutForDisagreement"
}
};
//You don't need the props for everything, just the className, but I'm leaving them here for now.
describe('Test the log out for disagreement button', () => {
it('renders correctly enzyme and has onclick', async () => {
console.log("This is the store at the begining after initial load: {" + Object.entries(sessionStorage).map(([key, val] = sessionItem) => {
return `${key}: ${val}`}) + "}");
const { getByText } = render(<LogOutForDisagreement id="logOutForDisagreementClipboard" className={props.className} />);
const spy = jest.spyOn(store, 'clearAll');
await fireEvent.click(getByText("Log Out"));
console.log("This is in the store after the event fires: {" + Object.entries(sessionStorage).map(([key, val] = sessionItem) => {
return `${key}: ${val}`}) + "}");
expect(spy).toHaveBeenCalled();
expect(toJson(getByText)).toMatchSnapshot();
//spy.mockRestore();
});
});
I still get an error that says the following:
Test the log out for disagreement button › renders correctly enzyme and has onclick
expect(jest.fn()).toHaveBeenCalled()
Expected number of calls: >= 1
Received number of calls: 0
37 | return `${key}: ${val}`}) + "}");
38 |
> 39 | expect(spy).toHaveBeenCalled();
| ^
40 |
41 | expect(toJson(getByText)).toMatchSnapshot();
42 | //spy.mockRestore();
...
--------------------------------------------|---------|----------|---------|---------|----------------------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
--------------------------------------------|---------|----------|---------|---------|--------------------
...
LogOutForDisagreement.js | 83.33 | 100 | 100 | 83.33 | 19
I'm just missing one line. Does anyone know how to get 100% test coverage on this? I know this is picky, but I use this same package in a lot of places.
I'm just posting the answer for anyone who runs into a similar issue. I used a tutorial from youtube.com that also used a third party library (axios) and mocked the built in axios functions: https://www.youtube.com/watch?v=gA-uNj2FgdM. I also had to pass the props down to the component and include onChange in the component, but now I have 100% test coverage.
LogOutForDisagreement.test.js
import React from 'react';
import { render, fireEvent, cleanup, screen } from '#testing-library/react';
import toJson from 'enzyme-to-json';
import LogOutForDisagreement from './LogOutForDisagreement';
import mockStore from 'store2';
jest.mock('store2');
mockStore.clearAll(() => Promise.resolve({}));
let props = {
props:
{
user: {
loggedIn: false,
email: 'email#example.com',
password:'0cc3dd4ad221f83270f876908064fa67d43407aa4bd580731b30c58c4fe2d8de00464107e5a143594068461af21ece9fb542e2f254b6f895eea6a6627f4f7c438a42fb18af7f539c4be2456661a5beec4a561f9443988182445b3952a5d9321a2d725b24b151f79a2806432c848b0b0c02576676e3a5c6f0661b4e318ca4f3e134d066808fab8fdd5322ed5cf5ad68aae43254a8fefdb69809c1bfdc07fe0365f38baa424d0c059c3a9fbff1f3525dae410740b9719929ef3f34235da519591f0410a08438132600fa802079b8d6f372f6dc439eb1b100aed28bb55cc3c6dc8982644940bd506278943fa8e430836cb874283e9f4438aac04a817f86bd1606036f03d196a211bdd91ac683d4ec63fcd503aa97b53d5c2571d39855b2be5f77be80a7f767271c8789aec26c66530c22387007c704f96b1a76a47e8e13fb263a0a6b24b2959495d34b47e03bcb95e8af13b555c5c403ec01427182afa1bae35ff81224b051fde7a61bd9044ed74042444a05e06186eedacc38c0128ff7d70c',
firstName: 'FirstName'
},
className: "logOutForDisagreement"
}
};
//You don't need the props for everything, just the className, but I'm leaving them h
describe('Test the log out for disagreement button', () => {
it('renders correctly enzyme and has onclick', async () => {
const { getByText } = render(<LogOutForDisagreement id="logOutForDisagreementClipboard" className={props.className} props={props} onChange={jest.fn()} />);
fireEvent.click(screen.getByRole('button', {
name: /log out/i
}));
//await expect(spy).toHaveBeenCalled();
expect(mockStore.clearAll).toHaveBeenCalledTimes(1);
expect(toJson(getByText)).toMatchSnapshot();
});
});
Code Coverage
--------------------------------------------|---------|----------|---------|---------|----------------------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
--------------------------------------------|---------|----------|---------|---------|----------------------------------
All files | 25.95 | 8.06 | 18.94 | 26.33 |
src/components/privacyPolicyComponents | 25 | 0 | 21.05 | 25 |
LogOutForDisagreement.js | 100 | 100 | 100 | 100 |
Does anyone know why it reports that I called the function twice now? Should I just use toHaveBeenCalled? I still get an error that it got called twice.
expect(jest.fn()).toHaveBeenCalledTimes(expected)
Expected number of calls: 1
Received number of calls: 2
When I was working with HTML content displayed with Read more and Read less(Show more or Show less) on my webpage, I struggled a lot googling to get a quick solution. Unfortunately, I failed to collect a single source of the solution. I started writing my own solution and decided to share my own solution if it helps someone.
At first, You need to install "html-truncate"
npm i html-truncate
import React, {useState, useCallback, useEffect} from 'react';
import truncate from "truncate-html";
const ReadMoreMaster = (props) => {
let {byWords,parentClass, readLess, readMore, length, ellipsis, spaceBefore} = props;
const [showMore, setShowMore] = useState(false);
truncate.setup({
byWords: !!byWords,
ellipsis: ellipsis ? ellipsis : '',
})
const [state, setState] = useState({
showRealData: false,
realData: props.children,
truncatedData: truncate(props.children, length ? length : 2)
});
const handleShowData = useCallback(() => {
setState(prevState => ({
...prevState,
showRealData: !prevState.showRealData
}));
}, [setState]);
useEffect(() => {
if(byWords && props.children) {
const textDetails = props.children.replace(/<[^>]+>/g, '');
const arr = textDetails.split(" ");
if(arr.length > length) {
setShowMore(true);
}
}
}, [byWords])
return (
<div className={parentClass}>
<div
dangerouslySetInnerHTML={{
__html: `${
!state.showRealData ? state.truncatedData : state.realData
}`
}}
/>
{spaceBefore === false ? '' : ' '}
{
showMore &&
<a href={void (0)} className="read-more" onClick={handleShowData}>
{!state.showRealData ? readMore ? readMore : 'Read More' : readLess ? readLess : 'Read Less'}
</a>
}
</div>
);
};
export default ReadMoreMaster;
Now call the component like below:
<ReadMoreMaster
byWords={true}
length={150}
ellipsis="..."
>
<p>your content with or without HTML tag</p>
</ReadMoreMaster>
Supported props are:
// ...Supported Props...
// parentClass="test" || default value : null
// byWords={true} || default value : false
// ellipsis="..." || default value : null
// length={5} || default value : 2
// readMore="See More" || default value : Read More
// readLess="See less" || default value : Read Less
// spaceBefore={false} || default value : true
All done. Enjoy!!!
I'm using react testing library and I want to test a button which is :
<button data-testid="btn" onClick={() => props.history.push(`/post/${value}`)} > Search </button>
<input data-testid="input" type="text" value={value} onChange={(e) => handle(e.target.value)} />
and I test it's onClick function like this :
test('Button should navigate the url when enabled', () => {
render(<Home />);
const input = screen.getByTestId('input');
const btn = screen.getByTestId('btn');
fireEvent.change(input, { target: { value: '12' } });
fireEvent.click(btn);
});
But it gives me an error :
TypeError: Cannot read property 'push' of undefined
19 | <button
20 | data-testid="btn"
> 21 | onClick={() => props.history.push(`/post/${value}`)}
| ^
22 | disabled={!value}
23 | type="submit"
24 | >
The app itself works fine when I npm start but fails only when testing it .
How can I solve this push of undefined ?
You have to pass props to the rendered component, in this case - <Home />:
import history from 'history' // Or a file you made that contains an instance of history
test('Button should navigate the url when enabled', () => {
render(<Home history={history} {...otherProps} />);
const input = screen.getByTestId('input');
const btn = screen.getByTestId('btn');
fireEvent.change(input, { target: { value: '12' } });
fireEvent.click(btn);
});
How to test values that can depend on current localization?
function DateNavigation({ locale, date }) {
return (locale === 'en')
? <span>{moment(date).format('DD.MM.YYYY')}</span>
: <span>{moment(date).format('YYYY.MM.DD')}</span>
}
This test doesn't work sometimes because of locations
it('display date correctly', () => {
const component = mount(<DateNavigation date={'2022-01-31'} />);
expect(component.html()).toEqual("<span>31.01.2022</span>");
});
You can use jest.mock(moduleName, factory, options) to mock moment(), moment().format() functions and its returned value. Test results must be predictable. For the test about date, we need to make the test case return a certain value in different systems, time zones and localization.
E.g.
DateNavigation.jsx:
import React from 'react';
import moment from 'moment';
function DateNavigation({ locale, date }) {
return locale === 'en' ? (
<span>{moment(date).format('DD.MM.YYYY')}</span>
) : (
<span>{moment(date).format('YYYY.MM.DD')}</span>
);
}
export default DateNavigation;
DateNavigation.test.jsx:
import DateNavigation from './DateNavigation';
import moment from 'moment';
jest.mock('moment', () => {
const mMoment = { format: jest.fn() };
return jest.fn(() => mMoment);
});
describe('62042433', () => {
it('display date correctly', () => {
const date = '31.01.2022';
moment().format.mockReturnValueOnce(date);
const component = mount(<DateNavigation date={'2022-01-31'} locale={'en'} />);
expect(component.html()).toEqual('<span>31.01.2022</span>');
expect(moment).toBeCalledWith('2022-01-31');
expect(moment().format).toBeCalledWith('DD.MM.YYYY');
});
});
unit test results:
PASS stackoverflow/62042433/DateNavigation.test.jsx (13.544s)
62042433
✓ display date correctly (54ms)
--------------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
--------------------|---------|----------|---------|---------|-------------------
All files | 100 | 50 | 100 | 100 |
DateNavigation.jsx | 100 | 50 | 100 | 100 | 6
--------------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 15.23s
Background Info: My React app was crashing on mobile. Chrome dev tools indicated garbage collection wasn't triggering. Looking into the heap the top constructors by retained size were all referencing intersection observor (used for infinite scrolling in many of my react components).
Question: How can I go about fixing the memory leaks caused by intersection observor? Is there a way to trigger garbage collection when the components unmount?
Sample Component using Intersection Observor for Infinite Scroll:
import React, { useEffect, useState, useRef, useCallback } from 'react'
import { Link } from 'react-router-dom'
import { uuid, fabricateTimeStamp, getRandom } from '../containers/helperFunctions'
import { avatarQuery } from '../words'
import quote from 'inspirational-quotes'
import history from '../history'
import { thumbsUp, thumbsDown, arrowDrop } from './svgs'
import {
fetchAvatars as callAvatarsAPI,
fetchVideos as callVideosAPI } from '../containers/api'
const ActivityFeed = (props) => {
const [firstRenderDone, setFirstRenderDone] = useState()
const [comments, setComments] = useState([])
useEffect(() => {
const mobile = window.innerWidth <= 600
if (mobile) fetchAvatars('woman', 3)
if (!mobile) fetchAvatars('woman', 6)
if (history.location.pathname.includes('/video/') || history.location.pathname.includes('/search/')) {
document.querySelector('.activityFeedContainer').classList.toggle('hide')
}
}, [])
// if user clicks nav button, remove all comments generated by infinite scroll
useEffect(() => {
setComments(prevState => (prevState.slice(0, 8)))
}, [props.button])
// INFINITE SCROLL
// Callback is triggered when ref is set in mapCommentsToHTML
const observer = useRef()
const lastActivityPost = useCallback(lastPostNode => {
observer.current = new IntersectionObserver(entries => {
const lastPost = entries[0]
if (lastPost.isIntersecting) fetchAvatars(getRandom(avatarQuery), 6)
})
if (lastPostNode) observer.current.observe(lastPostNode)
})
const fetchAvatars = async (query, amount) => {
let response = await callAvatarsAPI(query, amount)
response = response.data.hits
mapCommentsToHTML(response)
}
const mapCommentsToHTML = (response) => {
const picsMappedToHTML = response.map((pic, index) => {
return (
<div className="commentWrapper" key={uuid()} ref={response.length === index + 1 ? lastActivityPost : null}>
<div className="avatarPlaceholder--comments">
{props.page === 'channel'
? <img
className="avatarPlaceholder--img"
src={
props.userAvatar.previewURL ? props.userAvatar.previewURL
: props.userAvatar.userImageURL === "" ? 'https://i.imgur.com/ZwDgXSF.jpg'
: props.userAvatar.userImageURL}
alt="An Activity Feed User Avatar" />
: <Link to={`/channel/${pic.id}`}>
<img
className="avatarPlaceholder--img"
src={pic.previewURL}
alt="An Activity Feed User Avatar" />
</Link>
}
</div>
<div className="commentContainer" >
<Link to={`/channel/${pic.id}`}>
<h5 className="commentorName">{props.page === 'channel' ? props.userName : pic.user}</h5>
</Link>
<span className="dateOfComment">{fabricateTimeStamp(index)}</span>
<p className={`${props.page}-comment`}>{quote.getQuote().text}</p>
<div className="thumbs">
<span className="thumbsUpIcon">
{thumbsUp(16)}
</span>
<span className="thumbsDownIcon">
{thumbsDown(16)}
</span>
</div>
<p className="replyText">REPLY</p>
<div className="viewReplies">
<span className="arrowDrop">
{arrowDrop()}
</span>
View {Math.floor(Math.random() * 50) + 2} Replies
</div>
</div>
</div>
)
})
setComments(prevState => ([...prevState, ...picsMappedToHTML]))
}
return (
<aside className="activityFeedContainer">
<h1 className={`${props.page}--activity-feed-title`}>Activity Feed</h1>
<hr className="home--activityfeed-hr" />
<div className="commentSection--activityfeed">
{comments}
</div>
</aside>
)
}
export default ActivityFeed
Chrome Performance Timeline (dip # 27 secs is due to clicking an <a> tag which triggered page refresh)
Heap Snapshots:
I believe you need to stop observing at some point , you might want to
call disconnect on your IntersectionObserver when you unmount