How to test a stateless component - reactjs

I am trying to test below component but getting error, its a functional component with some data.
The below component receive list of informations from parent component and renders, its work perfectly, but when writing test cases its failing using jest and enzyme
import React from "react";
export const InfoToolTip = data => {
const { informations = [] } = data.data;
const listOfInfo = () => {
return informations.map((info, index) => {
const { name, id } = info;
return [
<li>
<a
href={`someURL?viewMode=id`}
>
{name}
</a>
</li>
];
});
};
return (
<div className="tooltip">
<ul className="desc">{listOfInfo()}</ul>
</div>
);
};
Test case
import React from "react";
import { shallow, mount } from "enzyme";
import { InfoToolTip } from "../index.js";
describe("InfoToolTip", () => {
it("tooltip should render properly",() => {
const wrapper = mount(<InfoToolTip />);
});
});
Error:
TypeError: Cannot match against 'undefined' or 'null'.

When you mount InfoToolTip you don't pass any props while in component you try to destructure data prop:
const { informations = [] } = data.data;
So you could fix it this way:
const wrapper = mount(<InfoToolTip data={{}} />);
Related question.

Related

How to test if props are being rendered, in circumstances where props are being passed as an object

I'm using React Testing Library to test a cafe review app. I have a parent component CafeList that passes an object containing data about the cafes to a child component Cafe, which renders out the cafe data. The object being passed takes the form { name:name,photoURL:photoURL, id:cafe.id}, and I want to test that the name property is being rendered in Cafes.
I'm having trouble though because I don't know how to test a specific value of an object when using RTL - any suggestions?
Here's the parent component CafeList.jsx
import React, { useState,useEffect } from 'react'
import db from '../fbConfig'
import Cafe from './Cafe'
const CafeList = () => {
const [cafes,setCafe] = useState([])
useEffect(() => {
let cafeArray = []
db.collection('cafes')
.get()
.then(snapshot => {
snapshot.forEach(cafe => {
cafeArray.push(cafe)
})
setCafe(cafeArray)
})
},[])
const [...cafeData] = cafes.map((cafe) => {
const { name, photoURL } = cafe.data()
return { name:name,photoURL:photoURL, id:cafe.id}
})
return(
<div className="cafe-container-container">
<h2 className = 'main-subheading'>Reviews</h2>
<Cafe cafes = {cafeData}/>
</div>
)
}
...and the child component Cafe.jsx
import React from 'react'
import {Link} from 'react-router-dom'
const Cafe = ({ cafes }) => {
return (
<div className="cafe-grid">
{
cafes.map((cafe) => {
return (
<Link
to={`/cafe-reviews/${cafe.id}`}
style={{ textDecoration: "none", color: "#686262" }}
>
<div className="cafe-container">
<h3>{cafe.name}</h3>
<img src={cafe.photoURL}></img>
</div>
</Link>
)
})
}
</div>
)
}
export default Cafe
and lastly, here's the test I wrote
import { render, screen } from '#testing-library/react'
import '#testing-library/jest-dom'
import Cafe from '../components/CafeList'
const testArray = [{name: 'this is the name',photoUrl:'photoURL',id: 'id'}]
test('is cafe name prop being passed ', () =>{
render(<Cafe cafes = {testArray}/>)
const nameElement = screen.getByText(/this is the name/i)
expect(nameElement).toBeInTheDocument()
})

Test failed of a Component in react using typescript

