Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add toHaveStyleRule to jest-emotion #662

Merged
merged 9 commits into from
May 25, 2018
12 changes: 10 additions & 2 deletions packages/jest-emotion/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {
} from './replace-class-names'
import type { Emotion } from 'create-emotion'

export { createMatchers } from './matchers'

type Options = {
classNameReplacer: ClassNameReplacer,
DOMElements: boolean
Expand Down Expand Up @@ -37,12 +39,14 @@ function getClassNamesFromDOMElement(selectors, node) {
return getClassNames(selectors, node.getAttribute('class'))
}

function getClassNamesFromNodes(nodes) {
export function getClassNamesFromNodes(nodes) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you move this into a utils file and import it from that file because exporting from here means it’s exported from the main package so people could start using it and if we wanted to change it we would have to do a major version

return nodes.reduce(
(selectors, node) =>
isReactElement(node)
? getClassNamesFromProps(selectors, node.props)
: getClassNamesFromDOMElement(selectors, node),
: isEnzymeElement(node)
? getClassNamesFromProps(selectors, node.props())
: getClassNamesFromDOMElement(selectors, node),
[]
)
}
Expand All @@ -60,6 +64,10 @@ function isReactElement(val) {
return val.$$typeof === Symbol.for('react.test.json')
}

function isEnzymeElement(val) {
return typeof val.findWhere === 'function'
}

const domElementPattern = /^((HTML|SVG)\w*)?Element$/

function isDOMElement(val) {
Expand Down
86 changes: 86 additions & 0 deletions packages/jest-emotion/src/matchers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import chalk from 'chalk'
import * as css from 'css'
import { getClassNamesFromNodes } from './index'

/*
* Taken from
* https://github.com/facebook/jest/blob/be4bec387d90ac8d6a7596be88bf8e4994bc3ed9/packages/expect/src/jasmine_utils.js#L234
*/
function isA(typeName, value) {
return Object.prototype.toString.apply(value) === `[object ${typeName}]`
}

/*
* Taken from
* https://github.com/facebook/jest/blob/be4bec387d90ac8d6a7596be88bf8e4994bc3ed9/packages/expect/src/jasmine_utils.js#L36
*/
function isAsymmetric(obj) {
return obj && isA('Function', obj.asymmetricMatch)
}

function valueMatches(declaration, value) {
if (value instanceof RegExp) {
return value.test(declaration.value)
}

if (isAsymmetric(value)) {
return value.asymmetricMatch(declaration.value)
}

return value === declaration.value
}

function getStylesFromClassNames(classNames: Array<string>, emotion) {
let styles = ''
// This could be done in a more efficient way
// but it would be a breaking change to do so
// because it would change the ordering of styles
Object.keys(emotion.caches.registered).forEach(className => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

personally I would replace forEach with reduce here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually copied that block of code from index.js because it did what I needed. Agree it should be refactored 👍

let indexOfClassName = classNames.indexOf(className)
if (indexOfClassName !== -1) {
let nameWithoutKey = classNames[indexOfClassName].substring(
emotion.caches.key.length + 1
)
// $FlowFixMe
styles += emotion.caches.inserted[nameWithoutKey]
}
})
return styles
}

export function createMatchers(emotion) {
function toHaveStyleRule(received, property, value) {
const selectors = getClassNamesFromNodes([received])
const cssString = getStylesFromClassNames(selectors, emotion)
const styles = css.parse(cssString)

const declaration = styles.stylesheet.rules
.reduce((decs, rule) => Object.assign([], decs, rule.declarations), [])
.filter(dec => dec.type === 'declaration' && dec.property === property)
.pop()

if (!declaration) {
return {
pass: false,
message: () => `Property not found: ${property}`
}
}

const pass = valueMatches(declaration, value)

const message = () =>
`Expected ${property}${pass ? ' not ' : ' '}to match:\n` +
` ${chalk.green(value)}\n` +
'Received:\n' +
` ${chalk.red(declaration.value)}`

return {
pass,
message
}
}

return {
toHaveStyleRule
}
}
71 changes: 71 additions & 0 deletions packages/jest-emotion/test/matchers.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import React from 'react'
import renderer from 'react-test-renderer'
import * as enzyme from 'enzyme'
import * as emotion from 'emotion'
import { createSerializer, createMatchers } from '../src'

expect.addSnapshotSerializer(createSerializer(emotion))
expect.extend(createMatchers(emotion))

describe('toHaveStyleRule', () => {
const divStyle = emotion.css`
color: red;
`

const svgStyle = emotion.css`
width: 100%;
`

it('matches styles on the top-most node passed in', () => {
const tree = renderer
.create(
<div className={divStyle}>
<svg className={svgStyle} />
</div>
)
.toJSON()

expect(tree).toHaveStyleRule('color', 'red')
expect(tree).not.toHaveStyleRule('width', '100%')

const svgNode = tree.children[0]

expect(svgNode).toHaveStyleRule('width', '100%')
expect(svgNode).not.toHaveStyleRule('color', 'red')
})

it('supports asymmetric matchers', () => {
const tree = renderer
.create(
<div className={divStyle}>
<svg className={svgStyle} />
</div>
)
.toJSON()

expect(tree).toHaveStyleRule('color', expect.anything())
expect(tree).not.toHaveStyleRule('padding', expect.anything())

const svgNode = tree.children[0]

expect(svgNode).toHaveStyleRule('width', expect.stringMatching(/.*%$/))
})

it('supports enzyme render methods', () => {
const Component = () => (
<div className={divStyle}>
<svg className={svgStyle} />
</div>
)
const enzymeMethods = ['shallow']

enzymeMethods.forEach(method => {
const wrapper = enzyme[method](<Component />)
expect(wrapper).toHaveStyleRule('color', 'red')
expect(wrapper).not.toHaveStyleRule('width', '100%')
const svgNode = wrapper.find('svg')
expect(svgNode).toHaveStyleRule('width', '100%')
expect(svgNode).not.toHaveStyleRule('color', 'red')
})
})
})