diff --git a/lib/jasmine-core/jasmine.js b/lib/jasmine-core/jasmine.js index f2d8a9ed..4d5e330e 100644 --- a/lib/jasmine-core/jasmine.js +++ b/lib/jasmine-core/jasmine.js @@ -162,6 +162,7 @@ getJasmineRequireObj().requireMatchers = function(jRequire, j$) { 'toHaveClass', 'toHaveClasses', 'toHaveSpyInteractions', + 'toHaveNoOtherSpyInteractions', 'toMatch', 'toThrow', 'toThrowError', @@ -2898,6 +2899,10 @@ getJasmineRequireObj().CallTracker = function(j$) { this.saveArgumentsByValue = function() { opts.cloneArgs = true; }; + + this.unverifiedCount = function() { + return calls.reduce((count, call) => count + (call.verified ? 0 : 1), 0); + }; } return CallTracker; @@ -6231,6 +6236,8 @@ getJasmineRequireObj().toHaveBeenCalled = function(j$) { result.pass = actual.calls.any(); + actual.calls.all().forEach(call => (call.verified = true)); + result.message = result.pass ? 'Expected spy ' + actual.and.identity + ' not to have been called.' : 'Expected spy ' + actual.and.identity + ' to have been called.'; @@ -6295,6 +6302,9 @@ getJasmineRequireObj().toHaveBeenCalledBefore = function(j$) { result.pass = latest1stSpyCall < first2ndSpyCall; if (result.pass) { + firstSpy.calls.mostRecent().verified = true; + latterSpy.calls.first().verified = true; + result.message = 'Expected spy ' + firstSpy.and.identity + @@ -6351,7 +6361,7 @@ getJasmineRequireObj().toHaveBeenCalledOnceWith = function(j$) { * @example * expect(mySpy).toHaveBeenCalledOnceWith('foo', 'bar', 2); */ - function toHaveBeenCalledOnceWith(util) { + function toHaveBeenCalledOnceWith(matchersUtil) { return { compare: function() { const args = Array.prototype.slice.call(arguments, 0), @@ -6360,20 +6370,29 @@ getJasmineRequireObj().toHaveBeenCalledOnceWith = function(j$) { if (!j$.isSpy(actual)) { throw new Error( - getErrorMsg('Expected a spy, but got ' + util.pp(actual) + '.') + getErrorMsg( + 'Expected a spy, but got ' + matchersUtil.pp(actual) + '.' + ) ); } const prettyPrintedCalls = actual.calls .allArgs() .map(function(argsForCall) { - return ' ' + util.pp(argsForCall); + return ' ' + matchersUtil.pp(argsForCall); }); if ( actual.calls.count() === 1 && - util.contains(actual.calls.allArgs(), expectedArgs) + matchersUtil.contains(actual.calls.allArgs(), expectedArgs) ) { + const firstIndex = actual.calls + .all() + .findIndex(call => matchersUtil.equals(call.args, expectedArgs)); + if (firstIndex > -1) { + actual.calls.all()[firstIndex].verified = true; + } + return { pass: true, message: @@ -6381,7 +6400,7 @@ getJasmineRequireObj().toHaveBeenCalledOnceWith = function(j$) { actual.and.identity + ' to have been called 0 times, multiple times, or once, but with arguments different from:\n' + ' ' + - util.pp(expectedArgs) + + matchersUtil.pp(expectedArgs) + '\n' + 'But the actual call was:\n' + prettyPrintedCalls.join(',\n') + @@ -6392,7 +6411,7 @@ getJasmineRequireObj().toHaveBeenCalledOnceWith = function(j$) { function getDiffs() { return actual.calls.allArgs().map(function(argsForCall, callIx) { const diffBuilder = new j$.DiffBuilder(); - util.equals(argsForCall, expectedArgs, diffBuilder); + matchersUtil.equals(argsForCall, expectedArgs, diffBuilder); return diffBuilder.getMessage(); }); } @@ -6425,7 +6444,7 @@ getJasmineRequireObj().toHaveBeenCalledOnceWith = function(j$) { actual.and.identity + ' to have been called only once, and with given args:\n' + ' ' + - util.pp(expectedArgs) + + matchersUtil.pp(expectedArgs) + '\n' + butString() }; @@ -6474,23 +6493,35 @@ getJasmineRequireObj().toHaveBeenCalledTimes = function(j$) { } actual = args[0]; - const calls = actual.calls.count(); + + const callsCount = actual.calls.count(); const timesMessage = expected === 1 ? 'once' : expected + ' times'; - result.pass = calls === expected; + + result.pass = callsCount === expected; + + if (result.pass) { + const allCalls = actual.calls.all(); + const max = Math.min(expected, callsCount); + + for (let i = 0; i < max; i++) { + allCalls[i].verified = true; + } + } + result.message = result.pass ? 'Expected spy ' + actual.and.identity + ' not to have been called ' + timesMessage + '. It was called ' + - calls + + callsCount + ' times.' : 'Expected spy ' + actual.and.identity + ' to have been called ' + timesMessage + '. It was called ' + - calls + + callsCount + ' times.'; return result; } @@ -6546,6 +6577,11 @@ getJasmineRequireObj().toHaveBeenCalledWith = function(j$) { } if (matchersUtil.contains(actual.calls.allArgs(), expectedArgs)) { + actual.calls + .all() + .filter(call => matchersUtil.equals(call.args, expectedArgs)) + .forEach(call => (call.verified = true)); + result.pass = true; result.message = function() { return ( @@ -6672,6 +6708,94 @@ getJasmineRequireObj().toHaveClasses = function(j$) { return toHaveClasses; }; +getJasmineRequireObj().toHaveNoOtherSpyInteractions = function(j$) { + const getErrorMsg = j$.formatErrorMsg( + '', + 'expect().toHaveNoOtherSpyInteractions()' + ); + + /** + * {@link expect} the actual (a {@link SpyObj}) spies to have not been called except interactions which was already tracked with `toHaveBeenCalled`. + * @function + * @name matchers#toHaveNoOtherSpyInteractions + * @example + * expect(mySpyObj).toHaveNoOtherSpyInteractions(); + * expect(mySpyObj).not.toHaveNoOtherSpyInteractions(); + */ + function toHaveNoOtherSpyInteractions(matchersUtil) { + return { + compare: function(actual) { + const result = {}; + + if (!j$.isObject_(actual)) { + throw new Error( + getErrorMsg('Expected an object, but got ' + typeof actual + '.') + ); + } + + if (arguments.length > 1) { + throw new Error(getErrorMsg('Does not take arguments')); + } + + result.pass = true; + let hasSpy = false; + const unexpectedCalls = []; + + for (const spy of Object.values(actual)) { + if (!j$.isSpy(spy)) { + continue; + } + + hasSpy = true; + + const unverifiedCalls = spy.calls + .all() + .filter(call => !call.verified); + + if (unverifiedCalls.length > 0) { + result.pass = false; + } + + unverifiedCalls.forEach(unverifiedCall => { + unexpectedCalls.push([ + spy.and.identity, + matchersUtil.pp(unverifiedCall.args) + ]); + }); + } + + if (!hasSpy) { + throw new Error( + getErrorMsg( + 'Expected an object with spies, but object has no spies.' + ) + ); + } + + if (result.pass) { + result.message = + "Expected to have other spy interactions but it didn't."; + } else { + const ppUnexpectedCalls = unexpectedCalls + .map( + ([spyName, arguments]) => ` ${spyName} called with ${arguments}` + ) + .join(',\n'); + + result.message = + 'Expected to have no other spy interactions, but it had the following calls:\n' + + ppUnexpectedCalls + + '.\n\n'; + } + + return result; + } + }; + } + + return toHaveNoOtherSpyInteractions; +}; + getJasmineRequireObj().toHaveSize = function(j$) { /** * {@link expect} the actual size to be equal to the expected, using array-like length or object keys size. @@ -9250,7 +9374,8 @@ getJasmineRequireObj().Spy = function(j$) { const callData = { object: context, invocationOrder: nextOrder(), - args: Array.prototype.slice.apply(args) + args: Array.prototype.slice.apply(args), + verified: false }; callTracker.track(callData); diff --git a/spec/core/integration/MatchersSpec.js b/spec/core/integration/MatchersSpec.js index b44a40a5..55a9df66 100755 --- a/spec/core/integration/MatchersSpec.js +++ b/spec/core/integration/MatchersSpec.js @@ -675,6 +675,23 @@ describe('Matchers (Integration)', function() { }); }); + describe('toHaveNoOtherSpyInteractions', function() { + let spyObj; + + beforeEach(function() { + spyObj = env.createSpyObj('NewClass', ['spyA', 'spyB']); + }); + + verifyPasses(function(env) { + env.expect(spyObj).toHaveNoOtherSpyInteractions(); + }); + + verifyFails(function(env) { + spyObj.spyA(); + env.expect(spyObj).toHaveNoOtherSpyInteractions(); + }); + }); + describe('toMatch', function() { verifyPasses(function(env) { env.expect('foo').toMatch(/oo$/); diff --git a/spec/core/matchers/toHaveBeenCalledBeforeSpec.js b/spec/core/matchers/toHaveBeenCalledBeforeSpec.js index 251a2c9e..34e4c5cc 100644 --- a/spec/core/matchers/toHaveBeenCalledBeforeSpec.js +++ b/spec/core/matchers/toHaveBeenCalledBeforeSpec.js @@ -112,4 +112,20 @@ describe('toHaveBeenCalledBefore', function() { 'Expected spy first spy to not have been called before spy second spy, but it was' ); }); + + it('set the correct calls as verified when passing', function() { + const matcher = jasmineUnderTest.matchers.toHaveBeenCalledBefore(), + firstSpy = new jasmineUnderTest.Spy('first spy'), + secondSpy = new jasmineUnderTest.Spy('second spy'); + + firstSpy(); + secondSpy(); + + matcher.compare(firstSpy, secondSpy); + + expect(firstSpy.calls.count()).toBe(1); + expect(firstSpy.calls.unverifiedCount()).toBe(0); + expect(secondSpy.calls.count()).toBe(1); + expect(secondSpy.calls.unverifiedCount()).toBe(0); + }); }); diff --git a/spec/core/matchers/toHaveBeenCalledOnceWithSpec.js b/spec/core/matchers/toHaveBeenCalledOnceWithSpec.js index 997f6f5d..fb4bbc4b 100644 --- a/spec/core/matchers/toHaveBeenCalledOnceWithSpec.js +++ b/spec/core/matchers/toHaveBeenCalledOnceWithSpec.js @@ -105,4 +105,18 @@ describe('toHaveBeenCalledOnceWith', function() { matcher.compare(fn); }).toThrowError(/Expected a spy, but got Function./); }); + + it('set the correct calls as verified when passing', function() { + const pp = jasmineUnderTest.makePrettyPrinter(), + util = new jasmineUnderTest.MatchersUtil({ pp: pp }), + matcher = jasmineUnderTest.matchers.toHaveBeenCalledOnceWith(util), + calledSpy = new jasmineUnderTest.Spy('called-spy'); + + calledSpy('x'); + + matcher.compare(calledSpy, 'x'); + + expect(calledSpy.calls.count()).toBe(1); + expect(calledSpy.calls.unverifiedCount()).toBe(0); + }); }); diff --git a/spec/core/matchers/toHaveBeenCalledSpec.js b/spec/core/matchers/toHaveBeenCalledSpec.js index f8fa0ffe..a12cde3f 100644 --- a/spec/core/matchers/toHaveBeenCalledSpec.js +++ b/spec/core/matchers/toHaveBeenCalledSpec.js @@ -50,4 +50,16 @@ describe('toHaveBeenCalled', function() { 'Expected spy sample-spy to have been called.' ); }); + + it('set the correct calls as verified when passing', function() { + const matcher = jasmineUnderTest.matchers.toHaveBeenCalled(), + spy = new jasmineUnderTest.Spy('sample-spy'); + + spy(); + + matcher.compare(spy); + + expect(spy.calls.count()).toBe(1); + expect(spy.calls.unverifiedCount()).toBe(0); + }); }); diff --git a/spec/core/matchers/toHaveBeenCalledTimesSpec.js b/spec/core/matchers/toHaveBeenCalledTimesSpec.js index 141abeb9..af1d1d26 100644 --- a/spec/core/matchers/toHaveBeenCalledTimesSpec.js +++ b/spec/core/matchers/toHaveBeenCalledTimesSpec.js @@ -87,4 +87,17 @@ describe('toHaveBeenCalledTimes', function() { ' times.' ); }); + + it('set the correct calls as verified when passing', function() { + const matcher = jasmineUnderTest.matchers.toHaveBeenCalledTimes(), + spy = new jasmineUnderTest.Spy('sample-spy'); + + spy(); + spy(); + + matcher.compare(spy, 2); + + expect(spy.calls.count()).toBe(2); + expect(spy.calls.unverifiedCount()).toBe(0); + }); }); diff --git a/spec/core/matchers/toHaveBeenCalledWithSpec.js b/spec/core/matchers/toHaveBeenCalledWithSpec.js index d77800ed..ac7f3ad4 100644 --- a/spec/core/matchers/toHaveBeenCalledWithSpec.js +++ b/spec/core/matchers/toHaveBeenCalledWithSpec.js @@ -2,6 +2,7 @@ describe('toHaveBeenCalledWith', function() { it('passes when the actual was called with matching parameters', function() { const matchersUtil = { contains: jasmine.createSpy('delegated-contains').and.returnValue(true), + equals: jasmine.createSpy('delegated-equals').and.returnValue(true), pp: jasmineUnderTest.makePrettyPrinter() }, matcher = jasmineUnderTest.matchers.toHaveBeenCalledWith(matchersUtil), @@ -92,4 +93,20 @@ describe('toHaveBeenCalledWith', function() { matcher.compare(fn); }).toThrowError(/Expected a spy, but got Function./); }); + + it('set the correct calls as verified when passing', function() { + const matchersUtil = { + contains: jasmine.createSpy('delegated-contains').and.returnValue(true), + equals: jasmine.createSpy('delegated-equals').and.returnValue(true), + pp: jasmineUnderTest.makePrettyPrinter() + }, + matcher = jasmineUnderTest.matchers.toHaveBeenCalledWith(matchersUtil), + calledSpy = new jasmineUnderTest.Spy('called-spy'); + + calledSpy('a', 'b'); + matcher.compare(calledSpy, 'a', 'b'); + + expect(calledSpy.calls.count()).toBe(1); + expect(calledSpy.calls.unverifiedCount()).toBe(0); + }); }); diff --git a/spec/core/matchers/toHaveNoOtherSpyInteractionsSpec.js b/spec/core/matchers/toHaveNoOtherSpyInteractionsSpec.js new file mode 100644 index 00000000..02603097 --- /dev/null +++ b/spec/core/matchers/toHaveNoOtherSpyInteractionsSpec.js @@ -0,0 +1,151 @@ +describe('toHaveNoOtherSpyInteractions', function() { + it('passes when there are no spy interactions', function() { + let matcher = jasmineUnderTest.matchers.toHaveNoOtherSpyInteractions(); + let spyObj = jasmineUnderTest + .getEnv() + .createSpyObj('NewClass', ['spyA', 'spyB']); + + let result = matcher.compare(spyObj); + expect(result.pass).toBeTrue(); + }); + + it('passes when there are multiple spy interactions where checked by toHaveBeenCalled', function() { + let matcher = jasmineUnderTest.matchers.toHaveNoOtherSpyInteractions(); + let toHaveBeenCalledMatcher = jasmineUnderTest.matchers.toHaveBeenCalled(); + let spyObj = jasmineUnderTest + .getEnv() + .createSpyObj('NewClass', ['spyA', 'spyB']); + + spyObj.spyA(); + spyObj.spyB(); + spyObj.spyA(); + toHaveBeenCalledMatcher.compare(spyObj.spyA); + toHaveBeenCalledMatcher.compare(spyObj.spyB); + let result = matcher.compare(spyObj); + expect(result.pass).toBeTrue(); + }); + + it('fails when there are spy interactions', function() { + const matchersUtil = new jasmineUnderTest.MatchersUtil({ + pp: jasmineUnderTest.makePrettyPrinter() + }); + let matcher = jasmineUnderTest.matchers.toHaveNoOtherSpyInteractions( + matchersUtil + ); + let spyObj = jasmineUnderTest + .getEnv() + .createSpyObj('NewClass', ['spyA', 'spyB']); + + spyObj.spyA('x'); + + let result = matcher.compare(spyObj); + expect(result.pass).toBeFalse(); + expect(result.message).toContain( + 'Expected to have no other spy interactions, but it had the following calls:' + ); + }); + + it('shows the right message is negated', function() { + const matchersUtil = new jasmineUnderTest.MatchersUtil({ + pp: jasmineUnderTest.makePrettyPrinter() + }); + let matcher = jasmineUnderTest.matchers.toHaveNoOtherSpyInteractions( + matchersUtil + ); + let spyObj = jasmineUnderTest + .getEnv() + .createSpyObj('NewClass', ['spyA', 'spyB']); + + spyObj.spyA(); + spyObj.spyB(); + + let result = matcher.compare(spyObj); + expect(result.pass).toBeFalse(), + expect(result.message).toContain( + 'Expected to have no other spy interactions, but it had the following calls:' + ); + }); + + it('passes when only non-observed spy object interactions are interacted', function() { + let matcher = jasmineUnderTest.matchers.toHaveNoOtherSpyInteractions(); + let spyObj = jasmineUnderTest + .getEnv() + .createSpyObj('NewClass', ['spyA', 'spyB']); + spyObj.otherMethod = function() {}; + + spyObj.otherMethod(); + + let result = matcher.compare(spyObj); + expect(result.pass).toBeTrue(); + expect(result.message).toContain( + "Expected to have other spy interactions but it didn't" + ); + }); + + it(`throws an error if a non-object is passed`, function() { + let matcher = jasmineUnderTest.matchers.toHaveNoOtherSpyInteractions(); + + expect(function() { + matcher.compare(true); + }).toThrowError(Error, /Expected an object, but got/); + + expect(function() { + matcher.compare(123); + }).toThrowError(Error, /Expected an object, but got/); + + expect(function() { + matcher.compare('string'); + }).toThrowError(Error, /Expected an object, but got/); + }); + + it('throws an error if arguments are passed', function() { + let matcher = jasmineUnderTest.matchers.toHaveNoOtherSpyInteractions(); + let spyObj = jasmineUnderTest + .getEnv() + .createSpyObj('NewClass', ['spyA', 'spyB']); + + expect(function() { + matcher.compare(spyObj, 'an argument'); + }).toThrowError(Error, /Does not take arguments/); + }); + + it('throws an error if the spy object has no spies', function() { + let matcher = jasmineUnderTest.matchers.toHaveNoOtherSpyInteractions(); + const spyObj = jasmineUnderTest + .getEnv() + .createSpyObj('NewClass', ['notSpy']); + // Removing spy since spy objects cannot be created without spies. + spyObj.notSpy = function() {}; + + expect(function() { + matcher.compare(spyObj); + }).toThrowError( + Error, + /Expected an object with spies, but object has no spies/ + ); + }); + + it('handles multiple interactions with a single spy', function() { + const matchersUtil = new jasmineUnderTest.MatchersUtil({ + pp: jasmineUnderTest.makePrettyPrinter() + }), + matcher = jasmineUnderTest.matchers.toHaveNoOtherSpyInteractions( + matchersUtil + ), + toHaveBeenCalledWithMatcher = jasmineUnderTest.matchers.toHaveBeenCalledWith( + matchersUtil + ), + spyObj = jasmineUnderTest + .getEnv() + .createSpyObj('NewClass', ['spyA', 'spyB']); + + spyObj.spyA('x'); + spyObj.spyA('y'); + + toHaveBeenCalledWithMatcher.compare(spyObj.spyA, 'x'); + + let result = matcher.compare(spyObj); + + expect(result.pass).toBeFalse(); + }); +}); diff --git a/src/core/CallTracker.js b/src/core/CallTracker.js index 872e5cf8..d123aed5 100644 --- a/src/core/CallTracker.js +++ b/src/core/CallTracker.js @@ -125,6 +125,10 @@ getJasmineRequireObj().CallTracker = function(j$) { this.saveArgumentsByValue = function() { opts.cloneArgs = true; }; + + this.unverifiedCount = function() { + return calls.reduce((count, call) => count + (call.verified ? 0 : 1), 0); + }; } return CallTracker; diff --git a/src/core/Spy.js b/src/core/Spy.js index 8701c52b..108cd835 100644 --- a/src/core/Spy.js +++ b/src/core/Spy.js @@ -26,7 +26,8 @@ getJasmineRequireObj().Spy = function(j$) { const callData = { object: context, invocationOrder: nextOrder(), - args: Array.prototype.slice.apply(args) + args: Array.prototype.slice.apply(args), + verified: false }; callTracker.track(callData); diff --git a/src/core/matchers/requireMatchers.js b/src/core/matchers/requireMatchers.js index aa636472..5eb64617 100755 --- a/src/core/matchers/requireMatchers.js +++ b/src/core/matchers/requireMatchers.js @@ -30,6 +30,7 @@ getJasmineRequireObj().requireMatchers = function(jRequire, j$) { 'toHaveClass', 'toHaveClasses', 'toHaveSpyInteractions', + 'toHaveNoOtherSpyInteractions', 'toMatch', 'toThrow', 'toThrowError', diff --git a/src/core/matchers/toHaveBeenCalled.js b/src/core/matchers/toHaveBeenCalled.js index f4fd131a..bb544410 100644 --- a/src/core/matchers/toHaveBeenCalled.js +++ b/src/core/matchers/toHaveBeenCalled.js @@ -34,6 +34,8 @@ getJasmineRequireObj().toHaveBeenCalled = function(j$) { result.pass = actual.calls.any(); + actual.calls.all().forEach(call => (call.verified = true)); + result.message = result.pass ? 'Expected spy ' + actual.and.identity + ' not to have been called.' : 'Expected spy ' + actual.and.identity + ' to have been called.'; diff --git a/src/core/matchers/toHaveBeenCalledBefore.js b/src/core/matchers/toHaveBeenCalledBefore.js index 3f352d21..85a0af99 100644 --- a/src/core/matchers/toHaveBeenCalledBefore.js +++ b/src/core/matchers/toHaveBeenCalledBefore.js @@ -50,6 +50,9 @@ getJasmineRequireObj().toHaveBeenCalledBefore = function(j$) { result.pass = latest1stSpyCall < first2ndSpyCall; if (result.pass) { + firstSpy.calls.mostRecent().verified = true; + latterSpy.calls.first().verified = true; + result.message = 'Expected spy ' + firstSpy.and.identity + diff --git a/src/core/matchers/toHaveBeenCalledOnceWith.js b/src/core/matchers/toHaveBeenCalledOnceWith.js index 1d36f615..d8d4373f 100644 --- a/src/core/matchers/toHaveBeenCalledOnceWith.js +++ b/src/core/matchers/toHaveBeenCalledOnceWith.js @@ -13,7 +13,7 @@ getJasmineRequireObj().toHaveBeenCalledOnceWith = function(j$) { * @example * expect(mySpy).toHaveBeenCalledOnceWith('foo', 'bar', 2); */ - function toHaveBeenCalledOnceWith(util) { + function toHaveBeenCalledOnceWith(matchersUtil) { return { compare: function() { const args = Array.prototype.slice.call(arguments, 0), @@ -22,20 +22,29 @@ getJasmineRequireObj().toHaveBeenCalledOnceWith = function(j$) { if (!j$.isSpy(actual)) { throw new Error( - getErrorMsg('Expected a spy, but got ' + util.pp(actual) + '.') + getErrorMsg( + 'Expected a spy, but got ' + matchersUtil.pp(actual) + '.' + ) ); } const prettyPrintedCalls = actual.calls .allArgs() .map(function(argsForCall) { - return ' ' + util.pp(argsForCall); + return ' ' + matchersUtil.pp(argsForCall); }); if ( actual.calls.count() === 1 && - util.contains(actual.calls.allArgs(), expectedArgs) + matchersUtil.contains(actual.calls.allArgs(), expectedArgs) ) { + const firstIndex = actual.calls + .all() + .findIndex(call => matchersUtil.equals(call.args, expectedArgs)); + if (firstIndex > -1) { + actual.calls.all()[firstIndex].verified = true; + } + return { pass: true, message: @@ -43,7 +52,7 @@ getJasmineRequireObj().toHaveBeenCalledOnceWith = function(j$) { actual.and.identity + ' to have been called 0 times, multiple times, or once, but with arguments different from:\n' + ' ' + - util.pp(expectedArgs) + + matchersUtil.pp(expectedArgs) + '\n' + 'But the actual call was:\n' + prettyPrintedCalls.join(',\n') + @@ -54,7 +63,7 @@ getJasmineRequireObj().toHaveBeenCalledOnceWith = function(j$) { function getDiffs() { return actual.calls.allArgs().map(function(argsForCall, callIx) { const diffBuilder = new j$.DiffBuilder(); - util.equals(argsForCall, expectedArgs, diffBuilder); + matchersUtil.equals(argsForCall, expectedArgs, diffBuilder); return diffBuilder.getMessage(); }); } @@ -87,7 +96,7 @@ getJasmineRequireObj().toHaveBeenCalledOnceWith = function(j$) { actual.and.identity + ' to have been called only once, and with given args:\n' + ' ' + - util.pp(expectedArgs) + + matchersUtil.pp(expectedArgs) + '\n' + butString() }; diff --git a/src/core/matchers/toHaveBeenCalledTimes.js b/src/core/matchers/toHaveBeenCalledTimes.js index 78054af8..4a16995c 100644 --- a/src/core/matchers/toHaveBeenCalledTimes.js +++ b/src/core/matchers/toHaveBeenCalledTimes.js @@ -36,23 +36,35 @@ getJasmineRequireObj().toHaveBeenCalledTimes = function(j$) { } actual = args[0]; - const calls = actual.calls.count(); + + const callsCount = actual.calls.count(); const timesMessage = expected === 1 ? 'once' : expected + ' times'; - result.pass = calls === expected; + + result.pass = callsCount === expected; + + if (result.pass) { + const allCalls = actual.calls.all(); + const max = Math.min(expected, callsCount); + + for (let i = 0; i < max; i++) { + allCalls[i].verified = true; + } + } + result.message = result.pass ? 'Expected spy ' + actual.and.identity + ' not to have been called ' + timesMessage + '. It was called ' + - calls + + callsCount + ' times.' : 'Expected spy ' + actual.and.identity + ' to have been called ' + timesMessage + '. It was called ' + - calls + + callsCount + ' times.'; return result; } diff --git a/src/core/matchers/toHaveBeenCalledWith.js b/src/core/matchers/toHaveBeenCalledWith.js index 2fa26d3c..da2d1074 100644 --- a/src/core/matchers/toHaveBeenCalledWith.js +++ b/src/core/matchers/toHaveBeenCalledWith.js @@ -44,6 +44,11 @@ getJasmineRequireObj().toHaveBeenCalledWith = function(j$) { } if (matchersUtil.contains(actual.calls.allArgs(), expectedArgs)) { + actual.calls + .all() + .filter(call => matchersUtil.equals(call.args, expectedArgs)) + .forEach(call => (call.verified = true)); + result.pass = true; result.message = function() { return ( diff --git a/src/core/matchers/toHaveNoOtherSpyInteractions.js b/src/core/matchers/toHaveNoOtherSpyInteractions.js new file mode 100644 index 00000000..9c7d2710 --- /dev/null +++ b/src/core/matchers/toHaveNoOtherSpyInteractions.js @@ -0,0 +1,87 @@ +getJasmineRequireObj().toHaveNoOtherSpyInteractions = function(j$) { + const getErrorMsg = j$.formatErrorMsg( + '', + 'expect().toHaveNoOtherSpyInteractions()' + ); + + /** + * {@link expect} the actual (a {@link SpyObj}) spies to have not been called except interactions which was already tracked with `toHaveBeenCalled`. + * @function + * @name matchers#toHaveNoOtherSpyInteractions + * @example + * expect(mySpyObj).toHaveNoOtherSpyInteractions(); + * expect(mySpyObj).not.toHaveNoOtherSpyInteractions(); + */ + function toHaveNoOtherSpyInteractions(matchersUtil) { + return { + compare: function(actual) { + const result = {}; + + if (!j$.isObject_(actual)) { + throw new Error( + getErrorMsg('Expected an object, but got ' + typeof actual + '.') + ); + } + + if (arguments.length > 1) { + throw new Error(getErrorMsg('Does not take arguments')); + } + + result.pass = true; + let hasSpy = false; + const unexpectedCalls = []; + + for (const spy of Object.values(actual)) { + if (!j$.isSpy(spy)) { + continue; + } + + hasSpy = true; + + const unverifiedCalls = spy.calls + .all() + .filter(call => !call.verified); + + if (unverifiedCalls.length > 0) { + result.pass = false; + } + + unverifiedCalls.forEach(unverifiedCall => { + unexpectedCalls.push([ + spy.and.identity, + matchersUtil.pp(unverifiedCall.args) + ]); + }); + } + + if (!hasSpy) { + throw new Error( + getErrorMsg( + 'Expected an object with spies, but object has no spies.' + ) + ); + } + + if (result.pass) { + result.message = + "Expected to have other spy interactions but it didn't."; + } else { + const ppUnexpectedCalls = unexpectedCalls + .map( + ([spyName, arguments]) => ` ${spyName} called with ${arguments}` + ) + .join(',\n'); + + result.message = + 'Expected to have no other spy interactions, but it had the following calls:\n' + + ppUnexpectedCalls + + '.\n\n'; + } + + return result; + } + }; + } + + return toHaveNoOtherSpyInteractions; +};