Test the methods inside a functional component using react - reactjs

I have the following component and I am trying to test these two methods onHandleClick and onHandleSave for checking the visible states but I am not sure how to achieve this. Can anyone help me with this?
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
export const MyComponent = (props) => {
const {
visible,
setVisible,
products,
setProducts,
} = props;
const onHandleSave = (e, product) => {
e.stopPropagation();
setProducts(product)
setVisible(visible);
};
const onHandleClick = (e, product) => {
e.stopPropagation();
setProducts(product);
setVisible(!visible);
};
const onToggle = () => {
setVisible(!visible);
};
const mapProducts = () => {
if (products === undefined) {
return [];
}
return products.sort().map((product, key) => (
<Modal
className="product-option"
id="product"
key={key}
onClick={(e) => onHandleClick(e, product)}
>
{product.text}
<div className="set-default">
<span role="button" id="save-selection" tabIndex="0" onClick={(e) => onHandleSave(e, product)} aria-hidden="true" className="hovered-icon">
<Tooltip className="tooltip-content">
<small>Save</small>
</Tooltip>
</span>
</div>
</Modal>
));
};
return (
<div className="wrapper">
<button
className="products"
onClick={onToggle}
id="products"
>
{selectedItem.text}
</ button>
{visible && (
<Modal>
<div className="product-content">{mapProducts()}</div>
</Modal>
)}
</div>
);
};
export default MyComponent;
So far, I have tried to write the test like this:
import React from 'react';
import { shallow, shallowWithIntl } from 'enzyme';
import { MyComponent } from '.';
describe('<Product />', () => {
let props;
let wrapper;
beforeEach(() => {
props = {
visible: true,
setVisible: jest.fn(() => 'setVisible'),
onToggle: jest.fn(() => 'onToggleCurrency'),
onHandleClick: jest.fn(() => 'onHandleClick'),
onHandleSave: jest.fn(() => 'onHandleSave'),
};
wrapper = shallow(<MyComponent {...props} />);
});
it('check for visibility states', () => {
wrapper = shallow(<MyComponent {...props} />);
wrapper.instance().onHandleSave = jest.fn();
wrapper.instance().forceUpdate();
expect(wrapper.instance().onHandleSave).toBeCalledWith({ visible: true });
});
But no luck as the test fails. Any helps would be highly appreciated. Thanks in advance

Accessing the internal functions this way will not work, because you don't have access to the scope inside the function. These consts are technically private!
You could do it otherwise and make them accessible, but I wouldn't recommend you to do that. I'd recommend you to test from a perspective which is as close to the user's experience:
The user clicked the save -> Unit test needs to assert that the callback to setVisible and setProducts was called correctly. By accessing the mocks on props.setVisible (for example), you will be able to assert that this was called.
For further reading: https://www.stevethedev.com/blog/programming/public-private-and-protected-scope-javascript
So what you would be looking for actually is a way to click the button. Using Enzyme, you'll find plenty of resources in the net on how to do that.
Since you asked, you could make these functions accessible by setting them as class methods, adding them to the prototype, or by setting them as attributes of the component function-object:
const myFunction = () => {}
myFunction.foo = 'bar'
// you can now access myFunction.foo, as in JavaScript even a function
// is an object
To my experience you should refrain from doing so, but since you asked, here's an answer.

Related

Using a variable in enzyme wrapper find method

Clunking through learning testing with jest + enzyme. I have an array, OptionsArray, with some options that get mapped to buttons in a component. I figured that in the testing suite for the component, I could just do
import React from 'react';
import { shallow, ShallowWrapper } from 'enzyme';
import { OptionsArray } from './ConfigOptions';
import Foo from './Foo';
describe('Foo', () => {
let wrapper: ShallowWrapper;
const numberOfOptions = OptionsArray.length;
beforeEach(() => (wrapper = shallow(<Foo />)));
it('renders exactly one Button Item for each option', () => {
/* eslint-disable-next-line testing-library/no-debugging-utils */
console.log(wrapper.debug());
OptionsArray.forEach((option) => {
console.log(option.value);
});
OptionsArray.forEach((option) => {
expect(wrapper.find(option.value)).toHaveLength(1);
});
});
});
I see the options fine in the console output, but then I get:
Foo › renders exactly one Button Item for each option
expect(received).toHaveLength(expected)
Expected length: 1
Received length: 0
So I'm guessing that I'm passing the variable to find incorrectly? Is there a better way to do this?
Adding component Foo:
/* Foo.tsx */
import React, { useState } from 'react';
import { Button, ListGroup } from 'react-bootstrap';
import { OptionsArray } from './ConfigOptions';
import './Foo.scss';
const Foo: React.FC<> = () => {
const [options, setOptions] = useState(OptionsArray);
return (
<div className="Foo">
<ListGroup>
{OptionsArray.map((option, i) => (
<ListGroup.Item key={i}>
<Button
id={i.toString()}
value={option.value}
onClick={(e) => handleClick(e.currentTarget.id)}
variant={option.isSet ? 'primary' : 'outline-primary'}
>
{option.value}
</Button>
{option.content}
</ListGroup.Item>
))}
</ListGroup>
</div>
);
};
export default Foo;
And the OptionsArray:
import React from 'react';
export const OptionsArray = [
{
value: 'OptionA',
content: (
<React.Fragment>
<br />
<p>Here is a description of OptionA.</p>
</React.Fragment>
),
isSet: false,
},
{
value: 'OptionB',
content: (
<React.Fragment>
<br />
<p>Here is a description of OptionB.</p>
</React.Fragment>
),
isSet: false,
},
];
I figured it out. As usual, just a misunderstanding on my part. I was trying to use find to get the Button components by text, but this isn't how find works. Instead, I needed to use the findWhere method and a predicate to hunt down the exact components I was looking for. Here was my solution:
import React from 'react';
import { shallow, ShallowWrapper } from 'enzyme';
import { OptionsArray } from './ConfigOptions';
import Foo from './Foo';
describe('Foo', () => {
let wrapper: ShallowWrapper;
const numberOfOptions = OptionsArray.length;
beforeEach(() => (wrapper = shallow(<Foo />)));
it('renders exactly one Button Item for each option', () => {
OptionsArray.forEach((option) => {
expect(wrapper.find({ value: option.value })).toHaveLength(1);
const ButtonWithText = wrapper.findWhere((node) => {
return node.name() === 'Button' && node.text() === option.value;
});
expect(ButtonWithText ).toHaveLength(1);
});
});
});

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!

How to access react-list getVisibleRange() within a functional component

react-list has a method "getVisibleRange()". getVisibleRange() => [firstIndex, lastIndex]. The examples show accessing this like so:
onScrollHandler() {
console.log('onScrollHandler visible', this.getVisibleRange());
}
with the "this" keyword, within a class component. Is it possible to access the getVisibleRange() method within a functional component? For example:
const handleScroll = () => {
let [firstIndex, lastIndex] = getVisibleRange() <-- ??
}
<div id="list" onScroll={handleScroll}></div>
UPDATE: reproducable code
import React, {useState, useEffect} from 'react'
import ReactList from 'react-list'
var faker = require('faker')
const TalentSearch = () => {
let items = [...new Array(500)].map(() => faker.fake(faker.name.findName()))
const renderItem = (index, key) => {
return <div key={key}>{items[index]}</div>
}
const handleScroll = () => {
// access getVisibleRange() here?
}
return (
<div>
<h1>Search Results</h1>
<div id="list" style={{overflow: 'auto', maxHeight: 400}} onScroll={handleScroll}>
<ReactList
itemRenderer={renderItem}
length={items.length}
initialIndex={50}
type='uniform'
scrollTo={50}
/>
</div>
</div>
)
}
export default TalentSearch
You need to access it through a reference, with hooks you may use useRef:
const TalentSearch = () => {
const listRef = useRef();
return <ReactList ref={listRef} />;
};
Then you can access the methods like so:
listRef.current.getVisibleRange();

jest/enzyme mock function in functional component

I have a functional component and I wanted to test it with mock function
(simplified demonstration)
const remove = () => {
... do something
}
const removeButton = (props) => (
<Button onClick={() => remove()}>
Remove
</Button>
);
I tried with this test case
it('test remove button', () => {
const test = shallow(<removeButton/>)
const mockFunction = jest.fn()
test.instance().remove = mockFunction
test.find('Button').simulate('click')
expect(mockFunction).toHaveBeenCalled()
})
.instance().remove could not mock the function because it is out of scope.
How would I mock the function remove ?
Here is a working example:
// ---- comp.js ----
import * as React from 'react';
import * as comp from './comp';
export const remove = () => {
// ...do something
}
export const RemoveButton = (props) => (
<div onClick={() => comp.remove()}>
Remove
</div>
);
// ---- comp.test.js ----
import * as React from 'react';
import { shallow } from 'enzyme';
import * as comp from './comp';
describe('removeButton', () => {
it('should call remove on click', () => {
const mock = jest.spyOn(comp, 'remove');
mock.mockImplementation(() => {});
const component = shallow(<comp.RemoveButton />);
component.find('div').simulate('click');
expect(mock).toHaveBeenCalled();
mock.mockRestore();
});
});
Note that to mock remove you need to export it and you need to import the module back into itself and use the import within your component.
Having said that, I agree that passing remove in as a prop is a better approach. It is much easier to test and makes your components more reusable.
You should pass the remove function as a prop, rather than just defining an adjacent variable that is private to a module.
const removeButton = (props) => (
<Button onClick={() => props.remove()}>
Remove
</Button>
)
// test file
it('test remove button', () => {
const mockFunction = jest.fn()
const test = shallow(<RemoveButton remove={mockFunction} />)
test.find('Button').simulate('click')
expect(mockFunction).toHaveBeenCalled()
})

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