Skip to content

Commit

Permalink
Add ability to match/assert on all the args at once
Browse files Browse the repository at this point in the history
- New when.allArgs matcher. Instructs jest-when to hand over the full set of arguments to the matcher. This allows a dev to perform more complex matches, or in some cases write more succinct matching logic.
  • Loading branch information
timkindberg committed Apr 30, 2021
1 parent ea49d39 commit 7163307
Show file tree
Hide file tree
Showing 3 changed files with 225 additions and 35 deletions.
50 changes: 50 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,56 @@ expect(fn(4)).toEqual('way!')
expect(fn(5)).toEqual(undefined)
```

#### Supports matching or asserting against all of the arguments together using `when.allArgs`:

Pass a single special matcher, `when.allArgs`, if you'd like to handle all of the arguments
with one function matcher. The function will receive all of the arguments as an array and you
are responsible for returning true if they are a match, or false if not. The function also is
provided with the powerful `equals` utility from Jasmine.


This allows some convenient patterns:
- Less verbose for variable args where all need to be of a certain type or match (e.g. all numbers)
- Can be useful for partial matching, because you can assert just the first arg for example and ignore the rest

E.g. All args should be numbers:
```javascript
const areNumbers = (args, equals) => args.every(arg => equals(arg, expect.any(Number)))
when(fn).calledWith(when.allArgs(areNumbers)).mockReturnValue('yay!')

expect(fn(3, 6, 9)).toEqual('yay!')
expect(fn(3, 666)).toEqual('yay!')
expect(fn(-100, 2, 3.234234, 234, 90e3)).toEqual('yay!')
expect(fn(123, 'not a number')).toBeUndefined()
```

E.g. Single arg match:
```javascript
const argAtIndex = (index, matcher) => when.allArgs((args, equals) => equals(args[index], matcher))

when(fn).calledWith(argAtIndex(0, expect.any(Number))).mockReturnValue('yay!')

expect(fn(3, 6, 9)).toEqual('yay!')
expect(fn(3, 666)).toEqual('yay!')
expect(fn(-100, 2, 3.234234, 234, 90e3)).toEqual('yay!')
expect(fn(123, 'not a number')).toBeUndefined()
```

E.g. Partial match, only first defined matching args matter:
```javascript
const fn = jest.fn()
const partialArgs = (...argsToMatch) => when.allArgs((args, equals) => equals(args, expect.arrayContaining(argsToMatch)))

when(fn)
.calledWith(partialArgs(1, 2, 3))
.mockReturnValue('x')

