Compare commits

..

27 Commits

Author SHA1 Message Date
Steve Gravrock
d5872bba66 Fixed Safari footnotes in release notes 2025-02-08 11:13:28 -08:00
Steve Gravrock
c4f4edda1b Bump version to 5.6.0 2025-02-08 11:10:06 -08:00
Steve Gravrock
cf057b6631 Fixed parse error from jsdoc
"arguments" isn't a legal argument name in strict mode JS. The JS
runtimes that Jasmine runs in allow it, but jsdoc doesn't.
2025-02-08 10:25:41 -08:00
Steve Gravrock
2a7a157713 toHaveNoOtherSpyInteractions message tweaks 2025-01-20 11:31:47 -08:00
Steve Gravrock
7463fe511b Match messages exactly in toHaveNoOtherSpyInteractions specs 2025-01-20 11:31:18 -08:00
Steve Gravrock
1b724daa10 Merge branch 'Eradev-issue-1991'
* Merges #2051 from @Eradev
* Fixes #1991
2025-01-20 11:31:06 -08:00
Eradev
888d3b6250 Modified error message 2025-01-18 17:55:00 -05:00
Eradev
289afbf0a6 Remove jsdoc from unverifiedCount 2025-01-18 17:25:07 -05:00
Steve Gravrock
9b89bee4f5 Demote Safari to best-effort support 2025-01-18 11:31:17 -08:00
Eradev
3f8f488a58 Fix broken tests 2025-01-11 12:41:28 -05:00
Steve Gravrock
592d47e971 Update copyright date 2025-01-11 08:33:37 -08:00
Eradev
4732012f1c toHaveNoOtherSpyInteractions implementation 2025-01-10 21:05:12 -05:00
Steve Gravrock
7683325d68 Improved specs for async matcher error messages 2024-12-31 10:10:40 -08:00
Steve Gravrock
03d665e243 Merge branch 'improve_toBeRejectedWithError' of https://github.com/andiz2/jasmine 2024-12-31 10:07:53 -08:00
Andrei D
a1591da25d improved error msg on toBeRehectedWithError and all other built-in async matchers 2024-12-22 17:43:47 +02:00
Steve Gravrock
1f1e1209d2 Merge branch 'add_toBeNullish' of https://github.com/MattMcCherry/jasmine
* Merges #2045 from @MattMcCherry
2024-12-12 17:30:55 -08:00
Steve Gravrock
a389905a38 Merge branch 'feature/matcher-toHaveClasses' of https://github.com/aYorky/jasmine
* Merges #2046 from @aYorky
2024-12-11 19:19:58 -08:00
Matt McCherry
36dd6b07d1 add integration test for a falsy value 2024-12-10 11:39:44 +00:00
Matt McCherry
2f3689713b add tests for falsy values 2024-12-10 11:31:16 +00:00
Alex Yorkovich
819fab7b58 simplified match condition 2024-12-07 13:14:25 -06:00
Steve Gravrock
e5ca1f37f1 Use discussions for support requests, not issues 2024-12-07 09:32:57 -08:00
Alex Yorkovich
c3650ea7c7 updated release number 2024-12-04 12:34:36 -06:00
Alex Yorkovich
1805337424 Added new toHaveClasses matcher; tests included 2024-12-04 12:20:15 -06:00
Matt McCherry
27bb6ebac1 reset jasmine.js 2024-12-02 10:34:55 +00:00
Matt McCherry
580323c221 run prettier and fix tests 2024-12-02 10:34:55 +00:00
Matt McCherry
d9286c549f add integration test for toBeNullish 2024-12-02 10:34:55 +00:00
Matt McCherry
26dfa6d257 Add .toBeNullish matcher 2024-12-02 10:34:49 +00:00
42 changed files with 871 additions and 127 deletions

View File

@@ -1,5 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Questions, requests for help, etc
url: https://github.com/jasmine/jasmine/discussions/new/choose
about: Please start a discussion.
- name: Issues with the `jasmine` CLI
url: https://github.com/jasmine/jasmine-npm/issues
about: Please create issues related to the `jasmine` package in its repository.

View File

