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..91d78e20 100644 --- a/spec/core/matchers/toHaveBeenCalledWithSpec.js +++ b/spec/core/matchers/toHaveBeenCalledWithSpec.js @@ -92,4 +92,19 @@ 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('interaction-check').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..d5b022af --- /dev/null +++ b/spec/core/matchers/toHaveNoOtherSpyInteractionsSpec.js @@ -0,0 +1,137 @@ +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() { + let matcher = jasmineUnderTest.matchers.toHaveNoOtherSpyInteractions(); + let spyObj = jasmineUnderTest + .getEnv() + .createSpyObj('NewClass', ['spyA', 'spyB']); + + spyObj.spyA(); + + let result = matcher.compare(spyObj); + expect(result.pass).toBeFalse(); + expect(result.message).toContain( + "Unverified spies' calls have been found in:" + ); + }); + + it('shows the right message is negated', function() { + let matcher = jasmineUnderTest.matchers.toHaveNoOtherSpyInteractions(); + 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( + "Unverified spies' calls have been found in:" + ); + }); + + 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("Spies' calls are all verified."); + }); + + 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(), + 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..6baf28fc 100644 --- a/src/core/CallTracker.js +++ b/src/core/CallTracker.js @@ -125,6 +125,16 @@ getJasmineRequireObj().CallTracker = function(j$) { this.saveArgumentsByValue = function() { opts.cloneArgs = true; }; + + /** + * Get the number of unverified invocations of this spy. + * @name Spy#calls#unverifiedCount + * @function + * @return {Integer} + */ + 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..1721848f --- /dev/null +++ b/src/core/matchers/toHaveNoOtherSpyInteractions.js @@ -0,0 +1,75 @@ +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 unexpectedCallsIn = []; + + for (const spy of Object.values(actual)) { + if (!j$.isSpy(spy)) { + continue; + } + + hasSpy = true; + + if (!spy.calls.all().every(call => call.verified)) { + unexpectedCallsIn.push([ + spy.and.identity, + spy.calls.unverifiedCount() + ]); + + result.pass = false; + } + } + + if (!hasSpy) { + throw new Error( + getErrorMsg( + 'Expected an object with spies, but object has no spies.' + ) + ); + } + + result.message = result.pass + ? "Spies' calls are all verified." + : "Unverified spies' calls have been found in: " + + unexpectedCallsIn + .map( + ([spyName, unverifiedCount]) => + `${spyName} (${unverifiedCount} unverified call(s))` + ) + .join(', '); + + return result; + } + }; + } + + return toHaveNoOtherSpyInteractions; +};