Skip to content

Commit

Permalink
test(common): support isConformant for portals (#2146)
Browse files Browse the repository at this point in the history
* test(hasValidTypings): handle multiple required props

* test(hasValidTypings): improve error messages

* test(isConformant): support portal powered components

* test(assertNodeContains): fix isPresent jsdoc default

* test(Modal|Popup): add isConformant tests
  • Loading branch information
levithomason authored Sep 30, 2017
1 parent 943bf13 commit 50ce0ef
Show file tree
Hide file tree
Showing 7 changed files with 116 additions and 70 deletions.
3 changes: 3 additions & 0 deletions src/modules/Popup/Popup.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import { default as PopupHeader, PopupHeaderProps } from './PopupHeader';
export interface PopupProps extends PortalProps {
[key: string]: any;

/** An element type to render as (string or function). */
as?: any;

/** Display the popup without the pointing arrow */
basic?: boolean;

Expand Down
3 changes: 3 additions & 0 deletions src/modules/Popup/Popup.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ export const POSITIONS = [
*/
export default class Popup extends Component {
static propTypes = {
/** An element type to render as (string or function). */
as: customPropTypes.as,

/** Display the popup without the pointing arrow. */
basic: PropTypes.bool,

Expand Down
30 changes: 27 additions & 3 deletions test/specs/commonTests/hasValidTypings.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,38 @@ export default (Component, extractedInfo, options = {}) => {
const componentProps = _.keys(componentPropTypes)
const interfaceProps = _.without(_.map(props, 'name'), ...ignoredTypingsProps)

componentProps.should.to.deep.equal(interfaceProps)
componentProps.forEach((propName) => {
interfaceProps.should.include(
propName,
`propTypes define "${propName}" but it is missing in typings`,
)
})

interfaceProps.forEach((propName) => {
componentProps.should.include(
propName,
`Typings define prop "${propName}" but it is missing in propTypes`,
)
})
})

it('only necessary are required', () => {
const componentRequired = _.keys(requiredProps)
const interfaceRequired = _.filter(props, ['required', true])
const interfaceRequired = _.map(_.filter(props, ['required', true]), 'name')

componentRequired.should.to.deep.equal(_.map(interfaceRequired, 'name'))
componentRequired.forEach((propName) => {
interfaceRequired.should.include(
propName,
`Tests require prop "${propName}" but it is optional in typings`,
)
})

interfaceRequired.forEach((propName) => {
componentRequired.should.include(
propName,
`Typings require "${propName}" but it is optional in tests`,
)
})
})
})

Expand Down
146 changes: 80 additions & 66 deletions test/specs/commonTests/isConformant.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import ReactDOMServer from 'react-dom/server'
import * as semanticUIReact from 'semantic-ui-react'

import { META } from 'src/lib'
import { consoleUtil, sandbox, syntheticEvent } from 'test/utils'
import { assertBodyContains, consoleUtil, sandbox, syntheticEvent } from 'test/utils'
import helpers from './commonHelpers'
import componentInfo from './componentInfo'
import hasValidTypings from './hasValidTypings'
Expand All @@ -14,12 +14,12 @@ import hasValidTypings from './hasValidTypings'
* Assert Component conforms to guidelines that are applicable to all components.
* @param {React.Component|Function} Component A component that should conform.
* @param {Object} [options={}]
* @param {array} [options.ignoredTypingsProps=[]] Props that will be ignored in typings tests.
* @param {Object} [options.eventTargets={}] Map of events and the child component to target.
* @param {array} [options.rendersPortal=false] Does this component render a Portal powered component?
* @param {Object} [options.requiredProps={}] Props required to render Component without errors or warnings.
*/
export default (Component, options = {}) => {
const { eventTargets = {}, requiredProps = {} } = options
const { eventTargets = {}, requiredProps = {}, rendersPortal = false } = options
const { throwError } = helpers('isConformant', Component)

// tests depend on Component constructor names, enforce them
Expand Down Expand Up @@ -109,79 +109,74 @@ export default (Component, options = {}) => {
// 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 = {
[`data-${_.kebabCase(faker.hacker.noun())}`]: faker.hacker.verb(),
}
const propName = 'data-is-conformant-spread-props'
const props = { [propName]: true }

// descendants() accepts an enzyme <selector>
// props should be spread on some descendant
// we find the descendant with spread props via a matching props object selector
// we do not test Component for props, of course they exist as we are spreading them
shallow(<Component {...requiredProps} {...props} />)
.should.have.descendants(props)
})

describe('"as" prop (common)', () => {
it('renders the component as HTML tags or passes "as" to the next component', () => {
// silence element nesting warnings
consoleUtil.disableOnce()
if (!rendersPortal) {
describe('"as" prop (common)', () => {
it('renders the component as HTML tags or passes "as" to the next component', () => {
// silence element nesting warnings
consoleUtil.disableOnce()

const tags = ['a', 'em', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'i', 'p', 'span', 'strong']
try {
tags.forEach((tag) => {
shallow(<Component {...requiredProps} as={tag} />)
.should.have.tagName(tag)
})
} catch (err) {
tags.forEach((tag) => {
const wrapper = shallow(<Component {...requiredProps} as={tag} />)
wrapper.type().should.not.equal(Component)
wrapper.should.have.prop('as', tag)
})
}
})

const tags = ['a', 'em', 'div', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'i', 'p', 'span', 'strong']
try {
tags.forEach((tag) => {
shallow(<Component {...requiredProps} as={tag} />)
.should.have.tagName(tag)
})
} catch (err) {
tags.forEach((tag) => {
const wrapper = shallow(<Component {...requiredProps} as={tag} />)
wrapper.type().should.not.equal(Component)
wrapper.should.have.prop('as', tag)
})
}
})
it('renders as a functional component or passes "as" to the next component', () => {
const MyComponent = () => null

it('renders as a functional component or passes "as" to the next component', () => {
const MyComponent = () => null

try {
shallow(<Component {...requiredProps} as={MyComponent} />)
.type()
.should.equal(MyComponent)
} catch (err) {
const wrapper = shallow(<Component {...requiredProps} as={MyComponent} />)
wrapper.type().should.not.equal(Component)
wrapper.should.have.prop('as', MyComponent)
}
})
try {
shallow(<Component {...requiredProps} as={MyComponent} />)
.type()
.should.equal(MyComponent)
} catch (err) {
const wrapper = shallow(<Component {...requiredProps} as={MyComponent} />)
wrapper.type().should.not.equal(Component)
wrapper.should.have.prop('as', MyComponent)
}
})

it('renders as a ReactClass or passes "as" to the next component', () => {
class MyComponent extends React.Component { // eslint-disable-line react/prefer-stateless-function
render() {
return <div data-my-react-class />
it('renders as a ReactClass or passes "as" to the next component', () => {
class MyComponent extends React.Component { // eslint-disable-line react/prefer-stateless-function
render() {
return <div data-my-react-class />
}
}
}

try {
shallow(<Component {...requiredProps} as={MyComponent} />)
.type()
.should.equal(MyComponent)
} catch (err) {
const wrapper = shallow(<Component {...requiredProps} as={MyComponent} />)
wrapper.type().should.not.equal(Component)
wrapper.should.have.prop('as', MyComponent)
}
})
try {
shallow(<Component {...requiredProps} as={MyComponent} />)
.type()
.should.equal(MyComponent)
} catch (err) {
const wrapper = shallow(<Component {...requiredProps} as={MyComponent} />)
wrapper.type().should.not.equal(Component)
wrapper.should.have.prop('as', MyComponent)
}
})

it('passes extra props to the component it is renders as', () => {
const MyComponent = () => null
it('passes extra props to the component it is renders as', () => {
const MyComponent = () => null

shallow(<Component {...requiredProps} as={MyComponent} data-extra-prop='foo' />)
.should.have.descendants('[data-extra-prop="foo"]')
shallow(<Component {...requiredProps} as={MyComponent} data-extra-prop='foo' />)
.should.have.descendants('[data-extra-prop="foo"]')
})
})
})
}

describe('handles props', () => {
it('defines handled props in Component.handledProps', () => {
Expand Down Expand Up @@ -319,9 +314,28 @@ export default (Component, options = {}) => {
})

it("applies user's className to root component", () => {
const classes = faker.hacker.phrase()
shallow(<Component {...requiredProps} className={classes} />)
.should.have.className(classes)
const className = 'is-conformant-class-string'

// Portal powered components can render to two elements, a trigger and the actual component
// The actual component is shown when the portal is open
// If a trigger is rendered, open the portal and make assertions on the portal element
if (rendersPortal) {
const mountNode = document.createElement('div')
document.body.appendChild(mountNode)

const wrapper = mount(<Component {...requiredProps} className={className} />, { attachTo: mountNode })
wrapper.setProps({ open: true })

// portals/popups/etc may render the component to somewhere besides descendants
// we look for the component anywhere in the DOM
assertBodyContains(`.${className}`)

wrapper.detach()
document.body.removeChild(mountNode)
} else {
shallow(<Component {...requiredProps} className={className} />)
.should.have.className(className)
}
})

it("user's className does not override the default classes", () => {
Expand Down
1 change: 1 addition & 0 deletions test/specs/modules/Modal/Modal-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ describe('Modal', () => {
if (wrapper && wrapper.unmount) wrapper.unmount()
})

common.isConformant(Modal, { rendersPortal: true })
common.hasSubComponents(Modal, [ModalHeader, ModalContent, ModalActions, ModalDescription])
common.hasValidTypings(Modal)

Expand Down
1 change: 1 addition & 0 deletions test/specs/modules/Popup/Popup-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ describe('Popup', () => {
if (wrapper && wrapper.unmount) wrapper.unmount()
})

common.isConformant(Popup, { rendersPortal: true })
common.hasSubComponents(Popup, [PopupHeader, PopupContent])
common.hasValidTypings(Popup)

Expand Down
2 changes: 1 addition & 1 deletion test/utils/assertNodeContains.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@ export const assertNodeContains = (parentNode, childSelector, isPresent = true)
* Assert whether node is or is not a child of the document.body.
*
* @param {string} selector A DOM selector for the parent node
* @param {boolean} isPresent Indicating whether to assert is present or is not present
* @param {boolean} [isPresent=true] Indicating whether to assert is present or is not present
*/
export const assertBodyContains = (selector, isPresent) => assertNodeContains(document.body, selector, isPresent)

0 comments on commit 50ce0ef

Please sign in to comment.