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);
});
});
});
Related
This is the component code which I am going to test.
Here I used one state hook setCheck.
import React, { SyntheticEvent, useEffect, useState } from 'react';
import { Checkbox, Grid, Header } from 'semantic-ui-react';
interface IIdentityItem {
name: string,
comment: string,
checked: boolean,
handleSetCheckState: any,
index: number
};
export default ({ name, comment, checked, handleSetCheckState, index }: IIdentityItem) => {
const [check, setCheck] = useState(true);
useEffect(() => {
setCheck(checked);
}, []);
const onChange = (e: SyntheticEvent, data: object) => {
setCheck(!check);
handleSetCheckState(index, !check);
};
return (
<Grid className='p-16 py-9 bg-white'>
<Grid.Column width='eleven' textAlign='left'>
<Header as='p' className='description'>{name}</Header>
<Header as='p' className='comment'>{comment}</Header>
</Grid.Column>
<Grid.Column width='five' verticalAlign='middle'>
<Checkbox toggle checked={check} onChange={onChange} />
</Grid.Column>
</Grid>
)
}
This is the jest unit test code.
import ChainItem from './ChainItem';
import React from 'react';
import { create, act } from 'react-test-renderer';
import { BrowserRouter } from 'react-router-dom';
import { Checkbox } from 'semantic-ui-react';
const useStateSpy = jest.spyOn(React, "useState");
describe('ChainItem', () => {
let handleSetCheckState, index;
beforeEach(() => {
handleSetCheckState = jest.fn();
index = 0;
//useStateSpy.mockReturnValueOnce([true, setCheck]);
});
it('should work', () => {
let tree;
act(() => {
tree = create(
<ChainItem
handleSetCheckState={handleSetCheckState}
index={index} />
);
});
expect(tree).toMatchSnapshot();
});
it('functions ', () => {
let setCheck = jest.fn();
useStateSpy.mockImplementationOnce(function() { return [true, setCheck] });
let tree;
act(() => {
tree = create(
<ChainItem
handleSetCheckState={handleSetCheckState}
checked={true}
index={index} />
);
});
const items = tree.root.findAllByType(Checkbox);
act(() => items[0].props.onChange({
target: {
value: false
}
}));
expect(setCheck).toHaveBeenCalled();
expect(handleSetCheckState).toHaveBeenCalledWith(
index,
false
);
});
afterAll(() => jest.resetModules());
});
But setCheck is not getting called.
What have I done wrong?
Add Unit Test for React components, but still not working with react hook testings.
I'm trying to write a unit test for one of my React components written in TS:
import React, { useContext } from 'react';
import Lottie from 'lottie-react-web';
import { ConfigContext } from '../ConfigProvider';
import type { UIKitFC } from '../../types/react-extensions';
// interfaces
export interface LoadingOverlayProps {
size: 'large' | 'medium' | 'small';
testId?: string;
}
interface LoaderProps {
size: 'large' | 'medium' | 'small';
}
const G3Loader: React.FC<LoaderProps> = ({ size }) => {
const options = { animationData };
const pxSize =
size === 'small' ? '100px' : size === 'medium' ? '200px' : '300px';
const height = pxSize,
width = pxSize;
return (
<div className="loader-container">
<Lottie options={options} height={height} width={width} />
<div className="loader__loading-txt">
<div>
<h4>Loading...</h4>
</div>
</div>
</div>
);
};
/**
* Description of Loading Overlay component
*/
export const LoadingOverlay: UIKitFC<LoadingOverlayProps> = (props) => {
const { testId } = props;
const { namespace } = useContext(ConfigContext);
const { baseClassName } = LoadingOverlay.constants;
const componentClassName = `${namespace}-${baseClassName}`;
const componentTestId = testId || `${namespace}-${baseClassName}`;
return (
<div id={componentTestId} className={componentClassName}>
<G3Loader size={props.size} />
</div>
);
};
LoadingOverlay.constants = {
baseClassName: 'loadingOverlay',
};
LoadingOverlay.defaultProps = {
testId: 'loadingOverlay',
};
export default LoadingOverlay;
The component uses an imported module "Lottie" for some animation, but I'm not interested in testing it, I just want to test my component and its props.
The problem is, when I run my unit test, I get an error:
Error: Not implemented: HTMLCanvasElement.prototype.getContext (without installing the canvas npm package)
After some research, I've concluded that the error is caused by the Lottie import so I would like to mock it for the purpose of my test. I'm using Mocha and Sinon's stub functionality to try and mock the library import, but the same error persists, making me feel like I'm not stubbing the module out correctly. Here's my latest attempt at a unit test:
import React from 'react';
import * as Lottie from 'lottie-react-web';
import { render } from '#testing-library/react';
import { expect } from 'chai';
import * as sinon from 'sinon';
import LoadingOverlay from '../src/components/LoadingOverlay';
const TEST_ID = 'the-test-id';
const FakeLottie: React.FC = (props) => {
return <div>{props}</div>;
};
describe('Loading Overlay', () => {
// beforeEach(function () {
// sinon.stub(Lottie, 'default').callsFake((props) => FakeLottie(props));
// });
console.log('11111');
it('should have a test ID', () => {
sinon.stub(Lottie, 'default').callsFake((props) => FakeLottie(props));
console.log(Lottie);
const { getByTestId, debug } = render(
<LoadingOverlay testId={TEST_ID} size="small" />
);
debug();
expect(getByTestId(TEST_ID)).to.not.equal(null);
});
});
I'm not really sure what else to try, unit tests are not my forte... If anyone can help, that would be great.
I answered my own questions... Posting in case somebody else runs into the same issue.
The error was complaining about HTMLCanvasElement. It turns out the component I was trying to stub out was using the Canvas library itself which wasn't required when running in the browser, but since I was building a test, I just added the Canvas library to my package and the issue was solved. Full code below:
import React from 'react';
import { render, cleanup } from '#testing-library/react';
import { expect, assert } from 'chai';
import * as lottie from 'lottie-react-web';
import { createSandbox } from 'sinon';
import LoadingOverlay from '../src/components/LoadingOverlay';
// test ID
const TEST_ID = 'the-test-id';
// mocks
const sandbox = createSandbox();
const MockLottie = () => 'Mock Lottie';
describe('Loading Overlay', () => {
beforeEach(() => {
sandbox.stub(lottie, 'default').callsFake(MockLottie);
});
afterEach(() => {
sandbox.restore();
cleanup();
});
it('should have test ID', () => {
const { getByTestId } = render(
<LoadingOverlay testId={TEST_ID} size="medium" />
);
expect(getByTestId(TEST_ID)).to.not.equal(null);
});
});
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!
I'm having a bit of a hard time understanding how to test my modal component. I'm using the react-native-modals package and #testing-library/react-native with Jest. My component is a modal that pops up when a GraphQL error is passed to it.
./ErrorMessage.js
import React from 'react';
import PropTypes from 'prop-types';
import { Dimensions, Text } from 'react-native';
import Modal, { ModalContent, ScaleAnimation } from 'react-native-modals';
import { theme } from '../styles/theme.styles';
const ModalError = ({ error, onClose }) => {
if (!error || !error.message) {
return (
<Modal visible={false}>
<Text />
</Modal>
);
}
return (
<Modal
visible
modalAnimation={
new ScaleAnimation({
initialValue: 0,
useNativeDriver: true,
})
}
onTouchOutside={onClose}
swipeDirection={['up', 'down', 'left', 'right']}
swipeThreshold={200}
onSwipeOut={onClose}
modalStyle={modalStyle}
overlayOpacity={0.7}
>
<ModalContent>
<Text testID="graphql-error">{error.message}</Text>
</ModalContent>
</Modal>
);
};
ModalError.defaultProps = {
error: {},
};
ModalError.propTypes = {
error: PropTypes.object,
onClose: PropTypes.func.isRequired,
};
export default ModalError;
const window = Dimensions.get('window');
const modalStyle = {
backgroundColor: theme.lightRed,
borderLeftWidth: 5,
borderLeftColor: theme.red,
width: window.width / 1.12,
};
My test is pretty simple so far. I just want to make sure it's rendering the modal. I'm not exactly sure what needs to be mocked out here or how to do it.
./__tests__/ErrorMessage.test.js
import React from 'react';
import { MockedProvider } from '#apollo/react-testing';
import { GraphQLError } from 'graphql';
import { render } from '#testing-library/react-native';
import Error from '../ErrorMessage';
jest.mock('react-native-modals', () => 'react-native-modals');
const error = new GraphQLError('This is a test error message.');
const handleOnCloseError = jest.fn();
describe('<ErrorMessage>', () => {
it('should render an ErrorMessage modal component', () => {
const { container } = render(
<MockedProvider>
<Error error={error} onClose={handleOnCloseError} />
</MockedProvider>
);
expect(container).toMatchSnapshot();
});
});
The error that I'm getting is...
TypeError: _reactNativeModals.ScaleAnimation is not a constructor
18 | visible
19 | modalAnimation={
> 20 | new ScaleAnimation({
| ^
21 | initialValue: 0,
22 | useNativeDriver: true,
23 | })
And the snapshot is only printing...
./__tests__/__snapshots__/ErrorMessage.test.js.snap
// Jest Snapshot v1,
exports[`<ErrorMessage> should render an ErrorMessage modal component 1`] = `
<View
collapsable={true}
pointerEvents="box-none"
style={
Object {
"flex": 1,
}
}
/>
`;
How can I get past this error and make a proper snapshot?
you can use this -> https://github.com/testing-library/jest-native
In react native component,
...
<Modal
testID="test-modal"
deviceWidth={deviceWidth}
deviceHeight={deviceHeight}
isVisible={isModalVisible}. // isModalVisible = useState(true or false)
onBackdropPress={toggleModal}
backdropOpacity={0.5}
>
...
In test component,
...
const test = getByTestId("test-modal");
expect(test).toHaveProp("visible", true); // test success !
...
// components/Example/index.tsx
import React, { useState } from 'react';
import { Pressable, Text } from 'react-native';
import Modal from 'react-native-modal';
const Example: React.FC = () => {
const [isPrivacyPolicyVisible, setIsPrivacyPolicyVisible] = useState(false);
return (
<>
<Pressable onPress={() => setIsPrivacyPolicyVisible(true)}>
<Text>Privacy Policy</Text>
</Pressable>
<Modal
accessibilityLabel="privacy-policy-modal"
isVisible={isPrivacyPolicyVisible}>
<Text>Content</Text>
</Modal>
</>
);
};
export default Example;
// components/Example/index.test.tsx
import React from 'react';
import { fireEvent, render, waitFor } from '#testing-library/react-native';
import { Example } from 'components';
describe('Example Component', () => {
it('should render privacy policy.', async () => {
// Arrange
const { queryByText, queryByA11yLabel } = render(<Example />);
const button = queryByText(/privacy policy/i);
const modal = queryByA11yLabel('privacy-policy-modal');
// Act and Assert
expect(button).toBeTruthy();
expect(modal).toBeTruthy();
expect(modal.props).toMatchObject({
visible: false,
});
});
it('should show privacy policy modal.', async () => {
// Arrange
const { queryByText, queryByA11yLabel } = render(<Example />);
const button = queryByText(/privacy policy/i);
const modal = queryByA11yLabel('privacy-policy-modal');
// Act
await waitFor(() => {
fireEvent.press(button);
});
// Assert
expect(modal.props).toMatchObject({
visible: true,
});
});
});
when you do jest.mock('react-native-modals', () => 'react-native-modals'); you're replacing the whole library with the string 'react-native-modals' thus when you use it in your component it fails. You need to return a full mocked implementation from your mock function (second argument to jest.mock). It's also possible auto-mocking may work for you which would be done by simply doing: jest.mock('react-native-modals');
Here's the docks for jest.mock() with some examples of the various ways to use it: https://jestjs.io/docs/en/jest-object#jestmockmodulename-factory-options.
I've just written test file for my component, at the moment it's very rudimentary.. I'm quite inexperience in written test for frontend. I ran yarn test to this test file and it failed miserably..
Here is the message:
Unable to find an element with the text: Please review your billing details...
This is what I have so far for my test:
import React from 'react';
import { render, cleanup, waitForElement } from 'react-testing-library';
// React Router
import { MemoryRouter, Route } from "react-router";
import Show from './Show';
test('it shows the offer', async () => {
const { getByText } = render(
<MemoryRouter initialEntries={['/booking-requests/20-A1-C2/offer']}>
<Route
path="/booking-requests/:booking_request/offer"
render={props => (
<Show {...props} />
)}
/>
</MemoryRouter>
);
//displays the review prompt
await waitForElement(() => getByText('Please review your billing details, contract preview and Additions for your space. Once you’re happy, accept your offer'));
//displays the confirm button
await waitForElement(() => getByText('Confirm'));
});
and this is the component:
// #flow
import * as React from 'react';
import i18n from 'utils/i18n/i18n';
import { Btn } from '#appearhere/bloom';
import css from './Show.css';
import StepContainer from 'components/Layout/DynamicStepContainer/DynamicStepContainer';
const t = i18n.withPrefix('client.apps.offers.show');
const confirmOfferSteps = [
{
title: t('title'),
breadcrumb: t('breadcrumb'),
},
{
title: i18n.t('client.apps.offers.billing_information.title'),
breadcrumb: i18n.t('client.apps.offers.billing_information.breadcrumb'),
},
{
title: i18n.t('client.apps.offers.confirm_pay.title'),
breadcrumb: i18n.t('client.apps.offers.confirm_pay.breadcrumb'),
},
];
class Show extends React.Component<Props> {
steps = confirmOfferSteps;
renderCtaButton = (): React.Element<'Btn'> => {
const cta = t('cta');
return <Btn className={css.button} context='primary'>
{cta}
</Btn>
};
renderLeftContent = ({ isMobile }: { isMobile: boolean }): React.Element<'div'> => (
<div>
<p>{t('blurb')}</p>
{!isMobile && this.renderCtaButton()}
</div>
);
renderRightContent = () => {
return <div>Right content</div>;
};
render() {
const ctaButton = this.renderCtaButton();
return (
<StepContainer
steps={this.steps}
currentStep={1}
ctaButton={ctaButton}
leftContent={this.renderLeftContent}
rightContent={this.renderRightContent}
footer={ctaButton}
/>
);
}
}
export default Show;
what am I missing? Suggestions what else to add to my test file would be greatly appreciated!