* Adds toHaveSpyInteractions matcher
* Merges #1959 from @nitobuenida
* Fixes #1568
This commit is contained in:
Steve Gravrock
2022-03-18 11:04:37 -07:00
5 changed files with 288 additions and 0 deletions

View File

@@ -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(
'<toHaveSpyInteractions>',
'expect(<spyObj>).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(
'<toMatch>',

22
spec/core/integration/MatchersSpec.js Normal file → Executable file
View File

@@ -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$/);

View File

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

1
src/core/matchers/requireMatchers.js Normal file → Executable file
View File

@@ -27,6 +27,7 @@ getJasmineRequireObj().requireMatchers = function(jRequire, j$) {
'toHaveBeenCalledTimes',
'toHaveBeenCalledWith',
'toHaveClass',
'toHaveSpyInteractions',
'toMatch',
'toThrow',
'toThrowError',

View File

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