Commit 6676a7cc by Ari Rizzitano Committed by GitHub

Merge pull request #8 from edx/ari/100

(almost) 100% test coverage
parents 5e8bc736 eaa10656
...@@ -13,5 +13,11 @@ ...@@ -13,5 +13,11 @@
}, },
"env": { "env": {
"jest": true "jest": true
},
"overrides": {
"files": ["*.stories.jsx", "*.test.jsx"],
"rules": {
"import/no-extraneous-dependencies": "off" // storybook & enzyme should stay devDependencies
}
} }
} }
...@@ -83,16 +83,7 @@ exports[`Storyshots Dropdown basic usage 1`] = ` ...@@ -83,16 +83,7 @@ exports[`Storyshots Dropdown basic usage 1`] = `
<button <button
aria-expanded={false} aria-expanded={false}
aria-haspopup="true" aria-haspopup="true"
buttonType="secondary"
className="btn border-0 dropdown-toggle btn-secondary" className="btn border-0 dropdown-toggle btn-secondary"
classNames={
Array [
"border-0",
"dropdown-toggle",
]
}
display="Search Engines"
inputRef={[Function]}
onBlur={[Function]} onBlur={[Function]}
onClick={[Function]} onClick={[Function]}
onKeyDown={[Function]} onKeyDown={[Function]}
...@@ -151,14 +142,14 @@ exports[`Storyshots InputSelect basic usage 1`] = ` ...@@ -151,14 +142,14 @@ exports[`Storyshots InputSelect basic usage 1`] = `
className="form-group" className="form-group"
> >
<label <label
htmlFor="textInput5" htmlFor="asInput5"
> >
Fruits Fruits
</label> </label>
<select <select
aria-describedby={undefined} aria-describedby={undefined}
className="form-control" className="form-control"
id="textInput5" id="asInput5"
name="fruits" name="fruits"
onBlur={[Function]} onBlur={[Function]}
onChange={[Function]} onChange={[Function]}
...@@ -193,14 +184,14 @@ exports[`Storyshots InputSelect separate labels and values 1`] = ` ...@@ -193,14 +184,14 @@ exports[`Storyshots InputSelect separate labels and values 1`] = `
className="form-group" className="form-group"
> >
<label <label
htmlFor="textInput6" htmlFor="asInput6"
> >
New England States New England States
</label> </label>
<select <select
aria-describedby={undefined} aria-describedby={undefined}
className="form-control" className="form-control"
id="textInput6" id="asInput6"
name="new-england-states" name="new-england-states"
onBlur={[Function]} onBlur={[Function]}
onChange={[Function]} onChange={[Function]}
...@@ -245,14 +236,14 @@ exports[`Storyshots InputSelect separate option groups 1`] = ` ...@@ -245,14 +236,14 @@ exports[`Storyshots InputSelect separate option groups 1`] = `
className="form-group" className="form-group"
> >
<label <label
htmlFor="textInput7" htmlFor="asInput7"
> >
Northeast States Northeast States
</label> </label>
<select <select
aria-describedby={undefined} aria-describedby={undefined}
className="form-control" className="form-control"
id="textInput7" id="asInput7"
name="northeast-states" name="northeast-states"
onBlur={[Function]} onBlur={[Function]}
onChange={[Function]} onChange={[Function]}
...@@ -345,14 +336,14 @@ exports[`Storyshots InputSelect with validation 1`] = ` ...@@ -345,14 +336,14 @@ exports[`Storyshots InputSelect with validation 1`] = `
className="form-group" className="form-group"
> >
<label <label
htmlFor="textInput8" htmlFor="asInput8"
> >
Favorite Color Favorite Color
</label> </label>
<select <select
aria-describedby={undefined} aria-describedby={undefined}
className="form-control" className="form-control"
id="textInput8" id="asInput8"
name="color" name="color"
onBlur={[Function]} onBlur={[Function]}
onChange={[Function]} onChange={[Function]}
...@@ -402,7 +393,7 @@ exports[`Storyshots InputText minimal usage 1`] = ` ...@@ -402,7 +393,7 @@ exports[`Storyshots InputText minimal usage 1`] = `
className="form-group" className="form-group"
> >
<label <label
htmlFor="textInput9" htmlFor="asInput9"
> >
First Name First Name
</label> </label>
...@@ -411,7 +402,7 @@ exports[`Storyshots InputText minimal usage 1`] = ` ...@@ -411,7 +402,7 @@ exports[`Storyshots InputText minimal usage 1`] = `
aria-invalid={false} aria-invalid={false}
className="form-control" className="form-control"
disabled={false} disabled={false}
id="textInput9" id="asInput9"
name="name" name="name"
onBlur={[Function]} onBlur={[Function]}
onChange={[Function]} onChange={[Function]}
...@@ -428,16 +419,16 @@ exports[`Storyshots InputText validation 1`] = ` ...@@ -428,16 +419,16 @@ exports[`Storyshots InputText validation 1`] = `
className="form-group" className="form-group"
> >
<label <label
htmlFor="textInput10" htmlFor="asInput10"
> >
Username Username
</label> </label>
<input <input
aria-describedby="undefined description-textInput10" aria-describedby="undefined description-asInput10"
aria-invalid={false} aria-invalid={false}
className="form-control" className="form-control"
disabled={false} disabled={false}
id="textInput10" id="asInput10"
name="username" name="username"
onBlur={[Function]} onBlur={[Function]}
onChange={[Function]} onChange={[Function]}
...@@ -448,7 +439,7 @@ exports[`Storyshots InputText validation 1`] = ` ...@@ -448,7 +439,7 @@ exports[`Storyshots InputText validation 1`] = `
/> />
<small <small
className="form-text" className="form-text"
id="description-textInput10" id="description-asInput10"
> >
The unique name that identifies you throughout the site. The unique name that identifies you throughout the site.
</small> </small>
......
...@@ -12,6 +12,7 @@ ...@@ -12,6 +12,7 @@
"deploy-storybook": "storybook-to-ghpages", "deploy-storybook": "storybook-to-ghpages",
"lint": "eslint --ext .js --ext .jsx .", "lint": "eslint --ext .js --ext .jsx .",
"precommit": "yarn run lint", "precommit": "yarn run lint",
"snapshot": "jest --updateSnapshot",
"start": "start-storybook -p 6006", "start": "start-storybook -p 6006",
"test": "jest --coverage" "test": "jest --coverage"
}, },
......
...@@ -5,11 +5,23 @@ import PropTypes from 'prop-types'; ...@@ -5,11 +5,23 @@ import PropTypes from 'prop-types';
import buttons from 'bootstrap/scss/_buttons.scss'; import buttons from 'bootstrap/scss/_buttons.scss';
function Button(props) { function Button(props) {
const {
buttonType,
className,
display,
inputRef,
onBlur,
onClick,
onKeyDown,
type,
...other
} = props;
return ( return (
<button <button
className={classNames([ className={classNames([
buttons.btn, buttons.btn,
...props.classNames, ...props.className,
], { ], {
[buttons[`btn-${props.buttonType}`]]: props.buttonType !== undefined, [buttons[`btn-${props.buttonType}`]]: props.buttonType !== undefined,
})} })}
...@@ -18,7 +30,7 @@ function Button(props) { ...@@ -18,7 +30,7 @@ function Button(props) {
onKeyDown={props.onKeyDown} onKeyDown={props.onKeyDown}
type={props.type} type={props.type}
ref={props.inputRef} ref={props.inputRef}
{...props} {...other}
> >
{props.display} {props.display}
</button> </button>
...@@ -27,7 +39,7 @@ function Button(props) { ...@@ -27,7 +39,7 @@ function Button(props) {
Button.propTypes = { Button.propTypes = {
buttonType: PropTypes.string, buttonType: PropTypes.string,
classNames: PropTypes.arrayOf(PropTypes.string), className: PropTypes.arrayOf(PropTypes.string),
display: PropTypes.string.isRequired, display: PropTypes.string.isRequired,
inputRef: PropTypes.func, inputRef: PropTypes.func,
onBlur: PropTypes.func, onBlur: PropTypes.func,
...@@ -38,7 +50,7 @@ Button.propTypes = { ...@@ -38,7 +50,7 @@ Button.propTypes = {
Button.defaultProps = { Button.defaultProps = {
buttonType: undefined, buttonType: undefined,
classNames: [], className: [],
inputRef: () => {}, inputRef: () => {},
onBlur: () => {}, onBlur: () => {},
onClick: () => {}, onClick: () => {},
......
/* eslint-disable import/no-extraneous-dependencies */ /* eslint-disable import/no-extraneous-dependencies */
/* eslint-disable no-console */
import React from 'react'; import React from 'react';
import { shallow, mount } from 'enzyme'; import { shallow, mount } from 'enzyme';
import CheckBox from './index'; import CheckBox from './index';
...@@ -45,16 +44,16 @@ describe('<CheckBox />', () => { ...@@ -45,16 +44,16 @@ describe('<CheckBox />', () => {
}); });
it('check that callback function is triggered when clicked', () => { it('check that callback function is triggered when clicked', () => {
const spy = jest.fn();
const wrapper = shallow( const wrapper = shallow(
<CheckBox <CheckBox
name="checkbox" name="checkbox"
descibedBy="checkbox" descibedBy="checkbox"
label="check me out!" label="check me out!"
checked="false" checked="false"
onChange={() => console.log('the checkbox changed state')} onChange={spy}
/>, />,
); );
const spy = jest.spyOn(wrapper.instance(), 'onChange');
expect(spy).toHaveBeenCalledTimes(0); expect(spy).toHaveBeenCalledTimes(0);
wrapper.find('input').simulate('click'); wrapper.find('input').simulate('click');
......
import React from 'react'; import React from 'react';
import { inputProps } from '../utils/asInput'; import { inputProps } from '../asInput';
import newId from '../utils/newId'; import newId from '../utils/newId';
class CheckBox extends React.Component { class CheckBox extends React.Component {
......
/* eslint-disable import/no-extraneous-dependencies */ /* eslint-disable import/no-extraneous-dependencies */
import React from 'react'; import React from 'react';
import { shallow } from 'enzyme'; import { shallow, mount } from 'enzyme';
import Dropdown from './index'; import Dropdown, { triggerKeys } from './index';
const props = { const props = {
title: 'Example', title: 'Example',
menuItems: [ menuItems: [
{ label: 'Example 1', href: 'http://example1.com' }, { label: 'Example 1', href: 'http://example1.com' },
{ label: 'Example 2', href: 'http://example2.com' }, { label: 'Example 2', href: 'http://example2.com' },
{ label: 'Example 3', href: 'http://example3.com' },
], ],
}; };
const menuOpen = (isOpen, wrapper) => {
expect(wrapper.hasClass('show')).toEqual(isOpen);
expect(wrapper.find('[type="button"]').prop('aria-expanded')).toEqual(isOpen);
expect(wrapper.find('[aria-hidden=false]').exists()).toEqual(isOpen);
};
describe('<Dropdown />', () => { describe('<Dropdown />', () => {
it('renders correctly', () => { describe('renders', () => {
const wrapper = shallow( const wrapper = shallow(
<Dropdown <Dropdown
{...props} {...props}
/>, />,
); );
const menu = wrapper.find('ul'); const menu = wrapper.find('ul');
const button = wrapper.find('[type="button"]'); const button = wrapper.find('[type="button"]');
it('with menu and toggle', () => {
expect(button.exists()).toEqual(true); expect(button.exists()).toEqual(true);
expect(button.prop('aria-expanded')).toEqual(false);
expect(menu.exists()).toEqual(true);
expect(menu.find('li')).toHaveLength(2);
expect(menu.prop('aria-label')).toEqual(props.title); expect(menu.prop('aria-label')).toEqual(props.title);
expect(menu.prop('aria-hidden')).toEqual(true); expect(menu.exists()).toEqual(true);
expect(menu.find('li')).toHaveLength(props.menuItems.length);
}); });
it('renders correctly', () => { it('with menu closed', () => {
const wrapper = shallow( menuOpen(false, wrapper);
});
});
describe('opens', () => {
let wrapper;
beforeEach(() => {
wrapper = mount(
<Dropdown <Dropdown
{...props} {...props}
/>, />,
); );
});
expect(wrapper.find('[type="button"]').exists()).toEqual(true); it('on toggle click', () => {
expect(wrapper.find('li')).toHaveLength(2); wrapper.find('[type="button"]').simulate('click');
expect(wrapper.find('[aria-expanded=false]').exists()).toEqual(true); menuOpen(true, wrapper);
}); });
it('opens on click', () => { triggerKeys.OPEN_MENU.forEach((key) => {
const wrapper = shallow( it(`on ${key}`, () => {
wrapper.find('[type="button"]').simulate('keyDown', { key });
menuOpen(true, wrapper);
});
});
});
describe('closes', () => {
let wrapper;
beforeEach(() => {
wrapper = mount(
<Dropdown <Dropdown
{...props} {...props}
/>, />,
); );
wrapper.find('[type="button"]').simulate('click');
});
const button = wrapper.find('[type="button"]'); it('on toggle click', () => {
wrapper.find('[type="button"]').simulate('click');
menuOpen(false, wrapper);
});
button.simulate('click'); it('on document click', () => {
expect(wrapper.find('[aria-hidden=false]').exists()).toEqual(true); document.querySelector('body').click();
menuOpen(false, wrapper);
});
triggerKeys.CLOSE_MENU.forEach((key) => {
it(`on button ${key}`, () => {
wrapper.find('[type="button"]').simulate('keyDown', { key });
menuOpen(false, wrapper);
});
it(`on menu item ${key}`, () => {
wrapper.find('a').at(0).simulate('keyDown', { key });
menuOpen(false, wrapper);
});
});
});
it('does not close when document click is inside the menu', () => {
const div = document.createElement('div');
document.body.appendChild(div);
const wrapper = mount(
<Dropdown
{...props}
/>,
{ attachTo: div },
);
wrapper.find('[type="button"]').simulate('click');
document.querySelector('ul').click();
menuOpen(true, wrapper);
});
describe('focuses', () => {
let wrapper;
beforeEach(() => {
wrapper = mount(
<Dropdown
{...props}
/>,
);
wrapper.find('[type="button"]').simulate('click');
});
it('first menu item on open', () => {
expect(wrapper.find('a').at(0).matchesElement(document.activeElement)).toEqual(true);
});
describe('forward in list', () => {
triggerKeys.NAVIGATE_DOWN.forEach((key) => {
it(`on ${key}`, () => {
wrapper.find('a').at(0).simulate('keyDown', { key });
expect(wrapper.find('a').at(1).matchesElement(document.activeElement)).toEqual(true);
});
});
});
describe('backward in list', () => {
triggerKeys.NAVIGATE_UP.forEach((key) => {
it(`on ${key}`, () => {
wrapper.find('a').at(0).simulate('keyDown', { key: triggerKeys.NAVIGATE_DOWN[0] });
wrapper.find('a').at(1).simulate('keyDown', { key });
expect(wrapper.find('a').at(0).matchesElement(document.activeElement)).toEqual(true);
});
});
});
it('first menu item after looping through', () => {
wrapper.find('a').at(0).simulate('keyDown', { key: triggerKeys.NAVIGATE_DOWN[0] });
wrapper.find('a').at(1).simulate('keyDown', { key: triggerKeys.NAVIGATE_DOWN[0] });
wrapper.find('a').at(2).simulate('keyDown', { key: triggerKeys.NAVIGATE_DOWN[0] });
expect(wrapper.find('a').at(0).matchesElement(document.activeElement)).toEqual(true);
});
it('toggle on close', () => {
wrapper.find('a').at(0).simulate('keyDown', { key: triggerKeys.CLOSE_MENU[0] });
expect(wrapper.find('[type="button"]').matchesElement(document.activeElement)).toEqual(true);
});
}); });
}); });
...@@ -7,7 +7,7 @@ import borders from 'bootstrap/scss/utilities/_borders.scss'; ...@@ -7,7 +7,7 @@ import borders from 'bootstrap/scss/utilities/_borders.scss';
import pc from '../utils/base-styles.scss'; import pc from '../utils/base-styles.scss';
import Button from '../Button'; import Button from '../Button';
const triggerKeys = { export const triggerKeys = {
OPEN_MENU: ['ArrowDown', 'Space'], OPEN_MENU: ['ArrowDown', 'Space'],
CLOSE_MENU: ['Escape'], CLOSE_MENU: ['Escape'],
NAVIGATE_DOWN: ['ArrowDown', 'Tab'], NAVIGATE_DOWN: ['ArrowDown', 'Tab'],
...@@ -45,7 +45,7 @@ class Dropdown extends React.Component { ...@@ -45,7 +45,7 @@ class Dropdown extends React.Component {
} }
componentDidUpdate() { componentDidUpdate() {
if (this.state.open && this.menuItems.length > 0) { if (this.state.open) {
this.menuItems[this.state.focusIndex].focus(); this.menuItems[this.state.focusIndex].focus();
} else if (this.toggleElem) { } else if (this.toggleElem) {
this.toggleElem.focus(); this.toggleElem.focus();
...@@ -134,7 +134,7 @@ class Dropdown extends React.Component { ...@@ -134,7 +134,7 @@ class Dropdown extends React.Component {
display={this.props.title} display={this.props.title}
onClick={this.toggle} onClick={this.toggle}
onKeyDown={this.handleToggleKeyDown} onKeyDown={this.handleToggleKeyDown}
classNames={[ className={[
borders['border-0'], borders['border-0'],
dd['dropdown-toggle'], dd['dropdown-toggle'],
]} ]}
......
...@@ -2,7 +2,7 @@ import React from 'react'; ...@@ -2,7 +2,7 @@ import React from 'react';
import { Input } from 'reactstrap'; import { Input } from 'reactstrap';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import asInput, { inputProps } from '../utils/asInput'; import asInput, { inputProps } from '../asInput';
class Select extends React.Component { class Select extends React.Component {
static getOption(option, i) { static getOption(option, i) {
......
import React from 'react'; import React from 'react';
import { Input } from 'reactstrap'; import { Input } from 'reactstrap';
import asInput, { inputProps } from '../utils/asInput'; import asInput, { inputProps } from '../asInput';
function Text(props) { function Text(props) {
return ( return (
......
/* eslint-disable import/no-extraneous-dependencies */
/* eslint-disable react/prop-types */
import React from 'react';
import { mount } from 'enzyme';
import asInput, { getDisplayName } from './index';
function testComponent(props) {
return (
<input
defaultValue={props.value}
onBlur={props.onBlur}
onChange={props.onChange}
/>
);
}
const InputTestComponent = asInput(testComponent);
const baseProps = {
type: 'text',
name: 'test',
label: 'test component',
description: 'i am a test component',
};
describe('getDisplayName', () => {
it('returns the proper display name', () => {
expect(getDisplayName({ displayName: 'foo' })).toEqual('foo');
expect(getDisplayName({ name: 'bar' })).toEqual('bar');
expect(getDisplayName({})).toEqual('Component');
});
});
describe('asInput()', () => {
it('renders', () => {
const props = {
...baseProps,
value: 'foofoo',
};
const wrapper = mount(
<InputTestComponent {...props} />,
);
expect(wrapper.find('label').text()).toEqual(props.label);
expect(wrapper.find('#description-asInput1').text()).toEqual(props.description);
expect(wrapper.state('value')).toEqual(props.value);
});
describe('fires', () => {
it('blur handler', () => {
const spy = jest.fn();
const props = {
...baseProps,
onBlur: spy,
};
const wrapper = mount(
<InputTestComponent {...props} />,
);
wrapper.find('input').simulate('blur');
expect(spy).toHaveBeenCalledTimes(1);
});
it('change handler', () => {
const spy = jest.fn();
const props = {
...baseProps,
onChange: spy,
};
const wrapper = mount(
<InputTestComponent {...props} />,
);
wrapper.find('input').simulate('change');
expect(spy).toHaveBeenCalledTimes(1);
});
describe('validator', () => {
it('on blur', () => {
const spy = jest.fn();
spy.mockReturnValueOnce({ isValid: true });
const props = {
...baseProps,
validator: spy,
};
const wrapper = mount(
<InputTestComponent {...props} />,
);
wrapper.find('input').simulate('blur');
expect(spy).toHaveBeenCalledTimes(1);
});
it('and displays error message when invalid', () => {
const spy = jest.fn();
const validationResult = {
isValid: false,
validationMessage: 'Invalid!!1',
};
spy.mockReturnValueOnce(validationResult);
const props = {
...baseProps,
validator: spy,
};
const wrapper = mount(
<InputTestComponent {...props} />,
);
wrapper.find('input').simulate('blur');
expect(spy).toHaveBeenCalledTimes(1);
const err = wrapper.find('.form-control-feedback');
expect(err.exists()).toEqual(true);
expect(err.text()).toEqual(validationResult.validationMessage);
});
});
});
});
...@@ -3,9 +3,10 @@ import React from 'react'; ...@@ -3,9 +3,10 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { FormGroup, FormFeedback, FormText } from 'reactstrap'; import { FormGroup, FormFeedback, FormText } from 'reactstrap';
import newId from './newId'; import newId from '../utils/newId';
const getDisplayName = WrappedComponent => WrappedComponent.displayName || WrappedComponent.name || 'Component'; export const getDisplayName = WrappedComponent =>
WrappedComponent.displayName || WrappedComponent.name || 'Component';
export const inputProps = { export const inputProps = {
label: PropTypes.string.isRequired, label: PropTypes.string.isRequired,
...@@ -29,7 +30,7 @@ const asInput = (WrappedComponent) => { ...@@ -29,7 +30,7 @@ const asInput = (WrappedComponent) => {
this.handleChange = this.handleChange.bind(this); this.handleChange = this.handleChange.bind(this);
this.handleBlur = this.handleBlur.bind(this); this.handleBlur = this.handleBlur.bind(this);
const id = newId('textInput'); const id = newId('asInput');
this.state = { this.state = {
id, id,
value: this.props.value, value: this.props.value,
......
import newId from './newId';
describe('newId', () => {
it('increments on each call', () => {
expect(newId()).toEqual('id1');
expect(newId('foo-')).toEqual('foo-2');
expect(newId('bar-')).toEqual('bar-3');
});
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment