toHaveNoOtherSpyInteractions implementation

This commit is contained in:
Eradev
2025-01-10 21:05:12 -05:00
parent 7683325d68
commit 4732012f1c
16 changed files with 354 additions and 12 deletions

View File

@@ -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() { describe('toMatch', function() {
verifyPasses(function(env) { verifyPasses(function(env) {
env.expect('foo').toMatch(/oo$/); env.expect('foo').toMatch(/oo$/);

View File

@@ -112,4 +112,20 @@ describe('toHaveBeenCalledBefore', function() {
'Expected spy first spy to not have been called before spy second spy, but it was' '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);
});
}); });

View File

@@ -105,4 +105,18 @@ describe('toHaveBeenCalledOnceWith', function() {
matcher.compare(fn); matcher.compare(fn);
}).toThrowError(/Expected a spy, but got Function./); }).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);
});
}); });

View File

@@ -50,4 +50,16 @@ describe('toHaveBeenCalled', function() {
'Expected spy sample-spy to have been called.' '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);
});
}); });

View File

@@ -87,4 +87,17 @@ describe('toHaveBeenCalledTimes', function() {
' times.' ' 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);
});
}); });

View File

@@ -92,4 +92,19 @@ describe('toHaveBeenCalledWith', function() {
matcher.compare(fn); matcher.compare(fn);
}).toThrowError(/Expected a spy, but got Function./); }).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);
});
}); });

View File

@@ -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();
});
});

View File

