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 @@
},
"env": {
"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`] = `
<button
aria-expanded={false}
aria-haspopup="true"
buttonType="secondary"
className="btn border-0 dropdown-toggle btn-secondary"
classNames={
Array [
"border-0",
"dropdown-toggle",
]
}
display="Search Engines"
inputRef={[Function]}
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
......@@ -151,14 +142,14 @@ exports[`Storyshots InputSelect basic usage 1`] = `
className="form-group"
>
<label
htmlFor="textInput5"
htmlFor="asInput5"
>
Fruits
</label>
<select
aria-describedby={undefined}
className="form-control"
id="textInput5"
id="asInput5"
name="fruits"
onBlur={[Function]}
onChange={[Function]}
......@@ -193,14 +184,14 @@ exports[`Storyshots InputSelect separate labels and values 1`] = `
className="form-group"
>
<label
htmlFor="textInput6"
htmlFor="asInput6"
>
New England States
</label>
<select
aria-describedby={undefined}
className="form-control"
id="textInput6"
id="asInput6"
name="new-england-states"
onBlur={[Function]}
onChange={[Function]}
......@@ -245,14 +236,14 @@ exports[`Storyshots InputSelect separate option groups 1`] = `
className="form-group"
>
<label
htmlFor="textInput7"
htmlFor="asInput7"
>
Northeast States
</label>
<select
aria-describedby={undefined}
className="form-control"
id="textInput7"
id="asInput7"
name="northeast-states"
onBlur={[Function]}
onChange={[Function]}
......@@ -345,14 +336,14 @@ exports[`Storyshots InputSelect with validation 1`] = `
className="form-group"
>
<label
htmlFor="textInput8"
htmlFor="asInput8"
>
Favorite Color
</label>
<select
aria-describedby={undefined}
className="form-control"
id="textInput8"
id="asInput8"
name="color"
onBlur={[Function]}
onChange={[Function]}
......@@ -402,7 +393,7 @@ exports[`Storyshots InputText minimal usage 1`] = `
className="form-group"
>
<label
htmlFor="textInput9"
htmlFor="asInput9"
>
First Name
</label>
......@@ -411,7 +402,7 @@ exports[`Storyshots InputText minimal usage 1`] = `
aria-invalid={false}
className="form-control"
disabled={false}
id="textInput9"
id="asInput9"
name="name"
onBlur={[Function]}
onChange={[Function]}
......@@ -428,16 +419,16 @@ exports[`Storyshots InputText validation 1`] = `
className="form-group"
>
<label
htmlFor="textInput10"
htmlFor="asInput10"
>
Username
</label>
<input
aria-describedby="undefined description-textInput10"
aria-describedby="undefined description-asInput10"
aria-invalid={false}
className="form-control"
disabled={false}
id="textInput10"
id="asInput10"
name="username"
onBlur={[Function]}
onChange={[Function]}
......@@ -448,7 +439,7 @@ exports[`Storyshots InputText validation 1`] = `
/>
<small
className="form-text"
id="description-textInput10"
id="description-asInput10"
>
The unique name that identifies you throughout the site.
</small>
......
......@@ -12,6 +12,7 @@
"deploy-storybook": "storybook-to-ghpages",
"lint": "eslint --ext .js --ext .jsx .",
"precommit": "yarn run lint",
"snapshot": "jest --updateSnapshot",
"start": "start-storybook -p 6006",
"test": "jest --coverage"
},
......
......@@ -5,11 +5,23 @@ import PropTypes from 'prop-types';
import buttons from 'bootstrap/scss/_buttons.scss';
function Button(props) {
const {
buttonType,
className,
display,
inputRef,
onBlur,
onClick,
onKeyDown,
type,
...other
} = props;
return (
<button
className={classNames([
buttons.btn,
...props.classNames,
...props.className,
], {
[buttons[`btn-${props.buttonType}`]]: props.buttonType !== undefined,
})}
......@@ -18,7 +30,7 @@ function Button(props) {
onKeyDown={props.onKeyDown}
type={props.type}
ref={props.inputRef}
{...props}
{...other}
>
{props.display}
</button>
......@@ -27,7 +39,7 @@ function Button(props) {
Button.propTypes = {
buttonType: PropTypes.string,
classNames: PropTypes.arrayOf(PropTypes.string),
className: PropTypes.arrayOf(PropTypes.string),
display: PropTypes.string.isRequired,
inputRef: PropTypes.func,
onBlur: PropTypes.func,
......@@ -38,7 +50,7 @@ Button.propTypes = {
Button.defaultProps = {
buttonType: undefined,
classNames: [],
className: [],
inputRef: () => {},
onBlur: () => {},
onClick: () => {},
......
/* eslint-disable import/no-extraneous-dependencies */
/* eslint-disable no-console */
import React from 'react';
import { shallow, mount } from 'enzyme';
import CheckBox from './index';
......@@ -45,16 +44,16 @@ describe('<CheckBox />', () => {
});
it('check that callback function is triggered when clicked', () => {
const spy = jest.fn();
const wrapper = shallow(
<CheckBox
name="checkbox"
descibedBy="checkbox"
label="check me out!"
checked="false"
onChange={() => console.log('the checkbox changed state')}
onChange={spy}
/>,
);
const spy = jest.spyOn(wrapper.instance(), 'onChange');
expect(spy).toHaveBeenCalledTimes(0);
wrapper.find('input').simulate('click');
......
import React from 'react';
import { inputProps } from '../utils/asInput';
import { inputProps } from '../asInput';
import newId from '../utils/newId';
class CheckBox extends React.Component {
......
/* eslint-disable import/no-extraneous-dependencies */
import React from 'react';
import { shallow } from 'enzyme';
import { shallow, mount } from 'enzyme';
import Dropdown from './index';
import Dropdown, { triggerKeys } from './index';
const props = {
title: 'Example',
menuItems: [
{ label: 'Example 1', href: 'http://example1.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 />', () => {
it('renders correctly', () => {
describe('renders', () => {
const wrapper = shallow(
<Dropdown
{...props}
/>,
);
const menu = wrapper.find('ul');
const button = wrapper.find('[type="button"]');
expect(button.exists()).toEqual(true);
expect(button.prop('aria-expanded')).toEqual(false);
it('with menu and toggle', () => {
expect(button.exists()).toEqual(true);
expect(menu.prop('aria-label')).toEqual(props.title);
expect(menu.exists()).toEqual(true);
expect(menu.find('li')).toHaveLength(props.menuItems.length);
});
expect(menu.exists()).toEqual(true);
expect(menu.find('li')).toHaveLength(2);
expect(menu.prop('aria-label')).toEqual(props.title);
expect(menu.prop('aria-hidden')).toEqual(true);
it('with menu closed', () => {
menuOpen(false, wrapper);
});
});
it('renders correctly', () => {
const wrapper = shallow(
<Dropdown
{...props}
/>,
);
describe('opens', () => {
let wrapper;
beforeEach(() => {
wrapper = mount(
<Dropdown
{...props}
/>,
);
});
it('on toggle click', () => {
wrapper.find('[type="button"]').simulate('click');
menuOpen(true, wrapper);
});
expect(wrapper.find('[type="button"]').exists()).toEqual(true);
expect(wrapper.find('li')).toHaveLength(2);
expect(wrapper.find('[aria-expanded=false]').exists()).toEqual(true);
triggerKeys.OPEN_MENU.forEach((key) => {
it(`on ${key}`, () => {
wrapper.find('[type="button"]').simulate('keyDown', { key });
menuOpen(true, wrapper);
});
});
});
it('opens on click', () => {
const wrapper = shallow(
describe('closes', () => {
let wrapper;
beforeEach(() => {
wrapper = mount(
<Dropdown
{...props}
/>,
);
wrapper.find('[type="button"]').simulate('click');
});
it('on toggle click', () => {
wrapper.find('[type="button"]').simulate('click');
menuOpen(false, wrapper);
});
it('on document click', () => {
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);
});
const button = wrapper.find('[type="button"]');
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);
});
button.simulate('click');
expect(wrapper.find('[aria-hidden=false]').exists()).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';
import pc from '../utils/base-styles.scss';
import Button from '../Button';
const triggerKeys = {
export const triggerKeys = {
OPEN_MENU: ['ArrowDown', 'Space'],
CLOSE_MENU: ['Escape'],
NAVIGATE_DOWN: ['ArrowDown', 'Tab'],
......@@ -45,7 +45,7 @@ class Dropdown extends React.Component {
}
componentDidUpdate() {
if (this.state.open && this.menuItems.length > 0) {
if (this.state.open) {
this.menuItems[this.state.focusIndex].focus();
} else if (this.toggleElem) {
this.toggleElem.focus();
......@@ -134,7 +134,7 @@ class Dropdown extends React.Component {
display={this.props.title}
onClick={this.toggle}
onKeyDown={this.handleToggleKeyDown}
classNames={[
className={[
borders['border-0'],
dd['dropdown-toggle'],
]}
......
......@@ -2,7 +2,7 @@ import React from 'react';
import { Input } from 'reactstrap';
import PropTypes from 'prop-types';
import asInput, { inputProps } from '../utils/asInput';
import asInput, { inputProps } from '../asInput';
class Select extends React.Component {
static getOption(option, i) {
......
import React from 'react';
import { Input } from 'reactstrap';
import asInput, { inputProps } from '../utils/asInput';
import asInput, { inputProps } from '../asInput';
function Text(props) {
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';
import PropTypes from 'prop-types';
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 = {
label: PropTypes.string.isRequired,
......@@ -29,7 +30,7 @@ const asInput = (WrappedComponent) => {
this.handleChange = this.handleChange.bind(this);
this.handleBlur = this.handleBlur.bind(this);
const id = newId('textInput');
const id = newId('asInput');
this.state = {
id,
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