diff --git a/lib/jasmine-core/jasmine.js b/lib/jasmine-core/jasmine.js index c71314df..a077925a 100644 --- a/lib/jasmine-core/jasmine.js +++ b/lib/jasmine-core/jasmine.js @@ -152,6 +152,7 @@ getJasmineRequireObj().requireMatchers = function(jRequire, j$) { 'toHaveBeenCalledTimes', 'toHaveBeenCalledWith', 'toHaveClass', + 'toHaveSpyInteractions', 'toMatch', 'toThrow', 'toThrowError', @@ -6930,6 +6931,81 @@ getJasmineRequireObj().toHaveSize = function(j$) { return toHaveSize; }; +getJasmineRequireObj().toHaveSpyInteractions = function(j$) { + var getErrorMsg = j$.formatErrorMsg( + '', + 'expect().toHaveSpyInteractions()' + ); + + /** + * {@link expect} the actual (a {@link SpyObj}) spies to have been called. + * @function + * @name matchers#toHaveSpyInteractions + * @example + * expect(mySpyObj).toHaveSpyInteractions(); + * expect(mySpyObj).not.toHaveSpyInteractions(); + */ + function toHaveSpyInteractions(matchersUtil) { + return { + compare: function(actual) { + var result = {}; + + if (!j$.isObject_(actual)) { + throw new Error( + getErrorMsg('Expected a spy object, but got ' + typeof actual + '.') + ); + } + + if (arguments.length > 1) { + throw new Error(getErrorMsg('Does not take arguments')); + } + + result.pass = false; + let hasSpy = false; + const calledSpies = []; + for (const spy of Object.values(actual)) { + if (!j$.isSpy(spy)) continue; + hasSpy = true; + + if (spy.calls.any()) { + result.pass = true; + calledSpies.push([spy.and.identity, spy.calls.count()]); + } + } + + if (!hasSpy) { + throw new Error( + getErrorMsg( + 'Expected a spy object with spies, but object has no spies.' + ) + ); + } + + let resultMessage; + if (result.pass) { + resultMessage = + 'Expected spy object spies not to have been called, ' + + 'but the following spies were called: '; + resultMessage += calledSpies + .map(([spyName, spyCount]) => { + return `${spyName} called ${spyCount} time(s)`; + }) + .join(', '); + } else { + resultMessage = + 'Expected spy object spies to have been called, ' + + 'but no spies were called.'; + } + result.message = resultMessage; + + return result; + } + }; + } + + return toHaveSpyInteractions; +}; + getJasmineRequireObj().toMatch = function(j$) { var getErrorMsg = j$.formatErrorMsg( '', diff --git a/spec/core/integration/MatchersSpec.js b/spec/core/integration/MatchersSpec.js old mode 100644 new mode 100755 index 9e3f7f6c..30a9e656 --- a/spec/core/integration/MatchersSpec.js +++ b/spec/core/integration/MatchersSpec.js @@ -625,6 +625,28 @@ describe('Matchers (Integration)', function() { }); }); + describe('toHaveSpyInteractions', function() { + let spyObj; + beforeEach(function() { + spyObj = env.createSpyObj('NewClass', ['spyA', 'spyB']); + spyObj.otherMethod = function() {}; + }); + + verifyPasses(function(env) { + spyObj.spyA(); + env.expect(spyObj).toHaveSpyInteractions(); + }); + + verifyFails(function(env) { + env.expect(spyObj).toHaveSpyInteractions(); + }); + + verifyFails(function(env) { + spyObj.otherMethod(); + env.expect(spyObj).toHaveSpyInteractions(); + }); + }); + describe('toMatch', function() { verifyPasses(function(env) { env.expect('foo').toMatch(/oo$/); diff --git a/spec/core/matchers/toHaveSpyInteractionsSpec.js b/spec/core/matchers/toHaveSpyInteractionsSpec.js new file mode 100755 index 00000000..3d0e577c --- /dev/null +++ b/spec/core/matchers/toHaveSpyInteractionsSpec.js @@ -0,0 +1,115 @@ +describe('toHaveSpyInteractions', function() { + it('passes when there are spy interactions', function() { + let matcher = jasmineUnderTest.matchers.toHaveSpyInteractions(); + let spyObj = jasmineUnderTest + .getEnv() + .createSpyObj('NewClass', ['spyA', 'spyB']); + + spyObj.spyA(); + + let result = matcher.compare(spyObj); + expect(result.pass).toBe(true); + }); + + it('passes when there are multiple spy interactions', function() { + let matcher = jasmineUnderTest.matchers.toHaveSpyInteractions(); + let spyObj = jasmineUnderTest + .getEnv() + .createSpyObj('NewClass', ['spyA', 'spyB']); + + spyObj.spyA(); + spyObj.spyB(); + spyObj.spyA(); + + let result = matcher.compare(spyObj); + expect(result.pass).toBe(true); + }); + + it('fails when there are no spy interactions', function() { + let matcher = jasmineUnderTest.matchers.toHaveSpyInteractions(); + let spyObj = jasmineUnderTest + .getEnv() + .createSpyObj('NewClass', ['spyA', 'spyB']); + + let result = matcher.compare(spyObj); + expect(result.pass).toBe(false); + expect(result.message).toContain( + 'Expected spy object spies to have been called' + ); + }); + + it('shows the right message is negated', function() { + let matcher = jasmineUnderTest.matchers.toHaveSpyInteractions(); + let spyObj = jasmineUnderTest + .getEnv() + .createSpyObj('NewClass', ['spyA', 'spyB']); + + spyObj.spyA(); + + let result = matcher.compare(spyObj); + expect(result.pass).toBe(true); + expect(result.message).toContain( + // Will be shown only on negate. + 'Expected spy object spies not to have been called' + ); + }); + + it('fails when only non-observed spy object interactions are interacted', function() { + let matcher = jasmineUnderTest.matchers.toHaveSpyInteractions(); + let spyObj = jasmineUnderTest + .getEnv() + .createSpyObj('NewClass', ['spyA', 'spyB']); + spyObj.otherMethod = function() {}; + + spyObj.otherMethod(); + + let result = matcher.compare(spyObj); + expect(result.pass).toBe(false); + expect(result.message).toContain( + 'Expected spy object spies to have been called' + ); + }); + + it(`throws an error if a non-object is passed`, function() { + let matcher = jasmineUnderTest.matchers.toHaveSpyInteractions(); + + expect(function() { + matcher.compare(true); + }).toThrowError(Error, /Expected a spy object, but got/); + + expect(function() { + matcher.compare(123); + }).toThrowError(Error, /Expected a spy object, but got/); + + expect(function() { + matcher.compare('string'); + }).toThrowError(Error, /Expected a spy object, but got/); + }); + + it('throws an error if arguments are passed', function() { + let matcher = jasmineUnderTest.matchers.toHaveSpyInteractions(); + 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.toHaveSpyInteractions(); + 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 a spy object with spies, but object has no spies/ + ); + }); +}); diff --git a/src/core/matchers/requireMatchers.js b/src/core/matchers/requireMatchers.js old mode 100644 new mode 100755 index c14ceac9..3c9d6983 --- a/src/core/matchers/requireMatchers.js +++ b/src/core/matchers/requireMatchers.js @@ -27,6 +27,7 @@ getJasmineRequireObj().requireMatchers = function(jRequire, j$) { 'toHaveBeenCalledTimes', 'toHaveBeenCalledWith', 'toHaveClass', + 'toHaveSpyInteractions', 'toMatch', 'toThrow', 'toThrowError', diff --git a/src/core/matchers/toHaveSpyInteractions.js b/src/core/matchers/toHaveSpyInteractions.js new file mode 100755 index 00000000..ca091ead --- /dev/null +++ b/src/core/matchers/toHaveSpyInteractions.js @@ -0,0 +1,74 @@ +getJasmineRequireObj().toHaveSpyInteractions = function(j$) { + var getErrorMsg = j$.formatErrorMsg( + '', + 'expect().toHaveSpyInteractions()' + ); + + /** + * {@link expect} the actual (a {@link SpyObj}) spies to have been called. + * @function + * @name matchers#toHaveSpyInteractions + * @example + * expect(mySpyObj).toHaveSpyInteractions(); + * expect(mySpyObj).not.toHaveSpyInteractions(); + */ + function toHaveSpyInteractions(matchersUtil) { + return { + compare: function(actual) { + var result = {}; + + if (!j$.isObject_(actual)) { + throw new Error( + getErrorMsg('Expected a spy object, but got ' + typeof actual + '.') + ); + } + + if (arguments.length > 1) { + throw new Error(getErrorMsg('Does not take arguments')); + } + + result.pass = false; + let hasSpy = false; + const calledSpies = []; + for (const spy of Object.values(actual)) { + if (!j$.isSpy(spy)) continue; + hasSpy = true; + + if (spy.calls.any()) { + result.pass = true; + calledSpies.push([spy.and.identity, spy.calls.count()]); + } + } + + if (!hasSpy) { + throw new Error( + getErrorMsg( + 'Expected a spy object with spies, but object has no spies.' + ) + ); + } + + let resultMessage; + if (result.pass) { + resultMessage = + 'Expected spy object spies not to have been called, ' + + 'but the following spies were called: '; + resultMessage += calledSpies + .map(([spyName, spyCount]) => { + return `${spyName} called ${spyCount} time(s)`; + }) + .join(', '); + } else { + resultMessage = + 'Expected spy object spies to have been called, ' + + 'but no spies were called.'; + } + result.message = resultMessage; + + return result; + } + }; + } + + return toHaveSpyInteractions; +};