Skip to content

Commit

Permalink
Adding support for Jest asymmetric matchers and more (#148)
Browse files Browse the repository at this point in the history
* Adding support for matching against Jest asymmetric matchers and 'undefined'

also implementing consistent messages across react and react native as well as fixing skipped and broken react native tests

* updating ts definitions
  • Loading branch information
santino authored and MicheleBertoli committed Jun 24, 2018
1 parent b4bac45 commit 0d4c0ea
Show file tree
Hide file tree
Showing 9 changed files with 269 additions and 96 deletions.
24 changes: 21 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -306,16 +306,34 @@ test('it works', () => {
# toHaveStyleRule

The `toHaveStyleRule` matcher is useful to test if a given rule is applied to a component.
The first argument is the expected property, the second is the expected value (string or RegExp).
The first argument is the expected property, the second is the expected value which can be a String, RegExp, Jest asymmetric matcher or `undefined`.

```js
test('it works', () => {
const Button = styled.button`
color: red;
border: 0.05em solid ${props => props.transparent ? 'transparent' : 'black'};
cursor: ${props => !props.disabled && 'pointer'};
opacity: ${props => props.disabled && '.65'};
`

test('it applies default styles', () => {
const tree = renderer.create(<Button />).toJSON()
expect(tree).toHaveStyleRule('color', 'red')
expect(tree).toHaveStyleRule('border', '0.05em solid black')
expect(tree).toHaveStyleRule('cursor', 'pointer')
expect(tree).toHaveStyleRule('opacity', undefined) // equivalent of the following
expect(tree).not.toHaveStyleRule('opacity', expect.any(String))
})

test('it applies styles according to passed props', () => {
const tree = renderer.create(<Button disabled transparent />).toJSON()
expect(tree).toHaveStyleRule('border', expect.stringContaining('transparent'))
expect(tree).toHaveStyleRule('cursor', undefined)
expect(tree).toHaveStyleRule('opacity', '.65')
})
```

The matcher supports a third `options` parameter which makes it possible to search for rules nested within an [At-rule](https://developer.mozilla.org/en/docs/Web/CSS/At-rule) ([media](https://developer.mozilla.org/en-US/docs/Web/CSS/@media)) or to add modifiers to the class selector. This feature is supported in React only, and more options are coming soon.
The matcher supports an optional third `options` parameter which makes it possible to search for rules nested within an [At-rule](https://developer.mozilla.org/en/docs/Web/CSS/At-rule) ([media](https://developer.mozilla.org/en-US/docs/Web/CSS/@media)) or to add modifiers to the class selector. This feature is supported in React only, and more options are coming soon.

```js
const Button = styled.button`
Expand Down
20 changes: 14 additions & 6 deletions src/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
interface AsymmetricMatcher {
$$typeof: Symbol;
sample?: string | RegExp | object | Array<any>, Function;
}

type Value = string | RegExp | AsymmetricMatcher | undefined

interface Options {
media?: string;
modifier?: string;
supports?: string;
}

declare namespace jest {
interface Options {
media?: string;
modifier?: string;
supports?: string;
}

interface Matchers<R> {
toHaveStyleRule(property: string, value: string | RegExp, options?: Options): R;
toHaveStyleRule(property: string, value: Value, options?: Options): R;
}
}
40 changes: 9 additions & 31 deletions src/native/toHaveStyleRule.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
function toHaveStyleRule(component, name, expected) {
const { matcherTest, buildReturnMessage } = require('../utils')

function toHaveStyleRule(component, property, expected) {
const styles = component.props.style.filter(x => x)

/**
* Convert style name to camel case (so we can compare)
*/
const camelCasedName = name.replace(/-(\w)/, (_, match) =>
const camelCasedProperty = property.replace(/-(\w)/, (_, match) =>
match.toUpperCase()
)

Expand All @@ -13,37 +15,13 @@ function toHaveStyleRule(component, name, expected) {
* stylename against this object
*/
const mergedStyles = styles.reduce((acc, item) => ({ ...acc, ...item }), {})
const received = mergedStyles[camelCasedName]
const pass = received === expected
const received = mergedStyles[camelCasedProperty]
const pass = matcherTest(received, expected)

if (!received) {
const error = `${name} isn't in the style rules`
return {
message: () =>
`${this.utils.matcherHint('.toHaveStyleRule')}\n\n` +
`Expected ${component.type} to have a style rule:\n` +
` ${this.utils.printExpected(`${name}: ${expected}`)}\n` +
'Received:\n' +
` ${this.utils.printReceived(error)}`,
pass: false,
}
return {
pass,
message: buildReturnMessage(this.utils, pass, property, received, expected),
}

const diff =
'' +
` ${this.utils.printExpected(`${name}: ${expected}`)}\n` +
'Received:\n' +
` ${this.utils.printReceived(`${name}: ${received}`)}`

const message = pass
? () =>
`${this.utils.matcherHint('.not.toHaveStyleRule')}\n\n` +
`Expected ${component.type} not to contain:\n${diff}`
: () =>
`${this.utils.matcherHint('.toHaveStyleRule')}\n\n` +
`Expected ${component.type} to have a style rule:\n${diff}`

return { message, pass }
}

module.exports = toHaveStyleRule
55 changes: 25 additions & 30 deletions src/toHaveStyleRule.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const { getCSS } = require('./utils')
const { getCSS, matcherTest, buildReturnMessage } = require('./utils')

const shouldDive = node =>
typeof node.dive === 'function' && typeof node.type() !== 'string'
Expand Down Expand Up @@ -76,9 +76,14 @@ const getRules = (ast, classNames, options) => {
)
}

const die = (utils, property) => ({
const handleMissingRules = options => ({
pass: false,
message: () => `Property not found: ${utils.printReceived(property)}`,
message: () =>
`No style rules found on passed Component${
Object.keys(options).length
? ` using options:\n${JSON.stringify(options)}`
: ''
}`,
})

const getDeclaration = (rule, property) =>
Expand All @@ -92,41 +97,31 @@ const getDeclaration = (rule, property) =>
const getDeclarations = (rules, property) =>
rules.map(rule => getDeclaration(rule, property)).filter(Boolean)

const normalizeModifierOption = modifier =>
Array.isArray(modifier) ? modifier.join('') : modifier
/* eslint-disable prettier/prettier */
const normalizeOptions = ({ modifier, ...options }) =>
modifier
? Object.assign(options, {
modifier: Array.isArray(modifier) ? modifier.join('') : modifier,
})
: options
/* eslint-enable prettier/prettier */

function toHaveStyleRule(received, property, value, options = {}) {
const classNames = getClassNames(received)
function toHaveStyleRule(component, property, expected, options = {}) {
const ast = getCSS()
options.modifier = normalizeModifierOption(options.modifier)
const rules = getRules(ast, classNames, options)
const classNames = getClassNames(component)
const normalizedOptions = normalizeOptions(options)
const rules = getRules(ast, classNames, normalizedOptions)

if (!rules.length) {
return die(this.utils, property)
}
if (!rules.length) return handleMissingRules(normalizedOptions)

const declarations = getDeclarations(rules, property)

if (!declarations.length) {
return die(this.utils, property)
}

const declaration = declarations.pop()

const pass =
value instanceof RegExp
? value.test(declaration.value)
: value === declaration.value

const message = () =>
`Expected ${property}${pass ? ' not ' : ' '}to match:\n` +
` ${this.utils.printExpected(value)}\n` +
'Received:\n' +
` ${this.utils.printReceived(declaration.value)}`
const declaration = declarations.pop() || {}
const received = declaration.value
const pass = matcherTest(received, expected)

return {
pass,
message,
message: buildReturnMessage(this.utils, pass, property, received, expected),
}
}

Expand Down
28 changes: 28 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,36 @@ const getHashes = () =>
.concat(getComponentIDs())
.filter(Boolean)

const buildReturnMessage = (utils, pass, property, received, expected) => () =>
`${utils.printReceived(
!received && !pass
? `Property '${property}' not found in style rules`
: `Value mismatch for property '${property}'`
)}\n\n` +
'Expected\n' +
` ${utils.printExpected(`${property}: ${expected}`)}\n` +
'Received:\n' +
` ${utils.printReceived(`${property}: ${received}`)}`

const matcherTest = (received, expected) => {
try {
const matcher =
expected === undefined ||
expected.$$typeof === Symbol.for('jest.asymmetricMatcher')
? expected
: expect.stringMatching(expected)

expect(received).toEqual(matcher)
return true
} catch (error) {
return false
}
}

module.exports = {
resetStyleSheet,
getCSS,
getHashes,
buildReturnMessage,
matcherTest,
}
24 changes: 20 additions & 4 deletions test/__snapshots__/toHaveStyleRule.spec.js.snap
Original file line number Diff line number Diff line change
@@ -1,10 +1,26 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`message when property not found 1`] = `"Property not found: \\"a\\""`;
exports[`message when property not found 1`] = `
"\\"Property 'background-color' not found in style rules\\"
Expected
\\"background-color: black\\"
Received:
\\"background-color: undefined\\""
`;

exports[`message when rules not found 1`] = `"No style rules found on passed Component"`;

exports[`message when rules not found using options 1`] = `
"No style rules found on passed Component using options:
{\\"media\\":\\"(max-width:640px)\\",\\"modifier\\":\\":hover\\"}"
`;

exports[`message when value does not match 1`] = `
"Expected background to match:
\\"red\\"
"\\"Value mismatch for property 'background'\\"
Expected
\\"background: red\\"
Received:
[31m\\"orange\\"[39m"
[31m\\"background: orange\\"[39m"
`;
17 changes: 13 additions & 4 deletions test/native/__snapshots__/toHaveStyleRule.spec.js.snap
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`message when property not found 1`] = `
"\\"Property 'color' not found in style rules\\"
Expected
\\"color: black\\"
Received:
\\"color: undefined\\""
`;

exports[`message when value does not match 1`] = `
"[2mexpect([22m[31mreceived[39m[2m).toHaveStyleRule([22m[32mexpected[39m[2m)[22m
"[31m\\"Value mismatch for property 'background-color'\\"[39m
Expected View to have a style rule:
[32m\\"background: red\\"[39m
Expected
[32m\\"background-color: red\\"[39m
Received:
[31m\\"background isn't in the style rules\\"[39m"
[31m\\"background-color: orange\\"[39m"
`;
Loading

0 comments on commit 0d4c0ea

Please sign in to comment.