@@ -1,73 +0,0 @@
name: Question or Support Request
description: I need help using Jasmine
labels: ["question"]
body:
- type: markdown
attributes:
value: |
Jasmine is supported by volunteers working in their free time. Although we're generally willing to help, we're going to ask you to put in some effort first to help us help you.
## Troubleshooting
Please take the time to rule out problems with your code or third party libraries before opening an issue. If you're running into an error, try to determine whether the error is coming from Jasmine, another library, or your own code.
Check the [FAQ](https://jasmine.github.io/pages/faq.html) and any other relevant [documentation](https://jasmine.github.io/pages/docs_home.html) to see if your question has already been answered. Consider searching [Stack Overflow](https://stackoverflow.com/questions/tagged/jasmine) and past issues in this repository for related questions as well.
## Special troubleshooting steps for asynchronous scenarios
If the issue has to do with testing asynchronous code, please read the [async tutorial](https://jasmine.github.io/tutorials/async) and the async section of the FAQ. In particular, check for the following common errors:
* Are you trying to write a synchronous test for asynchronous code?
* Does the test signal completion before the code under test finishes?
* Do expectations run before the code that they're trying to verify?
## Consider asking Angular questions in an Angular forum
Questions like "how do I test this Angular service" are mostly about Angular, not Jasmine. You'll likely get better responses in an Angular forum. Here's a rule of thumb: If you can't demonstrate the problem without Angular, you probably have an Angular question.
## Try the latest version of Jasmine
If at all possible, upgrade to the latest versions of `jasmine-core` and any other relevant packages (e.g. `jasmine`, `jasmine-browser-runner`).
## Put together a [minimal, reproducible example](https://stackoverflow.com/help/minimal-reproducible-example)
Please help us help you by creating a minimal but complete setup that demonstrates the problem. Remove any code and libraries that aren't absolutely necessary, but make sure it doesn't depend on any code you haven't included. In many cases a simple code snippet is enough. In cases involving external libraries, *especially* Karma or Angular, we're likely to need a runable Git repository or jsbin/stackblitz/etc.
- type: textarea
id: question
attributes:
label: Your question
description: Clearly describe what you'd like help with.
validations:
required: true
- type: textarea
id: code-sample
attributes:
label: Example code that demonstrates the problem
description: Please include either a code snippet that demonstrates the problem or a link to a repository or jsbin/stackblitz/etc containing a minimal, reproducible example as described above.
render: JavaScript
validations:
required: true
- type: input
id: jasmine-core-version
attributes:
label: jasmine-core version
validations:
required: true
- type: textarea
id: other-versions
attributes:
label: Versions of other relevant packages
placeholder: |
jasmine-browser-runner 1.2.0
fancy-reporter 132.4.8
- type: input
id: browser-or-node-version
attributes:
label: Node.js or browser version
placeholder: E.g. "node 16.2.0" or "Safari 15"
validations:
required: true
- type: input
id: os
attributes:
label: Operating System
placeholder: E.g. "Windows 10", "MacOS 12.5", "MCC Interim Linux 0.99.p8"
validations:
required: true

View File

@@ -1,5 +1,5 @@
Copyright (c) 2008-2019 Pivotal Labs
Copyright (c) 2008-2024 The Jasmine developers
Copyright (c) 2008-2025 The Jasmine developers
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the

View File

@@ -30,7 +30,7 @@ Microsoft Edge) as well as Node.
| Environment | Supported versions |
|-------------------|----------------------------|
| Node | 18, 20, 22 |
| Safari | 15-17 |
| Safari | 15*, 16*, 17* |
| Chrome | Evergreen |
| Firefox | Evergreen, 102*, 115*, 128 |
| Edge | Evergreen |
@@ -39,9 +39,9 @@ For evergreen browsers, each version of Jasmine is tested against the version of
at the time of release. Other browsers, as well as older & newer versions of some supported browsers, are likely to work.
However, Jasmine isn't tested against them and they aren't actively supported.
\* Environments that are past end of life are supported on a best-effort basis.
They may be dropped in a future minor release of Jasmine if continued support
becomes impractical.
\* Supported on a best-effort basis. Support for these versions may be dropped
if it becomes impractical, and bugs affecting only these versions may not be
treated as release blockers.
To find out what environments work with a particular Jasmine release, see the [release notes](https://github.com/jasmine/jasmine/tree/main/release_notes).
@@ -60,5 +60,5 @@ To find out what environments work with a particular Jasmine release, see the [r
* Sheel Choksi
Copyright (c) 2008-2019 Pivotal Labs<br>
Copyright (c) 2008-2024 The Jasmine developers<br>
Copyright (c) 2008-2025 The Jasmine developers<br>
This software is licensed under the [MIT License](https://github.com/jasmine/jasmine/blob/main/LICENSE).

View File

@@ -1,6 +1,6 @@
/*
Copyright (c) 2008-2019 Pivotal Labs
Copyright (c) 2008-2024 The Jasmine developers
Copyright (c) 2008-2025 The Jasmine developers
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the

View File

@@ -1,6 +1,6 @@
/*
Copyright (c) 2008-2019 Pivotal Labs
Copyright (c) 2008-2024 The Jasmine developers
Copyright (c) 2008-2025 The Jasmine developers
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the

View File

@@ -1,6 +1,6 @@
/*
Copyright (c) 2008-2019 Pivotal Labs
Copyright (c) 2008-2024 The Jasmine developers
Copyright (c) 2008-2025 The Jasmine developers
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the

View File

@@ -1,6 +1,6 @@
/*
Copyright (c) 2008-2019 Pivotal Labs
Copyright (c) 2008-2024 The Jasmine developers
Copyright (c) 2008-2025 The Jasmine developers
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
@@ -150,6 +150,7 @@ getJasmineRequireObj().requireMatchers = function(jRequire, j$) {
'toBeTrue',
'toBeTruthy',
'toBeUndefined',
'toBeNullish',
'toContain',
'toEqual',
'toHaveSize',
@@ -159,7 +160,9 @@ getJasmineRequireObj().requireMatchers = function(jRequire, j$) {
'toHaveBeenCalledTimes',
'toHaveBeenCalledWith',
'toHaveClass',
'toHaveClasses',
'toHaveSpyInteractions',
'toHaveNoOtherSpyInteractions',
'toMatch',
'toThrow',
'toThrowError',
@@ -2896,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;
@@ -4332,7 +4339,9 @@ getJasmineRequireObj().toBePending = function(j$) {
return {
compare: function(actual) {
if (!j$.isPromiseLike(actual)) {
throw new Error('Expected toBePending to be called on a promise.');
throw new Error(
`Expected toBePending to be called on a promise but was on a ${typeof actual}.`
);
}
const want = {};
return Promise.race([actual, Promise.resolve(want)]).then(
@@ -4364,7 +4373,9 @@ getJasmineRequireObj().toBeRejected = function(j$) {
return {
compare: function(actual) {
if (!j$.isPromiseLike(actual)) {
throw new Error('Expected toBeRejected to be called on a promise.');
throw new Error(
`Expected toBeRejected to be called on a promise but was on a ${typeof actual}.`
);
}
return actual.then(
function() {
@@ -4397,7 +4408,7 @@ getJasmineRequireObj().toBeRejectedWith = function(j$) {
compare: function(actualPromise, expectedValue) {
if (!j$.isPromiseLike(actualPromise)) {
throw new Error(
'Expected toBeRejectedWith to be called on a promise.'
`Expected toBeRejectedWith to be called on a promise but was on a ${typeof actualPromise}.`
);
}
@@ -4461,7 +4472,7 @@ getJasmineRequireObj().toBeRejectedWithError = function(j$) {
compare: function(actualPromise, arg1, arg2) {
if (!j$.isPromiseLike(actualPromise)) {
throw new Error(
'Expected toBeRejectedWithError to be called on a promise.'
`Expected toBeRejectedWithError to be called on a promise but was on a ${typeof actualPromise}.`
);
}
@@ -4579,7 +4590,9 @@ getJasmineRequireObj().toBeResolved = function(j$) {
return {
compare: function(actual) {
if (!j$.isPromiseLike(actual)) {
throw new Error('Expected toBeResolved to be called on a promise.');
throw new Error(
`Expected toBeResolved to be called on a promise but was on a ${typeof actual}.`
);
}
return actual.then(
@@ -4619,7 +4632,9 @@ getJasmineRequireObj().toBeResolvedTo = function(j$) {
return {
compare: function(actualPromise, expectedValue) {
if (!j$.isPromiseLike(actualPromise)) {
throw new Error('Expected toBeResolvedTo to be called on a promise.');
throw new Error(
`Expected toBeResolvedTo to be called on a promise but was on a ${typeof actualPromise}.`
);
}
function prefix(passed) {
@@ -6010,6 +6025,28 @@ getJasmineRequireObj().toBeNull = function() {
return toBeNull;
};
getJasmineRequireObj().toBeNullish = function() {
/**
* {@link expect} the actual value to be `null` or `undefined`.
* @function
* @name matchers#toBeNullish
* @since 5.6.0
* @example
* expect(result).toBeNullish():
*/
function toBeNullish() {
return {
compare: function(actual) {
return {
pass: null === actual || void 0 === actual
};
}
};
}
return toBeNullish;
};
getJasmineRequireObj().toBePositiveInfinity = function(j$) {
/**
* {@link expect} the actual value to be `Infinity` (infinity).
@@ -6199,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.';
@@ -6263,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 +
@@ -6319,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),
@@ -6328,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:
@@ -6349,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') +
@@ -6360,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();
});
}
@@ -6393,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()
};
@@ -6442,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;
}
@@ -6514,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 (
@@ -6605,6 +6673,127 @@ getJasmineRequireObj().toHaveClass = function(j$) {
return toHaveClass;
};
getJasmineRequireObj().toHaveClasses = function(j$) {
/**
* {@link expect} the actual value to be a DOM element that has the expected classes
* @function
* @name matchers#toHaveClasses
* @since 5.6.0
* @param {Object} expected - The class names to test for
* @example
* const el = document.createElement('div');
* el.className = 'foo bar baz';
* expect(el).toHaveClasses(['bar', 'baz']);
*/
function toHaveClasses(matchersUtil) {
return {
compare: function(actual, expected) {
if (!isElement(actual)) {
throw new Error(matchersUtil.pp(actual) + ' is not a DOM element');
}
return {
pass: expected.every(e => actual.classList.contains(e))
};
}
};
}
function isElement(maybeEl) {
return (
maybeEl && maybeEl.classList && j$.isFunction_(maybeEl.classList.contains)
);
}
return toHaveClasses;
};
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 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 a spy object to have other spy interactions but it didn't.";
} else {
const ppUnexpectedCalls = unexpectedCalls
.map(([spyName, args]) => ` ${spyName} called with ${args}`)
.join(',\n');
result.message =
'Expected a spy object to have no other spy interactions, but it had the following calls:\n' +
ppUnexpectedCalls +
'.';
}
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.
@@ -9183,7 +9372,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);
@@ -11041,5 +11231,5 @@ getJasmineRequireObj().UserContext = function(j$) {
};
getJasmineRequireObj().version = function() {
return '5.5.0';
return '5.6.0';
};

View File

@@ -1,7 +1,7 @@
{
"name": "jasmine-core",
"license": "MIT",
"version": "5.5.0",
"version": "5.6.0",
"repository": {
"type": "git",
"url": "https://github.com/jasmine/jasmine.git"

51
release_notes/5.6.0.md Normal file
View File

@@ -0,0 +1,51 @@
# Jasmine Core 5.6.0 Release Notes
## Changes
* Added [toHaveNoOtherSpyInteractions](https://jasmine.github.io/api/5.6/matchers.html#toHaveNoOtherSpyInteractions) matcher
* Merges [#2051](https://github.com/jasmine/jasmine/pull/2051) from @Eradev
* Fixes [#1991](https://github.com/jasmine/jasmine/issues/1991)
* Added [toBeNullish](https://jasmine.github.io/api/5.6/matchers.html#toBeNullish) matcher
* Merges [#2045](https://github.com/jasmine/jasmine/pull/2045) from @MattMcCherry
* Improved error messages when non-promises are passed to built-in async matchers
* Merges [#2049](https://github.com/jasmine/jasmine/pull/2049) from @andiz2
* Fixes [#2037](https://github.com/jasmine/jasmine/issues/2037)
* Added [toHaveClasses](https://jasmine.github.io/api/5.6/matchers.html#toHaveClasses) matcher
* Merges [#2046](https://github.com/jasmine/jasmine/pull/2046) from @aYorky
## Documentation updates
* Demoted Safari to best-effort support
Due to limited availability of Safari versions for contributors and maintainers
as well as in CI, Safari will be supported on the same best-effort basis as
environments that are past end of life, such as previous Firefox ESR versions.
See [this discussion](https://github.com/jasmine/jasmine/discussions/2050) for
more information about why this change was made and what to expect.
## Supported environments
This version has been tested in the following environments.
| Environment | Supported versions |
|-------------------|-------------------------|
| Node | 18, 20, 22 |
| Safari | 15**, 16**, 17** |
| Chrome | 133* |
| Firefox | 102**, 115**, 128, 135* |
| Edge | 132* |
\* Evergreen browser. Each version of Jasmine is tested against the latest
version available at release time.<br>
\** Supported on a best-effort basis. Support for these versions may be dropped
if it becomes impractical, and bugs affecting only these versions may not be
treated as release blockers.
------
_Release Notes generated with _[Anchorman](http://github.com/infews/anchorman)_

View File

@@ -469,6 +469,24 @@ describe('Matchers (Integration)', function() {
});
});
describe('toBeNullish', function() {
verifyPasses(function(env) {
env.expect(undefined).toBeNullish();
});
verifyPasses(function(env) {
env.expect(null).toBeNullish();
});
verifyFails(function(env) {
env.expect(1).toBeNullish();
});
verifyFails(function(env) {
env.expect('').toBeNullish();
});
});
describe('toContain', function() {
verifyPasses(function(env) {
env.addCustomEqualityTester(function(a, b) {
@@ -621,6 +639,20 @@ describe('Matchers (Integration)', function() {
});
});
describe('toHaveClasses', function() {
verifyPasses(function(env) {
const el = specHelpers.domHelpers.createElementWithClassName(
'foo bar baz'
);
env.expect(el).toHaveClasses(['bar', 'baz']);
});
verifyFails(function(env) {
const el = specHelpers.domHelpers.createElementWithClassName('foo bar');
env.expect(el).toHaveClasses(['bar', 'baz']);
});
});
describe('toHaveSpyInteractions', function() {
let spyObj;
beforeEach(function() {
@@ -643,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$/);

View File

@@ -38,6 +38,8 @@ describe('toBePending', function() {
return matcher.compare(actual);
}
expect(f).toThrowError('Expected toBePending to be called on a promise.');
expect(f).toThrowError(
'Expected toBePending to be called on a promise but was on a string.'
);
});
});

View File

@@ -28,6 +28,8 @@ describe('toBeRejected', function() {
return matcher.compare(actual);
}
expect(f).toThrowError('Expected toBeRejected to be called on a promise.');
expect(f).toThrowError(
'Expected toBeRejected to be called on a promise but was on a string.'
);
});
});

View File

@@ -232,7 +232,7 @@ describe('#toBeRejectedWithError', function() {
}
expect(f).toThrowError(
'Expected toBeRejectedWithError to be called on a promise.'
'Expected toBeRejectedWithError to be called on a promise but was on a string.'
);
});
});

View File

@@ -83,7 +83,7 @@ describe('#toBeRejectedWith', function() {
}
expect(f).toThrowError(
'Expected toBeRejectedWith to be called on a promise.'
'Expected toBeRejectedWith to be called on a promise but was on a string.'
);
});
});

View File

@@ -35,6 +35,8 @@ describe('toBeResolved', function() {
return matcher.compare(actual);
}
expect(f).toThrowError('Expected toBeResolved to be called on a promise.');
expect(f).toThrowError(
'Expected toBeResolved to be called on a promise but was on a string.'
);
});
});

View File

@@ -93,7 +93,7 @@ describe('#toBeResolvedTo', function() {
}
expect(f).toThrowError(
'Expected toBeResolvedTo to be called on a promise.'
'Expected toBeResolvedTo to be called on a promise but was on a string.'
);
});
});

View File

@@ -0,0 +1,57 @@
describe('toBeNullish', function() {
it('passes for null values', function() {
const matcher = jasmineUnderTest.matchers.toBeNullish();
const result = matcher.compare(null);
expect(result.pass).toBe(true);
});
it('passes for undefined values', function() {
const matcher = jasmineUnderTest.matchers.toBeNullish();
const result = matcher.compare(void 0);
expect(result.pass).toBe(true);
});
it('fails when matching defined values', function() {
const matcher = jasmineUnderTest.matchers.toBeNullish();
const result = matcher.compare('foo');
expect(result.pass).toBe(false);
});
describe('falsy values', () => {
it('fails for 0', function() {
const matcher = jasmineUnderTest.matchers.toBeNullish();
const result = matcher.compare(0);
expect(result.pass).toBe(false);
});
it('fails for -0', function() {
const matcher = jasmineUnderTest.matchers.toBeNullish();
const result = matcher.compare(-0);
expect(result.pass).toBe(false);
});
it('fails for empty string', function() {
const matcher = jasmineUnderTest.matchers.toBeNullish();
const result = matcher.compare('');
expect(result.pass).toBe(false);
});
it('fails for false', function() {
const matcher = jasmineUnderTest.matchers.toBeNullish();
const result = matcher.compare(false);
expect(result.pass).toBe(false);
});
it('fails for NaN', function() {
const matcher = jasmineUnderTest.matchers.toBeNullish();
const result = matcher.compare(NaN);
expect(result.pass).toBe(false);
});
it('fails for 0n', function() {
const matcher = jasmineUnderTest.matchers.toBeNullish();
const result = matcher.compare(BigInt(0));
expect(result.pass).toBe(false);
});
});
});

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'
);
});
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);
}).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.'
);
});
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.'
);
});
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

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