When ever I run the test it fails. I don't know what mistake I am making.
How can I test those if statements and the child component. I am using jest and enzyme for react tests.
This is my test file:
import React from "react";
import { shallow } from "enzyme";
import LaunchContainer from "./index";
import Launch from "./Launch";
describe("render LaunchContainer component", () => {
let container: any;
beforeEach(() => { container = shallow(<LaunchContainer setid={()=>{}} setsuccess={()=>{}} />) });
it("should render LaunchContainer component", () => {
expect(container.containsMatchingElement(<Launch setsuccess={()=>{}} setid={()=>{}} data={{}}/>)).toEqual(true);
});
})
The parent component for which the test is used:
import React from "react";
import { useLaunchesQuery } from "../../generated/graphql";
import Launch from './Launch';
interface Props {
setid: any;
setsuccess: any;
}
const LaunchContainer: React.FC<Props> = ({setid, setsuccess}) => {
const {data, loading, error} = useLaunchesQuery();
if (loading) {
return <div>loading...</div>
}
if (error || !data) {
return <div>error</div>
}
return <Launch setid={setid} data={data} setsuccess={setsuccess} />
}
export default LaunchContainer;
The child component to be added in test:
import React from "react";
import { LaunchesQuery } from "../../generated/graphql";
import './style.css';
interface Props {
data: LaunchesQuery;
setid: any;
setsuccess: any;
}
const Launch: React.FC<Props> = ({setid, data, setsuccess}) => {
return (
<div className="launches">
<h3>All Space X Launches</h3>
<ol className="LaunchesOL">
{!!data.launches && data.launches.map((launch, i) => !!launch &&
<li key={i} className="LaunchesItem" onClick={() => {
setid(launch.flight_number?.toString())
setsuccess(JSON.stringify(launch.launch_success))
}}>
{launch.mission_name} - {launch.launch_year} (<span className={launch.launch_success? "LaunchDetailsSuccess": launch.launch_success===null? "": "LaunchDetailsFailed"}>{JSON.stringify(launch.launch_success)}</span>)
</li>
)}
</ol>
</div>
);
}
export default Launch;
To test those if statements you should mock your useLaunchesQuery to return the loading, error and data values that hit your if statements. You can use mockImplementationOnce or mockReturnValueOnce. For example you could write
import { useLaunchesQuery } from "../../generated/graphql";
/**
* You mock your entire file /generated/graphql so it returns
* an object containing the mock of useLaunchesQuery.
* Note that every members in this file and not specified in this mock
* will not be usable.
*/
jest.mock('../../generated/graphql', () => ({
useLaunchesQuery: jest.fn(() => ({
loading: false,
error: false,
data: [/**Whatever your mocked data is like */]
}))
}))
const setid = jest.fn();
const setsuccess = jest.fn();
/**
* A good practice is to make a setup function that returns
* your tested component so so you can call it in every test
*/
function setup(props?: any) { // type of your component's props
// You pass mock functions declared in the upper scope so you can
// access it later to assert that they have been called the way they should
return <LaunchContainer setid={setid} setsuccess={setsuccess} {...props} />
// Spread the props passed in parameters so you overide with whatever you want
}
describe("render LaunchContainer component", () => {
it('should show loading indicator', () => {
/**
* Here I use mockReturnValueOnce so useLaunchesQuery is mocked to return
* this value only for the next call of it
*/
(useLaunchesQuery as jest.Mock).mockReturnValueOnce({ loading: true, error: false, data: [] })
/** Note that shallow won't render any child of LaunchContainer other than basic JSX tags (div, span, etc)
* So you better use mount instead
*/
const container = mount(setup()); // here i could pass a value for setid or setsuccess
// Put an id on your loading indicator first
expect(container.find('#loading-indicator').exists()).toBeTruthy()
/** The logic for the other if statement remains the same */
})
}

Testing effect of provider updates

I'm attempting to flesh put my understanding of Jest testing as some of my components are lacking subsequent testing. Consider this:
import React, { ReactElement, useEffect, useState } from "react";
import { IPlaylist } from "src/interfaces/playlist";
import usePlaylists from "src/providers/playlists/hooks/use-playlists";
const JestTesting = (): ReactElement => {
const { playlists, addPlaylist } = usePlaylists();
const examplePlaylist = {
id: `${Math.floor(Math.random() * 1000)}`,
name: "testing",
created: new Date().toISOString(),
createdBy: "Biggus Dickus",
updated: new Date().toISOString(),
version: 0,
tracks: []
};
const createPlaylist = () =>
addPlaylist(examplePlaylist);
useEffect(() => {
if (playlists.length > 0) {
console.log("playlists updated")
}
}, [playlists]);
return (
<div>
<h2>Jest Testing</h2>
{playlists.map((p) => <h3 key={p.id}>{p.name}</h3>)}
<button onClick={ () => createPlaylist() }>
Create New Playlist
</button>
</div>
);
};
export default JestTesting;
This is just a very simple component that leverages the custom provider I made; the provider (in this component) has an initial playlist value of an empty array and a function for updating that array.
Here's the test:
import React from "react";
import { render, RenderResult, fireEvent, screen } from "#testing-library/react";
import JestTesting from "..";
const mockPlaylists = jest.fn().mockReturnValue([]);
const mockAddPlaylist = jest.fn();
// First mock provider attempt
jest.mock("src/providers/playlists/hooks/use-playlists", () => () => {
return {
playlists: mockPlaylists(),
addPlaylist: mockAddPlaylist
};
});
const clickCreatNewPlaylistBtn = () =>
fireEvent.click(screen.getByText("Create New Playlist"));
describe("JestTesting", () => {
let rendered: RenderResult;
const renderComponent = () => render(<JestTesting />);
beforeEach(() => {
rendered = renderComponent();
});
it("renders the component", () => {
expect(rendered.container).toMatchSnapshot();
});
describe("when new playlist is created", () => {
it("updates view/snapshot", () => {
clickCreatNewPlaylistBtn();
expect(mockAddPlaylist).toHaveBeenCalled();
expect(rendered.container).toMatchSnapshot();
});
});
});
which spits out this snapshot:
// Jest Snapshot v1,
exports[`JestTesting renders the component 1`] = `
<div>
<div>
<h2>
Jest Testing
</h2>
<button>
Create New Playlist
</button>
</div>
</div>
`;
exports[`JestTesting when new playlist is created updates view/snapshot 1`] = `
<div>
<div>
<h2>
Jest Testing
</h2>
<button>
Create New Playlist
</button>
</div>
</div>
`;
The tests pass and I can see that the mockAddPlaylist function is called, but the problem is that the playlists array in the snapshot is never updated and the snapshots don't change between the first and the second test.
What am I doing wrong here? Do I need to wait for the effects of the addPlaylist function to finish? If so, what's the best way to do?
Thanks!

TypeScript - invoking React prop with Enzyme

I'm trying to convert my Jest tests using Enzyme to TypeScript, but running into one particular case that I'm not sure how to handle. Basically, I'm trying to call a function that is passed as a prop to a sub-component. The error I'm seeing is:
spec/javascript/_common/components/sidebar_spec.tsx:85:5 - error TS2349:
Cannot invoke an expression whose type lacks a call signature. Type '{}' has no compatible call signatures.
85 component.find(Link).at(0).prop('onNavigate')();
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
How do I get past this error? Not sure if it's helpful, but more context for the test:
it('does not hide the sidebar after a link is clicked', () => {
const component = shallow(<Sidebar />);
component.find(Link).at(0).prop('onNavigate')();
component.update();
expect(component.find(Link)).toHaveLength(3);
});
And a chunk of code from the Sidebar component:
class Sidebar extends React.Component<any, any> {
...
hideIfMobile() {
const {mobile} = this.state;
if (mobile) { this.setState({visible: false}); }
}
render() {
const {visible} = this.state;
if (!visible) {
return (
<div className='sidebar sidebar--hidden'>
{this.sidebarToggle()}
</div>
);
}
const linkProps = {
baseClass: 'sidebar__link',
onNavigate: this.hideIfMobile,
};
return (
<div className='sidebar sidebar--visible'>
<h2 className='sidebar__header'>{'Menu'}{this.sidebarToggle()}</h2>
<hr className='sidebar__divider' />
<Link to='root' {...linkProps}><h2>{'FOCUS'}</h2></Link>
<Link to='tasks' {...linkProps}><h2>{'ALL TASKS'}</h2></Link>
<Link to='timeframes' {...linkProps}><h2>{'TIMEFRAMES'}</h2></Link>
</div>
);
}
}
the Link component is wrapped in react-redux:
import {connect} from 'react-redux';
import Link from 'src/route/components/link';
import {getRouteName} from 'src/route/selectors';
import {setRoute} from 'src/route/action_creators';
function mapStateToProps(state) {
return {routeName: getRouteName(state)};
}
export default connect(mapStateToProps, {setRoute})(Link);
and the actual component:
class Link extends React.Component<any, any> {
navigate(event) {
event.preventDefault();
const {onNavigate, params, setRoute, to} = this.props;
setRoute({name: to, ...params});
if (onNavigate) { onNavigate(); }
}
path() {
const {params, to} = this.props;
const pathParams = mapValues(params, value => value.toString());
return findRoute(to).toPath(pathParams);
}
className() {
const {baseClass, className, to, routeName} = this.props;
return classnames(
baseClass,
{[`${baseClass}--active`]: baseClass && routeName === to},
className,
);
}
render() {
const {children} = this.props;
return (
<a
href={this.path()}
className={this.className()}
onClick={this.navigate}
>
{children}
</a>
);
}
}
It turns out Link in this case was defined earlier in my test file as:
const Link = 'Connect(Link)';
I switched this to import the actual link container and it resolved the issue.
import Link from 'src/route/containers/link';

Can a React portal be used in a Stateless Functional Component (SFC)?

I have used ReactDOM.createPortal inside the render method of a stateful component like so:
class MyComponent extends Component {
...
render() {
return (
<Wrapper>
{ReactDOM.createPortal(<FOO />, 'dom-location')}
</Wrapper>
)
}
}
... but can it also be used by a stateless (functional) component?
Will chime in with an option where you dont want to manually update your index.html and add extra markup, this snippet will dynamically create a div for you, then insert the children.
export const Portal = ({ children, className = 'root-portal', el = 'div' }) => {
const [container] = React.useState(() => {
// This will be executed only on the initial render
// https://reactjs.org/docs/hooks-reference.html#lazy-initial-state
return document.createElement(el);
});
React.useEffect(() => {
container.classList.add(className)
document.body.appendChild(container)
return () => {
document.body.removeChild(container)
}
}, [])
return ReactDOM.createPortal(children, container)
}
It can be done like this for a fixed component:
const MyComponent = () => ReactDOM.createPortal(<FOO/>, 'dom-location')
or, to make the function more flexible, by passing a component prop:
const MyComponent = ({ component }) => ReactDOM.createPortal(component, 'dom-location')
can it also be used by a stateless (functional) component
?
yes.
const Modal = (props) => {
const modalRoot = document.getElementById('myEle');
return ReactDOM.createPortal(props.children, modalRoot,);
}
Inside render :
render() {
const modal = this.state.showModal ? (
<Modal>
<Hello/>
</Modal>
) : null;
return (
<div>
<div id="myEle">
</div>
</div>
);
}
Working codesandbox#demo
TSX version based on #Samuel's answer (React 17, TS 4.1):
// portal.tsx
import * as React from 'react'
import * as ReactDOM from 'react-dom'
interface IProps {
className? : string
el? : string
children : React.ReactNode
}
/**
* React portal based on https://stackoverflow.com/a/59154364
* #param children Child elements
* #param className CSS classname
* #param el HTML element to create. default: div
*/
const Portal : React.FC<IProps> = ( { children, className, el = 'div' } : IProps ) => {
const [container] = React.useState(document.createElement(el))
if ( className )
container.classList.add(className)
React.useEffect(() => {
document.body.appendChild(container)
return () => {
document.body.removeChild(container)
}
}, [])
return ReactDOM.createPortal(children, container)
}
export default Portal
IMPORTANT useRef/useState to prevent bugs
It's important that you use useState or useRef to store the element you created via document.createElement because otherwise it gets recreated on every re-render
//This div with id of "overlay-portal" needs to be added to your index.html or for next.js _document.tsx
const modalRoot = document.getElementById("overlay-portal")!;
//we use useRef here to only initialize el once and not recreate it on every rerender, which would cause bugs
const el = useRef(document.createElement("div"));
useEffect(() => {
modalRoot.appendChild(el.current);
return () => {
modalRoot.removeChild(el.current);
};
}, []);
return ReactDOM.createPortal(
<div
onClick={onOutSideClick}
ref={overlayRef}
className={classes.overlay}
>
<div ref={imageRowRef} className={classes.fullScreenImageRow}>
{renderImages()}
</div>
<button onClick={onClose} className={classes.closeButton}>
<Image width={25} height={25} src="/app/close-white.svg" />
</button>
</div>,
el.current
);
Yes, according to docs the main requirements are:
The first argument (child) is any renderable React child, such as an element, string, or fragment. The second argument (container) is a DOM element.
In case of stateless component you can pass element via props and render it via portal.
Hope it will helps.
Portal with SSR (NextJS)
If you are trying to use any of the above with SSR (for example NextJS) you may run into difficulty.
The following should get you what you need. This methods allows for passing in an id/selector to use for the portal which can be helpful in some cases, otherwise it creates a default using __ROOT_PORTAL__.
If it can't find the selector then it will create and attach a div.
NOTE: you could also statically add a div and specify a known id in pages/_document.tsx (or .jsx) if again using NextJS. Pass in that id and it will attempt to find and use it.
import { PropsWithChildren, useEffect, useState, useRef } from 'react';
import { createPortal } from 'react-dom';
export interface IPortal {
selector?: string;
}
const Portal = (props: PropsWithChildren<IPortal>) => {
props = {
selector: '__ROOT_PORTAL__',
...props
};
const { selector, children } = props;
const ref = useRef<Element>()
const [mounted, setMounted] = useState(false);
const selectorPrefixed = '#' + selector.replace(/^#/, '');
useEffect(() => {
ref.current = document.querySelector(selectorPrefixed);
if (!ref.current) {
const div = document.createElement('div');
div.setAttribute('id', selector);
document.body.appendChild(div);
ref.current = div;
}
setMounted(true);
}, [selector]);
return mounted ? createPortal(children, ref.current) : null;
};
export default Portal;
Usage
The below is a quickie example of using the portal. It does NOT take into account position etc. Just something simple to show you usage. Sky is limit from there :)
import React, { useState, CSSProperties } from 'react';
import Portal from './path/to/portal'; // Path to above
const modalStyle: CSSProperties = {
padding: '3rem',
backgroundColor: '#eee',
margin: '0 auto',
width: 400
};
const Home = () => {
const [visible, setVisible] = useState(false);
return (
<>
<p>Hello World <a href="#" onClick={() => setVisible(true)}>Show Modal</a></p>
<Portal>
{visible ? <div style={modalStyle}>Hello Modal! <a href="#" onClick={() => setVisible(false)}>Close</a></div> : null}
</Portal>
</>
);
};
export default Home;
const X = ({ children }) => ReactDOM.createPortal(children, 'dom-location')
Sharing my solution:
// PortalWrapperModal.js
import React, { useRef, useEffect } from 'react';
import ReactDOM from 'react-dom';
import $ from 'jquery';
const PortalWrapperModal = ({
children,
onHide,
backdrop = 'static',
focus = true,
keyboard = false,
}) => {
const portalRef = useRef(null);
const handleClose = (e) => {
if (e) e.preventDefault();
if (portalRef.current) $(portalRef.current).modal('hide');
};
useEffect(() => {
if (portalRef.current) {
$(portalRef.current).modal({ backdrop, focus, keyboard });
$(portalRef.current).modal('show');
$(portalRef.current).on('hidden.bs.modal', onHide);
}
}, [onHide, backdrop, focus, keyboard]);
return ReactDOM.createPortal(
<>{children(portalRef, handleClose)}</>,
document.getElementById('modal-root')
);
};
export { PortalWrapperModal };

Resources