diff --git a/README.md b/README.md index 20d0241..d9df87e 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/when.js b/src/when.js index 78f2ce4..3947376 100644 --- a/src/when.js +++ b/src/when.js @@ -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) @@ -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 @@ -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() diff --git a/src/when.test.js b/src/when.test.js index 9215307..88ef06e 100644 --- a/src/when.test.js +++ b/src/when.test.js @@ -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', () => {