View File

@@ -0,0 +1,48 @@
describe('toHaveClasses', function() {
it('fails for a DOM element that lacks all the expected classes', function() {
const matcher = jasmineUnderTest.matchers.toHaveClasses(),
result = matcher.compare(
specHelpers.domHelpers.createElementWithClassName(''),
['foo', 'bar']
);
expect(result.pass).toBe(false);
});
it('passes for a DOM element that has all the expected classes', function() {
const matcher = jasmineUnderTest.matchers.toHaveClasses(),
el = specHelpers.domHelpers.createElementWithClassName('foo bar baz');
expect(matcher.compare(el, ['foo', 'bar']).pass).toBe(true);
});
it('fails for a DOM element that only has some matching classes', function() {
const matcher = jasmineUnderTest.matchers.toHaveClasses(),
el = specHelpers.domHelpers.createElementWithClassName('foo bar');
expect(matcher.compare(el, ['foo', 'can']).pass).toBe(false);
});
it('throws an exception when actual is not a DOM element', function() {
const matcher = jasmineUnderTest.matchers.toHaveClasses({
pp: jasmineUnderTest.makePrettyPrinter()
});
expect(function() {
matcher.compare('x', ['foo']);
}).toThrowError("'x' is not a DOM element");
expect(function() {
matcher.compare(undefined, ['foo']);
}).toThrowError('undefined is not a DOM element');
const textNode = specHelpers.domHelpers.document.createTextNode('');
expect(function() {
matcher.compare(textNode, ['foo']);
}).toThrowError('HTMLNode is not a DOM element');
expect(function() {
matcher.compare({ classList: '' }, ['foo']);
}).toThrowError("Object({ classList: '' }) is not a DOM element");
});
});

