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.
Related
I am trying to create a button that I can put anywhere on a page to submit a specific form. I'm interested in using Hooks and Context / Provider pattern to expose this state anywhere in the app.
The code below creates a context / provider that has access to a useState hook for setting the submit function.
import React from 'react';
import {createContext, ReactNode, useContext, useEffect, useRef, useState} from 'react';
export type HiddenButtonSubmitContext = {
submit: () => void,
setSubmitHandler: React.Dispatch<React.SetStateAction<() => void>>
}
export const defaultSetSubmitHandler = () => () => {
// eslint-disable-next-line no-console
console.log('submit handler was never set')
}
export const SubmitContext = createContext<HiddenButtonSubmitContext>({
submit: () => {
throw new Error('submit not set, make surea all calls to useHiddenButton are within HiddenButtonProvider');
},
setSubmitHandler: defaultSetSubmitHandler()
})
export const useHiddenButton = () => useContext(SubmitContext)
export const HiddenButtonProvider: React.FC<{ children?: ReactNode | undefined }> = ({ children }) => {
const [submit, setSubmitHandler] = useState(defaultSetSubmitHandler)
return (
<SubmitContext.Provider value={{submit, setSubmitHandler}}>
{children}
</SubmitContext.Provider>
)
}
export const HiddenButton = () => {
const hiddenButtonRef = useRef<any>()
const { setSubmitHandler } = useHiddenButton()
useEffect(() => {
setSubmitHandler(() => () => {
hiddenButtonRef.current.click()
})
}, [setSubmitHandler])
return (
<input type="submit" style={{ display: 'none' }} ref={hiddenButtonRef} />
)
}
Here's the example usage:
const Btn = () => {
const { submit } = useHiddenButton()
return <button onClick={() => submit()}>hi</button>
}
export function Example () {
return (
<HiddenButtonProvider>
<Btn />
<form onSubmit={(e) => {
e.preventDefault()
console.log('submitted')
}}>
<HiddenButton/>
</form>
</HiddenButtonProvider>
)
}
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);
});
});
});
This is my component, that contains a Select child component
import { hashHistory } from 'react-router'
import { useEffect } from 'react'
import Select from 'cm/components/common/select'
import { eventTracker, events } from 'cm/common/event-tracker'
import { eventMetricsMap } from './helpers'
import { compose } from 'cm/common/utils'
const SelectLanguage = ({ handlers, languages, selectedLanguage }) => {
useEffect(() => {
hashHistory.listen(({ pathname }) => {
if (pathname.indexOf('/library')) {
handlers.onSelect(null)
}
})
}, [])
const handleSelect = ({ value }) => {
handlers.onSelect(value)
return value.toLowerCase()
}
const trackSelectLanguage = language => {
eventTracker.track(
events.switchBuildingBlickLanguage,
{
from: eventMetricsMap[ 'buildingblocks' ][ 'from' ],
language
}
)
}
return (
<Select
className="search__form--lang dropdown-selection--fixed-width"
handlers={{
onSelect: compose(
trackSelectLanguage,
handleSelect
)
}}
items={languages}
selected={selectedLanguage}
type="medium-input"
/>
)
}
I'd like to make unit test for this component using Jest. My 4 test cases that I want to cover:
renders in the document
renders items
track select open click
handles action
Unfortunately last two cases fails and can't find a reason.
Do you know how to resolve it ?
import expect from 'expect'
import { fireEvent, render } from '#testing-library/react'
import SelectLanguage from './select-language'
const dummyEvents = {
switchBuildingBlockLanguage: 'switch building blocks language'
}
jest.mock(
'cm/common/event-tracker',
() => ({
eventTracker: {
track: () => {}
},
events: {
dummyEvents
}
})
)
const languages = [
{
name: 'English',
value: 'EN',
},
{
name: 'Nederlands',
value: 'NL'
}
]
jest.mock(
'cm/components/common/select',
() => ({ items, handlers }) => {
console.log('items', items)
return (<div className="dropdown">
<div className="dropdown__menu">
{items.map(({ name }) => (
<div className="dropdown__item" onClick={handlers.onSelect}>{name}</div>
))}
</div>
</div>)
}
)
const containerPath = '.dropdown'
const itemPath = '.dropdown__item'
describe('SelectLanguage component', () => {
it('renders in the document', () => {
const { container } = render(
<SelectLanguage languages={languages}/>
)
const $container = container.querySelector(containerPath)
expect($container).toBeTruthy()
})
it('renders items', () => {
const { container } = render(
<SelectLanguage languages={languages}/>
)
const $dropdownItems = container.querySelectorAll(itemPath)
expect([ ...$dropdownItems ].length).toBe(2)
})
it('track select open click', () => {
const spyFn = jest.fn()
const { container } = render(
<SelectLanguage
selectedLanguage={{
value: 'EN'
}}
languages={languages}
handlers={{
onSelect: spyFn
}}
/>
)
const $dropdownItem = container.querySelector(itemPath)
fireEvent.click($dropdownItem)
expect(spyFn).toHaveBeenCalledWith(
dummyEvents.switchBuildingBlockLanguage,
{
from: 'building blocks',
language: 'en'
}
)
})
it('handles action', () => {
const spyFn = jest.fn()
const { container } = render(
<SelectLanguage
selectedLanguage={{
value: 'EN'
}}
languages={languages}
handlers={{
onSelect: spyFn
}}
/>
)
const $dropdownItem = container.querySelector(itemPath)
fireEvent.click($dropdownItem)
expect(spyFn).toHaveBeenCalledWith('EN')
})
})
When using events you need to wait for them to complete before running the assertions.
testing-library/react has the waitFor function:
import { fireEvent, render, waitFor } from '#testing-library/react'
// ...
it('handles action', async () => {
// ...
fireEvent.click($dropdownItem)
await waitFor(() => {
expect(spyFn).toHaveBeenCalledWith(
dummyEvents.switchBuildingBlockLanguage,
{
from: 'building blocks',
language: 'en'
}
)
});
// ...
Trying to test this component, and im getting this
error
TypeError: this.props.onItemAdded is not a function
I've referenced this but this solution doesn't really apply to my problem
Enzyme test: TypeError: expect(...).find is not a function
How would i test the button functionality being that the button is a prop ?
todo-add-item.test.js
import React from "react";
import { shallow } from "enzyme";
import TodoAddItem from './todo-add-item';
describe('Should render add item component', ()=> {
it('should render add item component', () => {
const wrapper = shallow(<TodoAddItem/>)
})
})
describe('Should simulate button click', ()=> {
it('should simulate button click', () => {
const wrapper =shallow(<TodoAddItem/>)
wrapper.find('button').simulate('click') // getting the type error here.
})
})
todo-add-item.js
import React, { Component } from 'react';
import './todo-add-item.css';
export default class TodoAddItem extends Component {
render() {
return (
<div className="todo-add-item">
<button
className="test-button btn btn-outline-secondary float-left"
onClick={() => this.props.onItemAdded('Hello world')}>
Add Item
</button>
</div>
);
}
}
app.js
import React, { Component } from 'react';
import AppHeader from '../app-header';
import SearchPanel from '../search-panel';
import TodoList from '../todo-list';
import ItemStatusFilter from '../item-status-filter';
import TodoAddItem from '../todo-add-item';
import './app.css';
export default class App extends Component {
constructor() {
super();
this.createTodoItem = (label) => {
return {
label,
important: false,
done: false,
id: this.maxId++
}
};
this.maxId = 100;
this.state = {
todoData: [
this.createTodoItem('Drink Coffee'),
this.createTodoItem('Make Awesome App'),
this.createTodoItem('Have a lunch')
]
};
this.deleteItem = (id) => {
this.setState(({ todoData }) => {
const idx = todoData.findIndex((el) => el.id === id);
const newArray = [
...todoData.slice(0, idx),
...todoData.slice(idx + 1)
];
return {
todoData: newArray
};
});
};
this.addItem = (text) => {
const newItem = this.createTodoItem(text);
this.setState(({ todoData }) => {
const newArray = [
...todoData,
newItem
];
return {
todoData: newArray
};
});
};
this.onToggleImportant = (id) => {
console.log('toggle important', id);
};
this.onToggleDone = (id) => {
console.log('toggle done', id);
};
};
render() {
return (
<div className="todo-app">
<AppHeader toDo={ 1 } done={ 3 } />
<div className="top-panel d-flex">
<SearchPanel />
<ItemStatusFilter />
</div>
<TodoList
todos={ this.state.todoData }
onDeleted={ this.deleteItem }
onToggleImportant={ this.onToggleImportant }
onToggleDone={ this.onToggleDone } />
<TodoAddItem onItemAdded={ this.addItem } />
</div>
);
};
};
You don't pass any props to your component.
const wrapper =shallow(<TodoAddItem onItemAdded={() => jest.fn()}/>)
You can check props with .props()
Eg:
console.log('props',wrapper.find('button').props());
I am trying test a component in React w/ TypeScript using Jest and Enzyme.
My test is as follows:
import * as React from 'react';
import { shallow } from 'enzyme';
import * as sinon from 'sinon';
import { ButtonGroup } from '../../../src/components';
describe('.ButtonGroup', () => {
it('should render', () => {
const { wrapper } = setup({});
expect(wrapper.exists()).toBe(true);
});
it('should call the rightHandler handler on click', () => {
const onClickHandler = sinon.spy();
const wrapper = setup({ rightHandler: onClickHandler });
wrapper.find('a').simulate('click');
expect(onClickHandler).toHaveBeenCalled();
});
});
const setup = propOverrides => {
const props = Object.assign({ leftBtn: MOCK_LEFT, rightBtn: MOCK_RIGHT }, propOverrides);
const wrapper = shallow(<ButtonGroup {...props} />);
return { wrapper };
};
const MOCK_LEFT = { type: 'reset', className: 'is-light', value: 'Reset' };
const MOCK_RIGHT = { type: 'button', className: 'is-primary', value: 'Search' };
However I am getting an error: TypeError: wrapper.find is not a function
The component I am testing looks like
import * as React from 'react';
const ButtonGroup = ({ leftBtn, rightBtn, leftHandler = null, rightHandler = null }) => (
<div className="field is-grouped is-grouped-right">
<p className="control">
<input type={leftBtn.type} className={'button ' + leftBtn.className} value={leftBtn.value} onClick={leftHandler} />
</p>
<p className="control">
<input type={rightBtn.type} className={'button ' + rightBtn.className} value={rightBtn.value} onClick={rightHandler} />
</p>
</div>
);
export default ButtonGroup;
I would like to essentially asset that on click, the action I expect is called.
Try
const setup = propOverrides => {
const props = Object.assign({ leftBtn: MOCK_LEFT, rightBtn: MOCK_RIGHT }, propOverrides);
const wrapper = shallow(<ButtonGroup {...props} />);
return wrapper;
};
Doing return { wrapper } is the same as return { wrapper: wrapper } so it's not your DOM element anymore, but an object with a property wrapper containing your DOM element.
This is a small bin to illustrate the issue.