Last active
October 19, 2018 02:33
-
-
Save forgo/bb691c592bde48f918360435afe19fb7 to your computer and use it in GitHub Desktop.
convenience custom Jest matcher API for React components properties changing on events
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // 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