View File

@@ -0,0 +1,154 @@
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).toEqual(
'Expected a spy object to have no other spy interactions, but it had the following calls:\n' +
" NewClass.spyA called with [ 'x' ]."
);
});
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).toEqual(
'Expected a spy object to have no other spy interactions, but it had the following calls:\n' +
' NewClass.spyA called with [ ],\n' +
' NewClass.spyB called with [ ].'
);
});
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).toEqual(
"Expected a spy object 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('mySpyObj', ['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('mySpyObj', ['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();
});
});

View File

@@ -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;

View File

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

View File

@@ -12,7 +12,9 @@ getJasmineRequireObj().toBePending = function(j$) {
return {
compare: function(actual) {
if (!j$.isPromiseLike(actual)) {
throw new Error('Expected toBePending to be called on a promise.');
throw new Error(
`Expected toBePending to be called on a promise but was on a ${typeof actual}.`
);
}
const want = {};
return Promise.race([actual, Promise.resolve(want)]).then(

View File

@@ -14,7 +14,9 @@ getJasmineRequireObj().toBeRejected = function(j$) {
return {
compare: function(actual) {
if (!j$.isPromiseLike(actual)) {
throw new Error('Expected toBeRejected to be called on a promise.');
throw new Error(
`Expected toBeRejected to be called on a promise but was on a ${typeof actual}.`
);
}
return actual.then(
function() {

View File

@@ -16,7 +16,7 @@ getJasmineRequireObj().toBeRejectedWith = function(j$) {
compare: function(actualPromise, expectedValue) {
if (!j$.isPromiseLike(actualPromise)) {
throw new Error(
'Expected toBeRejectedWith to be called on a promise.'
`Expected toBeRejectedWith to be called on a promise but was on a ${typeof actualPromise}.`
);
}

View File

@@ -19,7 +19,7 @@ getJasmineRequireObj().toBeRejectedWithError = function(j$) {
compare: function(actualPromise, arg1, arg2) {
if (!j$.isPromiseLike(actualPromise)) {
throw new Error(
'Expected toBeRejectedWithError to be called on a promise.'
`Expected toBeRejectedWithError to be called on a promise but was on a ${typeof actualPromise}.`
);
}

View File

@@ -14,7 +14,9 @@ getJasmineRequireObj().toBeResolved = function(j$) {
return {
compare: function(actual) {
if (!j$.isPromiseLike(actual)) {
throw new Error('Expected toBeResolved to be called on a promise.');
throw new Error(
`Expected toBeResolved to be called on a promise but was on a ${typeof actual}.`
);
}
return actual.then(

View File

@@ -15,7 +15,9 @@ getJasmineRequireObj().toBeResolvedTo = function(j$) {
return {
compare: function(actualPromise, expectedValue) {
if (!j$.isPromiseLike(actualPromise)) {
throw new Error('Expected toBeResolvedTo to be called on a promise.');
throw new Error(
`Expected toBeResolvedTo to be called on a promise but was on a ${typeof actualPromise}.`
);
}
function prefix(passed) {

View File

@@ -18,6 +18,7 @@ getJasmineRequireObj().requireMatchers = function(jRequire, j$) {
'toBeTrue',
'toBeTruthy',
'toBeUndefined',
'toBeNullish',
'toContain',
'toEqual',
'toHaveSize',
@@ -27,7 +28,9 @@ getJasmineRequireObj().requireMatchers = function(jRequire, j$) {
'toHaveBeenCalledTimes',
'toHaveBeenCalledWith',
'toHaveClass',
'toHaveClasses',
'toHaveSpyInteractions',
'toHaveNoOtherSpyInteractions',
'toMatch',
'toThrow',
'toThrowError',

View File

@@ -0,0 +1,21 @@
getJasmineRequireObj().toBeNullish = function() {
/**
* {@link expect} the actual value to be `null` or `undefined`.
* @function
* @name matchers#toBeNullish
* @since 5.6.0
* @example
* expect(result).toBeNullish():
*/
function toBeNullish() {
return {
compare: function(actual) {
return {
pass: null === actual || void 0 === actual
};
}
};
}
return toBeNullish;
};

View File

@@ -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.';

View File

@@ -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 +

View File

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

View File

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

View File

@@ -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 (

View File

@@ -0,0 +1,34 @@
getJasmineRequireObj().toHaveClasses = function(j$) {
/**
* {@link expect} the actual value to be a DOM element that has the expected classes
* @function
* @name matchers#toHaveClasses
* @since 5.6.0
* @param {Object} expected - The class names to test for
* @example
* const el = document.createElement('div');
* el.className = 'foo bar baz';
* expect(el).toHaveClasses(['bar', 'baz']);
*/
function toHaveClasses(matchersUtil) {
return {
compare: function(actual, expected) {
if (!isElement(actual)) {
throw new Error(matchersUtil.pp(actual) + ' is not a DOM element');
}
return {
pass: expected.every(e => actual.classList.contains(e))
};
}
};
}
function isElement(maybeEl) {
return (
maybeEl && maybeEl.classList && j$.isFunction_(maybeEl.classList.contains)
);
}
return toHaveClasses;
};

View File

@@ -0,0 +1,85 @@
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 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 a spy object to have other spy interactions but it didn't.";
} else {
const ppUnexpectedCalls = unexpectedCalls
.map(([spyName, args]) => ` ${spyName} called with ${args}`)
.join(',\n');
result.message =
'Expected a spy object to have no other spy interactions, but it had the following calls:\n' +
ppUnexpectedCalls +
'.';
}
return result;
}
};
}
return toHaveNoOtherSpyInteractions;
};