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

Adding support for Jest asymmetric matchers and more #148

Merged
merged 2 commits into from
Jun 24, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 */
Copy link
Member

Choose a reason for hiding this comment

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

nit

const normalizeOptions = ({ modifier, ...options }) => {
  if (modifier) {
    return {
      ...options,
      modifier: Array.isArray(modifier) ? modifier.join('') : modifier,
    }
  }

  return options
}

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'm happy to implement following your suggestion, however I usually try to avoid "if statements" when they're not strictly required and in this case I did definitely prefer a simple ternary which also allowed to take advantage of ES6 arrow function implicit return as opposed to open a block.

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:
\\"orange\\""
\\"background: orange\\""
`;
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`] = `
"expect(received).toHaveStyleRule(expected)
"\\"Value mismatch for property 'background-color'\\"

Expected View to have a style rule:
\\"background: red\\"
Expected
\\"background-color: red\\"
Received:
\\"background isn't in the style rules\\""
\\"background-color: orange\\""
`;
Loading