@@ -125,6 +125,16 @@ getJasmineRequireObj().CallTracker = function(j$) {
this.saveArgumentsByValue = function() { this.saveArgumentsByValue = function() {
opts.cloneArgs = true; 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; return CallTracker;

View File

@@ -26,7 +26,8 @@ getJasmineRequireObj().Spy = function(j$) {
const callData = { const callData = {
object: context, object: context,
invocationOrder: nextOrder(), invocationOrder: nextOrder(),
args: Array.prototype.slice.apply(args) args: Array.prototype.slice.apply(args),
verified: false
}; };
callTracker.track(callData); callTracker.track(callData);

View File

@@ -30,6 +30,7 @@ getJasmineRequireObj().requireMatchers = function(jRequire, j$) {
'toHaveClass', 'toHaveClass',
'toHaveClasses', 'toHaveClasses',
'toHaveSpyInteractions', 'toHaveSpyInteractions',
'toHaveNoOtherSpyInteractions',
'toMatch', 'toMatch',
'toThrow', 'toThrow',
'toThrowError', 'toThrowError',

View File

@@ -34,6 +34,8 @@ getJasmineRequireObj().toHaveBeenCalled = function(j$) {
result.pass = actual.calls.any(); result.pass = actual.calls.any();
actual.calls.all().forEach(call => (call.verified = true));
result.message = result.pass result.message = result.pass
? 'Expected spy ' + actual.and.identity + ' not to have been called.' ? 'Expected spy ' + actual.and.identity + ' not to have been called.'
: 'Expected spy ' + actual.and.identity + ' to have been called.'; : 'Expected spy ' + actual.and.identity + ' to have been called.';

View File

@@ -50,6 +50,9 @@ getJasmineRequireObj().toHaveBeenCalledBefore = function(j$) {
result.pass = latest1stSpyCall < first2ndSpyCall; result.pass = latest1stSpyCall < first2ndSpyCall;
if (result.pass) { if (result.pass) {
firstSpy.calls.mostRecent().verified = true;
latterSpy.calls.first().verified = true;
result.message = result.message =
'Expected spy ' + 'Expected spy ' +
firstSpy.and.identity + firstSpy.and.identity +

View File

@@ -13,7 +13,7 @@ getJasmineRequireObj().toHaveBeenCalledOnceWith = function(j$) {
* @example * @example
* expect(mySpy).toHaveBeenCalledOnceWith('foo', 'bar', 2); * expect(mySpy).toHaveBeenCalledOnceWith('foo', 'bar', 2);
*/ */
function toHaveBeenCalledOnceWith(util) { function toHaveBeenCalledOnceWith(matchersUtil) {
return { return {
compare: function() { compare: function() {
const args = Array.prototype.slice.call(arguments, 0), const args = Array.prototype.slice.call(arguments, 0),
@@ -22,20 +22,29 @@ getJasmineRequireObj().toHaveBeenCalledOnceWith = function(j$) {
if (!j$.isSpy(actual)) { if (!j$.isSpy(actual)) {
throw new Error( 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 const prettyPrintedCalls = actual.calls
.allArgs() .allArgs()
.map(function(argsForCall) { .map(function(argsForCall) {
return ' ' + util.pp(argsForCall); return ' ' + matchersUtil.pp(argsForCall);
}); });
if ( if (
actual.calls.count() === 1 && 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 { return {
pass: true, pass: true,
message: message:
@@ -43,7 +52,7 @@ getJasmineRequireObj().toHaveBeenCalledOnceWith = function(j$) {
actual.and.identity + actual.and.identity +
' to have been called 0 times, multiple times, or once, but with arguments different from:\n' + ' to have been called 0 times, multiple times, or once, but with arguments different from:\n' +
' ' + ' ' +
util.pp(expectedArgs) + matchersUtil.pp(expectedArgs) +
'\n' + '\n' +
'But the actual call was:\n' + 'But the actual call was:\n' +
prettyPrintedCalls.join(',\n') + prettyPrintedCalls.join(',\n') +
@@ -54,7 +63,7 @@ getJasmineRequireObj().toHaveBeenCalledOnceWith = function(j$) {
function getDiffs() { function getDiffs() {
return actual.calls.allArgs().map(function(argsForCall, callIx) { return actual.calls.allArgs().map(function(argsForCall, callIx) {
const diffBuilder = new j$.DiffBuilder(); const diffBuilder = new j$.DiffBuilder();
util.equals(argsForCall, expectedArgs, diffBuilder); matchersUtil.equals(argsForCall, expectedArgs, diffBuilder);
return diffBuilder.getMessage(); return diffBuilder.getMessage();
}); });
} }
@@ -87,7 +96,7 @@ getJasmineRequireObj().toHaveBeenCalledOnceWith = function(j$) {
actual.and.identity + actual.and.identity +
' to have been called only once, and with given args:\n' + ' to have been called only once, and with given args:\n' +
' ' + ' ' +
util.pp(expectedArgs) + matchersUtil.pp(expectedArgs) +
'\n' + '\n' +
butString() butString()
}; };

View File

@@ -36,23 +36,35 @@ getJasmineRequireObj().toHaveBeenCalledTimes = function(j$) {
} }
actual = args[0]; actual = args[0];
const calls = actual.calls.count();
const callsCount = actual.calls.count();
const timesMessage = expected === 1 ? 'once' : expected + ' times'; 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 result.message = result.pass
? 'Expected spy ' + ? 'Expected spy ' +
actual.and.identity + actual.and.identity +
' not to have been called ' + ' not to have been called ' +
timesMessage + timesMessage +
'. It was called ' + '. It was called ' +
calls + callsCount +
' times.' ' times.'
: 'Expected spy ' + : 'Expected spy ' +
actual.and.identity + actual.and.identity +
' to have been called ' + ' to have been called ' +
timesMessage + timesMessage +
'. It was called ' + '. It was called ' +
calls + callsCount +
' times.'; ' times.';
return result; return result;
} }

View File

@@ -44,6 +44,11 @@ getJasmineRequireObj().toHaveBeenCalledWith = function(j$) {
} }
if (matchersUtil.contains(actual.calls.allArgs(), expectedArgs)) { 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.pass = true;
result.message = function() { result.message = function() {
return ( return (

View File

@@ -0,0 +1,75 @@
getJasmineRequireObj().toHaveNoOtherSpyInteractions = function(j$) {
const getErrorMsg = j$.formatErrorMsg(
'<toHaveNoOtherSpyInteractions>',
'expect(<spyObj>).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;
};