Skip to content

Commit

Permalink
Merge pull request #223 from TechnologyAdvice/feature/common-test-events
Browse files Browse the repository at this point in the history
Common test for events
  • Loading branch information
levithomason committed Apr 27, 2016
2 parents d719cc6 + d7d0a86 commit 73e8a53
Show file tree
Hide file tree
Showing 6 changed files with 110 additions and 22 deletions.
2 changes: 1 addition & 1 deletion src/collections/Form/Form.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export default class Form extends Component {
}

componentWillUnmount() {
this.element.off()
_.invoke(this, 'element.off')
}

static _meta = {
Expand Down
2 changes: 1 addition & 1 deletion src/collections/Menu/MenuItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import META from '../../utils/Meta'

const MenuItem = ({ __onClick, active, children, className, label, name, onClick, ...rest }) => {
const handleClick = (e) => {
__onClick(name)
if (__onClick) __onClick(name)
if (onClick) onClick(name)
}

Expand Down
2 changes: 1 addition & 1 deletion src/modules/Checkbox/Checkbox.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export default class Checkbox extends Component {
}

componentWillUnmount() {
this.element.off()
_.invoke(this, 'element.off')
}

static _meta = {
Expand Down
10 changes: 5 additions & 5 deletions src/modules/Dropdown/Dropdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,13 +108,13 @@ export default class Dropdown extends Component {
}

componentDidUpdate(prevProps, prevState) {
this.element.dropdown('refresh')
_.invoke(this, 'element.dropdown', 'refresh')
const isDOMInSync = _.isEqual(this.props.value, this.getValue())
if (!isDOMInSync && this.isSelection()) this._syncFromProps()
}

componentWillUnmount() {
this.element.off()
_.invoke(this, 'element.off')
}

handleAdd = (value) => {
Expand All @@ -137,7 +137,7 @@ export default class Dropdown extends Component {
// multiselect dropdown values should be arrays since they accept array values
// empty values should not be empty strings ""
getValue = () => {
const value = this.element.dropdown('get value')
const value = _.invoke(this, 'element.dropdown', 'get value')
if (value) {
// multiselect dropdown values are delimited strings
return this.isMultiple() ? value.split(Dropdown._DELIMITER) : value
Expand Down Expand Up @@ -168,7 +168,7 @@ export default class Dropdown extends Component {

isMultiple = () => _.includes(this.props.className, 'multiple')
isSelection = () => _.includes(this.props.className, 'selection')
setValue = (value) => this.element.dropdown('set exactly', value)
setValue = (value) => _.invoke(this, 'element.dropdown', 'set exactly', value)

render() {
const { children, className, defaultText, defaultValue, icon, options, text } = this.props
Expand All @@ -190,7 +190,7 @@ export default class Dropdown extends Component {
))
const componentProps = getComponentProps(this.props, pluginPropTypes)
return (
<div {...componentProps} className={classes} ref='element'>
<div {...componentProps} className={classes} ref='element' onChange={this.handleChange}>
{this.isSelection() && <input type='hidden' defaultValue={defaultValue} />}
{text && <div className='text'>{text}</div>}
{icon && <Icon className={iconClasses} />}
Expand Down
10 changes: 9 additions & 1 deletion src/modules/Progress/Progress.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ const pluginPropTypes = {
export default class Progress extends Component {
static propTypes = {
...pluginPropTypes,
onChange: PropTypes.func,
onError: PropTypes.func,
children: PropTypes.node,
className: PropTypes.string,
/**
Expand Down Expand Up @@ -75,7 +77,13 @@ export default class Progress extends Component {
)

return (
<div {...getComponentProps(this.props, pluginPropTypes)} className={classes} ref='element'>
<div
{...getComponentProps(this.props, pluginPropTypes)}
className={classes}
ref='element'
onChange={this.props.onChange}
onError={this.props.onError}
>
<div className='bar'>
{this.props.showProgress && <div className='progress' />}
</div>
Expand Down
106 changes: 93 additions & 13 deletions test/specs/commonTests.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import React, { createElement } from 'react'
import META from 'src/utils/Meta'
import * as stardust from 'stardust'
import * as consoleUtil from 'test/utils/consoleUtil'
import sandbox from 'test/utils/Sandbox-util'
import * as syntheticEvent from 'test/utils/syntheticEvent'

const componentCtx = require.context('../../src/', true, /(addons|collections|elements|modules|views).*\.js$/)

Expand Down Expand Up @@ -46,7 +48,7 @@ const componentInfo = componentCtx.keys().map(key => {
* @param {Object} [requiredProps={}] Props required to render Component without errors or warnings.
*/
export const isConformant = (Component, requiredProps = {}) => {
const info = componentInfo.find(i => i.constructorName === Component.prototype.constructor.name)
const info = _.find(componentInfo, i => i.constructorName === Component.prototype.constructor.name)
const { _meta, constructorName, componentClassName, filenameWithoutExt, sdClass } = info

const subComponentName = _meta.parent && _meta.name.replace(_meta.parent, '')
Expand Down Expand Up @@ -97,9 +99,9 @@ export const isConformant = (Component, requiredProps = {}) => {
}

// ----------------------------------------
// Handles props
// Props
// ----------------------------------------
it('spreads props', () => {
it('spreads user props', () => {
// JSX does not render custom html attributes so we prefix them with data-*.
// https://facebook.github.io/react/docs/jsx-gotchas.html#custom-html-attributes
const props = {
Expand All @@ -110,6 +112,67 @@ export const isConformant = (Component, requiredProps = {}) => {
.should.have.descendants(props)
})

// ----------------------------------------
// Events
// ----------------------------------------
it('handles events transparently', () => {
// Stardust events should be handled transparently, working just as they would in vanilla React.
// Example, both of these handler()s should be called with the same event:
//
// <Button onClick={handler} />
// <button onClick={handler} />
//
// This test catches the case where a developer forgot to call the event prop
// after handling it internally. It also catch cases where the synthetic event was not passed back.
_.each(syntheticEvent.types, ({ eventShape, listeners }) => {
_.each(listeners, listenerName => {
// onKeyDown => keyDown
const eventName = _.camelCase(listenerName.replace('on', ''))

// onKeyDown => handleKeyDown
const handlerName = _.camelCase(listenerName.replace('on', 'handle'))

const handlerSpy = sandbox.spy()
const props = {
...requiredProps,
[listenerName]: handlerSpy,
simulateEventOnThisComponent: true,
}

const wrapper = shallow(<Component {...props} />)

wrapper
.find('[simulateEventOnThisComponent]')
.simulate(eventName, eventShape)

// give event listeners opportunity to cleanup
if (wrapper && wrapper.unmount && wrapper.instance().componentWillUnmount) {
wrapper.instance().componentWillUnmount()
}

// <Dropdown onBlur={handleBlur} />
// ^ was not called on "blur"
const leftPad = ' '.repeat(constructorName.length + listenerName.length + 3)

handlerSpy.called.should.equal(true,
`<${constructorName} ${listenerName}={${handlerName}} />\n` +
`${leftPad} ^ was not called on "${eventName}"\n`
)

// TODO: https://github.com/TechnologyAdvice/stardust/issues/218
// some components currently return useful data in the first position
// update those to return the event first, then any data, finally uncomment this test
//
// handlerSpy.calledWithMatch(eventShape).should.equal(true,
// `<${constructorName} ${listenerName}={${handlerName}} />\n` +
// `${leftPad} ^ was not called with an "${listenerName}" event\n` +
// 'It was called with args:\n' +
// JSON.stringify(handlerSpy.args, null, 2)
// )
})
})
})

// ----------------------------------------
// Defines _meta
// ----------------------------------------
Expand Down Expand Up @@ -154,14 +217,14 @@ export const isConformant = (Component, requiredProps = {}) => {
// ----------------------------------------
// Handles className
// ----------------------------------------
describe('className', () => {
it(`has some child with the "${sdClass}" class`, () => {
describe('className (common)', () => {
it(`has the Stardust className "${sdClass}"`, () => {
render(<Component {...requiredProps} />)
.should.have.className(sdClass)
})

if (META.isSemanticUI(Component)) {
it(`has the component className "${componentClassName}"`, () => {
it(`has the Semantic UI className "${componentClassName}"`, () => {
render(<Component {...requiredProps} />)
.should.have.className(componentClassName)
})
Expand All @@ -170,7 +233,7 @@ export const isConformant = (Component, requiredProps = {}) => {
it("applies user's className to root component", () => {
const classes = faker.hacker.phrase()
shallow(<Component {...requiredProps} className={classes} />)
.hasClass(classes)
.should.have.className(classes)
})
})
}
Expand All @@ -182,7 +245,7 @@ export const isConformant = (Component, requiredProps = {}) => {
*/
export const hasUIClassName = (Component, requiredProps = {}) => {
it('has the "ui" className', () => {
shallow(createElement(Component, requiredProps))
shallow(<Component {...requiredProps} />)
.should.have.className('ui')
})
}
Expand All @@ -202,6 +265,18 @@ export const hasSubComponents = (Component, subComponents) => {
})
}

/**
* Assert a component can be receive focus via the tab key.
* @param {React.Component|Function} Component The Component.
* @param {Object} [requiredProps={}] Props required to render the component.
*/
export const isTabbable = (Component, requiredProps = {}) => {
it('is tabbable', () => {
shallow(<Component {...requiredProps} />)
.should.have.attr('tabindex', '0')
})
}

/**
* Assert a component renders children somewhere in the tree.
* @param {React.Component|Function} Component A component that should render children.
Expand Down Expand Up @@ -237,7 +312,12 @@ const _noDefaultClassNameFromProp = (Component, propKey, requiredProps = {}) =>
it('is not included in className when not defined', () => {
const wrapper = shallow(<Component {...requiredProps} />)
wrapper.should.not.have.className(propKey)
_.each(Component._meta.props[propKey], propVal => {

// not all component props define prop options in _meta.props
// if they do, ensure that none of the prop option values are in className
// SUI classes ought to be built up using a declarative component API
const propOptions = _.get(Component, `_meta.props[${propKey}]`)
_.each(propOptions, propVal => {
wrapper.should.not.have.className(propVal)
})
})
Expand Down Expand Up @@ -277,7 +357,7 @@ const _classNamePropValueBeforePropName = (Component, propKey, requiredProps) =>
* @param {Object} [requiredProps={}] Props required to render the component.
*/
export const propKeyOnlyToClassName = (Component, propKey, requiredProps = {}) => {
describe(propKey, () => {
describe(`${propKey} (common)`, () => {
_noDefaultClassNameFromProp(Component, propKey, requiredProps)

it(`adds prop name to className`, () => {
Expand All @@ -303,7 +383,7 @@ export const propKeyOnlyToClassName = (Component, propKey, requiredProps = {}) =
* @param {Object} [requiredProps={}] Props required to render the component.
*/
export const propValueOnlyToClassName = (Component, propKey, requiredProps = {}) => {
describe(propKey, () => {
describe(`${propKey} (common)`, () => {
_definesPropOptions(Component, propKey)
_noDefaultClassNameFromProp(Component, propKey, requiredProps)
_noClassNameFromBoolProps(Component, propKey, requiredProps)
Expand Down Expand Up @@ -332,7 +412,7 @@ export const propValueOnlyToClassName = (Component, propKey, requiredProps = {})
* @param {Object} [requiredProps={}] Props required to render the component.
*/
export const propKeyAndValueToClassName = (Component, propKey, requiredProps = {}) => {
describe(propKey, () => {
describe(`${propKey} (common)`, () => {
_definesPropOptions(Component, propKey)
_noDefaultClassNameFromProp(Component, propKey)
_noClassNameFromBoolProps(Component, propKey, requiredProps)
Expand All @@ -347,7 +427,7 @@ export const propKeyAndValueToClassName = (Component, propKey, requiredProps = {
* @param {Object} [requiredProps={}] Props required to render the component.
*/
export const propKeyOrValueToClassName = (Component, propKey, requiredProps = {}) => {
describe(propKey, () => {
describe(`${propKey} (common)`, () => {
_definesPropOptions(Component, propKey)
_noDefaultClassNameFromProp(Component, propKey, requiredProps)
_classNamePropValueBeforePropName(Component, propKey, requiredProps)
Expand Down

0 comments on commit 73e8a53

Please sign in to comment.