expect(fn(1, 2, 3)).toEqual('x')
expect(fn(1, 2, 3, 4, 5, 6)).toEqual('x')
expect(fn(1, 2)).toBeUndefined()
expect(fn(1, 2, 4)).toBeUndefined()
```

#### Assert the args:

Use `expectCalledWith` instead to run an assertion that the `fn` was called with the provided
Expand Down
21 changes: 17 additions & 4 deletions src/when.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const checkArgumentMatchers = (expectCall, args) => (match, matcher, i) => {
}

if (isFunctionMatcher) {
return matcher(arg)
return matcher(arg, utils.equals)
}

return utils.equals(arg, matcher)
Expand Down Expand Up @@ -80,9 +80,16 @@ class WhenMock {
// Do not let a once mock match more than once
if (once && called) continue

const isMatch =
args.length === matchers.length &&
matchers.reduce(checkArgumentMatchers(expectCall, args), true)
let isMatch = false

if (matchers[0]._isAllArgsFunctionMatcher) {
if (matchers.length > 1) throw new Error('When using when.allArgs, it must be the one and only matcher provided to calledWith. You have incorrectly provided other matchers along with when.allArgs.')
isMatch = checkArgumentMatchers(expectCall, [args])(true, matchers[0], 0)
} else {
isMatch =
args.length === matchers.length &&
matchers.reduce(checkArgumentMatchers(expectCall, args), true)
}

if (isMatch) {
this.callMocks[i].called = true
Expand Down Expand Up @@ -152,6 +159,12 @@ const when = (fn) => {
}
}

when.allArgs = (fn) => {
fn._isFunctionMatcher = true
fn._isAllArgsFunctionMatcher = true
return fn
}

const resetAllWhenMocks = () => {
registry.forEach(resetWhenMocksOnFn)
registry = new Set()
Expand Down
189 changes: 158 additions & 31 deletions src/when.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -187,49 +187,176 @@ describe('When', () => {
expect(fn(1, 'foo', true, 'whatever', undefined, 'oops')).toEqual(undefined)
})

it('works with custom function args', () => {
const fn = jest.fn()
describe('function matcher', () => {
it('works with custom function args', () => {
const fn = jest.fn()

const allValuesTrue = (arg) => Object.values(arg).every(Boolean)
const numberDivisibleBy3 = (arg) => arg % 3 === 0
const allValuesTrue = (arg) => Object.values(arg).every(Boolean)
const numberDivisibleBy3 = (arg) => arg % 3 === 0

when(fn)
.calledWith(when(allValuesTrue), when(numberDivisibleBy3))
.mockReturnValue('x')
when(fn)
.calledWith(when(allValuesTrue), when(numberDivisibleBy3))
.mockReturnValue('x')

expect(fn({ foo: true, bar: true }, 9)).toEqual('x')
expect(fn({ foo: true, bar: false }, 9)).toEqual(undefined)
expect(fn({ foo: true, bar: false }, 13)).toEqual(undefined)
})
expect(fn({ foo: true, bar: true }, 9)).toEqual('x')
expect(fn({ foo: true, bar: false }, 9)).toEqual(undefined)
expect(fn({ foo: true, bar: false }, 13)).toEqual(undefined)
})

it('expects with custom function args', () => {
const fn = jest.fn()
it('custom function args get access to the "equals" jasmine util', () => {
const fn = jest.fn()

const allValuesTrue = (arg) => Object.values(arg).every(Boolean)
const numberDivisibleBy3 = (arg) => arg % 3 === 0
const arrMatch = (arg, equals) => equals(arg, [1, 2, 3])

when(fn)
.expectCalledWith(when(allValuesTrue), when(numberDivisibleBy3))
.mockReturnValue('x')
when(fn)
.calledWith(when(arrMatch))
.mockReturnValue('x')

expect(fn([1, 2, 3])).toEqual('x')
})

it('expects with custom function args', () => {
const fn = jest.fn()

const allValuesTrue = (arg) => Object.values(arg).every(Boolean)
const numberDivisibleBy3 = (arg) => arg % 3 === 0

when(fn)
.expectCalledWith(when(allValuesTrue), when(numberDivisibleBy3))
.mockReturnValue('x')

expect(fn({ foo: true, bar: true }, 9)).toEqual('x')
expect(() => fn({ foo: false, bar: true }, 9)).toThrow(/Failed function matcher within expectCalledWith: allValuesTrue\(\{"foo":false,"bar":true\}\) did not return true/)
expect(() => fn({ foo: true, bar: true }, 13)).toThrow(/Failed function matcher within expectCalledWith: numberDivisibleBy3\(13\) did not return true/)
})

expect(fn({ foo: true, bar: true }, 9)).toEqual('x')
expect(() => fn({ foo: false, bar: true }, 9)).toThrow(/Failed function matcher within expectCalledWith: allValuesTrue\(\{"foo":false,"bar":true\}\) did not return true/)
expect(() => fn({ foo: true, bar: true }, 13)).toThrow(/Failed function matcher within expectCalledWith: numberDivisibleBy3\(13\) did not return true/)
it('does not call regular functions as function matchers', () => {
const fn = jest.fn()

const doNotCallMeBro = () => {
throw new Error('BOOM')
}

when(fn)
.expectCalledWith(doNotCallMeBro)
.mockReturnValue('x')

expect(fn(doNotCallMeBro)).toEqual('x')
expect(() => fn(doNotCallMeBro)).not.toThrow()
})
})

it('does not call regular functions as function matchers', () => {
const fn = jest.fn()
describe('when.allArgs', () => {
it('throws an error if you try to use other matches with it', () => {
const fn = jest.fn()

const doNotCallMeBro = () => {
throw new Error('BOOM')
}
when(fn)
.calledWith(when.allArgs(() => true), 1, 2, 3)
.mockReturnValue('x')

when(fn)
.expectCalledWith(doNotCallMeBro)
.mockReturnValue('x')
expect(() => fn(3, 6, 9)).toThrow(/When using when.allArgs, it must be the one and only matcher provided to calledWith. You have incorrectly provided other matchers along with when.allArgs./)
})

it('allows matching against all the args at once with when.allArgs', () => {
const fn = jest.fn()

const numberDivisibleBy3 = (args) => args.every(arg => arg % 3 === 0)

when(fn)
.calledWith(when.allArgs(numberDivisibleBy3))
.mockReturnValue('x')

expect(fn(3, 6, 9)).toEqual('x')
expect(fn(3, 6, 10)).toBeUndefined()
expect(fn(1, 2, 3)).toBeUndefined()
})

it('all args are numbers example', () => {
const fn = jest.fn()
const areNumbers = (args, equals) => args.every(arg => equals(arg, expect.any(Number)))

when(fn)
.calledWith(when.allArgs(areNumbers))
.mockReturnValue('x')

expect(fn(3, 6, 9)).toEqual('x')
expect(fn(3, 666)).toEqual('x')
expect(fn(-100, 2, 3.234234, 234, 90e3)).toEqual('x')
expect(fn(123, 'not a number')).toBeUndefined()
})

it('single arg match example', () => {
const fn = jest.fn()
const argAtIndex = (index, matcher) => when.allArgs((args, equals) => equals(args[index], matcher))

when(fn)
.calledWith(argAtIndex(0, expect.any(Number)))
.mockReturnValue('x')

expect(fn(1, 2, 3)).toEqual('x')
expect(fn(-123123, 'string', false, null)).toEqual('x')
expect(fn('not a string', 2, 3)).toBeUndefined()
})

it('partial match example', () => {
const fn = jest.fn()
const partialArgs = (...argsToMatch) => when.allArgs((args, equals) => equals(args, expect.arrayContaining(argsToMatch)))

when(fn)
.calledWith(partialArgs(1, 2, 3))
.mockReturnValue('x')

expect(fn(1, 2, 3)).toEqual('x')
expect(fn(1, 2, 3, 4, 5, 6)).toEqual('x')
expect(fn(1, 2)).toBeUndefined()
expect(fn(1, 2, 4)).toBeUndefined()
})

it('react use case from github', () => {
// SEE: https://github.com/timkindberg/jest-when/issues/66

const SomeChild = jest.fn()

when(SomeChild)
.calledWith({ xyz: '123' })
.mockReturnValue('hello world')

const propsOf = propsToMatch => when.allArgs(([props, refOrContext], equals) => equals(props, propsToMatch))

when(SomeChild)
.calledWith(propsOf({ xyz: '123' }))
.mockReturnValue('hello world')

expect(SomeChild({ xyz: '123' })).toEqual('hello world')
})

it('allows matching against all the args at once with when.allArgs using expect matchers', () => {
const fn = jest.fn()

when(fn)
.calledWith(when.allArgs(expect.arrayContaining([42])))
.mockReturnValue('x')
.calledWith(when.allArgs(expect.arrayContaining([expect.objectContaining({ foo: true })])))
.mockReturnValue('y')

expect(fn(3, 6, 42)).toEqual('x')
expect(fn({ foo: true, bar: true }, 'a', 'b', 'c')).toEqual('y')
expect(fn(1, 2, 3)).toBeUndefined()
})

it('allows asserting against all the args at once with when.allArgs', () => {
const fn = jest.fn()

const numberDivisibleBy3 = (args) => args.every(arg => arg % 3 === 0)

when(fn)
.expectCalledWith(when.allArgs(numberDivisibleBy3))
.mockReturnValue('x')

expect(fn(doNotCallMeBro)).toEqual('x')
expect(() => fn(doNotCallMeBro)).not.toThrow()
expect(fn(3, 6, 9)).toEqual('x')
expect(() => fn(3, 6, 10)).toThrow(/Failed function matcher within expectCalledWith: numberDivisibleBy3\(\[3,6,10]\) did not return true/)
expect(() => fn(1, 2, 3)).toThrow(/Failed function matcher within expectCalledWith: numberDivisibleBy3\(\[1,2,3]\) did not return true/)
})
})

it('supports compound when declarations', () => {
Expand Down

0 comments on commit 7163307

Please sign in to comment.