Skip to content

Instantly share code, notes, and snippets.

@forgo
Last active October 19, 2018 02:33
Show Gist options
  • Select an option

  • Save forgo/bb691c592bde48f918360435afe19fb7 to your computer and use it in GitHub Desktop.

Select an option

Save forgo/bb691c592bde48f918360435afe19fb7 to your computer and use it in GitHub Desktop.
convenience custom Jest matcher API for React components properties changing on events
// EXAMPLE USE CASE:
// ----------------
// Button.test.js
// import React from 'react'
// import Button from '../../src/common/input/Button'
// import renderer from 'react-test-renderer'
// import '../JestHelpers'
//
// describe('<Button/>', () => {
// test('background style changes on mouse over/out events', () => {
// // initially render button
// const component = renderer.create(<Button />)
//
// // expect background style to change onMouseOver
// expect(component).toChangeWithEvent(
// [
// {
// path: 'props.style.background',
// before: '#277CB2',
// after: 'linear-gradient(#277CB2, #28323E)',
// },
// ],
// 'props.onMouseOver'
// )
//
// // expect background style to change onMouseOut
// expect(component).toChangeWithEvent(
// [ 'props.style.background' ],
// 'props.onMouseOut'
// )
// })
// })
import _ from 'lodash'
const NEWLINE = '\r\n'
/**
* Deep diff between two object, using lodash
* @param {Object} object Object compared
* @param {Object} base Object to compare with
* @return {Object} Return a new object who represent the diff
*/
function difference(object, base){
function changes(object, base){
return _.transform(object, function(result, value, key){
if (!_.isEqual(value, base[key])) {
result[key] =
_.isObject(value) && _.isObject(base[key])
? changes(value, base[key])
: [ base[key], value ]
}
})
}
return changes(object, base)
}
// create our own convenience custom Jest matcher API for React components properties changing on events
expect.extend({
toChangeWithEvent(component, expectedChanges, event, eventArgs = {}) {
// capture component structure before event
let componentBefore = component.toJSON()
// trigger the event
const action = _.get(componentBefore, event)
if (action === undefined) {
const invalidEventFailure = `expect(receiver).toChangeWithEvent(expectedChanges, event, eventArgs = {})
${NEWLINE}Custom Jest matcher has an invalid \`event\`, or it was not found in the receiver.
The event should be a string defining the key path into the receiver which triggers a change:
${NEWLINE}String -> 'path.to.event' (e.g. - 'props.onMouseOver')
- If the event accepts any arguments, they can be passed via \`eventArgs\`
${NEWLINE}Invalid event: ${event} does not exist as a key path in the receiver.
`
return {
message: () => {
return invalidEventFailure
},
pass: false,
}
}
else {
action(...eventArgs)
}
// componentBefore.props[event](...eventArgs)
// re-render component on event
let componentAfter = component.toJSON()
const diff = difference(componentAfter, componentBefore)
// console.log("diff:", JSON.stringify(diff, null , 4))
const accountForChanges = (
type,
event,
base,
diff,
path,
changes,
noChanges,
unmatchedChanges,
before = undefined,
after = undefined
) => {
const actual = _.get(diff, path)
const actualBefore = actual !== undefined ? actual[0] : undefined
const actualAfter = actual !== undefined ? actual[1] : undefined
if (actualBefore === actualAfter) {
noChanges.push(path)
}
else {
changes.push(path)
if (before !== undefined && before !== actualBefore) {
unmatchedChanges.push(
`Expected \`${path}\` to have value \`${before}\` before \`${event}\` event, but was \`${actualBefore}\` instead.`
)
}
if (after !== undefined && after !== actualAfter) {
unmatchedChanges.push(
`Expected \`${path}\` to change to \`${after}\` after \`${event}\` event, but changed to \`${actualAfter}\` instead.`
)
}
}
}
let changes = []
let noChanges = []
let unmatchedChanges = []
let invalidChanges = []
expectedChanges.forEach(change => {
if (typeof change === 'object' && !Array.isArray(change)) {
accountForChanges(
'object',
event,
componentBefore,
diff,
change.path,
changes,
noChanges,
unmatchedChanges,
change.before,
change.after
)
}
else if (typeof change === 'string') {
accountForChanges(
'string',
event,
componentBefore,
diff,
change,
changes,
noChanges,
unmatchedChanges
)
}
else {
invalidChanges.push(change)
}
})
if (invalidChanges.length > 0) {
const invalidChangesString = invalidChanges.map(e => {
let type = typeof e
type = Array.isArray(e) ? 'array' : type
return '\t -> ' + JSON.stringify(e) + ` should NOT be a ${type}`
})
const invalidChangeFailure = `expect(receiver).toChangeWithEvent(expectedChanges, event, eventArgs = {})
${NEWLINE}Custom Jest matcher has an invalid entry in \`expectedChanges\` array.
There are two valid formats for elements of the \`expectedChanges\` array:
${NEWLINE}1) Object ->
{
path: 'path.to.property',
before: 'abc',
after: 'xyz',
}
- \`path\` represents the key path in this matcher's receiver
- Objects can assert values before and after the event.
- \`before\` and \`after\` keys can be left out if the values are inconsequential
${NEWLINE}2) String -> 'path.to.property'
- Strings accept any change in value for the key path in this matcher's receiver
${NEWLINE}Invalid expectedChanges:
${invalidChangesString.join(NEWLINE)}
`
return {
message: () => {
return invalidChangeFailure
},
pass: false,
}
}
// was the matcher negated with the `.not` modifier?
if (this.isNot) {
if (noChanges.length <= expectedChanges.length && changes.length > 0) {
const noChangeFailure = `The following properties should NOT have changed (but did) with \`${event}\` event:${NEWLINE}`
const changeList = changes.map(e => `\t-> ${e}`).join(NEWLINE)
return {
message: () => {
return noChangeFailure + changeList
},
pass: true,
}
}
else {
if (unmatchedChanges.length > 0) {
const unmatchedChangeFailure = `The following conditions were not satisfied:${NEWLINE}`
const unmatchedChangeList = unmatchedChanges
.map(e => `\t-> ${e}`)
.join(NEWLINE)
return {
message: () => {
return unmatchedChangeFailure + unmatchedChangeList
},
pass: true,
}
}
else {
return {
message: () => {
return 'Success: not changed with event'
},
pass: false,
}
}
}
}
else {
if (changes.length <= expectedChanges.length && noChanges.length > 0) {
const changeFailure = `The following properties should have changed with \`${event}\` event:${NEWLINE}`
const noChangeList = noChanges.map(e => `\t-> ${e}`).join(NEWLINE)
return {
message: () => {
return changeFailure + noChangeList
},
pass: false,
}
}
else {
if (unmatchedChanges.length > 0) {
const unmatchedChangeFailure = `The following conditions were not satisfied:${NEWLINE}`
const unmatchedChangeList = unmatchedChanges
.map(e => `\t-> ${e}`)
.join(NEWLINE)
return {
message: () => {
return unmatchedChangeFailure + unmatchedChangeList
},
pass: false,
}
}
return {
message: () => {
return 'Success: changed with event'
},
pass: true,
}
}
}
},
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment