Enzyme: How can I test a component with DOM side-effect? - reactjs

Say I have a component like so -
// #flow
import React, { PureComponent } from 'react';
export default class ReplaceLink extends Component {
containerRef = React.createRef();
componentDidMount() {
const links =
Array.from(this.containerRef.current.getElementsByTagName('a'));
links.forEach(a => a.setAttribute('href', 'dummylink'));
}
render = () => <div ref={this.containerRef}>{this.props.children}</div>;
}
which replaces href of links placed within it. But even when doing full dom rendering in enzyme, when I do a wrapper.debug() to see the result of the render, I still see original links only.
I've tried doing a force wrapper.update and using setTimeouts, but it just doesn't reflect the expected link.

One of reasons why direct DOM access is discouraged in React is that it makes testing more complicated.
The component can be rendered with skipped componentDidMount:
const wrapper = shallow(<ReplaceLink/>, { disableLifecycleMethods: true })
Then a ref can be mocked and componentDidMount can be called manually:
const setAttribute = jest.fn();
const getElementsByTagName = jest.fn().mockImplementation(() => [{ setAttribute }]);
wrapper.instance().containerRef.current = { getElementsByTagName };
wrapper.instance().componentDidMount();
Then stubbed DOM functions can be asserted that they were called.

Found the best way to test something like this is through the getDOMNode method.
First, make sure to use mount to render the wrapper, so we have a simulated DOM environment to query against.
Next, use wrapper.getDOMNode() to get the underlying DOM node.
Any changes made during the lifecycle methods to the underlying DOM will be reflected in this DOM reference.
Use .querySelector, or <insert-dom-query-method> to make assertions.
const wrapper = mount(
<ReplaceLink>
Google
</ReplaceLink>
);
const linkTags = wrapper.getDOMNode().querySelectorAll('a');
linkTags.forEach(tag => {
expect(tag.getAttribute('href')).toBe('dummy');
});

Related

react-test-renderer's create() vs. #testing-library/react's render()

I'm new to React and confused about all the testing libraries. I got my test code to work but it seems redundant to have to call create() from react-test-renderer in order to use its toMatchSnapshot() and have to call render() from #testing-library/react in order to use its assertions such as getByLabelText().
import {render} from '#testing-library/react';
import {act, create} from 'react-test-renderer';
it('renders a login screen', () => {
let mockInitialState: AppState = {
auth: initialAuthState
};
let component = <Root initialState={mockInitialState}/>;
let tree = null;
act(() => {
tree = create(component);
});
expect(tree).toMatchSnapshot();
const {getByLabelText, getByText} = render(component);
expect(getByLabelText(/Email Address.*/));
expect(getByLabelText(/Password*/));
expect(getByText('Sign in'));
});
As a newbie, it's hard for me to understand the difference between all these React libraries. But I'm thinking there must be a simpler way.
How can I simplify my test code so I only have to call one thing that renders the component so that I can do snapshot testing and more specific assertions?
I got the answer from Ziad Saab at Codementor.io:
create() allows you test against the virtual DOM (i.e. the "React DOM")
render() comes from react testing library and renders your tree but also allows you to have all the get*() assertions. It allows you to test against the DOM.
Here's how the code can be simplified:
it('renders a login screen', () => {
let mockInitialState: AppState = {
auth: initialAuthState
};
const {container, getByLabelText, getByText} = render(<Root initialState={mockInitialState}/>);
expect(container.firstChild).toMatchSnapshot();
expect(getByLabelText(/Email Address.*/));
expect(getByLabelText(/Password*/));
expect(getByText('Sign in'));
});
Ziad let me know that there was no reason to have act(), it was something to work around a bug in create(). Now that the code doesn't used create() there is no need for act().
As a result, my snapshot now contains class instead of className because class is what's in the actual HTML DOM whereas className is its equivalent in React's "Virtual DOM".
(Before) Snapshot with create() based on React's Virtual DOM:
className="MuiBox-root MuiBox-root-256"
(After) Snapshot with render() based on HTML DOM:
class="MuiBox-root MuiBox-root-256"
If you're using Create React App then I'd stick with react-testing-library since it comes with it.
Instead of container, you can also use asFragment for snapshot testing.
const {container} = render(<Root initialState={mockInitialState}/>);
expect(asFragment).toMatchSnapshot();

How do I test a method defined within a functional component, that interacts with DOM elements and has no arguments

I have been having trouble getting 100% test coverage on one of my buttons (A React functional components.) Basically when it is clicked, it executes some code and then also calls another method from within this onClick called resetButtons. This method will find all the buttons like it in the app and remove a class. This is a preemptive behavior so that only one button at a time can be active.
So far I have tested the click using .simulate, passing in a mocked domElement. And then test that the domElement.classList.add method is called with 'active'.
Obviously this being a DOM centered operation, I am finding it very difficult to test the resetButtons method that lies within the component. especially considering it doesn't have any methods.
I have tried defining the resetButtons method outside of the component and then exported it so the jest test could import it. However I have been unable to test the method as it seems to want it to be a spy or mock, and not the method itself. (Matcher error: received value must be a mock or spy function
)
Here is the react Functional Component:
import React from 'react';
import PropTypes from 'prop-types';
import classes from './MainButton.module.scss';
const MainButton = (props) => {
const resetButtons = () => {
const elements = document.getElementsByClassName('mainButton');
for (let i = 0; i < elements.length; i += 1) {
elements[i].classList.remove('active');
}
};
const handleClick = (event) => {
if (!event.target.classList.contains('active')) {
resetButtons();
event.target.classList.add('active');
props.setVisualState(props.className.split('-')[0]);
}
};
return (
<button
onClick={handleClick}
type="button"
className={`${classes.mainButton} ${props.className}`}
>
{props.children}
</button>
);
};
MainButton.propTypes = {
children: PropTypes.node,
className: PropTypes.string,
setVisualState: PropTypes.func.isRequired,
};
MainButton.defaultProps = {
children: 'Button',
className: '',
};
export default MainButton;
Here is the Test
import React from 'react';
import { shallow } from 'enzyme';
import MainButton from './MainButton';
describe('MainButton', () => {
const domElement = { classList: { contains: jest.fn(), remove: jest.fn(), add: jest.fn() } };
const setVisualStateMock = jest.fn();
const mainButton = shallow(<MainButton setVisualState={setVisualStateMock} />);
it(' is rendered properly', () => {
expect(mainButton).toMatchSnapshot();
});
describe('when clicked', () => {
beforeEach(() => {
mainButton.find('button').simulate('click', { target: domElement });
});
it('it runs `classlist.add` to assign `active` class', () => {
expect(domElement.classList.add).toHaveBeenCalledWith('active');
});
it('it runs set visual state to update `Allergen` container `state`', () => {
expect(setVisualStateMock).toHaveBeenCalled();
});
});
});
Currently the coverage report is reporting 92% coverage, but the branch is at 50 and the line that is causing the trouble is on line 9 (the elements[i].classList.remove('active'); line.
I know at 90% I should probably just move on but this is something I want to be able to figure out. Feel like getting head around this will make me a better tested.
Hope you guys can help!
Fumbling around in the DOM yourself is an anti-pattern. That's React's job. Instead of manipulating the dom with target.classList.add you should have a state property that holds the status which of your inputs is currently active. Then, while rendering you can say className={isActiveInput ? "active": null}.
Because the state is not specific to your MainButton component you would lift the state up. If you have the state somewhere in the parent you don't have to crudely search for DOM elements by classname and manipulate the dom yourself.
Simply put, the rule of React is: You define how things are supposed to look like, React takes care that your definition becomes reality in the dom. If you manipulate the DOM yourself - you're doing it wrong.
When all of this is done, you will have no problem at all with tests, because all you have to do is provide the proper state and props, which is easy, and check that your callback is triggered onClick.
EDIT: Advanced version would be to use Context, but I'd go with state lifting first.
You should be able to mount multiple MainButtons, click one and expect that the other(s) had domElement.classList.remove called on them.
However, user konqi is right in that React provides better ways of manipulating elements/components.
You could replace this test:
expect(domElement.classList.add).toHaveBeenCalledWith('active');
with a test that checks that the button has (or does not have) the active className (instead of checking that the function was called with the right argument). With that test in place, if you like, you could refactor this in the way that konqi suggests.

How to create an unit test for UncontrolledTooltip from reactstrap that does not handle state management directly?

I implemented simple UncontrolledTooltip from reactstrap. The doc (https://reactstrap.github.io/components/tooltips/) says
uncontrolled component can provide the functionality wanted without the need to manage/control the state of the component
If I want to implement an unit test (e.g. jest + enzyme) for testing its state as either open or close, how can I create a unit test without manually tinkering with state value? Is this possible to achieve it? It seems only possible with regular Tooltip component but I like to hear advice from seasoned engineers.
[Update]:
Upon request I include here tooltip and unit test I am trying to execute. At the moment, I want to simulate hover on the tooltip however mockHover.mock.calls.length returns as 0 which I interpret as mock function was not triggered.
Here is my Tooltip.
import React from 'react';
import { UncontrolledTooltip } from 'reactstrap';
export default class MyTooltip extends React.Component {
render() {
const { metaData, wg } = this.props;
return (
<div>
<UncontrolledTooltip placement="bottom" trigger={'hover'} target={wg}>
{metaData}
</UncontrolledTooltip>
</div>
);
}
}
Here is my unit test that use jest and enzyme:
describe('<MyTooltip />', () => {
it('Tooltip unit test', () => {
const mockHover = jest.fn();
const wrapper = shallow(<MyTooltip trigger={mockHover} />);
expect(wrapper.find(UncontrolledTooltip));
wrapper.find(UncontrolledTooltip).simulate('hover');
expect(mockHover.mock.calls.length).toEqual(1);
});
});
There are few important things to start from:
UncontrolledTooltip is part of 3rd party package so you won't test it explicitly.
Instead you better focus on testing your wrapper around UncontrolledTooltip.
simulate is nothing related to events browser's system. It's just a syntax sugar to do props().onHover(...). So if target component has such a prop - and it's a callback-function - it will be called. If there is no such a prop - it would be up to defaultProps what's going on. Anyway nothing like 'emulating mouse cursor over the element'.
shallow() will stop rendering at level of UncontrolledTooltip(its internals will not be rendered)
Keeping that in mind I see you able only:
your component finally renders UncontrolledTooltip with expected constant prop values
both metaData and wg props are passed down to UncontrolledTooltip
it('renders UncontrolledTooltips under the hood', () => {
const wg = '1';
const metaData = (<span>2</span>);
const wrapper = shallow(<MyTooltip wg={wg} metaData={metaData} />);
const innerTooltip = wrapper.find(UncontrolledTooltip);
/*
I don't validate `find(UncontrolledTooltip).toHaveLength(1)`
since assertion on `.find(..).props()` would throw exception otherwise
*/
expect(innerTooltip.props().placement).toEqual('bottom');
expect(innerTooltip.props().trigger).toEqual('hover');
expect(innerTooltip.props().wg).toEqual(wg);
expect(innerTooltip.props().metaData).toEqual(metaData);
});

Passing context to a dynamically added child in ReactJS

I'm trying to pass context to a React component, but because I am testing with Enzyme, I'd like to add the component to its parent dynamically so I can examine its state. The test looks like this:
describe('<BeaconConfig />', () => {
it('inherits the config from BeaconConfig', () => {
mount(<BeaconConfig persistent><div id="parent"></div></BeaconConfig>, { attachTo: document.body });
const wrapper = mount(<Beacon/>, { attachTo: document.getElementById('parent') });
expect(wrapper.state('persistent')).to.be(true);
});
});
The test fails because the persistent property of the Beacon component's state is undefined, although it should be inherited from BeaconConfig via context.
When I try putting Beacon directly inside the JSX when I mount BeaconConfig then it works fine, but in this case Enzyme won't let me get at the Beacon component state since it isn't the root.
Is it normal that React isn't propagating the context to my component when I add it dynamically to its parent?
It is normal that React isn't propagating the context - it doesn't look at the DOM and diff it with its VDOM in that way.
You'll want make it a child in the initial mount, and use the .find() or .children() methods of the MountWrapper (docs) to dig through the children, find the Beacon and do your assertions.
Here is the complete test I ended up using:
describe('Context', () => {
let wrapper;
let parent;
const open = stub().returns({});
const mockIndexedDB = { open };
const config = mount(<BeaconConfig persistent position="bottom" indexedDB={mockIndexedDB} />);
beforeEach(() => {
parent = document.createElement('div');
document.body.appendChild(parent);
wrapper = mount(<Beacon>some text</Beacon>, {
attachTo: parent,
context: config.instance().getChildContext()
});
});
afterEach(() => {
wrapper.detach();
document.body.removeChild(document.body.firstChild);
});
it('overrides the default values with the context if any', () => {
expect(wrapper.state('persistent')).to.be(true);
expect(wrapper.state('position')).to.be('bottom');
expect(open.calledOnce).to.equal(true);
});
});
#STRML had a good suggestion but I don't think that it is possible to access the state of a non-root component.
Instead I instantiate BeaconConfig in isolation and grab it's child context, passing that manually to Beacon using the options parameter of mount. This tests that BeaconConfig creates the right child context and that Beacon uses the context correctly. It doesn't test that BeaconConfig passes the config down to Beacon when the latter is a descendant, but we can presumably take that for granted since it is basic React functionality.

How to test properties and functions on a React component?

I've tried everything with enzyme, however, I can't find the correct way of testing these properties below. Keep in mind that this component is wrapped in a dummy Provider component so that I can pass the necessary props (i.e. Store) down for mounting purposes.
1) After mounting, a property is set on the instance (e.g. this.property)
2) An event listener has been added
3) On the event listener, someFunction is being called
class SampleComponent extends Component {
componentDidMount() {
this.property = 'property';
window.addEventListener('scroll', this.someFunction, true);
}
someFunction = () => {
return 'hello';
};
render() {
return <h1>Sample</h1>;
}
}
export default EvalueeExposureList;
Ok, I have updated my answer based on discussion with OP. The component under test has a redux provider and connected component as child therefore we are opting for the usage of enzymes shallow API.
In regards to tracking and testing the addEventListener you can use the sinon library to create a spy, which temporarily "replaces" the window.addEventListener. This grants you access to the call count as well as the arguments it was called with.
Using enzyme and mocha I created the following tests which were passing for me. The first two test covers all your cases above and for good measure I added another on how to test the output of the someFunction.
import React from 'react';
import { expect } from 'chai';
import sinon from 'sinon';
import { shallow } from 'enzyme';
// Under test.
import SampleComponent from './SampleComponent';
describe('SampleComponent', () => {
let addEventListenerSpy;
beforeEach(() => {
// This replaces the window.addEventListener with our spy.
addEventListenerSpy = sinon.spy(window, 'addEventListener');
});
afterEach(() => {
// Restore the original function.
window.addEventListener.restore();
});
// This asserts your No 1.
it(`should set the property`, () => {
const wrapper = shallow(<SampleComponent />);
wrapper.instance().componentDidMount(); // call it manually
expect(wrapper.instance().property).equal('property');
});
// This asserts your No 2 and No 3. We know that by having
// passed the someFunction as an argument to the event listener
// we can trust that it is called. There is no need for us
// to test the addEventListener API itself.
it(`should add a "scroll" event listener`, () => {
const wrapper = shallow(<SampleComponent />);
wrapper.instance().componentDidMount(); // call it manually
expect(addEventListenerSpy.callCount).equal(1);
expect(addEventListenerSpy.args[0][0]).equal('scroll');
expect(addEventListenerSpy.args[0][1]).equal(wrapper.instance().someFunction);
expect(addEventListenerSpy.args[0][2]).true;
});
it(`should return the expected output for the someFunction`, () => {
const wrapper = mount(<SampleComponent />);
expect(wrapper.instance().someFunction()).equal('hello');
});
});
It may be worth noting that I run my tests on node, but I have a jsdom setup in my mocha configuration, which is probably the candidate responsible for creating the window.addEventListener in for use in my test environment. Are you running your tests via the browser or node? If node you may need to do something similar to me.

Resources