Compare commits

...

86 Commits

Author SHA1 Message Date
Steve Gravrock
7ba53b25f7 Bump version to 5.12.0 2025-10-05 12:02:56 -07:00
Steve Gravrock
dbc1f9244e Revert "Clicking a link in the HTML reporter does exact filtering"
This change broke spec filtering in Karma by changing the format of the
`spec` query parameter. Although karma-jasmine-html-reporter uses
jasmine-core's HtmlSpecFilter, karm-jasmine provides its own spec filter
that interprets the query parameters itself.

This feature may be reintroduced in 6.0 as a breaking change.

This reverts commit 8309416cb2.
2025-10-05 09:59:36 -07:00
Steve Gravrock
489b83c61b Revert "Move knowledge of query parameters out of boot1.js"
This reverts commit 6715f24fd0.
2025-10-05 09:54:25 -07:00
Steve Gravrock
c590095662 Copy 6.0.0-alpha.0 release notes from branch 2025-09-29 19:53:25 -07:00
Steve Gravrock
0688db88e9 Bump version to 5.11.0 2025-09-26 16:53:15 -07:00
Steve Gravrock
124effe04b API reference docs for QueryString 2025-09-17 20:22:47 -07:00
Steve Gravrock
418e9a7728 Convert QueryString to an ES6 class 2025-09-17 18:35:01 -07:00
Steve Gravrock
6715f24fd0 Move knowledge of query parameters out of boot1.js 2025-09-17 18:23:45 -07:00
Steve Gravrock
fa481b2bd1 API reference docs for HTML reporter and spec filters 2025-09-17 07:30:34 -07:00
Steve Gravrock
8309416cb2 Clicking a link in the HTML reporter does exact filtering
This feature requires an update to boot1.js, as shown in this commit.
Users with an older boot1.js will get the older inexact filtering.
2025-09-17 07:30:20 -07:00
Steve Gravrock
4ccc7bf3ac Document the order property of jasmineStarted and jasmineDone 2025-09-15 18:38:09 -07:00
Steve Gravrock
cca6b2aa07 Adopt forbidDuplicateNames: true in jasmine-core's own tests 2025-09-15 18:38:09 -07:00
Steve Gravrock
78940aa0fb Drop support for Safari 15
Safari 15:

* Lacks structuredClone, which is starting to become useful
* Has stack trace quirks that are not well understood, not properly handled
  by Jasmine, and can cause problems in Jasmine's own tests
* Is not widely used
* Does not run on any OS that still receives security updates
2025-09-15 18:37:59 -07:00
Steve Gravrock
3d8396da0a More precisely characterize suite/spec reporting 2025-09-13 10:10:42 -07:00
Steve Gravrock
0bf9aff195 Extract configuration out of Env 2025-09-07 15:53:24 -07:00
Steve Gravrock
55b2e8846f Disambiguate options params in Env 2025-09-06 13:04:32 -07:00
Steve Gravrock
3493519c9f Fixed global error handling when the env is executed repeatedly 2025-09-06 10:32:56 -07:00
Steve Gravrock
62b5698a99 Clean up TreeRunner onComplete callback 2025-09-06 09:42:39 -07:00
Steve Gravrock
98849882a2 rm TODO comment about integrating detectLateRejectionHandling with clearStack
In theory, resetting clearStack's inline call count every time late
rejection handling does a setTimeout should reduce the performance penalty
in some environments.  In practice, it doesn't:

* In Chrome and FF, late rejection handling has no measurable penalty.
* In Safari, resetting the inline call count actually slows things down
  considerably(!).
* In Node, clearStack doesn't use setTimeout so there is no benefit.
2025-09-06 08:32:37 -07:00
Steve Gravrock
6665c4e123 Don't remove before and after fns from the top suite 2025-09-02 10:23:44 -07:00
Steve Gravrock
3698f6fb5d Support detectLateRejectionHandling in beforeAll and afterAll 2025-09-02 09:58:46 -07:00
Steve Gravrock
60f34ec087 Unify top suite and regular suite execution 2025-09-02 08:05:30 -07:00
Steve Gravrock
91bd3f8201 Optionally test in a subset of browsers locally
When run with --not-actually-all, scripts/run-all-browsers skips all
but the oldest and newest supported Firefox and Safari versions. This
provides a faster but still quite reliable mergeability check.

CI still tests against all supported browsers.
2025-09-01 08:58:32 -07:00
Steve Gravrock
ca4fbcbccb Clarify what's currently treated as private vs internal in Suite and Spec 2025-09-01 08:58:32 -07:00
Steve Gravrock
e1532be726 Convert Suite and SuiteMetadata to ES6 classes 2025-09-01 08:58:32 -07:00
Steve Gravrock
54465f6f6a Convert Spec to an es6 class 2025-09-01 08:58:32 -07:00
Steve Gravrock
fa9939ae94 Improve description of detectLateRejectionHandling feature in release notes 2025-08-30 14:00:00 -07:00
Steve Gravrock
7978ad9889 Fix fn names in release notes 2025-08-30 13:38:28 -07:00
Steve Gravrock
af4662ad31 Bump version to 5.10.0 2025-08-30 13:36:12 -07:00
Steve Gravrock
15c38c7728 Add Firefox 140 (current ESR) to supported browsers and demote 128 to best-effort 2025-08-30 13:31:54 -07:00
Steve Gravrock
b597975c7e Fix and enable pending async expectation message spec 2025-08-30 13:02:08 -07:00
Steve Gravrock
09ce3a30b6 Pend environment-specific specs rather than passing 2025-08-30 12:58:04 -07:00
Steve Gravrock
3bcbc2e3af Tweak spec duration margin 2025-08-30 12:37:15 -07:00
Steve Gravrock
fbaba902dc Merge branch 'bonkevin-html-reporter-with-duration'
* Merges #2073 from @bonkevin
* Adds spec duration to HTML reporter
2025-08-30 12:35:54 -07:00
Steve Gravrock
bf2e8e759e Merge branch 'bonkevin-spec-suite-properties-accessors'
* Merges #2072 from @bonkevin
* Adds Env#getSpecProperty
2025-08-30 12:31:40 -07:00
Steve Gravrock
50e566bd67 Move beforeAll failure reporting into TreeRunner 2025-08-30 07:42:29 -07:00
Steve Gravrock
4b7d5e3623 Update and move remaining disabled Runner specs 2025-08-30 07:21:29 -07:00
Steve Gravrock
6449832e7e rm redundant and long-disabled test for toEqual with DOM nodes 2025-08-29 06:57:32 -07:00
Kevin Bon
c6266b24b7 add test 2025-08-27 10:10:45 -04:00
bonkevin
f16b81d4ef feat: html-reporter with spec duration 2025-08-27 09:58:11 -04:00
kbon
7feec406d9 set the right spec property key 2025-08-25 23:12:29 -04:00
kbon
f822ffea21 feat(getSpecProperty) get a user-defined property 2025-08-25 23:00:06 -04:00
Steve Gravrock
db65c3b131 Fold TreeRunner#runQueueWithSkipPolicy into caller 2025-08-25 18:43:54 -07:00
Steve Gravrock
fd37a7eac0 Move/update/remove obsolste disabled TreeProcessor specs 2025-08-25 18:37:31 -07:00
Steve Gravrock
12219e80c1 Move spec execution from Spec to TreeRunner 2025-08-24 14:05:22 -07:00
Steve Gravrock
a980ae6bf2 Extract spec state management out of Spec#execute 2025-08-24 14:05:22 -07:00
Steve Gravrock
56ac8f5505 Refactor TreeRunner specs 2025-08-24 14:05:13 -07:00
Steve Gravrock
3780fe0b35 Convert some TreeRunner internals to promises 2025-08-24 14:04:47 -07:00
Steve Gravrock
164a393932 Move spec begin and end handling from Env/SuiteBuilder to TreeRunner 2025-08-23 09:03:57 -07:00
Steve Gravrock
759a867094 Backfill unit tests for spec autoCleanClosures 2025-08-23 09:03:57 -07:00
Steve Gravrock
f94d0ceda9 Validate queueableFns 2025-08-23 09:03:38 -07:00
Steve Gravrock
8d99f27be8 Throw an Error rather than a string when createSpyObj is called incorectly 2025-08-23 08:19:11 -07:00
Steve Gravrock
63774597f0 Extract tree running out into a separate class 2025-08-18 16:50:04 -07:00
Steve Gravrock
a3e1abfa12 Split the resulting execution tree out from TreeProcessor 2025-08-17 11:44:52 -07:00
Steve Gravrock
b89a870a59 Test TreeProcessor's public interface, not internal state 2025-08-17 11:44:37 -07:00
Steve Gravrock
ea3fc88803 Remove mutual recursion between Runner and TreeProcessor 2025-08-17 11:44:36 -07:00
Steve Gravrock
d5884e33c6 Move suite execution and spec queueRunner building from TreeProcesor to Runner
This:
* Sets the stage for getting suite and spec execution in one place
* Greatly simplifies the interaction between Runner and TreeProcessor
* Focuses TreeProcessor more on building execution trees
2025-08-17 11:43:00 -07:00
Steve Gravrock
138bf9be4b Focused integration tests for Runner and TreeProcessor
These are mostly adaptations of the execution tests from TreeProcessorSpec.js.
They are meant to support refactoring of the interface and responsibility
division between Runner and TreeProcessor. All these scenarios are probably
covered by nearly-end-to-end integration tests, but those are more difficult
to debug.
2025-08-17 09:23:29 -07:00
Steve Gravrock
98d5284c19 Check for and silence suite reentry warnings in Jasmine's own tests 2025-08-15 06:58:08 -07:00
Steve Gravrock
2299c85751 Deprecate spec/suite orders that interleave suites 2025-08-13 19:02:36 -07:00
Steve Gravrock
8e3ec25f6d Move invalid order exception throw into TreeProcessor 2025-08-13 18:30:51 -07:00
Steve Gravrock
b009cd2922 Convert TreeProcessor to a class 2025-08-12 18:23:57 -07:00
Steve Gravrock
8eee6ebb91 Runner: naming improvements, use private members 2025-08-11 23:22:07 -07:00
Steve Gravrock
c15a1aaa6d Rename queueRunnerFactory to runQueue throughout 2025-08-11 23:05:56 -07:00
Steve Gravrock
5b06531cac Prevent GloablErrors from being monkey patched
All current shipped versions of zone.js contain a monkey patch that fails
to pass constructor arguments on to GlobalErrors. That would crash Jasmine
if it was applied early enough to have any effect.

See <https://github.com/angular/angular/issues/63072>.
2025-08-11 18:08:47 -07:00
Steve Gravrock
42cca93926 Minor jsdoc cleanup 2025-08-09 08:35:49 -07:00
Steve Gravrock
395ef85954 Optionally detect late promise rejections and don't report them as errors 2025-08-09 08:35:08 -07:00
Steve Gravrock
5e88fde655 Backfill some unit tests for Runner's interaction with TreeProcessor 2025-07-29 09:59:45 -07:00
Steve Gravrock
bb777e93e5 Bump version to 5.9.0 2025-07-19 08:27:17 -07:00
Steve Gravrock
9d3fb167a2 Document that the filename property of suite and spec results is deprecated
See <https://github.com/jasmine/jasmine/issues/2065>.
2025-07-19 06:54:54 -07:00
Steve Gravrock
3176eaf1d8 Merge branch 'idConflict' of https://github.com/atscott/jasmine
* Avoid generating timers with IDs that conflict with native
* Fixes #2068
* Merges #2069 from @atscott
2025-07-15 16:50:57 -07:00
Andrew Scott
d31a431d1f fix(clock): Avoid generating timers with IDs that conflict with native
This commit attempts to ensure that the timers created by jasmine mock
clock do not conflict with the native timers. This also retains
pre-existing behavior whereby a native scheduled function cannot be
cleared if it was created prior to the mock clock being installed
(unless the mock clock is uninstalled first).

Prior to this commit, attempting to clear a native timer would result in
clearing a mocked scheduled function instead, in some scenarios where
the IDs conflicted.

fixes #2068
2025-07-14 16:55:05 -07:00
Steve Gravrock
84f78c1435 Split GlobalErrors into portable and platform-specific parts 2025-07-12 13:59:19 -07:00
Steve Gravrock
ff476b1982 Unify error dispatching between browser and node 2025-07-12 13:56:58 -07:00
Steve Gravrock
d53d2ff3eb Convert GlobalErrors to an ES6 class 2025-07-12 13:56:50 -07:00
Steve Gravrock
adfbd00c75 Refactor mocking in GlobalErrorsSpec 2025-07-12 13:56:48 -07:00
Steve Gravrock
495e5fcd50 Backfill integration tests for unhandled promise rejections 2025-07-11 21:36:30 -07:00
Steve Gravrock
bc2aa7be25 Start breaking up integration/EnvSpec.js 2025-07-11 07:39:39 -07:00
Steve Gravrock
af04599bb5 Relaxed timeout on flaky test 2025-07-09 06:56:08 -07:00
Steve Gravrock
21db6ec0e3 Removed unnecessary errorWithStack helper 2025-06-22 12:49:26 -07:00
Steve Gravrock
2d07b3e6d7 Removed protections against user code redefining undefined
Jasmine hasn't even run on platforms that allowed redefining undefined
since 2.x.
2025-06-22 12:23:18 -07:00
Steve Gravrock
6891789ed2 Don't test on Node versions before 18.20.5
18.20.5 is the oldest version supported by current selenium-webdriver.
Also, many dev dependencies require at least 18.18.0.
2025-06-14 10:22:03 -07:00
Steve Gravrock
7a3d3c9360 Removed shelljs dev dependency 2025-06-14 09:05:12 -07:00
Steve Gravrock
1b2922e008 Don't hardcode temp dir in buildStandaloneDist 2025-06-14 09:05:12 -07:00
Steve Gravrock
bd8d23f2a7 Removed rimraf dev dependency 2025-06-14 09:05:06 -07:00
Steve Gravrock
de26763868 CI: remove special case for Chrome 2025-06-09 15:05:50 -07:00
73 changed files with 8400 additions and 5806 deletions

View File

@@ -18,7 +18,7 @@ executors:
working_directory: ~/workspace
node18:
docker:
- image: cimg/node:18.0.0
- image: cimg/node:18.20.5
working_directory: ~/workspace
jobs:

View File

@@ -27,13 +27,13 @@ for information on writing specs, and [the FAQ](https://jasmine.github.io/pages/
Jasmine tests itself across popular browsers (Safari, Chrome, Firefox, and
Microsoft Edge) as well as Node.
| Environment | Supported versions |
|-------------------|----------------------------|
| Node | 18, 20, 22, 24 |
| Safari | 15*, 16*, 17* |
| Chrome | Evergreen |
| Firefox | Evergreen, 102*, 115*, 128 |
| Edge | Evergreen |
| Environment | Supported versions |
|-------------------|----------------------------------|
| Node | 18.20.5+*, 20, 22, 24 |
| Safari | 16*, 17* |
| Chrome | Evergreen |
| Firefox | Evergreen, 102*, 115*, 128*, 140 |
| Edge | Evergreen |
For evergreen browsers, each version of Jasmine is tested against the version of the browser that is available to us
at the time of release. Other browsers, as well as older & newer versions of some supported browsers, are likely to work.

View File

@@ -22,7 +22,9 @@ export default defineConfig([{
...globals.node,
},
ecmaVersion: 2018,
// 2022 isn't exactly right, but it's the earliest version that allows
// private properties.
ecmaVersion: 2022,
sourceType: "commonjs",
},

View File

@@ -81,6 +81,13 @@ jasmineRequire.HtmlReporter = function(j$) {
}
};
/**
* @class HtmlReporter
* @classdesc Displays results and allows re-running individual specs and suites.
* @implements {Reporter}
* @param options Options object. See lib/jasmine-core/boot1.js for details.
* @since 1.2.0
*/
function HtmlReporter(options) {
function config() {
return (options.env && options.env.configuration()) || {};
@@ -98,6 +105,11 @@ jasmineRequire.HtmlReporter = function(j$) {
const deprecationWarnings = [];
const failures = [];
/**
* Initializes the reporter. Should be called before {@link Env#execute}.
* @function
* @name HtmlReporter#initialize
*/
this.initialize = function() {
clearPrior();
htmlReporterMain = createDom(
@@ -557,6 +569,11 @@ jasmineRequire.HtmlReporter = function(j$) {
'a',
{ href: specHref(resultNode.result) },
specDescription
),
createDom(
'span',
{ className: 'jasmine-spec-duration' },
'(' + resultNode.result.duration + 'ms)'
)
)
);
@@ -876,6 +893,8 @@ jasmineRequire.HtmlReporter = function(j$) {
};
jasmineRequire.HtmlSpecFilter = function() {
// Legacy HTML spec filter, preserved for backward compatibility with
// boot files that predate HtmlExactSpecFilterV2
function HtmlSpecFilter(options) {
const filterString =
options &&
@@ -883,6 +902,13 @@ jasmineRequire.HtmlSpecFilter = function() {
options.filterString().replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
const filterPattern = new RegExp(filterString);
/**
* Determines whether the spec with the specified name should be executed.
* @name HtmlSpecFilter#matches
* @function
* @param {string} specName The full name of the spec
* @returns {boolean}
*/
this.matches = function(specName) {
return filterPattern.test(specName);
};
@@ -916,38 +942,57 @@ jasmineRequire.ResultsNode = function() {
};
jasmineRequire.QueryString = function() {
function QueryString(options) {
this.navigateWithNewParam = function(key, value) {
options.getWindowLocation().search = this.fullStringWithNewParam(
/**
* Reads and manipulates the query string.
* @since 2.0.0
*/
class QueryString {
#getWindowLocation;
/**
* @param options Object with a getWindowLocation property, which should be
* a function returning the current value of window.location.
*/
constructor(options) {
this.#getWindowLocation = options.getWindowLocation;
}
/**
* Sets the specified query parameter and navigates to the resulting URL.
* @param {string} key
* @param {string} value
*/
navigateWithNewParam(key, value) {
this.#getWindowLocation().search = this.fullStringWithNewParam(
key,
value
);
};
this.fullStringWithNewParam = function(key, value) {
const paramMap = queryStringToParamMap();
paramMap[key] = value;
return toQueryString(paramMap);
};
this.getParam = function(key) {
return queryStringToParamMap()[key];
};
return this;
function toQueryString(paramMap) {
const qStrPairs = [];
for (const prop in paramMap) {
qStrPairs.push(
encodeURIComponent(prop) + '=' + encodeURIComponent(paramMap[prop])
);
}
return '?' + qStrPairs.join('&');
}
function queryStringToParamMap() {
const paramStr = options.getWindowLocation().search.substring(1);
/**
* Returns a new URL based on the current location, with the specified
* query parameter set.
* @param {string} key
* @param {string} value
* @return {string}
*/
fullStringWithNewParam(key, value) {
const paramMap = this.#queryStringToParamMap();
paramMap[key] = value;
return toQueryString(paramMap);
}
/**
* Gets the value of the specified query parameter.
* @param {string} key
* @return {string}
*/
getParam(key) {
return this.#queryStringToParamMap()[key];
}
#queryStringToParamMap() {
const paramStr = this.#getWindowLocation().search.substring(1);
let params = [];
const paramMap = {};
@@ -967,5 +1012,15 @@ jasmineRequire.QueryString = function() {
}
}
function toQueryString(paramMap) {
const qStrPairs = [];
for (const prop in paramMap) {
qStrPairs.push(
encodeURIComponent(prop) + '=' + encodeURIComponent(paramMap[prop])
);
}
return '?' + qStrPairs.join('&');
}
return QueryString;
};

View File

@@ -229,6 +229,9 @@ body {
.jasmine_html-reporter .jasmine-specs li.jasmine-excluded a:before {
content: "• ";
}
.jasmine_html-reporter .jasmine-specs li .jasmine-spec-duration {
margin-left: 1em;
}
.jasmine_html-reporter .jasmine-description + .jasmine-suite {
margin-top: 0;
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "jasmine-core",
"license": "MIT",
"version": "5.8.0",
"version": "5.12.0",
"repository": {
"type": "git",
"url": "https://github.com/jasmine/jasmine.git"
@@ -49,12 +49,10 @@
"jasmine-browser-runner": "github:jasmine/jasmine-browser-runner",
"jsdom": "^26.0.0",
"prettier": "1.17.1",
"rimraf": "^5.0.10",
"sass": "^1.58.3",
"shelljs": "^0.9.2"
"sass": "^1.58.3"
},
"browserslist": [
"Safari >= 15",
"Safari >= 16",
"Firefox >= 102",
"last 2 Chrome versions",
"last 2 Edge versions"

54
release_notes/5.10.0.md Normal file
View File

@@ -0,0 +1,54 @@
# Jasmine Core 5.10.0 Release Notes
## New Features
* Optionally detect promise rejections that are handled after an initial
unhandled promise rejection event and don't report them as errors.
This is off by default because it comes with a performance cost. It can be
enabled by setting the `detectLateRejectionHandling` config property to true.
* Add `getSpecProperty` to retrieve data that was set with `setSpecProperty`.
* Merges [#2072](https://github.com/jasmine/jasmine/pull/2072) from @bonkevin
* Show spec duration in the HTML reporter.
* Merges [#2073](https://github.com/jasmine/jasmine/pull/2073) from @bonkevin
* Protect `GlobalErrors` against monkey patching.
All currently shipped versions of zone.js contain a monkey patch that fails
to pass constructor arguments on to `GlobalErrors`. This patch normally has
no effect because zone.js is normally installed after `GlobalErrors` is
instantiated, but it would crash Jasmine if it was applied early enough.
## Deprecations
* Issue a deprecation warning if the suite/spec order passed as a parameter to
`Env#execute` causes a suite to be re-entered.
## Changes to supported environments
* Added Firefox 140 (current ESR) to supported environments
* Demoted Firefox 128 (previous ESR) to best-effort support
## Internal improvements
* Core suite/spec execution flow has been significantly simplified.
## Supported environments
This version has been tested in the following environments.
| Environment | Supported versions |
|-------------------|--------------------------------|
| Node | 18.20.5**, 20, 22, 24 |
| Safari | 15**, 16**, 17** |
| Chrome | 139* |
| Firefox | 102**, 115**, 128**, 140, 142* |
| Edge | 139* |
\* 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)_

66
release_notes/5.11.0.md Normal file
View File

@@ -0,0 +1,66 @@
# Jasmine Core 5.11.0 Release Notes
## New features
* `detectLateRejectionHandling` works in `beforeAll` and `afterAll` as well as
in specs.
* Clicking a link in the HTML reporter does exact filtering rather than a
substring match.
If you use jasmine-browser-runner or load boot1.js directly from jasmine-core,
you'll get the new filtering behavior automatically. Otherwise, it requires
several changes to boot1.js:
1. Add `queryString` to the options object passed to `HtmlReporter`, and
remove `filterSpecs`.
2. Instantiate a `jasmine.HtmlExactSpecFilter` instead of a
`jasmine.HtmlSpecFilter`.
2. Change the body of `config.specFilter` from
`return specFilter.matches(spec.getFullName());` to
`return specFilter.matches(spec)`
For a working example, see lib/jasmine-core/boot1.js in this package.
Old boot1.js files will still work, but you'll get the old filtering behavior.
## Bug fixes
* Fixed global error handling when the env is executed repeatedly
## Changes to supported environments
* Safari 15 is no longer supported.
## Documentation improvements
* Added API reference docs for classes used in browser boot files
* Documented the order properties of `jasmineStarted` and `jasmineDone` events
## Internal improvements
* Unified top suite and regular suite execution
* Converted `Spec`, `Suite`, and `QueryString` to ES6 classes
* Extracted configuration out of `Env`
* Updated tests to characterize suite/spec reporting more completely
* Adopted `forbidDuplicateNames: true` in jasmine-core's own tests
## Supported environments
This version has been tested in the following environments.
| Environment | Supported versions |
|-------------------|--------------------------------|
| Node | 18.20.5**, 20, 22, 24 |
| Safari | 16**, 17** |
| Chrome | 140* |
| Firefox | 102**, 115**, 128**, 140, 143* |
| Edge | 140* |
\* 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)_

27
release_notes/5.12.0.md Normal file
View File

@@ -0,0 +1,27 @@
# Jasmine Core 5.12.0 Release Notes
This release reverts the exact spec filtering feature introduced in 5.11.0,
which broke spec filtering in Karma.
## Supported environments
This version has been tested in the following environments.
| Environment | Supported versions |
|-------------|--------------------------------|
| Node | 18.20.5**, 20, 22, 24 |
| Safari | 16**, 17** |
| Chrome | 141* |
| Firefox | 102**, 115**, 128**, 140, 143* |
| Edge | 140* |
\* 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)_

56
release_notes/5.9.0.md Normal file
View File

@@ -0,0 +1,56 @@
# Jasmine Core 5.9.0 Release Notes
## Bug fixes
* Avoid generating mock clock timer IDs that conflict with native ones
* Fixes [#2068](https://github.com/jasmine/jasmine/issues/2068)
* Merges [#2069](https://github.com/jasmine/jasmine/pull/2069) from @atscott
## Deprecations and changes to platform support
* Node versions before 18.20.5 are no longer supported
Older 18.x versions might still work, but Jasmine is no longer tested in them
prior to release.
* Document that the filename properties of suite and spec results are deprecated
These properties are incorrect in many configurations. They'll be removed in
the next major release unless there is enough user interest in fixing them.
See <https://github.com/jasmine/jasmine/issues/2065>.
## Internal improvements
* Extensive GlobalErrors refactoring
* Removed many of the error dispatching differences between browsers and Node
* Split into portable and platform-specific parts
* Converted to ES6 classes
* Removed unnecessary errorWithStack helper
Jasmine no longer runs on platforms that create errors without stack traces.
* Removed protections against user code redefining undefined
Jasmine no longer runs on platforms that allow redefining undefined.
* Removed rimraf and shelljs dev dependencies
## Supported environments
This version has been tested in the following environments.
| Environment | Supported versions |
|-------------------|-------------------------|
| Node | 18.20.5**, 20, 22, 24 |
| Safari | 15**, 16**, 17** |
| Chrome | 138* |
| Firefox | 102**, 115**, 128, 140* |
| Edge | 138* |
\* 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

@@ -0,0 +1,100 @@
# Jasmine Core 6.0.0-alpha.0 Release Notes
This is a pre-release, intended to offer a preview of breaking changes and to
solicit feedback.
## A Note About Pre-Release Compatibility
There may be additional breaking changes in future 6.0 pre-releases or in the
final 6.0 release. That's allowed by the semver specification, but users are
sometimes unpleasantly surprised by it.
NPM's implementation of carat version ranges assumes that subsequent
pre-releases and final releases are fully compatible with earlier pre-releases.
If your package.json contains `"jasmine-core": "^6.0.0-alpha.0`,
NPM might install any later 6.x version even though there is no guarantee of
compatibility. If that isn't ok, you should specify an exact pre-release version:
`"jasmine-core": "6.0.0-alpha.0`.
## Changes to supported environments
* Node 18 is no longer supported.
## Breaking changes
### General
* Private APIs have been removed from the `jasmine` namespace.
The purpose of this change is to reduce the risk of users inadvertently
depending on private APIs. Anything that's not covered by
[the documentation](https://jasmine.github.io/pages/docs_home.html) remains
private regardless of namespacing. Private APIs may be changed or removed in
any release. This change is being made in a major release as a courtesy to users of
libraries that depend on private APIs.
* Arguments to `setSpecProperty`/`setSuiteProperty` must be both
structured-cloneable and JSON-serializable.
* Mock clock timing functions cannot be spied on. Previously this "worked" but
prevented the mock clock from uninstalling itself.
* The default value of the `forbidDuplicateNames` config option has been
changed to true.
* The mock clock no longer supports the eval forms of `setTimeout` and
`setInterval`.
* If an execution order is passed to `Env#execute`, it must not enter any suite
more than once.
* The argument passed to spec filters is a
[spec metadata](https://jasmine.github.io/api/6.0.0-alpha.0/Spec.html)
instance, not the internal spec object.
### Changes that affect reporters
This release includes changes that are intended to streamline and clarify the
reporter interface, prevent sharing of mutable state, and prevent bugs involving
non-serializable objects. These changes should be compatible with most existing
reporters but could break reporters that manage their internal state in unusual
ways. Please [open an issue](https://github.com/jasmine/jasmine/issues/new?template=bug_report.yml)
if you find a published reporter package that works with jasmine-core 5.x but
not with this release.
* Irrelevant properties such as `status` and `failedExpectations` are omitted
from [the event passed to specStarted](https://jasmine.github.io/api/6.0.0-alpha.0/global.html#SpecStartedEvent).
* Reporter events are deep-cloned before being passed to each reporter. This
protects reporters against later mutation by jasmine-core or other reporters.
* The `expected` and `actual` properties of
[passed and failed expectations](https://jasmine.github.io/api/6.0.0-alpha.0/global.html#ExpectationResult)
have been removed.
* The [order](https://jasmine.github.io/api/6.0.0-alpha.0/global.html#Order)
property of the`jasmineStarted` and `jasmineDone` reporter events no longer
includes undocumented properties.
### Changes to Node boot functions
* [boot](https://jasmine.github.io/api/6.0.0-alpha.0/module-jasmine-core.html#.boot)
defaults to creating a new core instance each time it's called. This restores
the pre-5.0 default behavior.
* [noGlobals](https://jasmine.github.io/api/6.0.0-alpha.0/module-jasmine-core.html#.noGlobals)
no longer takes a parameter. It always returns the same object when called
repeatedly.
## Supported environments
This version has been tested in the following environments.
| Environment | Supported versions |
|-------------------|--------------------------------|
| Node | 20, 22, 24 |
| Safari | 16**, 17** |
| Chrome | 140* |
| Firefox | 102**, 115**, 128**, 140, 143* |
| Edge | 140* |
\* 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

@@ -1,22 +1,16 @@
const fs = require('fs');
const os = require('os');
const path = require('path');
const glob = require('glob');
const ejs = require('ejs');
const archiver = require('archiver');
const { rimrafSync } = require('rimraf');
const buildDistribution = require('./lib/buildDistribution');
const tmpDir = 'dist/tmp'
if (!fs.existsSync(tmpDir)) {
if (!fs.existsSync(path.dirname(tmpDir))) {
fs.mkdirSync(path.dirname(tmpDir));
}
fs.mkdirSync(tmpDir);
}
const prefix = path.join(os.tmpdir(), 'jasmine-build-standalone');
const tmpDir = fs.mkdtempSync(prefix);
buildStandaloneDist().finally(function() {
rimrafSync(tmpDir);
fs.rmSync(tmpDir, { recursive: true });
});
async function buildStandaloneDist() {
@@ -30,7 +24,7 @@ function compileSpecRunner(jasmineVersion) {
const template = fs.readFileSync('src/SpecRunner.html.ejs',
{encoding: 'utf8'});
const runnerHtml = ejs.render(template, { jasmineVersion });
fs.writeFileSync('dist/tmp/SpecRunner.html', runnerHtml,
fs.writeFileSync(path.join(tmpDir, 'SpecRunner.html'), runnerHtml,
{encoding: 'utf8'});
}
@@ -39,7 +33,7 @@ async function zipStandaloneDist(jasmineVersion) {
{
src: [
'LICENSE',
'dist/tmp/SpecRunner.html',
path.join(tmpDir, 'SpecRunner.html'),
]
},
{

View File

@@ -25,23 +25,23 @@ run_browser() {
passfile=`mktemp -t jasmine-results.XXXXXX` || exit 1
failfile=`mktemp -t jasmine-results.XXXXXX` || exit 1
# As of 2023-09-30, Sauce Connect doesn't work with the latest Chrome version
# on the default Linux. Run on Mac OS instead. The OS specification may need to
# be updated or removed when new Chrome versions stop being available on Mac OS
# 12, although historically this has taken several major OS versions.
# See <https://saucelabs.com/products/supported-browsers-devices>.
# On Saucelabs, the test suite frequently runs ~30s slower on Mac OS than it
# does on Linux, so it's probably worth removing the OS specification once Sauce
# Connect works with Chrome latest on Linux again.
run_browser chrome latest "macOS 12"
run_browser chrome latest
run_browser firefox latest
run_browser firefox 128
run_browser firefox 115
if [ "$1" = "--not-actually-all" ]; then
echo "SKIPPED: firefox 140" >> "$passfile"
echo "SKIPPED: firefox 128" >> "$passfile"
echo "SKIPPED: firefox 115" >> "$passfile"
else
run_browser firefox 140
run_browser firefox 128
run_browser firefox 115
fi
run_browser firefox 102
run_browser safari 17
run_browser safari 16
run_browser safari 15
run_browser MicrosoftEdge latest
echo

View File

@@ -129,33 +129,33 @@ describe('AsyncExpectation', function() {
});
});
it('prepends the context to a custom failure message from a function', function() {
pending('should actually work, but no custom matchers for async yet');
it('prepends the context to a custom failure message from a matcher', async function() {
const matchersUtil = {
buildFailureMessage: function() {
return 'failure message';
}
buildFailureMessage() {
return 'failure message';
},
addExpectationResult = jasmine.createSpy('addExpectationResult'),
actual = Promise.reject(new Error('nope')),
expectation = jasmineUnderTest.Expectation.asyncFactory({
actual: actual,
addExpectationResult: addExpectationResult,
matchersUtil: matchersUtil
});
pp(v) {
return v.toString();
}
};
const addExpectationResult = jasmine.createSpy('addExpectationResult');
const actual = Promise.reject(new Error('nope'));
const expectation = jasmineUnderTest.Expectation.asyncFactory({
actual: actual,
addExpectationResult: addExpectationResult,
matchersUtil: matchersUtil
});
return expectation
.withContext('Some context')
.toBeResolved()
.then(function() {
expect(addExpectationResult).toHaveBeenCalledWith(
false,
jasmine.objectContaining({
message: 'Some context: msg'
})
);
});
await expectation.withContext('Some context').toBeResolved();
expect(addExpectationResult).toHaveBeenCalledWith(
false,
jasmine.objectContaining({
message:
'Some context: Expected a promise to be resolved but it ' +
'was rejected with Error: nope.'
})
);
});
it('works with #not', function() {
@@ -277,23 +277,18 @@ describe('AsyncExpectation', function() {
it('reports a passing result to the spec when the comparison passes', function() {
const matchers = {
toFoo: function() {
return {
compare: function() {
return Promise.resolve({ pass: true });
}
};
}
},
matchersUtil = {
buildFailureMessage: jasmine.createSpy('buildFailureMessage')
},
addExpectationResult = jasmine.createSpy('addExpectationResult'),
errorWithStack = new Error('errorWithStack');
spyOn(jasmineUnderTest.util, 'errorWithStack').and.returnValue(
errorWithStack
);
toFoo: function() {
return {
compare: function() {
return Promise.resolve({ pass: true });
}
};
}
};
const matchersUtil = {
buildFailureMessage: jasmine.createSpy('buildFailureMessage')
};
const addExpectationResult = jasmine.createSpy('addExpectationResult');
const expectation = jasmineUnderTest.Expectation.asyncFactory({
customAsyncMatchers: matchers,
@@ -310,7 +305,7 @@ describe('AsyncExpectation', function() {
error: undefined,
expected: 'hello',
actual: 'an actual',
errorForStack: errorWithStack
errorForStack: jasmine.any(Error)
});
});
});
@@ -329,13 +324,8 @@ describe('AsyncExpectation', function() {
buildFailureMessage: function() {
return '';
}
},
addExpectationResult = jasmine.createSpy('addExpectationResult'),
errorWithStack = new Error('errorWithStack');
spyOn(jasmineUnderTest.util, 'errorWithStack').and.returnValue(
errorWithStack
);
};
const addExpectationResult = jasmine.createSpy('addExpectationResult');
const expectation = jasmineUnderTest.Expectation.asyncFactory({
customAsyncMatchers: matchers,
@@ -352,30 +342,25 @@ describe('AsyncExpectation', function() {
actual: 'an actual',
message: '',
error: undefined,
errorForStack: errorWithStack
errorForStack: jasmine.any(Error)
});
});
});
it('reports a failing result and a custom fail message to the spec when the comparison fails', function() {
const matchers = {
toFoo: function() {
return {
compare: function() {
return Promise.resolve({
pass: false,
message: 'I am a custom message'
});
}
};
}
},
addExpectationResult = jasmine.createSpy('addExpectationResult'),
errorWithStack = new Error('errorWithStack');
spyOn(jasmineUnderTest.util, 'errorWithStack').and.returnValue(
errorWithStack
);
toFoo: function() {
return {
compare: function() {
return Promise.resolve({
pass: false,
message: 'I am a custom message'
});
}
};
}
};
const addExpectationResult = jasmine.createSpy('addExpectationResult');
const expectation = jasmineUnderTest.Expectation.asyncFactory({
actual: 'an actual',
@@ -391,32 +376,27 @@ describe('AsyncExpectation', function() {
actual: 'an actual',
message: 'I am a custom message',
error: undefined,
errorForStack: errorWithStack
errorForStack: jasmine.any(Error)
});
});
});
it('reports a failing result with a custom fail message function to the spec when the comparison fails', function() {
const matchers = {
toFoo: function() {
return {
compare: function() {
return Promise.resolve({
pass: false,
message: function() {
return 'I am a custom message';
}
});
}
};
}
},
addExpectationResult = jasmine.createSpy('addExpectationResult'),
errorWithStack = new Error('errorWithStack');
spyOn(jasmineUnderTest.util, 'errorWithStack').and.returnValue(
errorWithStack
);
toFoo: function() {
return {
compare: function() {
return Promise.resolve({
pass: false,
message: function() {
return 'I am a custom message';
}
});
}
};
}
};
const addExpectationResult = jasmine.createSpy('addExpectationResult');
const expectation = jasmineUnderTest.Expectation.asyncFactory({
customAsyncMatchers: matchers,
@@ -432,28 +412,23 @@ describe('AsyncExpectation', function() {
actual: 'an actual',
message: 'I am a custom message',
error: undefined,
errorForStack: errorWithStack
errorForStack: jasmine.any(Error)
});
});
});
it('reports a passing result to the spec when the comparison fails for a negative expectation', function() {
const matchers = {
toFoo: function() {
return {
compare: function() {
return Promise.resolve({ pass: false });
}
};
}
},
addExpectationResult = jasmine.createSpy('addExpectationResult'),
actual = 'an actual',
errorWithStack = new Error('errorWithStack');
spyOn(jasmineUnderTest.util, 'errorWithStack').and.returnValue(
errorWithStack
);
toFoo: function() {
return {
compare: function() {
return Promise.resolve({ pass: false });
}
};
}
};
const addExpectationResult = jasmine.createSpy('addExpectationResult');
const actual = 'an actual';
const expectation = jasmineUnderTest.Expectation.asyncFactory({
customAsyncMatchers: matchers,
@@ -469,7 +444,7 @@ describe('AsyncExpectation', function() {
error: undefined,
expected: 'hello',
actual: actual,
errorForStack: errorWithStack
errorForStack: jasmine.any(Error)
});
});
});
@@ -488,14 +463,9 @@ describe('AsyncExpectation', function() {
buildFailureMessage: function() {
return 'default message';
}
},
addExpectationResult = jasmine.createSpy('addExpectationResult'),
actual = 'an actual',
errorWithStack = new Error('errorWithStack');
spyOn(jasmineUnderTest.util, 'errorWithStack').and.returnValue(
errorWithStack
);
};
const addExpectationResult = jasmine.createSpy('addExpectationResult');
const actual = 'an actual';
const expectation = jasmineUnderTest.Expectation.asyncFactory({
customAsyncMatchers: matchers,
@@ -512,31 +482,26 @@ describe('AsyncExpectation', function() {
actual: actual,
message: 'default message',
error: undefined,
errorForStack: errorWithStack
errorForStack: jasmine.any(Error)
});
});
});
it('reports a failing result and a custom fail message to the spec when the comparison passes for a negative expectation', function() {
const matchers = {
toFoo: function() {
return {
compare: function() {
return Promise.resolve({
pass: true,
message: 'I am a custom message'
});
}
};
}
},
addExpectationResult = jasmine.createSpy('addExpectationResult'),
actual = 'an actual',
errorWithStack = new Error('errorWithStack');
spyOn(jasmineUnderTest.util, 'errorWithStack').and.returnValue(
errorWithStack
);
toFoo: function() {
return {
compare: function() {
return Promise.resolve({
pass: true,
message: 'I am a custom message'
});
}
};
}
};
const addExpectationResult = jasmine.createSpy('addExpectationResult');
const actual = 'an actual';
const expectation = jasmineUnderTest.Expectation.asyncFactory({
customAsyncMatchers: matchers,
@@ -552,31 +517,26 @@ describe('AsyncExpectation', function() {
actual: actual,
message: 'I am a custom message',
error: undefined,
errorForStack: errorWithStack
errorForStack: jasmine.any(Error)
});
});
});
it("reports a passing result to the spec when the 'not' comparison passes, given a negativeCompare", function() {
const matchers = {
toFoo: function() {
return {
compare: function() {
return Promise.resolve({ pass: true });
},
negativeCompare: function() {
return Promise.resolve({ pass: true });
}
};
}
},
addExpectationResult = jasmine.createSpy('addExpectationResult'),
actual = 'an actual',
errorWithStack = new Error('errorWithStack');
spyOn(jasmineUnderTest.util, 'errorWithStack').and.returnValue(
errorWithStack
);
toFoo: function() {
return {
compare: function() {
return Promise.resolve({ pass: true });
},
negativeCompare: function() {
return Promise.resolve({ pass: true });
}
};
}
};
const addExpectationResult = jasmine.createSpy('addExpectationResult');
const actual = 'an actual';
const expectation = jasmineUnderTest.Expectation.asyncFactory({
customAsyncMatchers: matchers,
@@ -592,34 +552,29 @@ describe('AsyncExpectation', function() {
actual: actual,
message: '',
error: undefined,
errorForStack: errorWithStack
errorForStack: jasmine.any(Error)
});
});
});
it("reports a failing result and a custom fail message to the spec when the 'not' comparison fails, given a negativeCompare", function() {
const matchers = {
toFoo: function() {
return {
compare: function() {
return Promise.resolve({ pass: true });
},
negativeCompare: function() {
return Promise.resolve({
pass: false,
message: "I'm a custom message"
});
}
};
}
},
addExpectationResult = jasmine.createSpy('addExpectationResult'),
actual = 'an actual',
errorWithStack = new Error('errorWithStack');
spyOn(jasmineUnderTest.util, 'errorWithStack').and.returnValue(
errorWithStack
);
toFoo: function() {
return {
compare: function() {
return Promise.resolve({ pass: true });
},
negativeCompare: function() {
return Promise.resolve({
pass: false,
message: "I'm a custom message"
});
}
};
}
};
const addExpectationResult = jasmine.createSpy('addExpectationResult');
const actual = 'an actual';
const expectation = jasmineUnderTest.Expectation.asyncFactory({
customAsyncMatchers: matchers,
@@ -635,7 +590,7 @@ describe('AsyncExpectation', function() {
actual: actual,
message: "I'm a custom message",
error: undefined,
errorForStack: errorWithStack
errorForStack: jasmine.any(Error)
});
});
});
@@ -643,24 +598,19 @@ describe('AsyncExpectation', function() {
it('reports errorWithStack when a custom error message is returned', function() {
const customError = new Error('I am a custom error');
const matchers = {
toFoo: function() {
return {
compare: function() {
return Promise.resolve({
pass: false,
message: 'I am a custom message',
error: customError
});
}
};
}
},
addExpectationResult = jasmine.createSpy('addExpectationResult'),
errorWithStack = new Error('errorWithStack');
spyOn(jasmineUnderTest.util, 'errorWithStack').and.returnValue(
errorWithStack
);
toFoo: function() {
return {
compare: function() {
return Promise.resolve({
pass: false,
message: 'I am a custom message',
error: customError
});
}
};
}
};
const addExpectationResult = jasmine.createSpy('addExpectationResult');
const expectation = jasmineUnderTest.Expectation.asyncFactory({
actual: 'an actual',
@@ -676,30 +626,25 @@ describe('AsyncExpectation', function() {
actual: 'an actual',
message: 'I am a custom message',
error: undefined,
errorForStack: errorWithStack
errorForStack: jasmine.any(Error)
});
});
});
it("reports a custom message to the spec when a 'not' comparison fails", function() {
const matchers = {
toFoo: function() {
return {
compare: function() {
return Promise.resolve({
pass: true,
message: 'I am a custom message'
});
}
};
}
},
addExpectationResult = jasmine.createSpy('addExpectationResult'),
errorWithStack = new Error('errorWithStack');
spyOn(jasmineUnderTest.util, 'errorWithStack').and.returnValue(
errorWithStack
);
toFoo: function() {
return {
compare: function() {
return Promise.resolve({
pass: true,
message: 'I am a custom message'
});
}
};
}
};
const addExpectationResult = jasmine.createSpy('addExpectationResult');
const expectation = jasmineUnderTest.Expectation.asyncFactory({
actual: 'an actual',
@@ -715,32 +660,27 @@ describe('AsyncExpectation', function() {
actual: 'an actual',
message: 'I am a custom message',
error: undefined,
errorForStack: errorWithStack
errorForStack: jasmine.any(Error)
});
});
});
it("reports a custom message func to the spec when a 'not' comparison fails", function() {
const matchers = {
toFoo: function() {
return {
compare: function() {
return Promise.resolve({
pass: true,
message: function() {
return 'I am a custom message';
}
});
}
};
}
},
addExpectationResult = jasmine.createSpy('addExpectationResult'),
errorWithStack = new Error('errorWithStack');
spyOn(jasmineUnderTest.util, 'errorWithStack').and.returnValue(
errorWithStack
);
toFoo: function() {
return {
compare: function() {
return Promise.resolve({
pass: true,
message: function() {
return 'I am a custom message';
}
});
}
};
}
};
const addExpectationResult = jasmine.createSpy('addExpectationResult');
let expectation = jasmineUnderTest.Expectation.asyncFactory({
actual: 'an actual',
@@ -756,7 +696,7 @@ describe('AsyncExpectation', function() {
actual: 'an actual',
message: 'I am a custom message',
error: undefined,
errorForStack: errorWithStack
errorForStack: jasmine.any(Error)
});
});
});

View File

@@ -242,7 +242,7 @@ describe('Clock', function() {
expect(fakeGlobal.clearInterval).toBe(replacedClearInterval);
});
it('replaces the global timer functions on uninstall', function() {
it('restores the global timer functions on uninstall', function() {
const fakeSetTimeout = jasmine.createSpy('global setTimeout'),
fakeClearTimeout = jasmine.createSpy('global clearTimeout'),
fakeSetInterval = jasmine.createSpy('global setInterval'),
@@ -408,211 +408,219 @@ describe('Clock', function() {
expect(delayedFunctionScheduler.scheduleFunction).not.toHaveBeenCalled();
});
it('schedules the delayed function (via setTimeout) with the fake timer', function() {
const fakeSetTimeout = jasmine.createSpy('setTimeout'),
scheduleFunction = jasmine.createSpy('scheduleFunction'),
delayedFunctionScheduler = { scheduleFunction: scheduleFunction },
fakeGlobal = { setTimeout: fakeSetTimeout },
delayedFn = jasmine.createSpy('delayedFn'),
mockDate = {
install: function() {},
tick: function() {},
uninstall: function() {}
},
clock = new jasmineUnderTest.Clock(
fakeGlobal,
function() {
return delayedFunctionScheduler;
describe('setTimeout', function() {
it('schedules the delayed function with the fake timer', function() {
const fakeSetTimeout = jasmine.createSpy('setTimeout'),
scheduleFunction = jasmine.createSpy('scheduleFunction'),
delayedFunctionScheduler = { scheduleFunction: scheduleFunction },
fakeGlobal = { setTimeout: fakeSetTimeout },
delayedFn = jasmine.createSpy('delayedFn'),
mockDate = {
install: function() {},
tick: function() {},
uninstall: function() {}
},
mockDate
),
timeout = new clock.FakeTimeout();
clock = new jasmineUnderTest.Clock(
fakeGlobal,
function() {
return delayedFunctionScheduler;
},
mockDate
),
timeout = new clock.FakeTimeout();
clock.install();
clock.setTimeout(delayedFn, 0, 'a', 'b');
clock.install();
clock.setTimeout(delayedFn, 0, 'a', 'b');
expect(fakeSetTimeout).not.toHaveBeenCalled();
expect(fakeSetTimeout).not.toHaveBeenCalled();
if (!NODE_JS) {
expect(delayedFunctionScheduler.scheduleFunction).toHaveBeenCalledWith(
delayedFn,
0,
['a', 'b']
);
} else {
expect(delayedFunctionScheduler.scheduleFunction).toHaveBeenCalledWith(
delayedFn,
0,
['a', 'b'],
false,
timeout
);
}
if (!NODE_JS) {
expect(delayedFunctionScheduler.scheduleFunction).toHaveBeenCalledWith(
delayedFn,
0,
['a', 'b']
);
} else {
expect(delayedFunctionScheduler.scheduleFunction).toHaveBeenCalledWith(
delayedFn,
0,
['a', 'b'],
false,
timeout
);
}
});
it('returns an id for the delayed function', function() {
const fakeSetTimeout = jasmine.createSpy('setTimeout'),
scheduleId = 123,
scheduleFunction = jasmine
.createSpy('scheduleFunction')
.and.returnValue(scheduleId),
delayedFunctionScheduler = { scheduleFunction: scheduleFunction },
fakeGlobal = { setTimeout: fakeSetTimeout },
delayedFn = jasmine.createSpy('delayedFn'),
mockDate = {
install: function() {},
tick: function() {},
uninstall: function() {}
},
clock = new jasmineUnderTest.Clock(
fakeGlobal,
function() {
return delayedFunctionScheduler;
},
mockDate
);
clock.install();
const timeout = clock.setTimeout(delayedFn, 0);
if (!NODE_JS) {
expect(timeout).toEqual(123);
} else {
expect(timeout.constructor.name).toEqual('FakeTimeout');
}
});
});
it('returns an id for the delayed function', function() {
const fakeSetTimeout = jasmine.createSpy('setTimeout'),
scheduleId = 123,
scheduleFunction = jasmine
.createSpy('scheduleFunction')
.and.returnValue(scheduleId),
delayedFunctionScheduler = { scheduleFunction: scheduleFunction },
fakeGlobal = { setTimeout: fakeSetTimeout },
delayedFn = jasmine.createSpy('delayedFn'),
mockDate = {
install: function() {},
tick: function() {},
uninstall: function() {}
},
clock = new jasmineUnderTest.Clock(
fakeGlobal,
function() {
return delayedFunctionScheduler;
describe('clearTimeout', function() {
it('clears the scheduled function with the scheduler', function() {
const fakeClearTimeout = jasmine.createSpy('clearTimeout'),
delayedFunctionScheduler = jasmine.createSpyObj(
'delayedFunctionScheduler',
['removeFunctionWithId']
),
fakeGlobal = { setTimeout: fakeClearTimeout },
mockDate = {
install: function() {},
tick: function() {},
uninstall: function() {}
},
mockDate
);
clock = new jasmineUnderTest.Clock(
fakeGlobal,
function() {
return delayedFunctionScheduler;
},
mockDate
);
clock.install();
const timeout = clock.setTimeout(delayedFn, 0);
clock.install();
clock.clearTimeout(123);
if (!NODE_JS) {
expect(timeout).toEqual(123);
} else {
expect(timeout.constructor.name).toEqual('FakeTimeout');
}
expect(fakeClearTimeout).not.toHaveBeenCalled();
expect(
delayedFunctionScheduler.removeFunctionWithId
).toHaveBeenCalledWith(123);
});
});
it('clears the scheduled function with the scheduler', function() {
const fakeClearTimeout = jasmine.createSpy('clearTimeout'),
delayedFunctionScheduler = jasmine.createSpyObj(
'delayedFunctionScheduler',
['removeFunctionWithId']
),
fakeGlobal = { setTimeout: fakeClearTimeout },
mockDate = {
install: function() {},
tick: function() {},
uninstall: function() {}
},
clock = new jasmineUnderTest.Clock(
fakeGlobal,
function() {
return delayedFunctionScheduler;
describe('setInterval', function() {
it('schedules the delayed function with the fake timer', function() {
const fakeSetInterval = jasmine.createSpy('setInterval'),
scheduleFunction = jasmine.createSpy('scheduleFunction'),
delayedFunctionScheduler = { scheduleFunction: scheduleFunction },
fakeGlobal = { setInterval: fakeSetInterval },
delayedFn = jasmine.createSpy('delayedFn'),
mockDate = {
install: function() {},
tick: function() {},
uninstall: function() {}
},
mockDate
);
clock = new jasmineUnderTest.Clock(
fakeGlobal,
function() {
return delayedFunctionScheduler;
},
mockDate
),
timeout = new clock.FakeTimeout();
clock.install();
clock.clearTimeout(123);
clock.install();
clock.setInterval(delayedFn, 0, 'a', 'b');
expect(fakeClearTimeout).not.toHaveBeenCalled();
expect(delayedFunctionScheduler.removeFunctionWithId).toHaveBeenCalledWith(
123
);
expect(fakeSetInterval).not.toHaveBeenCalled();
if (!NODE_JS) {
expect(delayedFunctionScheduler.scheduleFunction).toHaveBeenCalledWith(
delayedFn,
0,
['a', 'b'],
true
);
} else {
expect(delayedFunctionScheduler.scheduleFunction).toHaveBeenCalledWith(
delayedFn,
0,
['a', 'b'],
true,
timeout
);
}
});
it('returns an id for the delayed function', function() {
const fakeSetInterval = jasmine.createSpy('setInterval'),
scheduleId = 123,
scheduleFunction = jasmine
.createSpy('scheduleFunction')
.and.returnValue(scheduleId),
delayedFunctionScheduler = { scheduleFunction: scheduleFunction },
fakeGlobal = { setInterval: fakeSetInterval },
delayedFn = jasmine.createSpy('delayedFn'),
mockDate = {
install: function() {},
tick: function() {},
uninstall: function() {}
},
clock = new jasmineUnderTest.Clock(
fakeGlobal,
function() {
return delayedFunctionScheduler;
},
mockDate
);
clock.install();
const interval = clock.setInterval(delayedFn, 0);
if (!NODE_JS) {
expect(interval).toEqual(123);
} else {
expect(interval.constructor.name).toEqual('FakeTimeout');
}
});
});
it('schedules the delayed function with the fake timer', function() {
const fakeSetInterval = jasmine.createSpy('setInterval'),
scheduleFunction = jasmine.createSpy('scheduleFunction'),
delayedFunctionScheduler = { scheduleFunction: scheduleFunction },
fakeGlobal = { setInterval: fakeSetInterval },
delayedFn = jasmine.createSpy('delayedFn'),
mockDate = {
install: function() {},
tick: function() {},
uninstall: function() {}
},
clock = new jasmineUnderTest.Clock(
fakeGlobal,
function() {
return delayedFunctionScheduler;
describe('clearInterval', function() {
it('clears the scheduled function with the scheduler', function() {
const clearInterval = jasmine.createSpy('clearInterval'),
delayedFunctionScheduler = jasmine.createSpyObj(
'delayedFunctionScheduler',
['removeFunctionWithId']
),
fakeGlobal = { setInterval: clearInterval },
mockDate = {
install: function() {},
tick: function() {},
uninstall: function() {}
},
mockDate
),
timeout = new clock.FakeTimeout();
clock = new jasmineUnderTest.Clock(
fakeGlobal,
function() {
return delayedFunctionScheduler;
},
mockDate
);
clock.install();
clock.setInterval(delayedFn, 0, 'a', 'b');
clock.install();
clock.clearInterval(123);
expect(fakeSetInterval).not.toHaveBeenCalled();
if (!NODE_JS) {
expect(delayedFunctionScheduler.scheduleFunction).toHaveBeenCalledWith(
delayedFn,
0,
['a', 'b'],
true
);
} else {
expect(delayedFunctionScheduler.scheduleFunction).toHaveBeenCalledWith(
delayedFn,
0,
['a', 'b'],
true,
timeout
);
}
});
it('returns an id for the delayed function', function() {
const fakeSetInterval = jasmine.createSpy('setInterval'),
scheduleId = 123,
scheduleFunction = jasmine
.createSpy('scheduleFunction')
.and.returnValue(scheduleId),
delayedFunctionScheduler = { scheduleFunction: scheduleFunction },
fakeGlobal = { setInterval: fakeSetInterval },
delayedFn = jasmine.createSpy('delayedFn'),
mockDate = {
install: function() {},
tick: function() {},
uninstall: function() {}
},
clock = new jasmineUnderTest.Clock(
fakeGlobal,
function() {
return delayedFunctionScheduler;
},
mockDate
);
clock.install();
const interval = clock.setInterval(delayedFn, 0);
if (!NODE_JS) {
expect(interval).toEqual(123);
} else {
expect(interval.constructor.name).toEqual('FakeTimeout');
}
});
it('clears the scheduled function with the scheduler', function() {
const clearInterval = jasmine.createSpy('clearInterval'),
delayedFunctionScheduler = jasmine.createSpyObj(
'delayedFunctionScheduler',
['removeFunctionWithId']
),
fakeGlobal = { setInterval: clearInterval },
mockDate = {
install: function() {},
tick: function() {},
uninstall: function() {}
},
clock = new jasmineUnderTest.Clock(
fakeGlobal,
function() {
return delayedFunctionScheduler;
},
mockDate
);
clock.install();
clock.clearInterval(123);
expect(clearInterval).not.toHaveBeenCalled();
expect(delayedFunctionScheduler.removeFunctionWithId).toHaveBeenCalledWith(
123
);
expect(clearInterval).not.toHaveBeenCalled();
expect(
delayedFunctionScheduler.removeFunctionWithId
).toHaveBeenCalledWith(123);
});
});
it('gives you a friendly reminder if the Clock is not installed and you tick', function() {

View File

@@ -0,0 +1,134 @@
describe('Configuration', function() {
const standardBooleanKeys = [
'random',
'stopOnSpecFailure',
'stopSpecOnExpectationFailure',
'failSpecWithNoExpectations',
'hideDisabled',
'autoCleanClosures',
'forbidDuplicateNames',
'detectLateRejectionHandling'
];
const allKeys = [
...standardBooleanKeys,
'seed',
'specFilter',
'verboseDeprecations'
];
Object.freeze(standardBooleanKeys);
Object.freeze(allKeys);
it('provides defaults', function() {
const subject = new jasmineUnderTest.Configuration();
expect(subject.random).toEqual(true);
expect(subject.seed).toBeNull();
expect(subject.stopOnSpecFailure).toEqual(false);
expect(subject.stopSpecOnExpectationFailure).toEqual(false);
expect(subject.failSpecWithNoExpectations).toEqual(false);
expect(subject.specFilter).toEqual(jasmine.any(Function));
expect(subject.specFilter()).toEqual(true);
expect(subject.hideDisabled).toEqual(false);
expect(subject.autoCleanClosures).toEqual(true);
expect(subject.forbidDuplicateNames).toEqual(false);
expect(subject.verboseDeprecations).toEqual(false);
expect(subject.detectLateRejectionHandling).toEqual(false);
});
describe('copy()', function() {
it('returns a copy of the configuration as a plain old JS object', function() {
const subject = new jasmineUnderTest.Configuration();
const copy = subject.copy();
expect(copy.constructor.name).toEqual('Object');
expect(new Set(Object.keys(copy))).toEqual(new Set(allKeys));
for (const k of allKeys) {
expect(copy[k]).toEqual(subject[k]);
}
});
});
describe('update()', function() {
it('does not update properties that are absent from the parameter', function() {
const subject = new jasmineUnderTest.Configuration();
const originalValues = subject.copy();
subject.update({});
expect(subject.copy()).toEqual(originalValues);
});
function booleanPropertyBehavior(key) {
it('does not update the property if the specified value is undefined', function() {
const subject = new jasmineUnderTest.Configuration();
const orig = subject[key];
subject.update({ [key]: undefined });
expect(subject[key]).toEqual(orig);
});
it('updates the property if the specified value is not undefined', function() {
const subject = new jasmineUnderTest.Configuration();
const orig = subject[key];
subject.update({ [key]: !orig });
expect(subject[key]).toEqual(!orig);
subject.update({ [key]: orig });
expect(subject[key]).toEqual(orig);
});
}
for (const k of standardBooleanKeys) {
describe(k, function() {
booleanPropertyBehavior(k);
});
}
// TODO: in the next major release, treat verboseDeprecations like other booleans
it('sets verboseDeprecations when present', function() {
const subject = new jasmineUnderTest.Configuration();
const orig = subject.verboseDeprecations;
subject.update({ verboseDeprecations: !orig });
expect(subject.verboseDeprecations).toEqual(!orig);
subject.update({ verboseDeprecations: orig });
expect(subject.verboseDeprecations).toEqual(orig);
// For backwards compatibility, explicitly setting to undefined should
// work. Undefined isn't officially valid but gets treated like false.
subject.update({ verboseDeprecations: undefined });
expect(subject.verboseDeprecations).toBeUndefined();
});
it('sets specFilter when truthy', function() {
const subject = new jasmineUnderTest.Configuration();
const orig = subject.specFilter;
subject.update({ specFilter: undefined });
expect(subject.specFilter).toBe(orig);
subject.update({ specFilter: false });
expect(subject.specFilter).toBe(orig);
function newSpecFilter() {}
subject.update({ specFilter: newSpecFilter });
expect(subject.specFilter).toBe(newSpecFilter);
});
it('sets seed when not undefined', function() {
const subject = new jasmineUnderTest.Configuration();
subject.update({ seed: undefined });
expect(subject.seed).toBeNull();
subject.update({ seed: 1234 });
expect(subject.seed).toEqual(1234);
subject.update({ seed: null });
expect(subject.seed).toBeNull();
});
});
});

View File

@@ -86,12 +86,13 @@ describe('DelayedFunctionScheduler', function() {
it('increments scheduled fns ids unless one is passed', function() {
const scheduler = new jasmineUnderTest.DelayedFunctionScheduler();
expect(scheduler.scheduleFunction(function() {}, 0)).toBe(1);
expect(scheduler.scheduleFunction(function() {}, 0)).toBe(2);
const initial = scheduler.scheduleFunction(function() {}, 0);
expect(scheduler.scheduleFunction(function() {}, 0)).toBe(initial + 1);
expect(scheduler.scheduleFunction(function() {}, 0)).toBe(initial + 2);
expect(scheduler.scheduleFunction(function() {}, 0, [], false, 123)).toBe(
123
);
expect(scheduler.scheduleFunction(function() {}, 0)).toBe(3);
expect(scheduler.scheduleFunction(function() {}, 0)).toBe(initial + 3);
});
it('#removeFunctionWithId removes a previously scheduled function with a given id', function() {
@@ -313,6 +314,28 @@ describe('DelayedFunctionScheduler', function() {
expect(tickDate).toHaveBeenCalledWith(1);
});
it('does not conflict with native timer IDs', function() {
const NODE_JS =
typeof process !== 'undefined' &&
process.versions &&
typeof process.versions.node === 'string';
if (NODE_JS) {
pending('numeric timer ID conflicts only relevant for browsers.');
}
const nativeTimeoutId = setTimeout(function() {}, 100);
const scheduler = new jasmineUnderTest.DelayedFunctionScheduler();
const fn = jasmine.createSpy('fn');
for (let i = 0; i < nativeTimeoutId; i++) {
scheduler.scheduleFunction(fn, 0, [], false);
}
scheduler.removeFunctionWithId(nativeTimeoutId);
scheduler.tick(1);
expect(fn).toHaveBeenCalledTimes(nativeTimeoutId);
});
describe('ticking inside a scheduled function', function() {
let clock;

View File

@@ -625,9 +625,12 @@ describe('Env', function() {
'popListener',
'removeOverrideListener'
]);
spyOn(jasmineUnderTest, 'GlobalErrors').and.returnValue(globalErrors);
env.cleanup_();
env = new jasmineUnderTest.Env();
env = new jasmineUnderTest.Env({
GlobalErrors: function() {
return globalErrors;
}
});
expect(globalErrors.install).toHaveBeenCalled();
});
});
@@ -641,9 +644,13 @@ describe('Env', function() {
'popListener',
'removeOverrideListener'
]);
spyOn(jasmineUnderTest, 'GlobalErrors').and.returnValue(globalErrors);
env.cleanup_();
env = new jasmineUnderTest.Env({ suppressLoadErrors: true });
env = new jasmineUnderTest.Env({
suppressLoadErrors: true,
GlobalErrors: function() {
return globalErrors;
}
});
expect(globalErrors.install).not.toHaveBeenCalled();
env.execute();
expect(globalErrors.install).toHaveBeenCalled();

View File

@@ -1,14 +1,17 @@
describe('GlobalErrors', function() {
it('calls the added handler on error', function() {
const fakeGlobal = browserGlobal();
const globals = browserGlobals();
const handler = jasmine.createSpy('errorHandler');
const errors = new jasmineUnderTest.GlobalErrors(fakeGlobal);
const errors = new jasmineUnderTest.GlobalErrors(
globals.global,
() => ({})
);
errors.install();
errors.pushListener(handler);
const error = new Error('nope');
dispatchErrorEvent(fakeGlobal, { error });
dispatchEvent(globals.listeners, 'error', { error });
expect(handler).toHaveBeenCalledWith(
jasmine.is(error),
@@ -17,17 +20,20 @@ describe('GlobalErrors', function() {
});
it('is not affected by overriding global.onerror', function() {
const fakeGlobal = browserGlobal();
const globals = browserGlobals();
const handler = jasmine.createSpy('errorHandler');
const errors = new jasmineUnderTest.GlobalErrors(fakeGlobal);
const errors = new jasmineUnderTest.GlobalErrors(
globals.global,
() => ({})
);
errors.install();
errors.pushListener(handler);
fakeGlobal.onerror = () => {};
globals.global.onerror = () => {};
const error = new Error('nope');
dispatchErrorEvent(fakeGlobal, { error });
dispatchEvent(globals.listeners, 'error', { error });
expect(handler).toHaveBeenCalledWith(
jasmine.is(error),
@@ -36,17 +42,20 @@ describe('GlobalErrors', function() {
});
it('only calls the most recent handler', function() {
const fakeGlobal = browserGlobal();
const globals = browserGlobals();
const handler1 = jasmine.createSpy('errorHandler1');
const handler2 = jasmine.createSpy('errorHandler2');
const errors = new jasmineUnderTest.GlobalErrors(fakeGlobal);
const errors = new jasmineUnderTest.GlobalErrors(
globals.global,
() => ({})
);
errors.install();
errors.pushListener(handler1);
errors.pushListener(handler2);
const error = new Error('nope');
dispatchErrorEvent(fakeGlobal, { error });
dispatchEvent(globals.listeners, 'error', { error });
expect(handler1).not.toHaveBeenCalled();
expect(handler2).toHaveBeenCalledWith(
@@ -56,10 +65,13 @@ describe('GlobalErrors', function() {
});
it('calls previous handlers when one is removed', function() {
const fakeGlobal = browserGlobal();
const globals = browserGlobals();
const handler1 = jasmine.createSpy('errorHandler1');
const handler2 = jasmine.createSpy('errorHandler2');
const errors = new jasmineUnderTest.GlobalErrors(fakeGlobal);
const errors = new jasmineUnderTest.GlobalErrors(
globals.global,
() => ({})
);
errors.install();
errors.pushListener(handler1);
@@ -68,7 +80,7 @@ describe('GlobalErrors', function() {
errors.popListener(handler2);
const error = new Error('nope');
dispatchErrorEvent(fakeGlobal, { error });
dispatchEvent(globals.listeners, 'error', { error });
expect(handler1).toHaveBeenCalledWith(
jasmine.is(error),
@@ -85,26 +97,32 @@ describe('GlobalErrors', function() {
});
it('uninstalls itself', function() {
const fakeGlobal = browserGlobal();
const errors = new jasmineUnderTest.GlobalErrors(fakeGlobal);
const globals = browserGlobals();
const errors = new jasmineUnderTest.GlobalErrors(
globals.global,
() => ({})
);
function unrelatedListener() {}
errors.install();
fakeGlobal.addEventListener('error', unrelatedListener);
globals.global.addEventListener('error', unrelatedListener);
errors.uninstall();
expect(fakeGlobal.listeners_.error).toEqual([unrelatedListener]);
expect(globals.listeners.error).toEqual([unrelatedListener]);
});
it('rethrows the original error when there is no handler', function() {
const fakeGlobal = browserGlobal();
const errors = new jasmineUnderTest.GlobalErrors(fakeGlobal);
const globals = browserGlobals();
const errors = new jasmineUnderTest.GlobalErrors(
globals.global,
() => ({})
);
const originalError = new Error('nope');
errors.install();
try {
dispatchErrorEvent(fakeGlobal, { error: originalError });
dispatchEvent(globals.listeners, 'error', { error: originalError });
} catch (e) {
expect(e).toBe(originalError);
}
@@ -113,191 +131,281 @@ describe('GlobalErrors', function() {
});
it('reports uncaught exceptions in node.js', function() {
const fakeGlobal = {
process: {
on: jasmine.createSpy('process.on'),
removeListener: jasmine.createSpy('process.removeListener'),
listeners: jasmine
.createSpy('process.listeners')
.and.returnValue(['foo']),
removeAllListeners: jasmine.createSpy('process.removeAllListeners')
}
},
handler = jasmine.createSpy('errorHandler'),
errors = new jasmineUnderTest.GlobalErrors(fakeGlobal);
const globals = nodeGlobals();
const errors = new jasmineUnderTest.GlobalErrors(
globals.global,
() => ({})
);
const handler = jasmine.createSpy('errorHandler');
function originalHandler() {}
globals.listeners.uncaughtException = [originalHandler];
errors.install();
expect(fakeGlobal.process.on).toHaveBeenCalledWith(
'uncaughtException',
expect(globals.listeners.uncaughtException).toEqual([
jasmine.any(Function)
);
expect(fakeGlobal.process.listeners).toHaveBeenCalledWith(
'uncaughtException'
);
expect(fakeGlobal.process.removeAllListeners).toHaveBeenCalledWith(
'uncaughtException'
);
]);
expect(globals.listeners.uncaughtException).not.toEqual([
originalHandler()
]);
errors.pushListener(handler);
const addedListener = fakeGlobal.process.on.calls.argsFor(0)[1];
addedListener(new Error('bar'));
dispatchEvent(globals.listeners, 'uncaughtException', new Error('bar'));
expect(handler).toHaveBeenCalledWith(new Error('bar'));
expect(handler).toHaveBeenCalledWith(new Error('bar'), undefined);
expect(handler.calls.argsFor(0)[0].jasmineMessage).toBe(
'Uncaught exception: Error: bar'
);
errors.uninstall();
expect(fakeGlobal.process.removeListener).toHaveBeenCalledWith(
'uncaughtException',
addedListener
);
expect(fakeGlobal.process.on).toHaveBeenCalledWith(
'uncaughtException',
'foo'
);
expect(globals.listeners.uncaughtException).toEqual([originalHandler]);
});
describe('Reporting unhandled promise rejections in node.js', function() {
it('reports rejections with `Error` reasons', function() {
const fakeGlobal = {
process: {
on: jasmine.createSpy('process.on'),
removeListener: jasmine.createSpy('process.removeListener'),
listeners: jasmine
.createSpy('process.listeners')
.and.returnValue(['foo']),
removeAllListeners: jasmine.createSpy('process.removeAllListeners')
}
},
handler = jasmine.createSpy('errorHandler'),
errors = new jasmineUnderTest.GlobalErrors(fakeGlobal);
const globals = nodeGlobals();
const errors = new jasmineUnderTest.GlobalErrors(
globals.global,
() => ({})
);
const handler = jasmine.createSpy('errorHandler');
function originalHandler() {}
globals.listeners.unhandledRejection = [originalHandler];
errors.install();
expect(fakeGlobal.process.on).toHaveBeenCalledWith(
'unhandledRejection',
expect(globals.listeners.unhandledRejection).toEqual([
jasmine.any(Function)
);
expect(fakeGlobal.process.listeners).toHaveBeenCalledWith(
'unhandledRejection'
);
expect(fakeGlobal.process.removeAllListeners).toHaveBeenCalledWith(
'unhandledRejection'
);
]);
expect(globals.listeners.unhandledRejection).not.toEqual([
originalHandler()
]);
errors.pushListener(handler);
const addedListener = fakeGlobal.process.on.calls.argsFor(1)[1];
addedListener(new Error('bar'));
dispatchEvent(globals.listeners, 'unhandledRejection', new Error('bar'));
expect(handler).toHaveBeenCalledWith(new Error('bar'));
expect(handler).toHaveBeenCalledWith(new Error('bar'), undefined);
expect(handler.calls.argsFor(0)[0].jasmineMessage).toBe(
'Unhandled promise rejection: Error: bar'
);
errors.uninstall();
expect(fakeGlobal.process.removeListener).toHaveBeenCalledWith(
'unhandledRejection',
addedListener
);
expect(fakeGlobal.process.on).toHaveBeenCalledWith(
'unhandledRejection',
'foo'
);
expect(globals.listeners.unhandledRejection).toEqual([originalHandler]);
});
it('reports rejections with non-`Error` reasons', function() {
const fakeGlobal = {
process: {
on: jasmine.createSpy('process.on'),
removeListener: function() {},
listeners: function() {
return [];
},
removeAllListeners: function() {}
}
},
handler = jasmine.createSpy('errorHandler'),
errors = new jasmineUnderTest.GlobalErrors(fakeGlobal);
const globals = nodeGlobals();
const errors = new jasmineUnderTest.GlobalErrors(
globals.global,
() => ({})
);
const handler = jasmine.createSpy('errorHandler');
errors.install();
errors.pushListener(handler);
expect(fakeGlobal.process.on.calls.argsFor(1)[0]).toEqual(
'unhandledRejection'
);
const addedListener = fakeGlobal.process.on.calls.argsFor(1)[1];
addedListener(17);
dispatchEvent(globals.listeners, 'unhandledRejection', 17);
expect(handler).toHaveBeenCalledWith(
new Error(
'Unhandled promise rejection: 17\n' +
'(Tip: to get a useful stack trace, use ' +
'Promise.reject(new Error(...)) instead of Promise.reject(...).)'
)
),
undefined
);
});
it('reports rejections with no reason provided', function() {
const fakeGlobal = {
process: {
on: jasmine.createSpy('process.on'),
removeListener: function() {},
listeners: function() {
return [];
},
removeAllListeners: function() {}
}
},
handler = jasmine.createSpy('errorHandler'),
errors = new jasmineUnderTest.GlobalErrors(fakeGlobal);
const globals = nodeGlobals();
const errors = new jasmineUnderTest.GlobalErrors(
globals.global,
() => ({})
);
const handler = jasmine.createSpy('errorHandler');
errors.install();
errors.pushListener(handler);
expect(fakeGlobal.process.on.calls.argsFor(1)[0]).toEqual(
'unhandledRejection'
);
const addedListener = fakeGlobal.process.on.calls.argsFor(1)[1];
addedListener(undefined);
dispatchEvent(globals.listeners, 'unhandledRejection', undefined);
expect(handler).toHaveBeenCalledWith(
new Error(
'Unhandled promise rejection with no error or message\n' +
'(Tip: to get a useful stack trace, use ' +
'Promise.reject(new Error(...)) instead of Promise.reject().)'
)
),
undefined
);
});
describe('When detectLateRejectionHandling is true', function() {
let globals, errors;
beforeEach(function() {
globals = nodeGlobals();
errors = new jasmineUnderTest.GlobalErrors(globals.global, () => ({
detectLateRejectionHandling: true
}));
});
it('subscribes and unsubscribes from the rejectionHandled event', function() {
function originalHandler() {}
globals.global.process.on('rejectionHandled', originalHandler);
errors.install();
expect(globals.listeners.rejectionHandled).toEqual([
jasmine.any(Function)
]);
expect(globals.listeners.rejectionHandled).not.toEqual([
originalHandler
]);
errors.uninstall();
expect(globals.listeners.rejectionHandled).toEqual([originalHandler]);
});
describe("When the unhandledRejection event doesn't have a promise", function() {
it('immediately reports the rejection', function() {
const handler = jasmine.createSpy('errorHandler');
errors.install();
errors.pushListener(handler);
dispatchEvent(
globals.listeners,
'unhandledRejection',
new Error('nope'),
undefined
);
expect(handler).toHaveBeenCalledWith(new Error('nope'), undefined);
expect(handler.calls.argsFor(0)[0].jasmineMessage).toBe(
'Unhandled promise rejection: Error: nope'
);
});
});
describe('When the unhandledRejection event has a promise property', function() {
it('does not immediately report the rejection', function() {
const handler = jasmine.createSpy('errorHandler');
errors.install();
errors.pushListener(handler);
const promise = Promise.reject('nope');
promise.catch(() => {});
dispatchEvent(
globals.listeners,
'unhandledRejection',
'nope',
promise
);
expect(handler).not.toHaveBeenCalled();
});
describe('When reportUnhandledRejections is called', function() {
it('reports rejections that have not been handled', function() {
const handler = jasmine.createSpy('errorHandler');
errors.install();
errors.pushListener(handler);
const reason = new Error('nope');
const promise = Promise.reject(reason);
promise.catch(() => {});
dispatchEvent(
globals.listeners,
'unhandledRejection',
reason,
promise
);
errors.reportUnhandledRejections();
expect(handler).toHaveBeenCalledWith(new Error('nope'), undefined);
expect(handler.calls.argsFor(0)[0].jasmineMessage).toBe(
'Unhandled promise rejection: Error: nope'
);
});
it('does not report rejections that have been handled', function() {
const handler = jasmine.createSpy('errorHandler');
errors.install();
errors.pushListener(handler);
const reason = new Error('nope');
const promise = Promise.reject(reason);
promise.catch(() => {});
dispatchEvent(
globals.listeners,
'unhandledRejection',
reason,
promise
);
dispatchEvent(globals.listeners, 'rejectionHandled', promise);
errors.reportUnhandledRejections();
expect(handler).not.toHaveBeenCalled();
});
it('does not report the same rejection on subsequent calls', function() {
const handler = jasmine.createSpy('errorHandler');
errors.install();
errors.pushListener(handler);
const promise = Promise.reject('nope');
promise.catch(() => {});
dispatchEvent(
globals.listeners,
'unhandledRejection',
'nope',
promise
);
errors.reportUnhandledRejections();
expect(handler).toHaveBeenCalled();
handler.calls.reset();
errors.reportUnhandledRejections();
expect(handler).not.toHaveBeenCalled();
});
});
});
});
});
describe('Reporting unhandled promise rejections in the browser', function() {
it('subscribes and unsubscribes from the unhandledrejection event', function() {
const fakeGlobal = browserGlobal();
const errors = new jasmineUnderTest.GlobalErrors(fakeGlobal);
const globals = browserGlobals();
const errors = new jasmineUnderTest.GlobalErrors(
globals.global,
() => ({})
);
errors.install();
expect(fakeGlobal.listeners_.unhandledrejection).toEqual([
expect(globals.listeners.unhandledrejection).toEqual([
jasmine.any(Function)
]);
errors.uninstall();
expect(fakeGlobal.listeners_.unhandledrejection).toEqual([]);
expect(globals.listeners.unhandledrejection).toEqual([]);
});
it('reports rejections whose reason is a string', function() {
const fakeGlobal = browserGlobal();
const globals = browserGlobals();
const handler = jasmine.createSpy('errorHandler');
const errors = new jasmineUnderTest.GlobalErrors(fakeGlobal);
const errors = new jasmineUnderTest.GlobalErrors(
globals.global,
() => ({})
);
errors.install();
errors.pushListener(handler);
const event = { reason: 'nope' };
dispatchUnhandledRejectionEvent(fakeGlobal, event);
dispatchEvent(globals.listeners, 'unhandledrejection', event);
expect(handler).toHaveBeenCalledWith(
'Unhandled promise rejection: nope',
@@ -306,16 +414,19 @@ describe('GlobalErrors', function() {
});
it('reports rejections whose reason is an Error', function() {
const fakeGlobal = browserGlobal();
const globals = browserGlobals();
const handler = jasmine.createSpy('errorHandler');
const errors = new jasmineUnderTest.GlobalErrors(fakeGlobal);
const errors = new jasmineUnderTest.GlobalErrors(
globals.global,
() => ({})
);
errors.install();
errors.pushListener(handler);
const reason = new Error('bar');
const event = { reason };
dispatchUnhandledRejectionEvent(fakeGlobal, event);
dispatchEvent(globals.listeners, 'unhandledrejection', event);
expect(handler).toHaveBeenCalledWith(
jasmine.objectContaining({
@@ -326,77 +437,178 @@ describe('GlobalErrors', function() {
event
);
});
describe('When detectLateRejectionHandling is true', function() {
let globals, errors;
beforeEach(function() {
globals = browserGlobals();
errors = new jasmineUnderTest.GlobalErrors(globals.global, () => ({
detectLateRejectionHandling: true
}));
});
it('subscribes and unsubscribes from the rejectionhandled event', function() {
errors.install();
expect(globals.listeners.rejectionhandled).toEqual([
jasmine.any(Function)
]);
errors.uninstall();
expect(globals.listeners.rejectionhandled).toEqual([]);
});
describe("When the unhandledrejection event doesn't have a promise property", function() {
it('immediately reports the rejection', function() {
const handler = jasmine.createSpy('errorHandler');
errors.install();
errors.pushListener(handler);
const event = { reason: 'nope' };
dispatchEvent(globals.listeners, 'unhandledrejection', event);
expect(handler).toHaveBeenCalledWith(
'Unhandled promise rejection: nope',
event
);
});
});
describe('When the unhandledrejection event has a promise property', function() {
it('does not immediately report the rejection', function() {
const handler = jasmine.createSpy('errorHandler');
errors.install();
errors.pushListener(handler);
const promise = Promise.reject('nope');
promise.catch(() => {});
dispatchEvent(globals.listeners, 'unhandledrejection', {
reason: 'nope',
promise
});
expect(handler).not.toHaveBeenCalled();
});
describe('When reportUnhandledRejections is called', function() {
it('reports rejections that have not been handled', function() {
const handler = jasmine.createSpy('errorHandler');
errors.install();
errors.pushListener(handler);
const promise = Promise.reject('nope');
promise.catch(() => {});
dispatchEvent(globals.listeners, 'unhandledrejection', {
reason: 'nope',
promise
});
errors.reportUnhandledRejections();
expect(handler).toHaveBeenCalledWith(
'Unhandled promise rejection: nope',
{ reason: 'nope', promise }
);
});
it('does not report rejections that have been handled', function() {
const handler = jasmine.createSpy('errorHandler');
errors.install();
errors.pushListener(handler);
const promise = Promise.reject('nope');
promise.catch(() => {});
dispatchEvent(globals.listeners, 'unhandledrejection', {
reason: 'nope',
promise
});
dispatchEvent(globals.listeners, 'rejectionhandled', { promise });
errors.reportUnhandledRejections();
expect(handler).not.toHaveBeenCalled();
});
it('does not report the same rejection on subsequent calls', function() {
const handler = jasmine.createSpy('errorHandler');
errors.install();
errors.pushListener(handler);
const promise = Promise.reject('nope');
promise.catch(() => {});
dispatchEvent(globals.listeners, 'unhandledrejection', {
reason: 'nope',
promise
});
errors.reportUnhandledRejections();
expect(handler).toHaveBeenCalled();
handler.calls.reset();
errors.reportUnhandledRejections();
expect(handler).not.toHaveBeenCalled();
});
});
});
});
});
describe('Reporting uncaught exceptions in node.js', function() {
it('prepends a descriptive message when the error is not an `Error`', function() {
const fakeGlobal = {
process: {
on: jasmine.createSpy('process.on'),
removeListener: function() {},
listeners: function() {
return [];
},
removeAllListeners: function() {}
}
};
const globals = nodeGlobals();
const errors = new jasmineUnderTest.GlobalErrors(
globals.global,
() => ({})
);
const handler = jasmine.createSpy('errorHandler');
const errors = new jasmineUnderTest.GlobalErrors(fakeGlobal);
errors.install();
errors.pushListener(handler);
uncaughtExceptionListener(fakeGlobal)(17);
dispatchEvent(globals.listeners, 'uncaughtException', 17);
expect(handler).toHaveBeenCalledWith(new Error('Uncaught exception: 17'));
expect(handler).toHaveBeenCalledWith(
new Error('Uncaught exception: 17'),
undefined
);
});
it('substitutes a descriptive message when the error is falsy', function() {
const fakeGlobal = {
process: {
on: jasmine.createSpy('process.on'),
removeListener: function() {},
listeners: function() {
return [];
},
removeAllListeners: function() {}
}
};
const globals = nodeGlobals();
const errors = new jasmineUnderTest.GlobalErrors(
globals.global,
() => ({})
);
const handler = jasmine.createSpy('errorHandler');
const errors = new jasmineUnderTest.GlobalErrors(fakeGlobal);
errors.install();
errors.pushListener(handler);
uncaughtExceptionListener(fakeGlobal)();
dispatchEvent(globals.listeners, 'uncaughtException', undefined);
expect(handler).toHaveBeenCalledWith(
new Error('Uncaught exception with no error or message')
new Error('Uncaught exception with no error or message'),
undefined
);
});
function uncaughtExceptionListener(global) {
// Grab the right listener
expect(global.process.on.calls.argsFor(0)[0]).toEqual(
'uncaughtException'
);
return global.process.on.calls.argsFor(0)[1];
}
});
describe('#setOverrideListener', function() {
it('overrides the existing handlers in browsers until removed', function() {
const fakeGlobal = browserGlobal();
const globals = browserGlobals();
const handler0 = jasmine.createSpy('handler0');
const handler1 = jasmine.createSpy('handler1');
const overrideHandler = jasmine.createSpy('overrideHandler');
const errors = new jasmineUnderTest.GlobalErrors(fakeGlobal);
const errors = new jasmineUnderTest.GlobalErrors(
globals.global,
() => ({})
);
errors.install();
errors.pushListener(handler0);
errors.setOverrideListener(overrideHandler, () => {});
errors.pushListener(handler1);
dispatchErrorEvent(fakeGlobal, { error: 'foo' });
dispatchEvent(globals.listeners, 'error', { error: 'foo' });
expect(overrideHandler).toHaveBeenCalledWith('foo');
expect(handler0).not.toHaveBeenCalled();
@@ -405,55 +617,48 @@ describe('GlobalErrors', function() {
errors.removeOverrideListener();
const event = { error: 'baz' };
dispatchErrorEvent(fakeGlobal, event);
dispatchEvent(globals.listeners, 'error', event);
expect(overrideHandler).not.toHaveBeenCalledWith('baz');
expect(handler1).toHaveBeenCalledWith('baz', event);
});
it('overrides the existing handlers in Node until removed', function() {
const globalEventListeners = {};
const fakeGlobal = {
process: {
on: (name, listener) => (globalEventListeners[name] = listener),
removeListener: () => {},
listeners: name => globalEventListeners[name],
removeAllListeners: name => (globalEventListeners[name] = [])
}
};
const globals = nodeGlobals();
const handler0 = jasmine.createSpy('handler0');
const handler1 = jasmine.createSpy('handler1');
const overrideHandler = jasmine.createSpy('overrideHandler');
const errors = new jasmineUnderTest.GlobalErrors(fakeGlobal);
const errors = new jasmineUnderTest.GlobalErrors(
globals.global,
() => ({})
);
errors.install();
errors.pushListener(handler0);
errors.setOverrideListener(overrideHandler);
errors.pushListener(handler1);
globalEventListeners['uncaughtException'](new Error('foo'));
dispatchEvent(globals.listeners, 'uncaughtException', new Error('foo'));
expect(overrideHandler).toHaveBeenCalledWith(new Error('foo'));
expect(handler0).not.toHaveBeenCalled();
expect(handler1).not.toHaveBeenCalled();
overrideHandler.calls.reset();
errors.removeOverrideListener();
globalEventListeners['uncaughtException'](new Error('bar'));
expect(overrideHandler).not.toHaveBeenCalledWith(new Error('bar'));
expect(handler1).toHaveBeenCalledWith(new Error('bar'));
dispatchEvent(globals.listeners, 'uncaughtException', new Error('bar'));
expect(overrideHandler).not.toHaveBeenCalled();
expect(handler1).toHaveBeenCalledWith(new Error('bar'), undefined);
});
it('handles unhandled promise rejections in browsers', function() {
const globalEventListeners = {};
const fakeGlobal = {
addEventListener(name, listener) {
globalEventListeners[name] = listener;
},
removeEventListener() {}
};
const globals = browserGlobals();
const handler = jasmine.createSpy('handler');
const overrideHandler = jasmine.createSpy('overrideHandler');
const errors = new jasmineUnderTest.GlobalErrors(fakeGlobal);
const errors = new jasmineUnderTest.GlobalErrors(
globals.global,
() => ({})
);
errors.install();
errors.pushListener(handler);
@@ -461,7 +666,7 @@ describe('GlobalErrors', function() {
const reason = new Error('bar');
globalEventListeners['unhandledrejection']({ reason: reason });
dispatchEvent(globals.listeners, 'unhandledrejection', { reason });
expect(overrideHandler).toHaveBeenCalledWith(
jasmine.objectContaining({
@@ -474,32 +679,21 @@ describe('GlobalErrors', function() {
});
it('handles unhandled promise rejections in Node', function() {
const globalEventListeners = {};
const fakeGlobal = {
process: {
on(name, listener) {
globalEventListeners[name] = listener;
},
removeListener() {},
listeners(name) {
return globalEventListeners[name];
},
removeAllListeners(name) {
globalEventListeners[name] = null;
}
}
};
const globals = nodeGlobals();
const handler0 = jasmine.createSpy('handler0');
const handler1 = jasmine.createSpy('handler1');
const overrideHandler = jasmine.createSpy('overrideHandler');
const errors = new jasmineUnderTest.GlobalErrors(fakeGlobal);
const errors = new jasmineUnderTest.GlobalErrors(
globals.global,
() => ({})
);
errors.install();
errors.pushListener(handler0);
errors.setOverrideListener(overrideHandler, () => {});
errors.pushListener(handler1);
globalEventListeners['unhandledRejection'](new Error('nope'));
dispatchEvent(globals.listeners, 'unhandledRejection', new Error('nope'));
expect(overrideHandler).toHaveBeenCalledWith(new Error('nope'));
expect(handler0).not.toHaveBeenCalled();
@@ -507,7 +701,7 @@ describe('GlobalErrors', function() {
});
it('throws if there is already an override handler', function() {
const errors = new jasmineUnderTest.GlobalErrors(browserGlobal());
const errors = new jasmineUnderTest.GlobalErrors(browserGlobals().global);
errors.setOverrideListener(() => {}, () => {});
expect(function() {
@@ -519,7 +713,7 @@ describe('GlobalErrors', function() {
describe('#removeOverrideListener', function() {
it("calls the handler's onRemove callback", function() {
const onRemove = jasmine.createSpy('onRemove');
const errors = new jasmineUnderTest.GlobalErrors(browserGlobal());
const errors = new jasmineUnderTest.GlobalErrors(browserGlobals().global);
errors.setOverrideListener(() => {}, onRemove);
errors.removeOverrideListener();
@@ -528,43 +722,69 @@ describe('GlobalErrors', function() {
});
it('does not throw if there is no handler', function() {
const errors = new jasmineUnderTest.GlobalErrors(browserGlobal());
const errors = new jasmineUnderTest.GlobalErrors(browserGlobals().global);
expect(() => errors.removeOverrideListener()).not.toThrow();
});
});
function browserGlobal() {
function browserGlobals() {
const listeners = {
error: [],
unhandledrejection: [],
rejectionhandled: []
};
return {
listeners_: { error: [], unhandledrejection: [] },
addEventListener(eventName, listener) {
this.listeners_[eventName].push(listener);
},
removeEventListener(eventName, listener) {
this.listeners_[eventName] = this.listeners_[eventName].filter(
l => l !== listener
);
listeners,
global: {
addEventListener(eventName, listener) {
listeners[eventName].push(listener);
},
removeEventListener(eventName, listener) {
listeners[eventName] = listeners[eventName].filter(
l => l !== listener
);
}
}
};
}
function dispatchErrorEvent(global, event) {
expect(global.listeners_.error.length)
.withContext('number of error listeners')
.toBeGreaterThan(0);
for (const l of global.listeners_.error) {
l(event);
}
function nodeGlobals() {
const listeners = {
uncaughtException: [],
unhandledRejection: [],
rejectionHandled: []
};
return {
listeners,
global: {
process: {
on(eventName, listener) {
listeners[eventName].push(listener);
},
removeListener(eventName, listener) {
listeners[eventName] = listeners[eventName].filter(
l => l !== listener
);
},
removeAllListeners(eventName) {
listeners[eventName] = [];
},
listeners(eventName) {
return listeners[eventName];
}
}
}
};
}
function dispatchUnhandledRejectionEvent(global, event) {
expect(global.listeners_.unhandledrejection.length)
.withContext('number of unhandledrejection listeners')
function dispatchEvent(listeners, eventName, ...args) {
expect(listeners[eventName].length)
.withContext(`number of ${eventName} listeners`)
.toBeGreaterThan(0);
for (const l of global.listeners_.unhandledrejection) {
l(event);
for (const l of listeners[eventName]) {
l.apply(null, args);
}
}
});

View File

@@ -1,4 +1,20 @@
describe('QueueRunner', function() {
it('validates that queueableFns are truthy', function() {
expect(function() {
new jasmineUnderTest.QueueRunner({
queueableFns: [undefined]
});
}).toThrowError('Received a falsy queueableFn');
});
it('validates that queueableFns have fn properties', function() {
expect(function() {
new jasmineUnderTest.QueueRunner({
queueableFns: [{ fn: undefined }]
});
}).toThrowError('Received a queueableFn with no fn');
});
it("runs all the functions it's passed", function() {
const calls = [],
queueableFn1 = { fn: jasmine.createSpy('fn1') },

View File

@@ -12,10 +12,10 @@ describe('ReportDispatcher', function() {
});
it('dispatches requested methods to added reporters', function() {
const queueRunnerFactory = jasmine.createSpy('queueRunner'),
const runQueue = jasmine.createSpy('runQueue'),
dispatcher = new jasmineUnderTest.ReportDispatcher(
['foo', 'bar'],
queueRunnerFactory
runQueue
),
reporter = jasmine.createSpyObj('reporter', ['foo', 'bar']),
anotherReporter = jasmine.createSpyObj('reporter', ['foo', 'bar']);
@@ -25,7 +25,7 @@ describe('ReportDispatcher', function() {
dispatcher.foo(123, 456);
expect(queueRunnerFactory).toHaveBeenCalledWith(
expect(runQueue).toHaveBeenCalledWith(
jasmine.objectContaining({
queueableFns: [
{ fn: jasmine.any(Function) },
@@ -35,7 +35,7 @@ describe('ReportDispatcher', function() {
})
);
let fns = queueRunnerFactory.calls.mostRecent().args[0].queueableFns;
let fns = runQueue.calls.mostRecent().args[0].queueableFns;
fns[0].fn();
expect(reporter.foo).toHaveBeenCalledWith(123, 456);
expect(reporter.foo.calls.mostRecent().object).toBe(reporter);
@@ -44,11 +44,11 @@ describe('ReportDispatcher', function() {
expect(anotherReporter.foo).toHaveBeenCalledWith(123, 456);
expect(anotherReporter.foo.calls.mostRecent().object).toBe(anotherReporter);
queueRunnerFactory.calls.reset();
runQueue.calls.reset();
dispatcher.bar('a', 'b');
expect(queueRunnerFactory).toHaveBeenCalledWith(
expect(runQueue).toHaveBeenCalledWith(
jasmine.objectContaining({
queueableFns: [
{ fn: jasmine.any(Function) },
@@ -58,7 +58,7 @@ describe('ReportDispatcher', function() {
})
);
fns = queueRunnerFactory.calls.mostRecent().args[0].queueableFns;
fns = runQueue.calls.mostRecent().args[0].queueableFns;
fns[0].fn();
expect(reporter.bar).toHaveBeenCalledWith('a', 'b');
@@ -67,17 +67,14 @@ describe('ReportDispatcher', function() {
});
it("does not dispatch to a reporter if the reporter doesn't accept the method", function() {
const queueRunnerFactory = jasmine.createSpy('queueRunner'),
dispatcher = new jasmineUnderTest.ReportDispatcher(
['foo'],
queueRunnerFactory
),
const runQueue = jasmine.createSpy('runQueue'),
dispatcher = new jasmineUnderTest.ReportDispatcher(['foo'], runQueue),
reporter = jasmine.createSpyObj('reporter', ['baz']);
dispatcher.addReporter(reporter);
dispatcher.foo(123, 456);
expect(queueRunnerFactory).toHaveBeenCalledWith(
expect(runQueue).toHaveBeenCalledWith(
jasmine.objectContaining({
queueableFns: []
})
@@ -85,33 +82,33 @@ describe('ReportDispatcher', function() {
});
it("allows providing a fallback reporter in case there's no other reporter", function() {
const queueRunnerFactory = jasmine.createSpy('queueRunner'),
const runQueue = jasmine.createSpy('runQueue'),
dispatcher = new jasmineUnderTest.ReportDispatcher(
['foo', 'bar'],
queueRunnerFactory
runQueue
),
reporter = jasmine.createSpyObj('reporter', ['foo', 'bar']);
dispatcher.provideFallbackReporter(reporter);
dispatcher.foo(123, 456);
expect(queueRunnerFactory).toHaveBeenCalledWith(
expect(runQueue).toHaveBeenCalledWith(
jasmine.objectContaining({
queueableFns: [{ fn: jasmine.any(Function) }],
isReporter: true
})
);
const fns = queueRunnerFactory.calls.mostRecent().args[0].queueableFns;
const fns = runQueue.calls.mostRecent().args[0].queueableFns;
fns[0].fn();
expect(reporter.foo).toHaveBeenCalledWith(123, 456);
});
it('does not call fallback reporting methods when another reporter is provided', function() {
const queueRunnerFactory = jasmine.createSpy('queueRunner'),
const runQueue = jasmine.createSpy('runQueue'),
dispatcher = new jasmineUnderTest.ReportDispatcher(
['foo', 'bar'],
queueRunnerFactory
runQueue
),
reporter = jasmine.createSpyObj('reporter', ['foo', 'bar']),
fallbackReporter = jasmine.createSpyObj('otherReporter', ['foo', 'bar']);
@@ -120,38 +117,38 @@ describe('ReportDispatcher', function() {
dispatcher.addReporter(reporter);
dispatcher.foo(123, 456);
expect(queueRunnerFactory).toHaveBeenCalledWith(
expect(runQueue).toHaveBeenCalledWith(
jasmine.objectContaining({
queueableFns: [{ fn: jasmine.any(Function) }],
isReporter: true
})
);
const fns = queueRunnerFactory.calls.mostRecent().args[0].queueableFns;
const fns = runQueue.calls.mostRecent().args[0].queueableFns;
fns[0].fn();
expect(reporter.foo).toHaveBeenCalledWith(123, 456);
expect(fallbackReporter.foo).not.toHaveBeenCalledWith(123, 456);
});
it('allows registered reporters to be cleared', function() {
const queueRunnerFactory = jasmine.createSpy('queueRunner'),
const runQueue = jasmine.createSpy('runQueue'),
dispatcher = new jasmineUnderTest.ReportDispatcher(
['foo', 'bar'],
queueRunnerFactory
runQueue
),
reporter1 = jasmine.createSpyObj('reporter1', ['foo', 'bar']),
reporter2 = jasmine.createSpyObj('reporter2', ['foo', 'bar']);
dispatcher.addReporter(reporter1);
dispatcher.foo(123);
expect(queueRunnerFactory).toHaveBeenCalledWith(
expect(runQueue).toHaveBeenCalledWith(
jasmine.objectContaining({
queueableFns: [{ fn: jasmine.any(Function) }],
isReporter: true
})
);
let fns = queueRunnerFactory.calls.mostRecent().args[0].queueableFns;
let fns = runQueue.calls.mostRecent().args[0].queueableFns;
fns[0].fn();
expect(reporter1.foo).toHaveBeenCalledWith(123);
@@ -159,14 +156,14 @@ describe('ReportDispatcher', function() {
dispatcher.addReporter(reporter2);
dispatcher.bar(456);
expect(queueRunnerFactory).toHaveBeenCalledWith(
expect(runQueue).toHaveBeenCalledWith(
jasmine.objectContaining({
queueableFns: [{ fn: jasmine.any(Function) }],
isReporter: true
})
);
fns = queueRunnerFactory.calls.mostRecent().args[0].queueableFns;
fns = runQueue.calls.mostRecent().args[0].queueableFns;
fns[0].fn();
expect(reporter1.bar).not.toHaveBeenCalled();
expect(reporter2.bar).toHaveBeenCalledWith(456);

630
spec/core/RunnerSpec.js Normal file
View File

@@ -0,0 +1,630 @@
describe('Runner', function() {
describe('Integration with TreeProcessor and TreeRunner', function() {
let suiteNumber,
specNumber,
runQueue,
globalErrors,
reportDispatcher,
failSpecWithNoExpectations,
detectLateRejectionHandling;
beforeEach(function() {
suiteNumber = 0;
specNumber = 0;
runQueue = jasmine.createSpy('runQueue');
globalErrors = 'the global errors instance';
reportDispatcher = jasmine.createSpyObj(
'reportDispatcher',
jasmineUnderTest.reporterEvents
);
for (const k of jasmineUnderTest.reporterEvents) {
reportDispatcher[k].and.returnValue(Promise.resolve());
}
// Reasonable defaults, may be overridden in some cases
failSpecWithNoExpectations = false;
detectLateRejectionHandling = false;
spyOn(jasmineUnderTest.TreeRunner.prototype, '_executeSpec');
});
function StubSuite(attrs) {
attrs = attrs || {};
this.id = 'suite' + suiteNumber++;
this.children = attrs.children || [];
this.canBeReentered = function() {
return !attrs.noReenter;
};
this.markedPending = attrs.markedPending || false;
this.sharedUserContext = function() {
return attrs.userContext || {};
};
this.result = {
id: this.id,
failedExpectations: []
};
this.getResult = jasmine.createSpy('getResult');
this.beforeAllFns = attrs.beforeAllFns || [];
this.afterAllFns = attrs.afterAllFns || [];
this.cleanupBeforeAfter = function() {};
this.startTimer = function() {};
this.endTimer = function() {};
}
function StubSpec(attrs) {
attrs = attrs || {};
this.id = 'spec' + specNumber++;
this.markedPending = attrs.markedPending || false;
this.execute = jasmine.createSpy(this.id + '#execute');
this.beforeAndAfterFns = () => ({ befores: [], afters: [] });
this.userContext = () => ({});
this.getFullName = () => '';
this.queueableFn = () => {};
}
function makeRunner(topSuite) {
const defaultOptions = {
getConfig: () => ({
specFilter: () => true,
failSpecWithNoExpectations,
detectLateRejectionHandling
}),
focusedRunables: () => [],
totalSpecsDefined: () => 1,
TreeProcessor: jasmineUnderTest.TreeProcessor,
runableResources: {
initForRunable: () => {},
clearForRunable: () => {}
},
reportDispatcher,
globalErrors,
runQueue
};
return new jasmineUnderTest.Runner({
...defaultOptions,
topSuite
});
}
function arrayNotContaining(item) {
return {
asymmetricMatch(other, matchersUtil) {
if (!jasmine.isArray_(other)) {
return false;
}
for (const x of other) {
if (matchersUtil.equals(x, item)) {
return false;
}
}
return true;
}
};
}
// Precondition: jasmineUnderTest.TreeRunner.prototype._executeSpec is a spy
function verifyAndFinishSpec(spec, queueableFn, shouldBeExcluded) {
const ex = jasmineUnderTest.TreeRunner.prototype._executeSpec;
ex.withArgs(spec, 'onComplete').and.callThrough();
queueableFn.fn('onComplete');
expect(ex).toHaveBeenCalledWith(spec, 'onComplete');
expect(runQueue).toHaveBeenCalledWith(
jasmine.objectContaining({
isLeaf: true,
SkipPolicy: jasmineUnderTest.CompleteOnFirstErrorSkipPolicy,
queueableFns: shouldBeExcluded
? arrayNotContaining(spec.queueableFn)
: jasmine.arrayContaining([spec.queueableFn])
})
);
}
it('runs a single spec', async function() {
const spec = new StubSpec();
const topSuite = new StubSuite({
children: [spec],
userContext: { root: 'context' }
});
detectLateRejectionHandling = true;
const subject = makeRunner(topSuite);
const promise = subject.execute();
await Promise.resolve();
expect(runQueue).toHaveBeenCalledWith({
onComplete: jasmine.any(Function),
onException: jasmine.any(Function),
userContext: { root: 'context' },
queueableFns: [{ fn: jasmine.any(Function) }],
onMultipleDone: null,
SkipPolicy: jasmineUnderTest.SkipAfterBeforeAllErrorPolicy
});
const runQueueArgs = runQueue.calls.mostRecent().args[0];
verifyAndFinishSpec(spec, runQueueArgs.queueableFns[0], false);
runQueueArgs.onComplete();
await promise;
});
it('runs an empty suite', async function() {
const suite = new StubSuite({ userContext: { for: 'suite' } });
const topSuite = new StubSuite({
children: [suite],
userContext: { for: 'topSuite' }
});
suite.parentSuite = topSuite;
const subject = makeRunner(topSuite);
const promise = subject.execute();
await Promise.resolve();
expect(runQueue).toHaveBeenCalledWith({
onComplete: jasmine.any(Function),
onException: jasmine.any(Function),
userContext: { for: 'topSuite' },
queueableFns: [{ fn: jasmine.any(Function) }],
onMultipleDone: null,
SkipPolicy: jasmineUnderTest.SkipAfterBeforeAllErrorPolicy
});
const runQueueArgs = runQueue.calls.mostRecent().args[0];
const nodeDone = jasmine.createSpy('nodeDone');
runQueueArgs.queueableFns[0].fn(nodeDone);
expect(runQueue).toHaveBeenCalledWith({
onComplete: jasmine.any(Function),
onMultipleDone: null,
queueableFns: [{ fn: jasmine.any(Function) }],
userContext: { for: 'suite' },
onException: jasmine.any(Function),
onMultipleDone: null,
SkipPolicy: jasmineUnderTest.SkipAfterBeforeAllErrorPolicy
});
runQueue.calls.mostRecent().args[0].queueableFns[0].fn('foo');
expect(reportDispatcher.suiteStarted).toHaveBeenCalledWith(suite.result);
suite.getResult.and.returnValue({ my: 'result' });
runQueue.calls.mostRecent().args[0].onComplete();
expect(reportDispatcher.suiteDone).toHaveBeenCalledWith({ my: 'result' });
runQueueArgs.onComplete();
await promise;
});
it('runs a non-empty suite', async function() {
const spec1 = new StubSpec();
const spec2 = new StubSpec();
const suite = new StubSuite({ children: [spec1, spec2] });
const topSuite = new StubSuite({ children: [suite] });
const subject = makeRunner(topSuite);
const promise = subject.execute();
await Promise.resolve();
expect(runQueue).toHaveBeenCalledTimes(1);
let queueableFns = runQueue.calls.mostRecent().args[0].queueableFns;
queueableFns[0].fn(function() {});
expect(runQueue).toHaveBeenCalledTimes(2);
queueableFns = runQueue.calls.mostRecent().args[0].queueableFns;
expect(queueableFns.length).toBe(3);
verifyAndFinishSpec(spec1, queueableFns[1], false);
verifyAndFinishSpec(spec2, queueableFns[2], false);
await expectAsync(promise).toBePending();
});
it('"runs" an excluded suite', async function() {
const spec = new StubSpec();
const parent = new StubSuite({ children: [spec] });
const topSuite = new StubSuite({ children: [parent] });
parent.parentSuite = topSuite;
const subject = makeRunner(topSuite);
// Empty list of runable IDs excludes everything
const promise = subject.execute([]);
await Promise.resolve();
expect(runQueue).toHaveBeenCalledTimes(1);
let queueableFns = runQueue.calls.mostRecent().args[0].queueableFns;
queueableFns[0].fn(function() {});
queueableFns = runQueue.calls.mostRecent().args[0].queueableFns;
expect(queueableFns.length).toBe(2);
queueableFns[0].fn();
expect(reportDispatcher.suiteStarted).toHaveBeenCalledWith(parent.result);
verifyAndFinishSpec(spec, queueableFns[1], true);
parent.getResult.and.returnValue(parent.result);
runQueue.calls.argsFor(1)[0].onComplete();
expect(reportDispatcher.suiteDone).toHaveBeenCalledWith(parent.result);
await expectAsync(promise).toBePending();
});
it('handles the failSpecWithNoExpectations option', async function() {
failSpecWithNoExpectations = true;
const spec = new StubSpec();
const parent = new StubSuite({ children: [spec] });
const topSuite = new StubSuite({ children: [parent] });
parent.parentSuite = topSuite;
const subject = makeRunner(topSuite);
const promise = subject.execute();
await Promise.resolve();
expect(runQueue).toHaveBeenCalledTimes(1);
let queueableFns = runQueue.calls.mostRecent().args[0].queueableFns;
queueableFns[0].fn(function() {});
queueableFns = runQueue.calls.mostRecent().args[0].queueableFns;
expect(queueableFns.length).toBe(2);
queueableFns[1].fn('foo');
expect(
jasmineUnderTest.TreeRunner.prototype._executeSpec
).toHaveBeenCalledWith(spec, 'foo');
await expectAsync(promise).toBePending();
});
it('runs beforeAlls and afterAlls for a suite with children', async function() {
const spec = new StubSpec();
const target = new StubSuite({
children: [spec],
beforeAllFns: [
{ fn: 'beforeAll1', timeout: 1 },
{ fn: 'beforeAll2', timeout: 2 }
],
afterAllFns: [
{ fn: 'afterAll1', timeout: 3 },
{ fn: 'afterAll2', timeout: 4 }
]
});
const topSuite = new StubSuite({ children: [target] });
const subject = makeRunner(topSuite);
const promise = subject.execute();
await Promise.resolve();
expect(runQueue).toHaveBeenCalledTimes(1);
const queueableFns = runQueue.calls.mostRecent().args[0].queueableFns;
queueableFns[0].fn(function() {});
expect(runQueue.calls.mostRecent().args[0].queueableFns).toEqual([
{ fn: jasmine.any(Function) },
{ fn: 'beforeAll1', timeout: 1 },
{ fn: 'beforeAll2', timeout: 2 },
{ fn: jasmine.any(Function) },
{ fn: 'afterAll1', timeout: 3 },
{ fn: 'afterAll2', timeout: 4 }
]);
await expectAsync(promise).toBePending();
});
it('does not run beforeAlls or afterAlls for a suite with no children', async function() {
const target = new StubSuite({
beforeAllFns: [{ fn: 'before' }],
afterAllFns: [{ fn: 'after' }]
});
const topSuite = new StubSuite({ children: [target] });
const subject = makeRunner(topSuite);
const promise = subject.execute();
await Promise.resolve();
expect(runQueue).toHaveBeenCalledTimes(1);
const queueableFns = runQueue.calls.mostRecent().args[0].queueableFns;
queueableFns[0].fn(function() {});
expect(runQueue.calls.mostRecent().args[0].queueableFns.length).toEqual(
1
);
await expectAsync(promise).toBePending();
});
it('does not run beforeAlls or afterAlls for a suite with only pending children', async function() {
const spec = new StubSpec({ markedPending: true });
const target = new StubSuite({
children: [spec],
beforeAllFns: [{ fn: 'before' }],
afterAllFns: [{ fn: 'after' }]
});
const topSuite = new StubSuite({ children: [target] });
const subject = makeRunner(topSuite);
const promise = subject.execute();
await Promise.resolve();
expect(runQueue).toHaveBeenCalledTimes(1);
const queueableFns = runQueue.calls.mostRecent().args[0].queueableFns;
queueableFns[0].fn(function() {});
expect(runQueue.calls.mostRecent().args[0].queueableFns.length).toEqual(
2
);
await expectAsync(promise).toBePending();
});
it('runs specs in the order specified', async function() {
const specs = [new StubSpec(), new StubSpec()];
const topSuite = new StubSuite({ children: specs });
const subject = makeRunner(topSuite);
const promise = subject.execute([specs[1].id, specs[0].id]);
await Promise.resolve();
expect(runQueue).toHaveBeenCalledTimes(1);
const queueableFns = runQueue.calls.mostRecent().args[0].queueableFns;
queueableFns[0].fn('done');
expect(
jasmineUnderTest.TreeRunner.prototype._executeSpec
).not.toHaveBeenCalledWith(specs[0], jasmine.anything());
expect(
jasmineUnderTest.TreeRunner.prototype._executeSpec
).toHaveBeenCalledWith(specs[1], 'done');
queueableFns[1].fn('done');
expect(
jasmineUnderTest.TreeRunner.prototype._executeSpec
).toHaveBeenCalledWith(specs[0], 'done');
await expectAsync(promise).toBePending();
});
it('runs specified specs before non-specified specs within a suite', async function() {
const specified = new StubSpec();
const nonSpecified = new StubSpec();
const topSuite = new StubSuite({ children: [nonSpecified, specified] });
const subject = makeRunner(topSuite);
const promise = subject.execute([specified.id]);
await Promise.resolve();
expect(runQueue).toHaveBeenCalledTimes(1);
const queueableFns = runQueue.calls.mostRecent().args[0].queueableFns;
queueableFns[0].fn('done');
expect(
jasmineUnderTest.TreeRunner.prototype._executeSpec
).not.toHaveBeenCalledWith(nonSpecified, jasmine.anything());
expect(
jasmineUnderTest.TreeRunner.prototype._executeSpec
).toHaveBeenCalledWith(specified, 'done');
queueableFns[1].fn('done');
expect(
jasmineUnderTest.TreeRunner.prototype._executeSpec
).toHaveBeenCalledWith(nonSpecified, 'done');
await expectAsync(promise).toBePending();
});
it('runs suites and specs with a specified order', async function() {
const specifiedSpec = new StubSpec();
const nonSpecifiedSpec = new StubSpec();
const specifiedSuite = new StubSuite({ children: [nonSpecifiedSpec] });
const topSuite = new StubSuite({
children: [specifiedSpec, specifiedSuite]
});
const subject = makeRunner(topSuite);
const promise = subject.execute([specifiedSuite.id, specifiedSpec.id]);
await Promise.resolve();
expect(runQueue).toHaveBeenCalledTimes(1);
const queueableFns = runQueue.calls.mostRecent().args[0].queueableFns;
queueableFns[0].fn();
expect(specifiedSpec.execute).not.toHaveBeenCalled();
const nodeQueueableFns = runQueue.calls.mostRecent().args[0].queueableFns;
nodeQueueableFns[1].fn('done');
expect(
jasmineUnderTest.TreeRunner.prototype._executeSpec
).toHaveBeenCalledWith(nonSpecifiedSpec, 'done');
queueableFns[1].fn('done');
expect(
jasmineUnderTest.TreeRunner.prototype._executeSpec
).toHaveBeenCalledWith(specifiedSpec, 'done');
await expectAsync(promise).toBePending();
});
it('runs suites and specs in the order they were declared', async function() {
const spec1 = new StubSpec();
const spec2 = new StubSpec();
const spec3 = new StubSpec();
const parent = new StubSuite({ children: [spec2, spec3] });
const topSuite = new StubSuite({ children: [spec1, parent] });
const subject = makeRunner(topSuite);
const promise = subject.execute();
await Promise.resolve();
expect(runQueue).toHaveBeenCalledTimes(1);
const queueableFns = runQueue.calls.mostRecent().args[0].queueableFns;
expect(queueableFns.length).toBe(2);
queueableFns[0].fn('done');
expect(
jasmineUnderTest.TreeRunner.prototype._executeSpec
).toHaveBeenCalledWith(spec1, 'done');
queueableFns[1].fn();
const childFns = runQueue.calls.mostRecent().args[0].queueableFns;
expect(childFns.length).toBe(3);
childFns[1].fn('done');
expect(
jasmineUnderTest.TreeRunner.prototype._executeSpec
).toHaveBeenCalledWith(spec2, 'done');
childFns[2].fn('done');
expect(
jasmineUnderTest.TreeRunner.prototype._executeSpec
).toHaveBeenCalledWith(spec3, 'done');
await expectAsync(promise).toBePending();
});
it('runs a suite multiple times if the order specified leaves and re-enters it', async function() {
const spec1 = new StubSpec();
const spec2 = new StubSpec();
const spec3 = new StubSpec();
const spec4 = new StubSpec();
const spec5 = new StubSpec();
const reentered = new StubSuite({ children: [spec1, spec2, spec3] });
const topSuite = new StubSuite({ children: [reentered, spec4, spec5] });
const subject = makeRunner(topSuite);
spyOn(jasmineUnderTest.getEnv(), 'deprecated');
const promise = subject.execute([
spec1.id,
spec4.id,
spec2.id,
spec5.id,
spec3.id
]);
await Promise.resolve();
expect(runQueue).toHaveBeenCalledTimes(1);
const queueableFns = runQueue.calls.mostRecent().args[0].queueableFns;
queueableFns[0].fn();
expect(runQueue.calls.mostRecent().args[0].queueableFns.length).toBe(2);
runQueue.calls.mostRecent().args[0].queueableFns[1].fn('done');
expect(
jasmineUnderTest.TreeRunner.prototype._executeSpec
).toHaveBeenCalledWith(spec1, 'done');
queueableFns[1].fn('done');
expect(
jasmineUnderTest.TreeRunner.prototype._executeSpec
).toHaveBeenCalledWith(spec4, 'done');
queueableFns[2].fn();
expect(runQueue.calls.count()).toBe(3);
expect(runQueue.calls.mostRecent().args[0].queueableFns.length).toBe(2);
runQueue.calls.mostRecent().args[0].queueableFns[1].fn('done');
expect(
jasmineUnderTest.TreeRunner.prototype._executeSpec
).toHaveBeenCalledWith(spec2, 'done');
queueableFns[3].fn('done');
expect(
jasmineUnderTest.TreeRunner.prototype._executeSpec
).toHaveBeenCalledWith(spec5, 'done');
queueableFns[4].fn();
expect(runQueue.calls.count()).toBe(4);
expect(runQueue.calls.mostRecent().args[0].queueableFns.length).toBe(2);
runQueue.calls.mostRecent().args[0].queueableFns[1].fn('done');
expect(
jasmineUnderTest.TreeRunner.prototype._executeSpec
).toHaveBeenCalledWith(spec3, 'done');
await expectAsync(promise).toBePending();
});
it('runs a parent of a suite with multiple segments correctly', async function() {
const spec1 = new StubSpec();
const spec2 = new StubSpec();
const spec3 = new StubSpec();
const spec4 = new StubSpec();
const spec5 = new StubSpec();
const parent = new StubSuite({ children: [spec1, spec2, spec3] });
const grandparent = new StubSuite({ children: [parent] });
const topSuite = new StubSuite({ children: [grandparent, spec4, spec5] });
const subject = makeRunner(topSuite);
spyOn(jasmineUnderTest.getEnv(), 'deprecated');
const promise = subject.execute([
spec1.id,
spec4.id,
spec2.id,
spec5.id,
spec3.id
]);
await Promise.resolve();
expect(runQueue).toHaveBeenCalledTimes(1);
const queueableFns = runQueue.calls.mostRecent().args[0].queueableFns;
expect(queueableFns.length).toBe(5);
queueableFns[0].fn();
expect(runQueue.calls.count()).toBe(2);
expect(runQueue.calls.mostRecent().args[0].queueableFns.length).toBe(2);
runQueue.calls.mostRecent().args[0].queueableFns[1].fn();
expect(runQueue.calls.count()).toBe(3);
runQueue.calls.mostRecent().args[0].queueableFns[1].fn('done');
expect(
jasmineUnderTest.TreeRunner.prototype._executeSpec
).toHaveBeenCalledWith(spec1, 'done');
queueableFns[1].fn('done');
expect(
jasmineUnderTest.TreeRunner.prototype._executeSpec
).toHaveBeenCalledWith(spec4, 'done');
queueableFns[2].fn();
expect(runQueue.calls.count()).toBe(4);
expect(runQueue.calls.mostRecent().args[0].queueableFns.length).toBe(2);
runQueue.calls.mostRecent().args[0].queueableFns[1].fn();
expect(runQueue.calls.count()).toBe(5);
runQueue.calls.mostRecent().args[0].queueableFns[1].fn('done');
expect(
jasmineUnderTest.TreeRunner.prototype._executeSpec
).toHaveBeenCalledWith(spec2, 'done');
queueableFns[3].fn('done');
expect(
jasmineUnderTest.TreeRunner.prototype._executeSpec
).toHaveBeenCalledWith(spec5, 'done');
queueableFns[4].fn();
expect(runQueue.calls.count()).toBe(6);
expect(runQueue.calls.mostRecent().args[0].queueableFns.length).toBe(2);
runQueue.calls.mostRecent().args[0].queueableFns[1].fn();
expect(runQueue.calls.count()).toBe(7);
runQueue.calls.mostRecent().args[0].queueableFns[1].fn('done');
expect(
jasmineUnderTest.TreeRunner.prototype._executeSpec
).toHaveBeenCalledWith(spec3, 'done');
await expectAsync(promise).toBePending();
});
it('runs large segments of nodes in the order they were declared', async function() {
const specs = [];
for (let i = 0; i < 11; i++) {
specs.push(new StubSpec());
}
const topSuite = new StubSuite({ children: specs });
const subject = makeRunner(topSuite);
const promise = subject.execute();
await Promise.resolve();
expect(runQueue).toHaveBeenCalledTimes(1);
const queueableFns = runQueue.calls.mostRecent().args[0].queueableFns;
expect(queueableFns.length).toBe(11);
for (let i = 0; i < 11; i++) {
queueableFns[i].fn('done');
expect(
jasmineUnderTest.TreeRunner.prototype._executeSpec
).toHaveBeenCalledWith(specs[i], 'done');
}
await expectAsync(promise).toBePending();
});
});
});

View File

@@ -33,357 +33,167 @@ describe('Spec', function() {
expect(jasmineUnderTest.Spec.isPendingSpecException(void 0)).toBe(false);
});
it('delegates execution to a QueueRunner', function() {
const fakeQueueRunner = jasmine.createSpy('fakeQueueRunner'),
spec = new jasmineUnderTest.Spec({
description: 'my test',
id: 'some-id',
queueableFn: { fn: function() {} }
describe('#executionFinished', function() {
it('removes the fn if autoCleanClosures is true', function() {
const spec = new jasmineUnderTest.Spec({
queueableFn: { fn: () => {} },
autoCleanClosures: true
});
spec.execute(fakeQueueRunner);
expect(fakeQueueRunner).toHaveBeenCalled();
});
it('should call the start callback on execution', function() {
const fakeQueueRunner = jasmine.createSpy('fakeQueueRunner'),
startCallback = jasmine.createSpy('startCallback'),
spec = new jasmineUnderTest.Spec({
id: 123,
description: 'foo bar',
queueableFn: { fn: function() {} },
onStart: startCallback
});
spec.execute(fakeQueueRunner);
fakeQueueRunner.calls.mostRecent().args[0].queueableFns[0].fn();
expect(startCallback).toHaveBeenCalled();
expect(startCallback.calls.first().object).toEqual(spec);
});
it('should call the start callback on execution but before any befores are called', function() {
const fakeQueueRunner = jasmine.createSpy('fakeQueueRunner');
let beforesWereCalled = false;
const startCallback = jasmine
.createSpy('start-callback')
.and.callFake(function() {
expect(beforesWereCalled).toBe(false);
});
const spec = new jasmineUnderTest.Spec({
queueableFn: { fn: function() {} },
beforeFns: function() {
return [
function() {
beforesWereCalled = true;
}
];
},
onStart: startCallback
spec.executionFinished();
expect(spec.queueableFn.fn).toBeFalsy();
});
spec.execute(fakeQueueRunner);
fakeQueueRunner.calls.mostRecent().args[0].queueableFns[0].fn();
expect(startCallback).toHaveBeenCalled();
});
it('provides all before fns and after fns to be run', function() {
const fakeQueueRunner = jasmine.createSpy('fakeQueueRunner'),
before = jasmine.createSpy('before'),
after = jasmine.createSpy('after'),
queueableFn = {
fn: jasmine.createSpy('test body').and.callFake(function() {
expect(before).toHaveBeenCalled();
expect(after).not.toHaveBeenCalled();
})
},
spec = new jasmineUnderTest.Spec({
queueableFn: queueableFn,
beforeAndAfterFns: function() {
return { befores: [before], afters: [after] };
}
it('removes the fn after execution if autoCleanClosures is undefined', function() {
const spec = new jasmineUnderTest.Spec({
queueableFn: { fn: () => {} },
autoCleanClosures: undefined
});
spec.execute(fakeQueueRunner);
const options = fakeQueueRunner.calls.mostRecent().args[0];
expect(options.queueableFns).toEqual([
{ fn: jasmine.any(Function) },
before,
queueableFn,
after,
{
fn: jasmine.any(Function),
type: 'specCleanup'
}
]);
});
it("tells the queue runner that it's a leaf node", function() {
const fakeQueueRunner = jasmine.createSpy('fakeQueueRunner'),
spec = new jasmineUnderTest.Spec({
queueableFn: { fn: function() {} },
beforeAndAfterFns: function() {
return { befores: [], afters: [] };
}
});
spec.execute(fakeQueueRunner);
expect(fakeQueueRunner).toHaveBeenCalledWith(
jasmine.objectContaining({
isLeaf: true
})
);
});
it('is marked pending if created without a function body', function() {
const startCallback = jasmine.createSpy('startCallback'),
resultCallback = jasmine.createSpy('resultCallback'),
spec = new jasmineUnderTest.Spec({
onStart: startCallback,
queueableFn: { fn: null },
resultCallback: resultCallback
});
expect(spec.status()).toBe('pending');
});
it('can be excluded at execution time by a parent', function() {
const fakeQueueRunner = jasmine.createSpy('fakeQueueRunner'),
startCallback = jasmine.createSpy('startCallback'),
specBody = jasmine.createSpy('specBody'),
resultCallback = jasmine.createSpy('resultCallback'),
spec = new jasmineUnderTest.Spec({
onStart: startCallback,
queueableFn: { fn: specBody },
resultCallback: resultCallback
});
spec.execute(fakeQueueRunner, 'cally-back', true);
expect(fakeQueueRunner).toHaveBeenCalledWith(
jasmine.objectContaining({
onComplete: jasmine.any(Function),
queueableFns: [
{ fn: jasmine.any(Function) },
{
fn: jasmine.any(Function),
type: 'specCleanup'
}
]
})
);
expect(specBody).not.toHaveBeenCalled();
const args = fakeQueueRunner.calls.mostRecent().args[0];
args.queueableFns[0].fn();
expect(startCallback).toHaveBeenCalled();
args.queueableFns[args.queueableFns.length - 1].fn();
expect(resultCallback).toHaveBeenCalled();
expect(spec.result.status).toBe('excluded');
});
it('can be marked pending, but still calls callbacks when executed', function() {
const fakeQueueRunner = jasmine.createSpy('fakeQueueRunner'),
startCallback = jasmine.createSpy('startCallback'),
resultCallback = jasmine.createSpy('resultCallback'),
spec = new jasmineUnderTest.Spec({
onStart: startCallback,
resultCallback: resultCallback,
description: 'with a spec',
parentSuiteId: 'suite1',
filename: 'someSpecFile.js',
getPath: function() {
return ['a suite', 'with a spec'];
},
queueableFn: { fn: null }
});
spec.pend();
expect(spec.status()).toBe('pending');
spec.execute(fakeQueueRunner);
expect(fakeQueueRunner).toHaveBeenCalled();
const args = fakeQueueRunner.calls.mostRecent().args[0];
args.queueableFns[0].fn();
expect(startCallback).toHaveBeenCalled();
args.queueableFns[1].fn('things');
expect(resultCallback).toHaveBeenCalledWith(
{
id: spec.id,
status: 'pending',
description: 'with a spec',
fullName: 'a suite with a spec',
parentSuiteId: 'suite1',
filename: 'someSpecFile.js',
failedExpectations: [],
passedExpectations: [],
deprecationWarnings: [],
pendingReason: '',
duration: jasmine.any(Number),
properties: null,
debugLogs: null
},
'things'
);
});
it('should call the done callback on execution complete', function() {
const done = jasmine.createSpy('done callback'),
spec = new jasmineUnderTest.Spec({
queueableFn: { fn: function() {} },
catchExceptions: function() {
return false;
},
resultCallback: function() {}
});
spec.execute(attrs => attrs.onComplete(), done);
expect(done).toHaveBeenCalled();
});
it('should call the done callback with an error if the spec is failed', function() {
const done = jasmine.createSpy('done callback'),
spec = new jasmineUnderTest.Spec({
queueableFn: { fn: function() {} },
catchExceptions: function() {
return false;
},
resultCallback: function() {}
});
function queueRunnerFactory(attrs) {
spec.result.status = 'failed';
attrs.onComplete();
}
spec.execute(queueRunnerFactory, done);
expect(done).toHaveBeenCalledWith(
jasmine.any(jasmineUnderTest.StopExecutionError)
);
});
it('should report the duration of the test', function() {
const timer = jasmine.createSpyObj('timer', {
start: null,
elapsed: 77000
});
let duration = undefined;
const spec = new jasmineUnderTest.Spec({
queueableFn: { fn: jasmine.createSpy('spec body') },
catchExceptions: function() {
return false;
},
resultCallback: function(result) {
duration = result.duration;
},
timer: timer
spec.executionFinished();
expect(spec.queueableFn.fn).toBeFalsy();
});
function queueRunnerFactory(config) {
config.queueableFns.forEach(function(qf) {
qf.fn();
it('does not remove the fn after execution if autoCleanClosures is false', function() {
function originalFn() {}
const spec = new jasmineUnderTest.Spec({
queueableFn: { fn: originalFn },
autoCleanClosures: false
});
config.onComplete();
}
spec.execute(queueRunnerFactory, function() {});
expect(duration).toBe(77000);
});
it('should report properties set during the test', function() {
const done = jasmine.createSpy('done callback'),
spec = new jasmineUnderTest.Spec({
queueableFn: { fn: jasmine.createSpy('spec body') },
catchExceptions: function() {
return false;
},
resultCallback: function() {}
});
spec.setSpecProperty('a', 4);
spec.execute(attrs => attrs.onComplete(), done);
expect(spec.result.properties).toEqual({ a: 4 });
});
it('#status returns passing by default', function() {
const spec = new jasmineUnderTest.Spec({
queueableFn: { fn: jasmine.createSpy('spec body') }
spec.executionFinished();
expect(spec.queueableFn.fn).toBe(originalFn);
});
expect(spec.status()).toBe('passed');
});
it('#status returns passed if all expectations in the spec have passed', function() {
const spec = new jasmineUnderTest.Spec({
queueableFn: { fn: jasmine.createSpy('spec body') }
});
spec.addExpectationResult(true, {});
expect(spec.status()).toBe('passed');
});
it('#status returns failed if any expectations in the spec have failed', function() {
const spec = new jasmineUnderTest.Spec({
queueableFn: { fn: jasmine.createSpy('spec body') }
});
spec.addExpectationResult(true, {});
spec.addExpectationResult(false, {});
expect(spec.status()).toBe('failed');
});
it('keeps track of passed and failed expectations', function() {
const fakeQueueRunner = jasmine.createSpy('queueRunner'),
resultCallback = jasmine.createSpy('resultCallback'),
spec = new jasmineUnderTest.Spec({
queueableFn: { fn: jasmine.createSpy('spec body') },
resultCallback: resultCallback
});
spec.addExpectationResult(true, { message: 'expectation1' });
spec.addExpectationResult(false, { message: 'expectation2' });
spec.execute(fakeQueueRunner);
const fns = fakeQueueRunner.calls.mostRecent().args[0].queueableFns;
fns[fns.length - 1].fn();
expect(resultCallback.calls.first().args[0].passedExpectations).toEqual([
jasmine.objectContaining({ message: 'expectation1' })
]);
expect(resultCallback.calls.first().args[0].failedExpectations).toEqual([
jasmine.objectContaining({ message: 'expectation2' })
]);
});
it("throws an ExpectationFailed error upon receiving a failed expectation when 'throwOnExpectationFailure' is set", function() {
const fakeQueueRunner = jasmine.createSpy('queueRunner'),
resultCallback = jasmine.createSpy('resultCallback'),
spec = new jasmineUnderTest.Spec({
queueableFn: { fn: function() {} },
resultCallback: resultCallback,
throwOnExpectationFailure: true
describe('#getSpecProperty', function() {
it('get the property value', function() {
const spec = new jasmineUnderTest.Spec({
queueableFn: { fn: () => {} }
});
spec.addExpectationResult(true, { message: 'passed' });
expect(function() {
spec.addExpectationResult(false, { message: 'failed' });
}).toThrowError(jasmineUnderTest.errors.ExpectationFailed);
spec.setSpecProperty('a', 4);
expect(spec.getSpecProperty('a')).toBe(4);
});
});
spec.execute(fakeQueueRunner);
describe('#setSpecProperty', function() {
it('adds the property to the result', function() {
const spec = new jasmineUnderTest.Spec({
queueableFn: { fn: () => {} }
});
const fns = fakeQueueRunner.calls.mostRecent().args[0].queueableFns;
fns[fns.length - 1].fn();
expect(resultCallback.calls.first().args[0].passedExpectations).toEqual([
jasmine.objectContaining({ message: 'passed' })
]);
expect(resultCallback.calls.first().args[0].failedExpectations).toEqual([
jasmine.objectContaining({ message: 'failed' })
]);
spec.setSpecProperty('a', 4);
expect(spec.result.properties).toEqual({ a: 4 });
});
it('replace the property result when it was previously set', function() {
const spec = new jasmineUnderTest.Spec({
queueableFn: { fn: () => {} }
});
spec.setSpecProperty('a', 'original-value');
spec.setSpecProperty('b', 'original-value');
spec.setSpecProperty('a', 'new-value');
expect(spec.result.properties).toEqual({
a: 'new-value',
b: 'original-value'
});
});
});
describe('status', function() {
it('is "passed" by default', function() {
const spec = new jasmineUnderTest.Spec({
queueableFn: { fn: () => {} }
});
expect(spec.getResult().status).toBe('passed');
});
it('is "passed" if all expectations passed', function() {
const spec = new jasmineUnderTest.Spec({
queueableFn: { fn: () => {} }
});
spec.addExpectationResult(true, {});
expect(spec.getResult().status).toBe('passed');
});
it('is "failed" if any expectation failed', function() {
const spec = new jasmineUnderTest.Spec({
queueableFn: { fn: () => {} }
});
spec.addExpectationResult(true, {});
spec.addExpectationResult(false, {});
expect(spec.getResult().status).toBe('failed');
});
it('is "pending" if created without a function body', function() {
const startCallback = jasmine.createSpy('startCallback'),
resultCallback = jasmine.createSpy('resultCallback'),
spec = new jasmineUnderTest.Spec({
onStart: startCallback,
queueableFn: { fn: null },
resultCallback: resultCallback
});
expect(spec.getResult().status).toBe('pending');
});
});
describe('#addExpectationResult', function() {
it('keeps track of passed and failed expectations', function() {
const spec = new jasmineUnderTest.Spec({
queueableFn: { fn: () => {} }
});
spec.addExpectationResult(true, { message: 'expectation1' });
spec.addExpectationResult(false, { message: 'expectation2' });
expect(spec.result.passedExpectations).toEqual([
jasmine.objectContaining({ message: 'expectation1' })
]);
expect(spec.result.failedExpectations).toEqual([
jasmine.objectContaining({ message: 'expectation2' })
]);
});
describe("when 'throwOnExpectationFailure' is set", function() {
it('throws an ExpectationFailed error', function() {
const spec = new jasmineUnderTest.Spec({
queueableFn: { fn: () => {} },
throwOnExpectationFailure: true
});
spec.addExpectationResult(true, { message: 'passed' });
expect(function() {
spec.addExpectationResult(false, { message: 'failed' });
}).toThrowError(jasmineUnderTest.errors.ExpectationFailed);
expect(spec.result.failedExpectations).toEqual([
jasmine.objectContaining({ message: 'failed' })
]);
});
});
describe("when 'throwOnExpectationFailure' is not set", function() {
it('does not throw', function() {
const spec = new jasmineUnderTest.Spec({
queueableFn: { fn: () => {} }
});
spec.addExpectationResult(false, { message: 'failed' });
expect(spec.result.failedExpectations).toEqual([
jasmine.objectContaining({ message: 'failed' })
]);
});
});
});
it('forwards late expectation failures to onLateError', function() {
@@ -514,125 +324,47 @@ describe('Spec', function() {
expect(spec.metadata.getPath()).toEqual(['expected val']);
});
describe('when a spec is marked pending during execution', function() {
it('should mark the spec as pending', function() {
const fakeQueueRunner = function(opts) {
opts.onException(
new Error(jasmineUnderTest.Spec.pendingSpecExceptionMessage)
);
},
spec = new jasmineUnderTest.Spec({
description: 'my test',
id: 'some-id',
queueableFn: { fn: function() {} }
});
spec.execute(fakeQueueRunner);
expect(spec.status()).toEqual('pending');
expect(spec.result.pendingReason).toEqual('');
});
it('should set the pendingReason', function() {
const fakeQueueRunner = function(opts) {
opts.onException(
new Error(
jasmineUnderTest.Spec.pendingSpecExceptionMessage +
'custom message'
)
);
},
spec = new jasmineUnderTest.Spec({
description: 'my test',
id: 'some-id',
queueableFn: { fn: function() {} }
});
spec.execute(fakeQueueRunner);
expect(spec.status()).toEqual('pending');
expect(spec.result.pendingReason).toEqual('custom message');
});
});
it('should log a failure when handling an exception', function() {
const fakeQueueRunner = jasmine.createSpy('queueRunner'),
resultCallback = jasmine.createSpy('resultCallback'),
spec = new jasmineUnderTest.Spec({
queueableFn: { fn: function() {} },
resultCallback: resultCallback
describe('#handleException', function() {
it('records a failure', function() {
const spec = new jasmineUnderTest.Spec({
queueableFn: {}
});
spec.handleException('foo');
spec.execute(fakeQueueRunner);
spec.handleException('foo');
const args = fakeQueueRunner.calls.mostRecent().args[0];
args.queueableFns[args.queueableFns.length - 1].fn();
expect(resultCallback.calls.first().args[0].failedExpectations).toEqual([
{
message: 'foo thrown',
matcherName: '',
passed: false,
expected: '',
actual: '',
stack: null
}
]);
});
it('should not log an additional failure when handling an ExpectationFailed error', function() {
const fakeQueueRunner = jasmine.createSpy('queueRunner'),
resultCallback = jasmine.createSpy('resultCallback'),
spec = new jasmineUnderTest.Spec({
queueableFn: { fn: function() {} },
resultCallback: resultCallback
});
spec.handleException(new jasmineUnderTest.errors.ExpectationFailed());
spec.execute(fakeQueueRunner);
const args = fakeQueueRunner.calls.mostRecent().args[0];
args.queueableFns[args.queueableFns.length - 1].fn();
expect(resultCallback.calls.first().args[0].failedExpectations).toEqual([]);
});
it('treats multiple done calls as late errors', function() {
const queueRunnerFactory = jasmine.createSpy('queueRunnerFactory'),
onLateError = jasmine.createSpy('onLateError'),
spec = new jasmineUnderTest.Spec({
onLateError: onLateError,
queueableFn: { fn: function() {} },
getPath: function() {
return ['a spec'];
expect(spec.result.failedExpectations).toEqual([
{
message: 'foo thrown',
matcherName: '',
passed: false,
expected: '',
actual: '',
stack: null
}
]);
});
it('does not record an additional failure when the error is ExpectationFailed', function() {
const spec = new jasmineUnderTest.Spec({
queueableFn: {}
});
spec.execute(queueRunnerFactory);
spec.handleException(new jasmineUnderTest.errors.ExpectationFailed());
expect(queueRunnerFactory).toHaveBeenCalled();
queueRunnerFactory.calls.argsFor(0)[0].onMultipleDone();
expect(onLateError).toHaveBeenCalledTimes(1);
expect(onLateError.calls.argsFor(0)[0]).toBeInstanceOf(Error);
expect(onLateError.calls.argsFor(0)[0].message).toEqual(
'An asynchronous spec, beforeEach, or afterEach function called its ' +
"'done' callback more than once.\n(in spec: a spec)"
);
expect(spec.result.failedExpectations).toEqual([]);
});
});
describe('#trace', function() {
describe('#debugLog', function() {
it('adds the messages to the result', function() {
const timer = jasmine.createSpyObj('timer', ['start', 'elapsed']),
spec = new jasmineUnderTest.Spec({
queueableFn: {
fn: function() {}
},
timer: timer
}),
t1 = 123,
t2 = 456;
const timer = jasmine.createSpyObj('timer', ['start', 'elapsed']);
const spec = new jasmineUnderTest.Spec({
queueableFn: { fn: () => {} },
timer: timer
});
const t1 = 123;
const t2 = 456;
spec.execute(() => {});
expect(spec.result.debugLogs).toBeNull();
timer.elapsed.and.returnValue(t1);
spec.debugLog('msg 1');
@@ -648,84 +380,36 @@ describe('Spec', function() {
});
describe('When the spec passes', function() {
it('omits the messages from the reported result', function() {
const resultCallback = jasmine.createSpy('resultCallback'),
spec = new jasmineUnderTest.Spec({
queueableFn: {
fn: function() {}
},
resultCallback: resultCallback
});
it('removes the logs from the result', function() {
const spec = new jasmineUnderTest.Spec({
queueableFn: { fn: () => {} }
});
function queueRunnerFactory(config) {
spec.debugLog('msg');
for (const fn of config.queueableFns) {
fn.fn();
}
config.onComplete(false);
}
spec.debugLog('msg');
spec.executionFinished();
spec.execute(queueRunnerFactory, function() {});
expect(resultCallback).toHaveBeenCalledWith(
jasmine.objectContaining({ debugLogs: null }),
undefined
);
});
it('removes the messages to save memory', function() {
const resultCallback = jasmine.createSpy('resultCallback'),
spec = new jasmineUnderTest.Spec({
queueableFn: {
fn: function() {}
},
resultCallback: resultCallback
});
function queueRunnerFactory(config) {
spec.debugLog('msg');
for (const fn of config.queueableFns) {
fn.fn();
}
config.onComplete(false);
}
spec.execute(queueRunnerFactory, function() {});
expect(resultCallback).toHaveBeenCalled();
expect(spec.result.debugLogs).toBeNull();
});
});
describe('When the spec fails', function() {
it('includes the messages in the reported result', function() {
const resultCallback = jasmine.createSpy('resultCallback'),
timer = jasmine.createSpyObj('timer', ['start', 'elapsed']),
spec = new jasmineUnderTest.Spec({
queueableFn: {
fn: function() {}
},
resultCallback: resultCallback,
timer: timer
}),
timestamp = 12345;
it('includes the messages in the result', function() {
const timer = jasmine.createSpyObj('timer', ['start', 'elapsed']);
const spec = new jasmineUnderTest.Spec({
queueableFn: { fn: () => {} },
timer: timer
});
const timestamp = 12345;
timer.elapsed.and.returnValue(timestamp);
function queueRunnerFactory(config) {
spec.debugLog('msg');
spec.handleException(new Error('nope'));
for (const fn of config.queueableFns) {
fn.fn();
}
config.onComplete(true);
}
spec.debugLog('msg');
spec.handleException(new Error('nope'));
spec.executionFinished();
spec.execute(queueRunnerFactory, function() {});
expect(resultCallback).toHaveBeenCalledWith(
jasmine.objectContaining({
debugLogs: [{ message: 'msg', timestamp: timestamp }]
}),
undefined
);
expect(spec.result.debugLogs).toEqual([
{ message: 'msg', timestamp: timestamp }
]);
});
});
});

View File

@@ -153,7 +153,7 @@ describe('Spies', function() {
it('should throw if you do not pass an array or object argument', function() {
expect(function() {
env.createSpyObj('BaseName');
}).toThrow(
}).toThrowError(
'createSpyObj requires a non-empty array or object of method names to create spies for'
);
});
@@ -161,7 +161,7 @@ describe('Spies', function() {
it('should throw if you pass an empty array argument', function() {
expect(function() {
env.createSpyObj('BaseName', []);
}).toThrow(
}).toThrowError(
'createSpyObj requires a non-empty array or object of method names to create spies for'
);
});
@@ -169,7 +169,7 @@ describe('Spies', function() {
it('should throw if you pass an empty object argument', function() {
expect(function() {
env.createSpyObj('BaseName', {});
}).toThrow(
}).toThrowError(
'createSpyObj requires a non-empty array or object of method names to create spies for'
);
});

View File

@@ -115,7 +115,7 @@ describe('Suite', function() {
const suite = new jasmineUnderTest.Suite({});
suite.addExpectationResult(false, {});
expect(suite.status()).toBe('failed');
expect(suite.getResult().status).toBe('failed');
});
it('retrieves a result with updated status', function() {
@@ -140,7 +140,7 @@ describe('Suite', function() {
suite.addExpectationResult(false, { message: 'failed' });
}).toThrowError(jasmineUnderTest.errors.ExpectationFailed);
expect(suite.status()).toBe('failed');
expect(suite.getResult().status).toBe('failed');
expect(suite.result.failedExpectations).toEqual([
jasmine.objectContaining({ message: 'failed' })
]);

File diff suppressed because it is too large Load Diff

717
spec/core/TreeRunnerSpec.js Normal file
View File

@@ -0,0 +1,717 @@
describe('TreeRunner', function() {
describe('spec execution', function() {
it('starts the timer, reports the spec started, and updates run state at the start of the queue', async function() {
const timer = jasmine.createSpyObj('timer', ['start']);
const spec = new jasmineUnderTest.Spec({
id: 'spec1',
queueableFn: {},
timer
});
const {
runQueue,
currentRunableTracker,
runableResources,
reportDispatcher,
suiteRunQueueArgs,
executePromise
} = runSingleSpecSuite(spec);
suiteRunQueueArgs.queueableFns[0].fn();
expect(runQueue).toHaveBeenCalledTimes(1);
const specRunQueueArgs = runQueue.calls.mostRecent().args[0];
const next = jasmine.createSpy('next');
specRunQueueArgs.queueableFns[0].fn(next);
expect(timer.start).toHaveBeenCalled();
expect(currentRunableTracker.currentRunable()).toBe(spec);
expect(runableResources.initForRunable).toHaveBeenCalledWith(
spec.id,
spec.parentSuiteId
);
expect(reportDispatcher.specStarted).toHaveBeenCalledWith(spec.result);
await Promise.resolve();
expect(reportDispatcher.specStarted).toHaveBeenCalledBefore(next);
await expectAsync(executePromise).toBePending();
});
it('stops the timer, updates run state, and reports the spec done at the end of the queue', async function() {
const timer = jasmine.createSpyObj('timer', ['start', 'elapsed']);
const spec = new jasmineUnderTest.Spec({
id: 'spec1',
queueableFn: {},
timer
});
const {
runQueue,
currentRunableTracker,
runableResources,
reportDispatcher,
suiteRunQueueArgs,
executePromise
} = runSingleSpecSuite(spec);
suiteRunQueueArgs.queueableFns[0].fn();
expect(runQueue).toHaveBeenCalledTimes(1);
const specRunQueueArgs = runQueue.calls.mostRecent().args[0];
const next = jasmine.createSpy('next');
timer.elapsed.and.returnValue('the elapsed time');
currentRunableTracker.setCurrentSpec(spec);
specRunQueueArgs.queueableFns[1].fn(next);
expect(currentRunableTracker.currentSpec()).toBeFalsy();
expect(runableResources.clearForRunable).toHaveBeenCalledWith(spec.id);
expect(reportDispatcher.specDone).toHaveBeenCalledWith(spec.result);
expect(spec.result.duration).toEqual('the elapsed time');
expect(spec.reportedDone).toEqual(true);
await Promise.resolve();
await Promise.resolve();
await Promise.resolve();
expect(reportDispatcher.specDone).toHaveBeenCalledBefore(next);
await expectAsync(executePromise).toBePending();
});
it('runs before and after fns', function() {
const before = { fn: jasmine.createSpy('before') };
const after = { fn: jasmine.createSpy('after') };
const queueableFn = {
fn: jasmine.createSpy('test body').and.callFake(function() {
expect(before).toHaveBeenCalled();
expect(after).not.toHaveBeenCalled();
})
};
const spec = new jasmineUnderTest.Spec({
queueableFn: queueableFn,
beforeAndAfterFns: function() {
return { befores: [before], afters: [after] };
}
});
const { runQueue, suiteRunQueueArgs } = runSingleSpecSuite(spec);
suiteRunQueueArgs.queueableFns[0].fn();
expect(runQueue).toHaveBeenCalledTimes(1);
const specRunQueueArgs = runQueue.calls.mostRecent().args[0];
expect(specRunQueueArgs.queueableFns[1]).toEqual(before);
expect(specRunQueueArgs.queueableFns[2]).toEqual(queueableFn);
expect(specRunQueueArgs.queueableFns[3]).toEqual(after);
});
it('marks specs pending at runtime', function() {
let spec;
const queueableFn = {
fn() {
spec.pend();
}
};
spec = new jasmineUnderTest.Spec({ queueableFn });
const { runQueue, suiteRunQueueArgs } = runSingleSpecSuite(spec);
suiteRunQueueArgs.queueableFns[0].fn();
expect(runQueue).toHaveBeenCalledTimes(1);
const specRunQueueArgs = runQueue.calls.mostRecent().args[0];
expect(specRunQueueArgs.queueableFns[1]).toEqual(queueableFn);
queueableFn.fn();
expect(spec.getResult().status).toEqual('pending');
expect(spec.getResult().pendingReason).toEqual('');
});
it('marks specs pending at runtime with a message', function() {
let spec;
const queueableFn = {
fn() {
spec.pend('some reason');
}
};
spec = new jasmineUnderTest.Spec({ queueableFn });
const { runQueue, suiteRunQueueArgs } = runSingleSpecSuite(spec);
suiteRunQueueArgs.queueableFns[0].fn();
expect(runQueue).toHaveBeenCalledTimes(1);
const specRunQueueArgs = runQueue.calls.mostRecent().args[0];
expect(specRunQueueArgs.queueableFns[1]).toEqual(queueableFn);
queueableFn.fn();
expect(spec.getResult().status).toEqual('pending');
expect(spec.getResult().pendingReason).toEqual('some reason');
});
it('passes failSpecWithNoExp to Spec#executionFinished', async function() {
const spec = new jasmineUnderTest.Spec({
id: 'spec1',
queueableFn: {}
});
spyOn(spec, 'executionFinished');
const {
runQueue,
suiteRunQueueArgs,
executePromise
} = runSingleSpecSuite(spec, { failSpecWithNoExpectations: true });
suiteRunQueueArgs.queueableFns[0].fn();
expect(runQueue).toHaveBeenCalledTimes(1);
const specRunQueueArgs = runQueue.calls.mostRecent().args[0];
expect(specRunQueueArgs.queueableFns[1].type).toEqual('specCleanup');
specRunQueueArgs.queueableFns[1].fn();
expect(spec.executionFinished).toHaveBeenCalledWith(false, true);
await expectAsync(executePromise).toBePending();
});
});
describe('Suite execution', function() {
it('reports the duration of the suite', async function() {
const timer = jasmine.createSpyObj('timer', ['start', 'elapsed']);
const topSuite = new jasmineUnderTest.Suite({ id: 'topSuite' });
const suite = new jasmineUnderTest.Suite({
id: 'suite1',
parentSuite: topSuite,
timer
});
topSuite.addChild(suite);
const executionTree = {
topSuite,
childrenOfTopSuite() {
return [{ suite }];
},
childrenOfSuiteSegment() {
return [];
},
isExcluded() {
return false;
}
};
const runQueue = jasmine.createSpy('runQueue');
const reportDispatcher = mockReportDispatcher();
const subject = new jasmineUnderTest.TreeRunner({
executionTree,
runQueue,
globalErrors: mockGlobalErrors(),
runableResources: mockRunableResources(),
reportDispatcher,
currentRunableTracker: new jasmineUnderTest.CurrentRunableTracker(),
getConfig() {
return {};
},
reportChildrenOfBeforeAllFailure() {}
});
const executePromise = subject.execute();
expect(runQueue).toHaveBeenCalledTimes(1);
const topSuiteRunQueueOpts = runQueue.calls.mostRecent().args[0];
runQueue.calls.reset();
topSuiteRunQueueOpts.queueableFns[0].fn(function() {});
expect(runQueue).toHaveBeenCalledTimes(1);
expect(timer.start).not.toHaveBeenCalled();
const suiteRunQueueOpts = runQueue.calls.mostRecent().args[0];
suiteRunQueueOpts.queueableFns[0].fn();
expect(timer.start).toHaveBeenCalled();
expect(timer.elapsed).not.toHaveBeenCalled();
timer.elapsed.and.returnValue('the duration');
suiteRunQueueOpts.onComplete();
expect(timer.elapsed).toHaveBeenCalled();
const result = suite.getResult();
expect(result.duration).toEqual('the duration');
expect(reportDispatcher.suiteDone).toHaveBeenCalledWith(result);
await expectAsync(executePromise).toBePending();
});
it('returns false if a suite failed', async function() {
const topSuite = new jasmineUnderTest.Suite({ id: 'topSuite' });
const failingSuite = new jasmineUnderTest.Suite({
id: 'failingSuite',
parentSuite: topSuite
});
const passingSuite = new jasmineUnderTest.Suite({
id: 'passingSuite',
parentSuite: topSuite
});
const executionTree = {
topSuite,
childrenOfTopSuite() {
return [{ suite: failingSuite }, { suite: passingSuite }];
},
childrenOfSuiteSegment() {
return [];
},
isExcluded() {
return false;
}
};
const runQueue = jasmine.createSpy('runQueue');
const reportDispatcher = mockReportDispatcher();
const subject = new jasmineUnderTest.TreeRunner({
executionTree,
runQueue,
globalErrors: mockGlobalErrors(),
runableResources: mockRunableResources(),
reportDispatcher,
currentRunableTracker: new jasmineUnderTest.CurrentRunableTracker(),
getConfig() {
return {};
},
reportChildrenOfBeforeAllFailure() {}
});
const executePromise = subject.execute();
expect(runQueue).toHaveBeenCalledTimes(1);
const topSuiteRunQueueOpts = runQueue.calls.mostRecent().args[0];
runQueue.calls.reset();
topSuiteRunQueueOpts.queueableFns[0].fn(function() {});
// Fail the first suite.
expect(runQueue).toHaveBeenCalledTimes(1);
const failingSuiteRunQueueOpts = runQueue.calls.mostRecent().args[0];
runQueue.calls.reset();
failingSuiteRunQueueOpts.queueableFns[0].fn();
failingSuite.addExpectationResult(false, {});
failingSuiteRunQueueOpts.onComplete();
// Passing the second suite should not reset the overall result.
topSuiteRunQueueOpts.queueableFns[1].fn(function() {});
expect(runQueue).toHaveBeenCalledTimes(1);
const passingSuiteRunQueueOpts = runQueue.calls.mostRecent().args[0];
passingSuiteRunQueueOpts.queueableFns[0].fn();
passingSuiteRunQueueOpts.onComplete();
topSuiteRunQueueOpts.onComplete();
const result = await executePromise;
expect(result.hasFailures).toEqual(true);
});
it('reports children when there is a beforeAll failure', async function() {
const topSuite = new jasmineUnderTest.Suite({ id: 'topSuite' });
const suite = new jasmineUnderTest.Suite({
id: 'suite',
parentSuite: topSuite
});
suite.beforeAll({ fn() {} });
const spec = new jasmineUnderTest.Spec({
id: 'spec',
parentSuite: suite,
queueableFn: { fn() {} }
});
suite.addChild(spec);
topSuite.addChild(suite);
const executionTree = {
topSuite,
childrenOfTopSuite() {
return [{ suite }];
},
childrenOfSuiteSegment() {
return [{ spec }];
},
isExcluded() {
return false;
}
};
const runQueue = jasmine.createSpy('runQueue');
const reportDispatcher = mockReportDispatcher();
const reportChildrenOfBeforeAllFailure = jasmine
.createSpy('reportChildrenOfBeforeAllFailure')
.and.returnValue(Promise.resolve());
const subject = new jasmineUnderTest.TreeRunner({
executionTree,
runQueue,
globalErrors: mockGlobalErrors(),
runableResources: mockRunableResources(),
reportDispatcher,
currentRunableTracker: new jasmineUnderTest.CurrentRunableTracker(),
reportChildrenOfBeforeAllFailure,
getConfig() {
return {};
}
});
const executePromise = subject.execute();
expect(runQueue).toHaveBeenCalledTimes(1);
const topSuiteRunQueueOpts = runQueue.calls.mostRecent().args[0];
runQueue.calls.reset();
topSuiteRunQueueOpts.queueableFns[0].fn(function() {});
expect(runQueue).toHaveBeenCalledTimes(1);
const suiteRunQueueOpts = runQueue.calls.mostRecent().args[0];
suiteRunQueueOpts.queueableFns[0].fn();
suite.hadBeforeAllFailure = true;
suiteRunQueueOpts.onComplete();
while (reportDispatcher.suiteDone.calls.count() === 0) {
await Promise.resolve();
}
expect(reportDispatcher.specDone).toHaveBeenCalledBefore(
reportDispatcher.suiteDone
);
await expectAsync(executePromise).toBePending();
});
it('throws if the wrong suite is completed', async function() {
const topSuite = new jasmineUnderTest.Suite({ id: 'topSuite' });
const suite = new jasmineUnderTest.Suite({
id: 'suite',
parentSuite: topSuite
});
const spec = new jasmineUnderTest.Spec({
id: 'spec',
parentSuite: suite,
queueableFn: { fn() {} }
});
const executionTree = {
topSuite,
childrenOfTopSuite() {
return [{ suite }];
},
childrenOfSuiteSegment() {
return [{ spec }];
},
isExcluded() {
return false;
}
};
const runQueue = jasmine.createSpy('runQueue');
const reportDispatcher = mockReportDispatcher();
const subject = new jasmineUnderTest.TreeRunner({
executionTree,
runQueue,
globalErrors: mockGlobalErrors(),
runableResources: mockRunableResources(),
reportDispatcher,
currentRunableTracker: new jasmineUnderTest.CurrentRunableTracker(),
getConfig() {
return {};
},
reportChildrenOfBeforeAllFailure() {}
});
const executePromise = subject.execute();
expect(runQueue).toHaveBeenCalledTimes(1);
const topSuiteRunQueueOpts = runQueue.calls.mostRecent().args[0];
runQueue.calls.reset();
topSuiteRunQueueOpts.queueableFns[0].fn(function() {});
expect(runQueue).toHaveBeenCalledTimes(1);
const suiteRunQueueOpts = runQueue.calls.mostRecent().args[0];
// Complete the suite without starting it
expect(function() {
suiteRunQueueOpts.onComplete();
}).toThrowError('Tried to complete the wrong suite');
await expectAsync(executePromise).toBePending();
});
});
it('does not remove before and after fns from the top suite', async function() {
const topSuite = new jasmineUnderTest.Suite({ id: 'topSuite' });
spyOn(topSuite, 'cleanupBeforeAfter');
const executionTree = {
topSuite,
childrenOfTopSuite() {
return [];
},
isExcluded() {
return false;
}
};
const runQueue = jasmine.createSpy('runQueue');
const subject = new jasmineUnderTest.TreeRunner({
executionTree,
runQueue,
globalErrors: mockGlobalErrors(),
runableResources: mockRunableResources(),
reportDispatcher: mockReportDispatcher(),
currentRunableTracker: new jasmineUnderTest.CurrentRunableTracker(),
getConfig() {
return {};
}
});
const executePromise = subject.execute();
expect(runQueue).toHaveBeenCalledTimes(1);
const topSuiteRunQueueOpts = runQueue.calls.mostRecent().args[0];
runQueue.calls.reset();
for (const qfn of topSuiteRunQueueOpts.queueableFns) {
qfn.fn();
}
topSuiteRunQueueOpts.onComplete();
await expectAsync(executePromise).toBeResolved();
expect(topSuite.cleanupBeforeAfter).not.toHaveBeenCalled();
});
describe('Late promise rejection handling', function() {
it('works for specs when the detectLateRejectionHandling param is true', function() {
const before = jasmine.createSpy('before');
const after = jasmine.createSpy('after');
const queueableFn = {
fn: jasmine.createSpy('test body').and.callFake(function() {
expect(before).toHaveBeenCalled();
expect(after).not.toHaveBeenCalled();
})
};
const spec = new jasmineUnderTest.Spec({
queueableFn,
beforeAndAfterFns: function() {
return { befores: [before], afters: [after] };
}
});
const {
runQueue,
setTimeout,
suiteRunQueueArgs,
globalErrors
} = runSingleSpecSuite(spec, { detectLateRejectionHandling: true });
suiteRunQueueArgs.queueableFns[0].fn();
expect(runQueue).toHaveBeenCalledTimes(1);
const specRunQueueOpts = runQueue.calls.mostRecent().args[0];
expect(specRunQueueOpts.queueableFns).toEqual([
{ fn: jasmine.any(Function) },
before,
queueableFn,
after,
{ fn: jasmine.any(Function) },
{
fn: jasmine.any(Function),
type: 'specCleanup'
}
]);
const done = jasmine.createSpy('done');
specRunQueueOpts.queueableFns[4].fn(done);
expect(globalErrors.reportUnhandledRejections).not.toHaveBeenCalled();
expect(done).not.toHaveBeenCalled();
expect(setTimeout).toHaveBeenCalledOnceWith(jasmine.any(Function));
setTimeout.calls.argsFor(0)[0]();
expect(globalErrors.reportUnhandledRejections).toHaveBeenCalled();
expect(globalErrors.reportUnhandledRejections).toHaveBeenCalledBefore(
done
);
});
it('works for beforeAll when the detectLateRejectionHandling param is true', async function() {
const topSuite = new jasmineUnderTest.Suite({ id: 'topSuite' });
const suite = new jasmineUnderTest.Suite({
id: 'suite',
parentSuite: topSuite
});
suite.beforeAll(function() {});
const spec = new jasmineUnderTest.Spec({
queueableFn: { fn() {} },
parentSuite: suite
});
const executionTree = {
topSuite,
childrenOfTopSuite() {
return [{ suite }];
},
childrenOfSuiteSegment() {
return [{ spec }];
},
isExcluded() {
return false;
}
};
const runQueue = jasmine.createSpy('runQueue');
const reportDispatcher = mockReportDispatcher();
const globalErrors = mockGlobalErrors();
const setTimeout = jasmine.createSpy('setTimeout');
const subject = new jasmineUnderTest.TreeRunner({
executionTree,
runQueue,
globalErrors,
runableResources: mockRunableResources(),
reportDispatcher,
setTimeout,
currentRunableTracker: new jasmineUnderTest.CurrentRunableTracker(),
getConfig() {
return { detectLateRejectionHandling: true };
},
reportChildrenOfBeforeAllFailure() {}
});
const executePromise = subject.execute();
expect(runQueue).toHaveBeenCalledTimes(1);
const topSuiteRunQueueOpts = runQueue.calls.mostRecent().args[0];
runQueue.calls.reset();
topSuiteRunQueueOpts.queueableFns[0].fn(function() {});
expect(runQueue).toHaveBeenCalledTimes(1);
const suiteRunQueueOpts = runQueue.calls.mostRecent().args[0];
expect(suiteRunQueueOpts.queueableFns).toEqual([
{ fn: jasmine.any(Function) }, // onStart
jasmine.objectContaining({ type: 'beforeAll' }),
{ fn: jasmine.any(Function) }, // detect late rejection handling
{ fn: jasmine.any(Function) } // spec
]);
suiteRunQueueOpts.queueableFns[0].fn();
const done = jasmine.createSpy('done');
suiteRunQueueOpts.queueableFns[2].fn(done);
expect(globalErrors.reportUnhandledRejections).not.toHaveBeenCalled();
expect(setTimeout).toHaveBeenCalledOnceWith(jasmine.any(Function));
setTimeout.calls.argsFor(0)[0]();
expect(globalErrors.reportUnhandledRejections).toHaveBeenCalledBefore(
done
);
await expectAsync(executePromise).toBePending();
});
it('works for afterAll when the detectLateRejectionHandling param is true', async function() {
const topSuite = new jasmineUnderTest.Suite({ id: 'topSuite' });
const suite = new jasmineUnderTest.Suite({
id: 'suite',
parentSuite: topSuite
});
suite.afterAll(function() {});
const spec = new jasmineUnderTest.Spec({
queueableFn: { fn() {} },
parentSuite: suite
});
const executionTree = {
topSuite,
childrenOfTopSuite() {
return [{ suite }];
},
childrenOfSuiteSegment() {
return [{ spec }];
},
isExcluded() {
return false;
}
};
const runQueue = jasmine.createSpy('runQueue');
const reportDispatcher = mockReportDispatcher();
const globalErrors = mockGlobalErrors();
const setTimeout = jasmine.createSpy('setTimeout');
const subject = new jasmineUnderTest.TreeRunner({
executionTree,
runQueue,
globalErrors,
runableResources: mockRunableResources(),
reportDispatcher,
setTimeout,
currentRunableTracker: new jasmineUnderTest.CurrentRunableTracker(),
getConfig() {
return { detectLateRejectionHandling: true };
},
reportChildrenOfBeforeAllFailure() {}
});
const executePromise = subject.execute();
expect(runQueue).toHaveBeenCalledTimes(1);
const topSuiteRunQueueOpts = runQueue.calls.mostRecent().args[0];
runQueue.calls.reset();
topSuiteRunQueueOpts.queueableFns[0].fn(function() {});
expect(runQueue).toHaveBeenCalledTimes(1);
const suiteRunQueueOpts = runQueue.calls.mostRecent().args[0];
expect(suiteRunQueueOpts.queueableFns).toEqual([
{ fn: jasmine.any(Function) }, // onStart
{ fn: jasmine.any(Function) }, // spec
jasmine.objectContaining({ type: 'afterAll' }),
{ fn: jasmine.any(Function) } // detect late rejection handling
]);
suiteRunQueueOpts.queueableFns[0].fn();
const done = jasmine.createSpy('done');
suiteRunQueueOpts.queueableFns[3].fn(done);
expect(globalErrors.reportUnhandledRejections).not.toHaveBeenCalled();
expect(setTimeout).toHaveBeenCalledOnceWith(jasmine.any(Function));
setTimeout.calls.argsFor(0)[0]();
expect(globalErrors.reportUnhandledRejections).toHaveBeenCalledBefore(
done
);
await expectAsync(executePromise).toBePending();
});
});
function runSingleSpecSuite(spec, optionalConfig) {
const topSuiteId = 'suite1';
spec.parentSuiteId = topSuiteId;
const topSuite = new jasmineUnderTest.Suite({ id: topSuiteId });
topSuite.addChild(spec);
const executionTree = {
topSuite,
childrenOfTopSuite() {
return [{ spec }];
},
isExcluded() {
return false;
}
};
const runQueue = jasmine.createSpy('runQueue');
const reportDispatcher = mockReportDispatcher();
const runableResources = mockRunableResources();
const globalErrors = mockGlobalErrors();
const setTimeout = jasmine.createSpy('setTimeout');
const currentRunableTracker = new jasmineUnderTest.CurrentRunableTracker();
const subject = new jasmineUnderTest.TreeRunner({
executionTree,
runQueue,
globalErrors,
setTimeout,
runableResources,
reportDispatcher,
currentRunableTracker,
getConfig() {
return optionalConfig || {};
},
reportChildrenOfBeforeAllFailure() {}
});
const executePromise = subject.execute();
expect(runQueue).toHaveBeenCalledTimes(1);
const suiteRunQueueArgs = runQueue.calls.mostRecent().args[0];
runQueue.calls.reset();
return {
runQueue,
globalErrors,
setTimeout,
currentRunableTracker,
runableResources,
reportDispatcher,
suiteRunQueueArgs,
executePromise
};
}
function mockReportDispatcher() {
const reportDispatcher = jasmine.createSpyObj(
'reportDispatcher',
jasmineUnderTest.reporterEvents
);
for (const k of jasmineUnderTest.reporterEvents) {
reportDispatcher[k].and.returnValue(Promise.resolve());
}
return reportDispatcher;
}
function mockRunableResources() {
return jasmine.createSpyObj('runableResources', [
'initForRunable',
'clearForRunable'
]);
}
function mockGlobalErrors() {
return jasmine.createSpyObj('globalErrors', ['reportUnhandledRejections']);
}
});

View File

@@ -128,17 +128,6 @@ describe('util', function() {
});
});
describe('isUndefined', function() {
it('reports if a variable is defined', function() {
let a;
expect(jasmineUnderTest.util.isUndefined(a)).toBe(true);
expect(jasmineUnderTest.util.isUndefined(undefined)).toBe(true);
const defined = 'diz be undefined yo';
expect(jasmineUnderTest.util.isUndefined(defined)).toBe(false);
});
});
describe('cloneArgs', function() {
it('clones primitives as-is', function() {
expect(jasmineUnderTest.util.cloneArgs([true, false])).toEqual([

View File

@@ -2,8 +2,7 @@ describe('base helpers', function() {
describe('isError_', function() {
it('correctly handles WebSocket events', function(done) {
if (typeof jasmine.getGlobal().WebSocket === 'undefined') {
done();
return;
pending('Environment does not provide WebSocket');
}
const obj = (function() {

View File

@@ -79,7 +79,7 @@ describe('buildExpectationResult', function() {
it('handles nodejs assertions', function() {
if (typeof require === 'undefined') {
return;
pending('This test only runs in Node');
}
const assert = require('assert');
const value = 8421;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -470,20 +470,28 @@ describe('Matchers (Integration)', function() {
});
describe('toBeNullish', function() {
verifyPasses(function(env) {
env.expect(undefined).toBeNullish();
describe('with undefined', function() {
verifyPasses(function(env) {
env.expect(undefined).toBeNullish();
});
});
verifyPasses(function(env) {
env.expect(null).toBeNullish();
describe('with null', function() {
verifyPasses(function(env) {
env.expect(null).toBeNullish();
});
});
verifyFails(function(env) {
env.expect(1).toBeNullish();
describe('with a truthy value', function() {
verifyFails(function(env) {
env.expect(1).toBeNullish();
});
});
verifyFails(function(env) {
env.expect('').toBeNullish();
describe('with a non-null falsy value', function() {
verifyFails(function(env) {
env.expect('').toBeNullish();
});
});
});
@@ -665,13 +673,17 @@ describe('Matchers (Integration)', function() {
env.expect(spyObj).toHaveSpyInteractions();
});
verifyFails(function(env) {
env.expect(spyObj).toHaveSpyInteractions();
describe('with no methods called', function() {
verifyFails(function(env) {
env.expect(spyObj).toHaveSpyInteractions();
});
});
verifyFails(function(env) {
spyObj.otherMethod();
env.expect(spyObj).toHaveSpyInteractions();
describe('with only non-spy methods called', function() {
verifyFails(function(env) {
spyObj.otherMethod();
env.expect(spyObj).toHaveSpyInteractions();
});
});
});

View File

@@ -621,9 +621,14 @@ describe('spec running', function() {
actions.push('spec3');
});
spyOn(jasmineUnderTest.getEnv(), 'deprecated');
await env.execute([spec2.id, spec3.id, spec1.id]);
expect(actions).toEqual(['spec2', 'spec3', 'spec1']);
expect(jasmineUnderTest.getEnv().deprecated).toHaveBeenCalledWith(
'The specified spec/suite order splits up a suite, running unrelated specs in the middle of it. This will become an error in a future release.'
);
});
it('refuses to re-enter suites with a beforeAll', async function() {

View File

@@ -250,10 +250,6 @@ describe('matchersUtil', function() {
});
it('passes for equivalent Promises (GitHub issue #1314)', function() {
if (typeof Promise === 'undefined') {
return;
}
const p1 = new Promise(function() {}),
p2 = new Promise(function() {}),
matchersUtil = new jasmineUnderTest.MatchersUtil();
@@ -263,14 +259,13 @@ describe('matchersUtil', function() {
});
describe('when running in a browser', function() {
function isNotRunningInBrowser() {
return typeof document === 'undefined';
}
beforeEach(function() {
if (typeof document === 'undefined') {
pending('This test only runs in browsers');
}
});
it('passes for equivalent DOM nodes', function() {
if (isNotRunningInBrowser()) {
return;
}
const a = document.createElement('div');
const matchersUtil = new jasmineUnderTest.MatchersUtil();
@@ -285,9 +280,6 @@ describe('matchersUtil', function() {
});
it('passes for equivalent objects from different frames', function() {
if (isNotRunningInBrowser()) {
return;
}
const matchersUtil = new jasmineUnderTest.MatchersUtil();
const iframe = document.createElement('iframe');
document.body.appendChild(iframe);
@@ -299,9 +291,6 @@ describe('matchersUtil', function() {
});
it('fails for DOM nodes with different attributes or child nodes', function() {
if (isNotRunningInBrowser()) {
return;
}
const matchersUtil = new jasmineUnderTest.MatchersUtil();
const a = document.createElement('div');
a.setAttribute('test-attr', 'attr-value');
@@ -325,14 +314,13 @@ describe('matchersUtil', function() {
});
describe('when running in Node', function() {
function isNotRunningInNode() {
return typeof require !== 'function';
}
beforeEach(function() {
if (typeof require !== 'function') {
pending('This test only runs in Node');
}
});
it('passes for equivalent objects from different vm contexts', function() {
if (isNotRunningInNode()) {
return;
}
const matchersUtil = new jasmineUnderTest.MatchersUtil();
const vm = require('vm');
const sandbox = {
@@ -344,9 +332,6 @@ describe('matchersUtil', function() {
});
it('passes for equivalent arrays from different vm contexts', function() {
if (isNotRunningInNode()) {
return;
}
const matchersUtil = new jasmineUnderTest.MatchersUtil();
const vm = require('vm');
const sandbox = {

View File

@@ -82,7 +82,7 @@ describe('toThrowError', function() {
it('passes if thrown is an instanceof Error regardless of global that contains its constructor', function() {
if (isNotRunningInBrowser()) {
return;
pending('This test only runs in browsers.');
}
const matcher = jasmineUnderTest.matchers.toThrowError();
@@ -281,22 +281,44 @@ describe('toThrowError', function() {
);
});
it('fails if thrown is a type of Error and the expected is a different Error', function() {
const matchersUtil = {
equals: jasmine.createSpy('delegated-equal').and.returnValue(false),
pp: jasmineUnderTest.makePrettyPrinter()
},
matcher = jasmineUnderTest.matchers.toThrowError(matchersUtil),
fn = function() {
throw new TypeError('foo');
};
describe('with a string', function() {
it('fails if thrown is a type of Error and the expected is a different Error', function() {
const matchersUtil = {
equals: jasmine.createSpy('delegated-equal').and.returnValue(false),
pp: jasmineUnderTest.makePrettyPrinter()
},
matcher = jasmineUnderTest.matchers.toThrowError(matchersUtil),
fn = function() {
throw new TypeError('foo');
};
const result = matcher.compare(fn, TypeError, 'bar');
const result = matcher.compare(fn, TypeError, 'bar');
expect(result.pass).toBe(false);
expect(result.message()).toEqual(
"Expected function to throw TypeError with message 'bar', but it threw TypeError with message 'foo'."
);
expect(result.pass).toBe(false);
expect(result.message()).toEqual(
"Expected function to throw TypeError with message 'bar', but it threw TypeError with message 'foo'."
);
});
});
describe('with a regex', function() {
it('fails if thrown is a type of Error and the expected is a different Error', function() {
const matchersUtil = {
equals: jasmine.createSpy('delegated-equal').and.returnValue(false),
pp: jasmineUnderTest.makePrettyPrinter()
},
matcher = jasmineUnderTest.matchers.toThrowError(matchersUtil),
fn = function() {
throw new TypeError('foo');
};
const result = matcher.compare(fn, TypeError, /bar/);
expect(result.pass).toBe(false);
expect(result.message()).toEqual(
"Expected function to throw TypeError with a message matching /bar/, but it threw TypeError with message 'foo'."
);
});
});
it('passes if thrown is a type of Error and has the same type as the expected Error and the message matches the expected message', function() {
@@ -316,22 +338,4 @@ describe('toThrowError', function() {
'Expected function not to throw TypeError with a message matching /foo/.'
);
});
it('fails if thrown is a type of Error and the expected is a different Error', function() {
const matchersUtil = {
equals: jasmine.createSpy('delegated-equal').and.returnValue(false),
pp: jasmineUnderTest.makePrettyPrinter()
},
matcher = jasmineUnderTest.matchers.toThrowError(matchersUtil),
fn = function() {
throw new TypeError('foo');
};
const result = matcher.compare(fn, TypeError, /bar/);
expect(result.pass).toBe(false);
expect(result.message()).toEqual(
"Expected function to throw TypeError with a message matching /bar/, but it threw TypeError with message 'foo'."
);
});
});

View File

@@ -499,7 +499,7 @@ describe('HtmlReporter', function() {
expect(duration.innerHTML).toMatch(/finished in 0.1s/);
});
it('reports the suite and spec names with status', function() {
it('reports the suite names with status, and spec names with status and duration', function() {
const container = document.createElement('div'),
getContainer = function() {
return container;
@@ -532,7 +532,8 @@ describe('HtmlReporter', function() {
fullName: 'A Suite with a spec',
status: 'passed',
failedExpectations: [],
passedExpectations: [{ passed: true }]
passedExpectations: [{ passed: true }],
duration: 1230
};
reporter.specStarted(specResult);
reporter.specDone(specResult);
@@ -549,7 +550,8 @@ describe('HtmlReporter', function() {
fullName: 'A Suite inner suite with another spec',
status: 'passed',
failedExpectations: [],
passedExpectations: [{ passed: true }]
passedExpectations: [{ passed: true }],
duration: 1240
};
reporter.specStarted(specResult);
reporter.specDone(specResult);
@@ -567,7 +569,8 @@ describe('HtmlReporter', function() {
fullName: 'A Suite inner with a failing spec',
status: 'failed',
failedExpectations: [{}],
passedExpectations: []
passedExpectations: [],
duration: 2090
};
reporter.specStarted(specResult);
reporter.specDone(specResult);
@@ -614,6 +617,9 @@ describe('HtmlReporter', function() {
expect(specLink.getAttribute('href')).toEqual(
'/?foo=bar&spec=A Suite with a spec'
);
const specDuration = spec.childNodes[1];
expect(specDuration.innerHTML).toEqual('(1230ms)');
});
it('has an options menu', function() {

View File

@@ -1,40 +0,0 @@
describe('MatchersSpec - HTML Dependent', function() {
let env, spec;
beforeEach(function() {
env = new jasmineUnderTest.Env();
env.describe('suite', function() {
spec = env.it('spec', function() {});
});
spyOn(spec, 'addExpectationResult');
addMatchers({
toPass: function() {
return lastResult().passed;
},
toFail: function() {
return !lastResult().passed;
}
});
});
afterEach(function() {
env.cleanup_();
});
function match(value) {
return spec.expect(value);
}
function lastResult() {
return spec.addExpectationResult.mostRecentCall.args[1];
}
xit('toEqual with DOM nodes', function() {
const nodeA = document.createElement('div');
const nodeB = document.createElement('div');
expect(match(nodeA).toEqual(nodeA)).toPass();
expect(match(nodeA).toEqual(nodeB)).toFail();
});
});

View File

@@ -1,24 +1,21 @@
const fs = require('node:fs');
const path = require('node:path');
const os = require('node:os');
const { rimrafSync } = require('rimraf');
const child_process = require('node:child_process');
describe('npm package', function() {
beforeAll(function() {
const shell = require('shelljs'),
pack = shell.exec('npm pack', { silent: true });
this.tarball = pack.stdout.split('\n')[0];
const packOutput = child_process.execSync('npm pack', {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe']
});
this.tarball = packOutput.split('\n')[0];
const prefix = path.join(os.tmpdir(), 'jasmine-npm-package');
this.tmpDir = fs.mkdtempSync(prefix);
const untar = shell.exec(
'tar -xzf ' + this.tarball + ' -C ' + this.tmpDir,
{
silent: true
}
);
expect(untar.code).toBe(0);
child_process.execSync(`tar -xzf ${this.tarball} -C ${this.tmpDir}`, {
encoding: 'utf8'
});
this.packagedCore = require(path.join(
this.tmpDir,
@@ -43,7 +40,7 @@ describe('npm package', function() {
afterAll(function() {
fs.unlinkSync(this.tarball);
rimrafSync(this.tmpDir);
fs.rmSync(this.tmpDir, { recursive: true });
});
it('has a root path', function() {

View File

@@ -26,6 +26,9 @@ module.exports = {
'helpers/defineJasmineUnderTest.js',
'helpers/resetEnv.js'
],
env: {
forbidDuplicateNames: true
},
random: true,
browser: {
name: process.env.JASMINE_BROWSER || 'firefox',

View File

@@ -12,5 +12,8 @@
"helpers/nodeDefineJasmineUnderTest.js",
"helpers/resetEnv.js"
],
"env": {
"forbidDuplicateNames": true
},
"random": true
}

View File

@@ -44,7 +44,7 @@ getJasmineRequireObj().clearStack = function(j$) {
function getUnclampedSetTimeout(global) {
const { setTimeout } = global;
if (j$.util.isUndefined(global.MessageChannel)) {
if (!global.MessageChannel) {
return setTimeout;
}
@@ -104,10 +104,7 @@ getJasmineRequireObj().clearStack = function(j$) {
// Unlike browsers, Node doesn't require us to do a periodic setTimeout
// so we avoid the overhead.
return nodeQueueMicrotaskImpl(global);
} else if (
SAFARI_OR_WIN_WEBKIT ||
j$.util.isUndefined(global.MessageChannel) /* tests */
) {
} else if (SAFARI_OR_WIN_WEBKIT || !global.MessageChannel /* tests */) {
// queueMicrotask is dramatically faster than MessageChannel in Safari
// and other WebKit-based browsers, such as the one distributed by Playwright
// to test Safari-like behavior on Windows.

186
src/core/Configuration.js Normal file
View File

@@ -0,0 +1,186 @@
getJasmineRequireObj().Configuration = function(j$) {
/**
* This represents the available options to configure Jasmine.
* Options that are not provided will use their default values.
* @see Env#configure
* @interface Configuration
* @since 3.3.0
*/
const defaultConfig = {
/**
* Whether to randomize spec execution order
* @name Configuration#random
* @since 3.3.0
* @type Boolean
* @default true
*/
random: true,
/**
* Seed to use as the basis of randomization.
* Null causes the seed to be determined randomly at the start of execution.
* @name Configuration#seed
* @since 3.3.0
* @type (number|string)
* @default null
*/
seed: null,
/**
* Whether to stop execution of the suite after the first spec failure
*
* <p>In parallel mode, `stopOnSpecFailure` works on a "best effort"
* basis. Jasmine will stop execution as soon as practical after a failure
* but it might not be immediate.</p>
* @name Configuration#stopOnSpecFailure
* @since 3.9.0
* @type Boolean
* @default false
*/
stopOnSpecFailure: false,
/**
* Whether to fail the spec if it ran no expectations. By default
* a spec that ran no expectations is reported as passed. Setting this
* to true will report such spec as a failure.
* @name Configuration#failSpecWithNoExpectations
* @since 3.5.0
* @type Boolean
* @default false
*/
failSpecWithNoExpectations: false,
/**
* Whether to cause specs to only have one expectation failure.
* @name Configuration#stopSpecOnExpectationFailure
* @since 3.3.0
* @type Boolean
* @default false
*/
stopSpecOnExpectationFailure: false,
/**
* A function that takes a spec and returns true if it should be executed
* or false if it should be skipped.
* @callback SpecFilter
* @param {Spec} spec - The spec that the filter is being applied to.
* @return boolean
*/
/**
* Function to use to filter specs
* @name Configuration#specFilter
* @since 3.3.0
* @type SpecFilter
* @default A function that always returns true.
*/
specFilter: function() {
return true;
},
/**
* Whether reporters should hide disabled specs from their output.
* Currently only supported by Jasmine's HTMLReporter
* @name Configuration#hideDisabled
* @since 3.3.0
* @type Boolean
* @default false
*/
hideDisabled: false,
/**
* Clean closures when a suite is done running (done by clearing the stored function reference).
* This prevents memory leaks, but you won't be able to run jasmine multiple times.
* @name Configuration#autoCleanClosures
* @since 3.10.0
* @type boolean
* @default true
*/
autoCleanClosures: true,
/**
* Whether to forbid duplicate spec or suite names. If set to true, using
* the same name multiple times in the same immediate parent suite is an
* error.
* @name Configuration#forbidDuplicateNames
* @type boolean
* @default false
*/
forbidDuplicateNames: false,
/**
* Whether to issue warnings for certain deprecated functionality
* every time it's used. If not set or set to false, deprecation warnings
* for methods that tend to be called frequently will be issued only once
* or otherwise throttled to prevent the suite output from being flooded
* with warnings.
* @name Configuration#verboseDeprecations
* @since 3.6.0
* @type Boolean
* @default false
*/
verboseDeprecations: false,
/**
* Whether to detect late promise rejection handling during spec
* execution. If this option is enabled, a promise rejection that triggers
* the JavaScript runtime's unhandled rejection event will not be treated
* as an error as long as it's handled before the spec finishes.
*
* This option is off by default because it imposes a performance penalty.
* @name Configuration#detectLateRejectionHandling
* @since 5.10.0
* @type Boolean
* @default false
*/
detectLateRejectionHandling: false
};
Object.freeze(defaultConfig);
class Configuration {
#values;
constructor() {
this.#values = { ...defaultConfig };
for (const k of Object.keys(defaultConfig)) {
Object.defineProperty(this, k, {
enumerable: true,
get() {
return this.#values[k];
}
});
}
}
copy() {
return { ...this.#values };
}
update(changes) {
const booleanProps = [
'random',
'failSpecWithNoExpectations',
'hideDisabled',
'stopOnSpecFailure',
'stopSpecOnExpectationFailure',
'autoCleanClosures',
'forbidDuplicateNames',
'detectLateRejectionHandling'
];
for (const k of booleanProps) {
if (typeof changes[k] !== 'undefined') {
this.#values[k] = changes[k];
}
}
if (changes.specFilter) {
this.#values.specFilter = changes.specFilter;
}
// 0 and null are valid values, so a truthiness check wouldn't work
if (typeof changes.seed !== 'undefined') {
this.#values.seed = changes.seed;
}
// TODO: in the next major release, make verboseDeprecations work like
// other boolean properties.
if (changes.hasOwnProperty('verboseDeprecations')) {
this.#values.verboseDeprecations = changes.verboseDeprecations;
}
}
}
return Configuration;
};

View File

@@ -0,0 +1,38 @@
getJasmineRequireObj().CurrentRunableTracker = function() {
class CurrentRunableTracker {
#currentSpec;
#currentlyExecutingSuites;
constructor() {
this.#currentlyExecutingSuites = [];
}
currentRunable() {
return this.currentSpec() || this.currentSuite();
}
currentSpec() {
return this.#currentSpec;
}
setCurrentSpec(spec) {
this.#currentSpec = spec;
}
currentSuite() {
return this.#currentlyExecutingSuites[
this.#currentlyExecutingSuites.length - 1
];
}
pushSuite(suite) {
this.#currentlyExecutingSuites.push(suite);
}
popSuite() {
this.#currentlyExecutingSuites.pop();
}
}
return CurrentRunableTracker;
};

View File

@@ -6,7 +6,7 @@ getJasmineRequireObj().DelayedFunctionScheduler = function(j$) {
this.scheduledLookup_ = [];
this.scheduledFunctions_ = {};
this.currentTime_ = 0;
this.delayedFnCount_ = 0;
this.delayedFnStartCount_ = 1e12; // arbitrarily large number to avoid collisions with native timer IDs;
this.deletedKeys_ = [];
this.tick = function(millis, tickDate) {
@@ -38,7 +38,7 @@ getJasmineRequireObj().DelayedFunctionScheduler = function(j$) {
}
millis = millis || 0;
timeoutKey = timeoutKey || ++this.delayedFnCount_;
timeoutKey = timeoutKey || ++this.delayedFnStartCount_;
runAtMillis = runAtMillis || this.currentTime_ + millis;
const funcToSchedule = {

View File

@@ -65,7 +65,7 @@ getJasmineRequireObj().Deprecator = function(j$) {
Deprecator.prototype.stackTrace_ = function() {
const formatter = new j$.ExceptionFormatter();
return formatter.stack(j$.util.errorWithStack()).replace(/^Error\n/m, '');
return formatter.stack(new Error()).replace(/^Error\n/m, '');
};
Deprecator.prototype.report_ = function(runnable, deprecation, options) {

View File

@@ -7,11 +7,12 @@ getJasmineRequireObj().Env = function(j$) {
* calling {@link jasmine.getEnv}.
* @hideconstructor
*/
function Env(options) {
options = options || {};
function Env(envOptions) {
envOptions = envOptions || {};
const self = this;
const global = options.global || j$.getGlobal();
const GlobalErrors = envOptions.GlobalErrors || j$.GlobalErrors;
const global = envOptions.global || j$.getGlobal();
const realSetTimeout = global.setTimeout;
const realClearTimeout = global.clearTimeout;
@@ -24,13 +25,27 @@ getJasmineRequireObj().Env = function(j$) {
new j$.MockDate(global)
);
const globalErrors = new j$.GlobalErrors();
const installGlobalErrors = (function() {
const globalErrors = new GlobalErrors(
undefined,
// Configuration is late-bound because GlobalErrors needs to be constructed
// before it's set to detect load-time errors in browsers
() => this.configuration()
);
const { installGlobalErrors, uninstallGlobalErrors } = (function() {
let installed = false;
return function() {
if (!installed) {
globalErrors.install();
installed = true;
return {
installGlobalErrors() {
if (!installed) {
globalErrors.install();
installed = true;
}
},
uninstallGlobalErrors() {
if (installed) {
globalErrors.uninstall();
installed = false;
}
}
};
})();
@@ -43,125 +58,14 @@ getJasmineRequireObj().Env = function(j$) {
globalErrors
});
let reporter;
let reportDispatcher;
let topSuite;
let runner;
let parallelLoadingState = null; // 'specs', 'helpers', or null for non-parallel
/**
* This represents the available options to configure Jasmine.
* Options that are not provided will use their default values.
* @see Env#configure
* @interface Configuration
* @since 3.3.0
*/
const config = {
/**
* Whether to randomize spec execution order
* @name Configuration#random
* @since 3.3.0
* @type Boolean
* @default true
*/
random: true,
/**
* Seed to use as the basis of randomization.
* Null causes the seed to be determined randomly at the start of execution.
* @name Configuration#seed
* @since 3.3.0
* @type (number|string)
* @default null
*/
seed: null,
/**
* Whether to stop execution of the suite after the first spec failure
*
* <p>In parallel mode, `stopOnSpecFailure` works on a "best effort"
* basis. Jasmine will stop execution as soon as practical after a failure
* but it might not be immediate.</p>
* @name Configuration#stopOnSpecFailure
* @since 3.9.0
* @type Boolean
* @default false
*/
stopOnSpecFailure: false,
/**
* Whether to fail the spec if it ran no expectations. By default
* a spec that ran no expectations is reported as passed. Setting this
* to true will report such spec as a failure.
* @name Configuration#failSpecWithNoExpectations
* @since 3.5.0
* @type Boolean
* @default false
*/
failSpecWithNoExpectations: false,
/**
* Whether to cause specs to only have one expectation failure.
* @name Configuration#stopSpecOnExpectationFailure
* @since 3.3.0
* @type Boolean
* @default false
*/
stopSpecOnExpectationFailure: false,
/**
* A function that takes a spec and returns true if it should be executed
* or false if it should be skipped.
* @callback SpecFilter
* @param {Spec} spec - The spec that the filter is being applied to.
* @return boolean
*/
/**
* Function to use to filter specs
* @name Configuration#specFilter
* @since 3.3.0
* @type SpecFilter
* @default A function that always returns true.
*/
specFilter: function() {
return true;
},
/**
* Whether or not reporters should hide disabled specs from their output.
* Currently only supported by Jasmine's HTMLReporter
* @name Configuration#hideDisabled
* @since 3.3.0
* @type Boolean
* @default false
*/
hideDisabled: false,
/**
* Clean closures when a suite is done running (done by clearing the stored function reference).
* This prevents memory leaks, but you won't be able to run jasmine multiple times.
* @name Configuration#autoCleanClosures
* @since 3.10.0
* @type boolean
* @default true
*/
autoCleanClosures: true,
/**
* Whether to forbid duplicate spec or suite names. If set to true, using
* the same name multiple times in the same immediate parent suite is an
* error.
* @name Configuration#forbidDuplicateNames
* @type boolean
* @default false
*/
forbidDuplicateNames: false,
/**
* Whether or not to issue warnings for certain deprecated functionality
* every time it's used. If not set or set to false, deprecation warnings
* for methods that tend to be called frequently will be issued only once
* or otherwise throttled to to prevent the suite output from being flooded
* with warnings.
* @name Configuration#verboseDeprecations
* @since 3.6.0
* @type Boolean
* @default false
*/
verboseDeprecations: false
};
const config = new j$.Configuration();
if (!options.suppressLoadErrors) {
if (!envOptions.suppressLoadErrors) {
installGlobalErrors();
globalErrors.pushListener(function loadtimeErrorHandler(error, event) {
topSuite.result.failedExpectations.push({
@@ -182,41 +86,15 @@ getJasmineRequireObj().Env = function(j$) {
* @argument {Configuration} configuration
* @function
*/
this.configure = function(configuration) {
this.configure = function(changes) {
if (parallelLoadingState) {
throw new Error(
'Jasmine cannot be configured via Env in parallel mode'
);
}
const booleanProps = [
'random',
'failSpecWithNoExpectations',
'hideDisabled',
'stopOnSpecFailure',
'stopSpecOnExpectationFailure',
'autoCleanClosures',
'forbidDuplicateNames'
];
booleanProps.forEach(function(prop) {
if (typeof configuration[prop] !== 'undefined') {
config[prop] = !!configuration[prop];
}
});
if (configuration.specFilter) {
config.specFilter = configuration.specFilter;
}
if (typeof configuration.seed !== 'undefined') {
config.seed = configuration.seed;
}
if (configuration.hasOwnProperty('verboseDeprecations')) {
config.verboseDeprecations = configuration.verboseDeprecations;
deprecator.verboseDeprecations(config.verboseDeprecations);
}
config.update(changes);
deprecator.verboseDeprecations(config.verboseDeprecations);
};
/**
@@ -227,11 +105,7 @@ getJasmineRequireObj().Env = function(j$) {
* @returns {Configuration}
*/
this.configuration = function() {
const result = {};
for (const property in config) {
result[property] = config[property];
}
return result;
return config.copy();
};
this.setDefaultSpyStrategy = function(defaultStrategyFn) {
@@ -434,7 +308,7 @@ getJasmineRequireObj().Env = function(j$) {
deprecator.addDeprecationWarning(runable, deprecation, options);
};
function queueRunnerFactory(options) {
function runQueue(options) {
options.clearStack = options.clearStack || clearStack;
options.timeout = {
setTimeout: realSetTimeout,
@@ -456,9 +330,7 @@ getJasmineRequireObj().Env = function(j$) {
expectationFactory,
asyncExpectationFactory,
onLateError: recordLateError,
specResultCallback,
specStarted,
queueRunnerFactory
runQueue
});
topSuite = suiteBuilder.topSuite;
const deprecator = new j$.Deprecator(topSuite);
@@ -481,11 +353,11 @@ getJasmineRequireObj().Env = function(j$) {
* @interface Reporter
* @see custom_reporter
*/
reporter = new j$.ReportDispatcher(
reportDispatcher = new j$.ReportDispatcher(
j$.reporterEvents,
function(options) {
options.SkipPolicy = j$.NeverSkipPolicy;
return queueRunnerFactory(options);
return runQueue(options);
},
recordLateError
);
@@ -495,10 +367,11 @@ getJasmineRequireObj().Env = function(j$) {
totalSpecsDefined: () => suiteBuilder.totalSpecsDefined,
focusedRunables: () => suiteBuilder.focusedRunables,
runableResources,
reporter,
queueRunnerFactory,
getConfig: () => config,
reportSpecDone
reportDispatcher,
runQueue,
TreeProcessor: j$.TreeProcessor,
globalErrors,
getConfig: () => config
});
this.setParallelLoadingState = function(state) {
@@ -561,7 +434,7 @@ getJasmineRequireObj().Env = function(j$) {
throw new Error('Reporters cannot be added via Env in parallel mode');
}
reporter.addReporter(reporterToAdd);
reportDispatcher.addReporter(reporterToAdd);
};
/**
@@ -573,7 +446,7 @@ getJasmineRequireObj().Env = function(j$) {
* @see custom_reporter
*/
this.provideFallbackReporter = function(reporterToAdd) {
reporter.provideFallbackReporter(reporterToAdd);
reportDispatcher.provideFallbackReporter(reporterToAdd);
};
/**
@@ -587,7 +460,7 @@ getJasmineRequireObj().Env = function(j$) {
throw new Error('Reporters cannot be removed via Env in parallel mode');
}
reporter.clearReporters();
reportDispatcher.clearReporters();
};
/**
@@ -740,28 +613,6 @@ getJasmineRequireObj().Env = function(j$) {
.metadata;
};
function specResultCallback(spec, result, next) {
runableResources.clearForRunable(spec.id);
runner.currentSpec = null;
if (result.status === 'failed') {
runner.hasFailures = true;
}
reportSpecDone(spec, result, next);
}
function specStarted(spec, suite, next) {
runner.currentSpec = spec;
runableResources.initForRunable(spec.id, suite.id);
reporter.specStarted(spec.result).then(next);
}
function reportSpecDone(spec, result, next) {
spec.reportedDone = true;
reporter.specDone(result).then(next);
}
this.it = function(description, fn, timeout) {
ensureIsNotNested('it');
const filename = callerCallerFilename();
@@ -781,6 +632,26 @@ getJasmineRequireObj().Env = function(j$) {
return suiteBuilder.fit(description, fn, timeout, filename).metadata;
};
/**
* Get a user-defined property as part of the properties field of {@link SpecResult}
* @name Env#getSpecProperty
* @since 5.10.0
* @function
* @param {String} key The name of the property
* @returns {*} The value of the property
*/
this.getSpecProperty = function(key) {
if (
!runner.currentRunable() ||
runner.currentRunable() == runner.currentSuite()
) {
throw new Error(
"'getSpecProperty' was used when there was no current spec"
);
}
return runner.currentRunable().getSpecProperty(key);
};
/**
* Sets a user-defined property that will be provided to reporters as part of the properties field of {@link SpecResult}
* @name Env#setSpecProperty
@@ -942,9 +813,7 @@ getJasmineRequireObj().Env = function(j$) {
};
this.cleanup_ = function() {
if (globalErrors) {
globalErrors.uninstall();
}
uninstallGlobalErrors();
};
}

View File

@@ -148,7 +148,7 @@ getJasmineRequireObj().Expectation = function(j$) {
return function() {
// Capture the call stack here, before we go async, so that it will contain
// frames that are relevant to the user instead of just parts of Jasmine.
const errorForStack = j$.util.errorWithStack();
const errorForStack = new Error();
return this.expector
.compare(name, matcherFactory, arguments)

View File

@@ -1,133 +1,44 @@
getJasmineRequireObj().GlobalErrors = function(j$) {
function GlobalErrors(global) {
global = global || j$.getGlobal();
class GlobalErrors {
#getConfig;
#adapter;
#handlers;
#overrideHandler;
#onRemoveOverrideHandler;
#pendingUnhandledRejections;
const handlers = [];
let overrideHandler = null,
onRemoveOverrideHandler = null;
constructor(global, getConfig) {
global = global || j$.getGlobal();
this.#getConfig = getConfig;
this.#pendingUnhandledRejections = new Map();
this.#handlers = [];
this.#overrideHandler = null;
this.#onRemoveOverrideHandler = null;
function onBrowserError(event) {
dispatchBrowserError(event.error, event);
}
function dispatchBrowserError(error, event) {
if (overrideHandler) {
// See discussion of spyOnGlobalErrorsAsync in base.js
overrideHandler(error);
return;
}
const handler = handlers[handlers.length - 1];
if (handler) {
handler(error, event);
} else {
throw error;
}
}
this.originalHandlers = {};
this.jasmineHandlers = {};
this.installOne_ = function installOne_(errorType, jasmineMessage) {
function taggedOnError(error) {
if (j$.isError_(error)) {
error.jasmineMessage = jasmineMessage + ': ' + error;
} else {
let substituteMsg;
if (error) {
substituteMsg = jasmineMessage + ': ' + error;
} else {
substituteMsg = jasmineMessage + ' with no error or message';
}
if (errorType === 'unhandledRejection') {
substituteMsg +=
'\n' +
'(Tip: to get a useful stack trace, use ' +
'Promise.reject(new Error(...)) instead of Promise.reject(' +
(error ? '...' : '') +
').)';
}
error = new Error(substituteMsg);
}
const handler = handlers[handlers.length - 1];
if (overrideHandler) {
// See discussion of spyOnGlobalErrorsAsync in base.js
overrideHandler(error);
return;
}
if (handler) {
handler(error);
} else {
throw error;
}
}
this.originalHandlers[errorType] = global.process.listeners(errorType);
this.jasmineHandlers[errorType] = taggedOnError;
global.process.removeAllListeners(errorType);
global.process.on(errorType, taggedOnError);
this.uninstall = function uninstall() {
const errorTypes = Object.keys(this.originalHandlers);
for (const errorType of errorTypes) {
global.process.removeListener(
errorType,
this.jasmineHandlers[errorType]
);
for (let i = 0; i < this.originalHandlers[errorType].length; i++) {
global.process.on(errorType, this.originalHandlers[errorType][i]);
}
delete this.originalHandlers[errorType];
delete this.jasmineHandlers[errorType];
}
const dispatch = {
onUncaughtException: this.#onUncaughtException.bind(this),
onUnhandledRejection: this.#onUnhandledRejection.bind(this),
onRejectionHandled: this.#onRejectionHandled.bind(this)
};
};
this.install = function install() {
if (
global.process &&
global.process.listeners &&
j$.isFunction_(global.process.on)
) {
this.installOne_('uncaughtException', 'Uncaught exception');
this.installOne_('unhandledRejection', 'Unhandled promise rejection');
this.#adapter = new NodeAdapter(global, dispatch);
} else {
global.addEventListener('error', onBrowserError);
const browserRejectionHandler = function browserRejectionHandler(
event
) {
if (j$.isError_(event.reason)) {
event.reason.jasmineMessage =
'Unhandled promise rejection: ' + event.reason;
dispatchBrowserError(event.reason, event);
} else {
dispatchBrowserError(
'Unhandled promise rejection: ' + event.reason,
event
);
}
};
global.addEventListener('unhandledrejection', browserRejectionHandler);
this.uninstall = function uninstall() {
global.removeEventListener('error', onBrowserError);
global.removeEventListener(
'unhandledrejection',
browserRejectionHandler
);
};
this.#adapter = new BrowserAdapter(global, dispatch);
}
};
}
install() {
this.#adapter.install();
}
uninstall() {
this.#adapter.uninstall();
}
// The listener at the top of the stack will be called with two arguments:
// the error and the event. Either of them may be falsy.
@@ -136,35 +47,246 @@ getJasmineRequireObj().GlobalErrors = function(j$) {
// browsers but will be falsy in Node.
// Listeners that are pushed after spec files have been loaded should be
// able to just use the error parameter.
this.pushListener = function pushListener(listener) {
handlers.push(listener);
};
pushListener(listener) {
this.#handlers.push(listener);
}
this.popListener = function popListener(listener) {
popListener(listener) {
if (!listener) {
throw new Error('popListener expects a listener');
}
handlers.pop();
};
this.#handlers.pop();
}
this.setOverrideListener = function(listener, onRemove) {
if (overrideHandler) {
setOverrideListener(listener, onRemove) {
if (this.#overrideHandler) {
throw new Error("Can't set more than one override listener at a time");
}
overrideHandler = listener;
onRemoveOverrideHandler = onRemove;
};
this.#overrideHandler = listener;
this.#onRemoveOverrideHandler = onRemove;
}
this.removeOverrideListener = function() {
if (onRemoveOverrideHandler) {
onRemoveOverrideHandler();
removeOverrideListener() {
if (this.#onRemoveOverrideHandler) {
this.#onRemoveOverrideHandler();
}
overrideHandler = null;
onRemoveOverrideHandler = null;
};
this.#overrideHandler = null;
this.#onRemoveOverrideHandler = null;
}
reportUnhandledRejections() {
for (const {
reason,
event
} of this.#pendingUnhandledRejections.values()) {
this.#dispatchError(reason, event);
}
this.#pendingUnhandledRejections.clear();
}
// Either error or event may be undefined
#onUncaughtException(error, event) {
this.#dispatchError(error, event);
}
// event or promise may be undefined
// event is passed through for backwards compatibility reasons. It's probably
// unnecessary, but user code could depend on it.
#onUnhandledRejection(reason, promise, event) {
if (this.#detectLateRejectionHandling() && promise) {
this.#pendingUnhandledRejections.set(promise, { reason, event });
} else {
this.#dispatchError(reason, event);
}
}
#detectLateRejectionHandling() {
return this.#getConfig().detectLateRejectionHandling;
}
#onRejectionHandled(promise) {
this.#pendingUnhandledRejections.delete(promise);
}
// Either error or event may be undefined
#dispatchError(error, event) {
if (this.#overrideHandler) {
// See discussion of spyOnGlobalErrorsAsync in base.js
this.#overrideHandler(error);
return;
}
const handler = this.#handlers[this.#handlers.length - 1];
if (handler) {
handler(error, event);
} else {
throw error;
}
}
}
class BrowserAdapter {
#global;
#dispatch;
#onError;
#onUnhandledRejection;
#onRejectionHandled;
constructor(global, dispatch) {
this.#global = global;
this.#dispatch = dispatch;
this.#onError = event => dispatch.onUncaughtException(event.error, event);
this.#onUnhandledRejection = this.#unhandledRejectionHandler.bind(this);
this.#onRejectionHandled = this.#rejectionHandledHandler.bind(this);
}
install() {
this.#global.addEventListener('error', this.#onError);
this.#global.addEventListener(
'unhandledrejection',
this.#onUnhandledRejection
);
this.#global.addEventListener(
'rejectionhandled',
this.#onRejectionHandled
);
}
uninstall() {
this.#global.removeEventListener('error', this.#onError);
this.#global.removeEventListener(
'unhandledrejection',
this.#onUnhandledRejection
);
this.#global.removeEventListener(
'rejectionhandled',
this.#onRejectionHandled
);
}
#unhandledRejectionHandler(event) {
const jasmineMessage = 'Unhandled promise rejection: ' + event.reason;
let reason;
if (j$.isError_(event.reason)) {
reason = event.reason;
reason.jasmineMessage = jasmineMessage;
} else {
reason = jasmineMessage;
}
this.#dispatch.onUnhandledRejection(reason, event.promise, event);
}
#rejectionHandledHandler(event) {
this.#dispatch.onRejectionHandled(event.promise);
}
}
class NodeAdapter {
#global;
#dispatch;
#originalHandlers;
#jasmineHandlers;
constructor(global, dispatch) {
this.#global = global;
this.#dispatch = dispatch;
this.#jasmineHandlers = {};
this.#originalHandlers = {};
this.onError = this.onError.bind(this);
this.onUnhandledRejection = this.onUnhandledRejection.bind(this);
}
install() {
this.#installHandler('uncaughtException', this.onError);
this.#installHandler('unhandledRejection', this.onUnhandledRejection);
this.#installHandler(
'rejectionHandled',
this.#dispatch.onRejectionHandled
);
}
uninstall() {
const errorTypes = Object.keys(this.#originalHandlers);
for (const errorType of errorTypes) {
this.#global.process.removeListener(
errorType,
this.#jasmineHandlers[errorType]
);
for (let i = 0; i < this.#originalHandlers[errorType].length; i++) {
this.#global.process.on(
errorType,
this.#originalHandlers[errorType][i]
);
}
delete this.#originalHandlers[errorType];
delete this.#jasmineHandlers[errorType];
}
}
#installHandler(errorType, handler) {
this.#originalHandlers[errorType] = this.#global.process.listeners(
errorType
);
this.#jasmineHandlers[errorType] = handler;
this.#global.process.removeAllListeners(errorType);
this.#global.process.on(errorType, handler);
}
#augmentError(error, isUnhandledRejection) {
let jasmineMessagePrefix;
if (isUnhandledRejection) {
jasmineMessagePrefix = 'Unhandled promise rejection';
} else {
jasmineMessagePrefix = 'Uncaught exception';
}
if (j$.isError_(error)) {
error.jasmineMessage = jasmineMessagePrefix + ': ' + error;
return error;
} else {
let substituteMsg;
if (error) {
substituteMsg = jasmineMessagePrefix + ': ' + error;
} else {
substituteMsg = jasmineMessagePrefix + ' with no error or message';
}
if (isUnhandledRejection) {
substituteMsg +=
'\n' +
'(Tip: to get a useful stack trace, use ' +
'Promise.reject(n' +
'ew Error(...)) instead of Promise.reject(' +
(error ? '...' : '') +
').)';
}
return new Error(substituteMsg);
}
}
onError(error) {
error = this.#augmentError(error, false);
this.#dispatch.onUncaughtException(error);
}
onUnhandledRejection(reason, promise) {
reason = this.#augmentError(reason, true);
this.#dispatch.onUnhandledRejection(reason, promise);
}
}
return GlobalErrors;

View File

@@ -15,7 +15,7 @@ getJasmineRequireObj().MockDate = function(j$) {
if (mockDate instanceof GlobalDate) {
currentTime = mockDate.getTime();
} else {
if (!j$.util.isUndefined(mockDate)) {
if (mockDate !== undefined) {
throw new Error(
'The argument to jasmine.clock().mockDate(), if specified, ' +
'should be a Date instance.'

View File

@@ -16,7 +16,7 @@ getJasmineRequireObj().makePrettyPrinter = function(j$) {
if (customFormatResult) {
this.emitScalar(customFormatResult);
} else if (j$.util.isUndefined(value)) {
} else if (value === undefined) {
this.emitScalar('undefined');
} else if (value === null) {
this.emitScalar('null');

View File

@@ -37,6 +37,17 @@ getJasmineRequireObj().QueueRunner = function(j$) {
function QueueRunner(attrs) {
this.id_ = nextid++;
this.queueableFns = attrs.queueableFns || [];
for (const f of this.queueableFns) {
if (!f) {
throw new Error('Received a falsy queueableFn');
}
if (!f.fn) {
throw new Error('Received a queueableFn with no fn');
}
}
this.onComplete = attrs.onComplete || emptyFn;
this.clearStack =
attrs.clearStack ||

View File

@@ -1,7 +1,7 @@
getJasmineRequireObj().ReportDispatcher = function(j$) {
'use strict';
function ReportDispatcher(methods, queueRunnerFactory, onLateError) {
function ReportDispatcher(methods, runQueue, onLateError) {
const dispatchedMethods = methods || [];
for (const method of dispatchedMethods) {
@@ -39,7 +39,7 @@ getJasmineRequireObj().ReportDispatcher = function(j$) {
}
return new Promise(function(resolve) {
queueRunnerFactory({
runQueue({
queueableFns: fns,
onComplete: resolve,
isReporter: true,

View File

@@ -1,51 +1,66 @@
getJasmineRequireObj().Runner = function(j$) {
class Runner {
constructor(options) {
this.topSuite_ = options.topSuite;
// TODO use names that read like getters
this.totalSpecsDefined_ = options.totalSpecsDefined;
this.focusedRunables_ = options.focusedRunables;
this.runableResources_ = options.runableResources;
this.queueRunnerFactory_ = options.queueRunnerFactory;
this.reporter_ = options.reporter;
this.getConfig_ = options.getConfig;
this.reportSpecDone_ = options.reportSpecDone;
this.hasFailures = false;
this.executedBefore_ = false;
#topSuite;
#getTotalSpecsDefined;
#getFocusedRunables;
#runableResources;
#runQueue;
#TreeProcessor;
#executionTree;
#globalErrors;
#reportDispatcher;
#getConfig;
#executedBefore;
#currentRunableTracker;
this.currentlyExecutingSuites_ = [];
this.currentSpec = null;
constructor(options) {
this.#topSuite = options.topSuite;
this.#getTotalSpecsDefined = options.totalSpecsDefined;
this.#getFocusedRunables = options.focusedRunables;
this.#runableResources = options.runableResources;
this.#runQueue = options.runQueue;
this.#TreeProcessor = options.TreeProcessor;
this.#globalErrors = options.globalErrors;
this.#reportDispatcher = options.reportDispatcher;
this.#getConfig = options.getConfig;
this.#executedBefore = false;
this.#currentRunableTracker = new j$.CurrentRunableTracker();
}
currentSpec() {
return this.#currentRunableTracker.currentSpec();
}
setCurrentSpec(spec) {
this.#currentRunableTracker.setCurrentSpec(spec);
}
currentRunable() {
return this.currentSpec || this.currentSuite();
return this.#currentRunableTracker.currentRunable();
}
currentSuite() {
return this.currentlyExecutingSuites_[
this.currentlyExecutingSuites_.length - 1
];
return this.#currentRunableTracker.currentSuite();
}
parallelReset() {
this.executedBefore_ = false;
this.#executedBefore = false;
}
async execute(runablesToRun) {
if (this.executedBefore_) {
this.topSuite_.reset();
if (this.#executedBefore) {
this.#topSuite.reset();
}
this.executedBefore_ = true;
this.#executedBefore = true;
this.hasFailures = false;
const focusedRunables = this.focusedRunables_();
const config = this.getConfig_();
const focusedRunables = this.#getFocusedRunables();
const config = this.#getConfig();
if (!runablesToRun) {
if (focusedRunables.length) {
runablesToRun = focusedRunables;
} else {
runablesToRun = [this.topSuite_.id];
runablesToRun = [this.#topSuite.id];
}
}
@@ -54,52 +69,9 @@ getJasmineRequireObj().Runner = function(j$) {
seed: j$.isNumber_(config.seed) ? config.seed + '' : config.seed
});
const processor = new j$.TreeProcessor({
tree: this.topSuite_,
const treeProcessor = new this.#TreeProcessor({
tree: this.#topSuite,
runnableIds: runablesToRun,
queueRunnerFactory: options => {
if (options.isLeaf) {
// A spec
options.SkipPolicy = j$.CompleteOnFirstErrorSkipPolicy;
} else {
// A suite
if (config.stopOnSpecFailure) {
options.SkipPolicy = j$.CompleteOnFirstErrorSkipPolicy;
} else {
options.SkipPolicy = j$.SkipAfterBeforeAllErrorPolicy;
}
}
return this.queueRunnerFactory_(options);
},
failSpecWithNoExpectations: config.failSpecWithNoExpectations,
nodeStart: (suite, next) => {
this.currentlyExecutingSuites_.push(suite);
this.runableResources_.initForRunable(suite.id, suite.parentSuite.id);
this.reporter_.suiteStarted(suite.result).then(next);
suite.startTimer();
},
nodeComplete: (suite, result, next) => {
if (suite !== this.currentSuite()) {
throw new Error('Tried to complete the wrong suite');
}
this.runableResources_.clearForRunable(suite.id);
this.currentlyExecutingSuites_.pop();
if (result.status === 'failed') {
this.hasFailures = true;
}
suite.endTimer();
if (suite.hadBeforeAllFailure) {
this.reportChildrenOfBeforeAllFailure_(suite).then(() => {
this.reportSuiteDone_(suite, result, next);
});
} else {
this.reportSuiteDone_(suite, result, next);
}
},
orderChildren: function(node) {
return order.sort(node.children);
},
@@ -107,20 +79,15 @@ getJasmineRequireObj().Runner = function(j$) {
return !config.specFilter(spec);
}
});
this.#executionTree = treeProcessor.processTree();
if (!processor.processTree().valid) {
throw new Error(
'Invalid order: would cause a beforeAll or afterAll to be run multiple times'
);
}
return this.execute2_(runablesToRun, order, processor);
return this.#execute2(runablesToRun, order);
}
async execute2_(runablesToRun, order, processor) {
const totalSpecsDefined = this.totalSpecsDefined_();
async #execute2(runablesToRun, order) {
const totalSpecsDefined = this.#getTotalSpecsDefined();
this.runableResources_.initForRunable(this.topSuite_.id);
this.#runableResources.initForRunable(this.#topSuite.id);
const jasmineTimer = new j$.Timer();
jasmineTimer.start();
@@ -132,31 +99,39 @@ getJasmineRequireObj().Runner = function(j$) {
* @property {Boolean} parallel - Whether Jasmine is being run in parallel mode.
* @since 2.0.0
*/
await this.reporter_.jasmineStarted({
await this.#reportDispatcher.jasmineStarted({
// In parallel mode, the jasmineStarted event is separately dispatched
// by jasmine-npm. This event only reaches reporters in non-parallel.
totalSpecsDefined,
/**
* Information about the ordering (random or not) of this execution of the suite.
* @typedef Order
* @property {boolean} random - Whether the suite is running in random order
* @property {string} seed - The random seed
*/
order: order,
parallel: false
});
this.currentlyExecutingSuites_.push(this.topSuite_);
await processor.execute();
this.#currentRunableTracker.pushSuite(this.#topSuite);
const treeRunner = new j$.TreeRunner({
executionTree: this.#executionTree,
globalErrors: this.#globalErrors,
runableResources: this.#runableResources,
reportDispatcher: this.#reportDispatcher,
runQueue: this.#runQueue,
getConfig: this.#getConfig,
currentRunableTracker: this.#currentRunableTracker
});
const { hasFailures } = await treeRunner.execute();
if (this.topSuite_.hadBeforeAllFailure) {
await this.reportChildrenOfBeforeAllFailure_(this.topSuite_);
}
this.runableResources_.clearForRunable(this.topSuite_.id);
this.currentlyExecutingSuites_.pop();
this.#runableResources.clearForRunable(this.#topSuite.id);
this.#currentRunableTracker.popSuite();
let overallStatus, incompleteReason, incompleteCode;
if (
this.hasFailures ||
this.topSuite_.result.failedExpectations.length > 0
) {
if (hasFailures || this.#topSuite.result.failedExpectations.length > 0) {
overallStatus = 'failed';
} else if (this.focusedRunables_().length > 0) {
} else if (this.#getFocusedRunables().length > 0) {
overallStatus = 'incomplete';
incompleteReason = 'fit() or fdescribe() was found';
incompleteCode = 'focused';
@@ -187,53 +162,13 @@ getJasmineRequireObj().Runner = function(j$) {
incompleteReason: incompleteReason,
incompleteCode: incompleteCode,
order: order,
failedExpectations: this.topSuite_.result.failedExpectations,
deprecationWarnings: this.topSuite_.result.deprecationWarnings
failedExpectations: this.#topSuite.result.failedExpectations,
deprecationWarnings: this.#topSuite.result.deprecationWarnings
};
this.topSuite_.reportedDone = true;
await this.reporter_.jasmineDone(jasmineDoneInfo);
this.#topSuite.reportedDone = true;
await this.#reportDispatcher.jasmineDone(jasmineDoneInfo);
return jasmineDoneInfo;
}
reportSuiteDone_(suite, result, next) {
suite.reportedDone = true;
this.reporter_.suiteDone(result).then(next);
}
async reportChildrenOfBeforeAllFailure_(suite) {
for (const child of suite.children) {
if (child instanceof j$.Suite) {
await this.reporter_.suiteStarted(child.result);
await this.reportChildrenOfBeforeAllFailure_(child);
// Marking the suite passed is consistent with how suites that
// contain failed specs but no suite-level failures are reported.
child.result.status = 'passed';
await this.reporter_.suiteDone(child.result);
} else {
/* a spec */
await this.reporter_.specStarted(child.result);
child.addExpectationResult(
false,
{
passed: false,
message:
'Not run because a beforeAll function failed. The ' +
'beforeAll failure will be reported on the suite that ' +
'caused it.'
},
true
);
child.result.status = 'failed';
await new Promise(resolve => {
this.reportSpecDone_(child, child.result, resolve);
});
}
}
}
}
return Runner;

View File

@@ -1,320 +1,249 @@
getJasmineRequireObj().Spec = function(j$) {
function Spec(attrs) {
this.expectationFactory = attrs.expectationFactory;
this.asyncExpectationFactory = attrs.asyncExpectationFactory;
this.resultCallback = attrs.resultCallback || function() {};
this.id = attrs.id;
this.filename = attrs.filename;
this.parentSuiteId = attrs.parentSuiteId;
this.description = attrs.description || '';
this.queueableFn = attrs.queueableFn;
this.beforeAndAfterFns =
attrs.beforeAndAfterFns ||
function() {
return { befores: [], afters: [] };
};
this.userContext =
attrs.userContext ||
function() {
return {};
};
this.onStart = attrs.onStart || function() {};
this.autoCleanClosures =
attrs.autoCleanClosures === undefined ? true : !!attrs.autoCleanClosures;
class Spec {
#autoCleanClosures;
#throwOnExpectationFailure;
#timer;
#metadata;
this.getPath = function() {
return attrs.getPath ? attrs.getPath(this) : [];
};
this.onLateError = attrs.onLateError || function() {};
this.catchingExceptions =
attrs.catchingExceptions ||
function() {
return true;
constructor(attrs) {
this.expectationFactory = attrs.expectationFactory;
this.asyncExpectationFactory = attrs.asyncExpectationFactory;
this.id = attrs.id;
this.filename = attrs.filename;
this.parentSuiteId = attrs.parentSuiteId;
this.description = attrs.description || '';
this.queueableFn = attrs.queueableFn;
this.beforeAndAfterFns =
attrs.beforeAndAfterFns ||
function() {
return { befores: [], afters: [] };
};
this.userContext =
attrs.userContext ||
function() {
return {};
};
this.getPath = function() {
return attrs.getPath ? attrs.getPath(this) : [];
};
this.throwOnExpectationFailure = !!attrs.throwOnExpectationFailure;
this.timer = attrs.timer || new j$.Timer();
if (!this.queueableFn.fn) {
this.exclude();
this.#autoCleanClosures =
attrs.autoCleanClosures === undefined
? true
: !!attrs.autoCleanClosures;
this.onLateError = attrs.onLateError || function() {};
this.#throwOnExpectationFailure = !!attrs.throwOnExpectationFailure;
this.#timer = attrs.timer || new j$.Timer();
if (!this.queueableFn.fn) {
this.exclude();
}
this.reset();
}
this.reset();
}
addExpectationResult(passed, data, isError) {
const expectationResult = j$.buildExpectationResult(data);
Spec.prototype.addExpectationResult = function(passed, data, isError) {
const expectationResult = j$.buildExpectationResult(data);
if (passed) {
this.result.passedExpectations.push(expectationResult);
} else {
if (this.reportedDone) {
this.onLateError(expectationResult);
if (passed) {
this.result.passedExpectations.push(expectationResult);
} else {
this.result.failedExpectations.push(expectationResult);
// TODO: refactor so that we don't need to override cached status
if (this.result.status) {
this.result.status = 'failed';
}
}
if (this.throwOnExpectationFailure && !isError) {
throw new j$.errors.ExpectationFailed();
}
}
};
Spec.prototype.setSpecProperty = function(key, value) {
this.result.properties = this.result.properties || {};
this.result.properties[key] = value;
};
Spec.prototype.execute = function(
queueRunnerFactory,
onComplete,
excluded,
failSpecWithNoExp
) {
const onStart = {
fn: done => {
this.timer.start();
this.onStart(this, done);
}
};
const complete = {
fn: done => {
if (this.autoCleanClosures) {
this.queueableFn.fn = null;
}
this.result.status = this.status(excluded, failSpecWithNoExp);
this.result.duration = this.timer.elapsed();
if (this.result.status !== 'failed') {
this.result.debugLogs = null;
}
this.resultCallback(this.result, done);
},
type: 'specCleanup'
};
const fns = this.beforeAndAfterFns();
const runnerConfig = {
isLeaf: true,
queueableFns: [...fns.befores, this.queueableFn, ...fns.afters],
onException: e => this.handleException(e),
onMultipleDone: () => {
// Issue a deprecation. Include the context ourselves and pass
// ignoreRunnable: true, since getting here always means that we've already
// moved on and the current runnable isn't the one that caused the problem.
this.onLateError(
new Error(
'An asynchronous spec, beforeEach, or afterEach function called its ' +
"'done' callback more than once.\n(in spec: " +
this.getFullName() +
')'
)
);
},
onComplete: () => {
if (this.result.status === 'failed') {
onComplete(new j$.StopExecutionError('spec failed'));
if (this.reportedDone) {
this.onLateError(expectationResult);
} else {
onComplete();
}
},
userContext: this.userContext(),
runnableName: this.getFullName.bind(this)
};
this.result.failedExpectations.push(expectationResult);
if (this.markedPending || excluded === true) {
runnerConfig.queueableFns = [];
// TODO: refactor so that we don't need to override cached status
if (this.result.status) {
this.result.status = 'failed';
}
}
if (this.#throwOnExpectationFailure && !isError) {
throw new j$.errors.ExpectationFailed();
}
}
}
runnerConfig.queueableFns.unshift(onStart);
runnerConfig.queueableFns.push(complete);
getSpecProperty(key) {
this.result.properties = this.result.properties || {};
return this.result.properties[key];
}
queueRunnerFactory(runnerConfig);
};
setSpecProperty(key, value) {
this.result.properties = this.result.properties || {};
this.result.properties[key] = value;
}
executionStarted() {
this.#timer.start();
}
executionFinished(excluded, failSpecWithNoExp) {
if (this.#autoCleanClosures) {
this.queueableFn.fn = null;
}
this.result.status = this.#status(excluded, failSpecWithNoExp);
this.result.duration = this.#timer.elapsed();
if (this.result.status !== 'failed') {
this.result.debugLogs = null;
}
}
reset() {
/**
* @typedef SpecResult
* @property {String} id - The unique id of this spec.
* @property {String} description - The description passed to the {@link it} that created this spec.
* @property {String} fullName - The full description including all ancestors of this spec.
* @property {String|null} parentSuiteId - The ID of the suite containing this spec, or null if this spec is not in a describe().
* @property {String} filename - Deprecated. The name of the file the spec was defined in.
* Note: The value may be incorrect if zone.js is installed or
* `it`/`fit`/`xit` have been replaced with versions that don't maintain the
* same call stack height as the originals. This property may be removed in
* a future version unless there is enough user interest in keeping it.
* See {@link https://github.com/jasmine/jasmine/issues/2065}.
* @property {ExpectationResult[]} failedExpectations - The list of expectations that failed during execution of this spec.
* @property {ExpectationResult[]} passedExpectations - The list of expectations that passed during execution of this spec.
* @property {ExpectationResult[]} deprecationWarnings - The list of deprecation warnings that occurred during execution this spec.
* @property {String} pendingReason - If the spec is {@link pending}, this will be the reason.
* @property {String} status - Once the spec has completed, this string represents the pass/fail status of this spec.
* @property {number} duration - The time in ms used by the spec execution, including any before/afterEach.
* @property {Object} properties - User-supplied properties, if any, that were set using {@link Env#setSpecProperty}
* @property {DebugLogEntry[]|null} debugLogs - Messages, if any, that were logged using {@link jasmine.debugLog} during a failing spec.
* @since 2.0.0
*/
this.result = {
id: this.id,
description: this.description,
fullName: this.getFullName(),
parentSuiteId: this.parentSuiteId,
filename: this.filename,
failedExpectations: [],
passedExpectations: [],
deprecationWarnings: [],
pendingReason: this.excludeMessage || '',
duration: null,
properties: null,
debugLogs: null
};
this.markedPending = this.markedExcluding;
this.reportedDone = false;
}
handleException(e) {
if (Spec.isPendingSpecException(e)) {
this.pend(extractCustomPendingMessage(e));
return;
}
if (e instanceof j$.errors.ExpectationFailed) {
return;
}
this.addExpectationResult(
false,
{
matcherName: '',
passed: false,
expected: '',
actual: '',
error: e
},
true
);
}
pend(message) {
this.markedPending = true;
if (message) {
this.result.pendingReason = message;
}
}
// Like pend(), but pending state will survive reset().
// Useful for fit, xit, where pending state remains.
exclude(message) {
this.markedExcluding = true;
if (this.message) {
this.excludeMessage = message;
}
this.pend(message);
}
// TODO: ensure that all access to result goes through .getResult()
// so that the status is correct.
getResult() {
this.result.status = this.#status();
return this.result;
}
#status(excluded, failSpecWithNoExpectations) {
if (excluded === true) {
return 'excluded';
}
if (this.markedPending) {
return 'pending';
}
if (
this.result.failedExpectations.length > 0 ||
(failSpecWithNoExpectations &&
this.result.failedExpectations.length +
this.result.passedExpectations.length ===
0)
) {
return 'failed';
}
return 'passed';
}
getFullName() {
return this.getPath().join(' ');
}
addDeprecationWarning(deprecation) {
if (typeof deprecation === 'string') {
deprecation = { message: deprecation };
}
this.result.deprecationWarnings.push(
j$.buildExpectationResult(deprecation)
);
}
debugLog(msg) {
if (!this.result.debugLogs) {
this.result.debugLogs = [];
}
/**
* @typedef DebugLogEntry
* @property {String} message - The message that was passed to {@link jasmine.debugLog}.
* @property {number} timestamp - The time when the entry was added, in
* milliseconds from the spec's start time
*/
this.result.debugLogs.push({
message: msg,
timestamp: this.#timer.elapsed()
});
}
Spec.prototype.reset = function() {
/**
* @typedef SpecResult
* @property {String} id - The unique id of this spec.
* @property {String} description - The description passed to the {@link it} that created this spec.
* @property {String} fullName - The full description including all ancestors of this spec.
* @property {String|null} parentSuiteId - The ID of the suite containing this spec, or null if this spec is not in a describe().
* @property {String} filename - The name of the file the spec was defined in.
* Note: The value may be incorrect if zone.js is installed or
* `it`/`fit`/`xit` have been replaced with versions that don't maintain the
* same call stack height as the originals.
* @property {ExpectationResult[]} failedExpectations - The list of expectations that failed during execution of this spec.
* @property {ExpectationResult[]} passedExpectations - The list of expectations that passed during execution of this spec.
* @property {ExpectationResult[]} deprecationWarnings - The list of deprecation warnings that occurred during execution this spec.
* @property {String} pendingReason - If the spec is {@link pending}, this will be the reason.
* @property {String} status - Once the spec has completed, this string represents the pass/fail status of this spec.
* @property {number} duration - The time in ms used by the spec execution, including any before/afterEach.
* @property {Object} properties - User-supplied properties, if any, that were set using {@link Env#setSpecProperty}
* @property {DebugLogEntry[]|null} debugLogs - Messages, if any, that were logged using {@link jasmine.debugLog} during a failing spec.
* @interface Spec
* @see Configuration#specFilter
* @since 2.0.0
*/
this.result = {
id: this.id,
description: this.description,
fullName: this.getFullName(),
parentSuiteId: this.parentSuiteId,
filename: this.filename,
failedExpectations: [],
passedExpectations: [],
deprecationWarnings: [],
pendingReason: this.excludeMessage || '',
duration: null,
properties: null,
debugLogs: null
};
this.markedPending = this.markedExcluding;
this.reportedDone = false;
};
Spec.prototype.handleException = function handleException(e) {
if (Spec.isPendingSpecException(e)) {
this.pend(extractCustomPendingMessage(e));
return;
}
if (e instanceof j$.errors.ExpectationFailed) {
return;
}
this.addExpectationResult(
false,
{
matcherName: '',
passed: false,
expected: '',
actual: '',
error: e
},
true
);
};
/*
* Marks state as pending
* @param {string} [message] An optional reason message
*/
Spec.prototype.pend = function(message) {
this.markedPending = true;
if (message) {
this.result.pendingReason = message;
}
};
/*
* Like {@link Spec#pend}, but pending state will survive {@link Spec#reset}
* Useful for fit, xit, where pending state remains.
* @param {string} [message] An optional reason message
*/
Spec.prototype.exclude = function(message) {
this.markedExcluding = true;
if (this.message) {
this.excludeMessage = message;
}
this.pend(message);
};
Spec.prototype.getResult = function() {
this.result.status = this.status();
return this.result;
};
Spec.prototype.status = function(excluded, failSpecWithNoExpectations) {
if (excluded === true) {
return 'excluded';
}
if (this.markedPending) {
return 'pending';
}
if (
this.result.failedExpectations.length > 0 ||
(failSpecWithNoExpectations &&
this.result.failedExpectations.length +
this.result.passedExpectations.length ===
0)
) {
return 'failed';
}
return 'passed';
};
Spec.prototype.getFullName = function() {
return this.getPath().join(' ');
};
Spec.prototype.addDeprecationWarning = function(deprecation) {
if (typeof deprecation === 'string') {
deprecation = { message: deprecation };
}
this.result.deprecationWarnings.push(
j$.buildExpectationResult(deprecation)
);
};
Spec.prototype.debugLog = function(msg) {
if (!this.result.debugLogs) {
this.result.debugLogs = [];
}
/**
* @typedef DebugLogEntry
* @property {String} message - The message that was passed to {@link jasmine.debugLog}.
* @property {number} timestamp - The time when the entry was added, in
* milliseconds from the spec's start time
*/
this.result.debugLogs.push({
message: msg,
timestamp: this.timer.elapsed()
});
};
const extractCustomPendingMessage = function(e) {
const fullMessage = e.toString(),
boilerplateStart = fullMessage.indexOf(Spec.pendingSpecExceptionMessage),
boilerplateEnd =
boilerplateStart + Spec.pendingSpecExceptionMessage.length;
return fullMessage.slice(boilerplateEnd);
};
Spec.pendingSpecExceptionMessage = '=> marked Pending';
Spec.isPendingSpecException = function(e) {
return !!(
e &&
e.toString &&
e.toString().indexOf(Spec.pendingSpecExceptionMessage) !== -1
);
};
/**
* @interface Spec
* @see Configuration#specFilter
* @since 2.0.0
*/
Object.defineProperty(Spec.prototype, 'metadata', {
// NOTE: Although most of jasmine-core only exposes these metadata objects,
// actual Spec instances are still passed to Configuration#specFilter. Until
// that is fixed, it's important to make sure that all metadata properties
// also exist in compatible form on the underlying Spec.
get: function() {
if (!this.metadata_) {
this.metadata_ = {
get metadata() {
// NOTE: Although most of jasmine-core only exposes these metadata objects,
// actual Spec instances are still passed to Configuration#specFilter. Until
// that is fixed, it's important to make sure that all metadata properties
// also exist in compatible form on the underlying Spec.
if (!this.#metadata) {
this.#metadata = {
/**
* The unique ID of this spec.
* @name Spec#id
@@ -353,9 +282,28 @@ getJasmineRequireObj().Spec = function(j$) {
};
}
return this.metadata_;
return this.#metadata;
}
});
}
const extractCustomPendingMessage = function(e) {
const fullMessage = e.toString(),
boilerplateStart = fullMessage.indexOf(Spec.pendingSpecExceptionMessage),
boilerplateEnd =
boilerplateStart + Spec.pendingSpecExceptionMessage.length;
return fullMessage.slice(boilerplateEnd);
};
Spec.pendingSpecExceptionMessage = '=> marked Pending';
Spec.isPendingSpecException = function(e) {
return !!(
e &&
e.toString &&
e.toString().indexOf(Spec.pendingSpecExceptionMessage) !== -1
);
};
return Spec;
};

View File

@@ -54,7 +54,9 @@ getJasmineRequireObj().SpyFactory = function(j$) {
}
if (methods.length === 0 && properties.length === 0) {
throw 'createSpyObj requires a non-empty array or object of method names to create spies for';
throw new Error(
'createSpyObj requires a non-empty array or object of method names to create spies for'
);
}
return obj;

View File

@@ -25,7 +25,7 @@ getJasmineRequireObj().SpyRegistry = function(j$) {
this.spyOn = function(obj, methodName) {
const getErrorMsg = spyOnMsg;
if (j$.util.isUndefined(obj) || obj === null) {
if (obj === undefined || obj === null) {
throw new Error(
getErrorMsg(
'could not find an object to spy upon for ' + methodName + '()'
@@ -33,11 +33,11 @@ getJasmineRequireObj().SpyRegistry = function(j$) {
);
}
if (j$.util.isUndefined(methodName) || methodName === null) {
if (methodName === undefined || methodName === null) {
throw new Error(getErrorMsg('No method name supplied'));
}
if (j$.util.isUndefined(obj[methodName])) {
if (obj[methodName] === undefined) {
throw new Error(getErrorMsg(methodName + '() method does not exist'));
}
@@ -102,7 +102,7 @@ getJasmineRequireObj().SpyRegistry = function(j$) {
accessType = accessType || 'get';
if (j$.util.isUndefined(obj)) {
if (!obj) {
throw new Error(
getErrorMsg(
'spyOn could not find an object to spy upon for ' +
@@ -112,7 +112,7 @@ getJasmineRequireObj().SpyRegistry = function(j$) {
);
}
if (j$.util.isUndefined(propertyName)) {
if (propertyName === undefined) {
throw new Error(getErrorMsg('No property name supplied'));
}
@@ -177,7 +177,7 @@ getJasmineRequireObj().SpyRegistry = function(j$) {
};
this.spyOnAllFunctions = function(obj, includeNonEnumerable) {
if (j$.util.isUndefined(obj)) {
if (!obj) {
throw new Error(
'spyOnAllFunctions could not find an object to spy upon'
);

View File

@@ -1,86 +1,276 @@
getJasmineRequireObj().Suite = function(j$) {
function Suite(attrs) {
this.env = attrs.env;
this.id = attrs.id;
this.parentSuite = attrs.parentSuite;
this.description = attrs.description;
this.reportedParentSuiteId = attrs.reportedParentSuiteId;
this.filename = attrs.filename;
this.expectationFactory = attrs.expectationFactory;
this.asyncExpectationFactory = attrs.asyncExpectationFactory;
this.throwOnExpectationFailure = !!attrs.throwOnExpectationFailure;
this.autoCleanClosures =
attrs.autoCleanClosures === undefined ? true : !!attrs.autoCleanClosures;
this.onLateError = attrs.onLateError || function() {};
class Suite {
#reportedParentSuiteId;
#throwOnExpectationFailure;
#autoCleanClosures;
#timer;
this.beforeFns = [];
this.afterFns = [];
this.beforeAllFns = [];
this.afterAllFns = [];
this.timer = attrs.timer || new j$.Timer();
this.children = [];
constructor(attrs) {
this.id = attrs.id;
this.parentSuite = attrs.parentSuite;
this.description = attrs.description;
this.filename = attrs.filename;
this.expectationFactory = attrs.expectationFactory;
this.asyncExpectationFactory = attrs.asyncExpectationFactory;
this.onLateError = attrs.onLateError || function() {};
this.#reportedParentSuiteId = attrs.reportedParentSuiteId;
this.#throwOnExpectationFailure = !!attrs.throwOnExpectationFailure;
this.#autoCleanClosures =
attrs.autoCleanClosures === undefined
? true
: !!attrs.autoCleanClosures;
this.#timer = attrs.timer || new j$.Timer();
this.reset();
}
this.beforeFns = [];
this.afterFns = [];
this.beforeAllFns = [];
this.afterAllFns = [];
this.children = [];
Suite.prototype.setSuiteProperty = function(key, value) {
this.result.properties = this.result.properties || {};
this.result.properties[key] = value;
};
this.reset();
}
Suite.prototype.getFullName = function() {
const fullName = [];
for (
let parentSuite = this;
parentSuite;
parentSuite = parentSuite.parentSuite
) {
if (parentSuite.parentSuite) {
fullName.unshift(parentSuite.description);
setSuiteProperty(key, value) {
this.result.properties = this.result.properties || {};
this.result.properties[key] = value;
}
getFullName() {
const fullName = [];
for (
let parentSuite = this;
parentSuite;
parentSuite = parentSuite.parentSuite
) {
if (parentSuite.parentSuite) {
fullName.unshift(parentSuite.description);
}
}
return fullName.join(' ');
}
// Mark the suite with "pending" status
pend() {
this.markedPending = true;
}
// Like pend(), but pending state will survive reset().
// Useful for fdescribe, xdescribe, where pending state should remain.
exclude() {
this.pend();
this.markedExcluding = true;
}
beforeEach(fn) {
this.beforeFns.unshift({ ...fn, suite: this });
}
beforeAll(fn) {
this.beforeAllFns.push({ ...fn, type: 'beforeAll', suite: this });
}
afterEach(fn) {
this.afterFns.unshift({ ...fn, suite: this, type: 'afterEach' });
}
afterAll(fn) {
this.afterAllFns.unshift({ ...fn, type: 'afterAll' });
}
startTimer() {
this.#timer.start();
}
endTimer() {
this.result.duration = this.#timer.elapsed();
}
cleanupBeforeAfter() {
if (this.#autoCleanClosures) {
removeFns(this.beforeAllFns);
removeFns(this.afterAllFns);
removeFns(this.beforeFns);
removeFns(this.afterFns);
}
}
return fullName.join(' ');
};
/*
* Mark the suite with "pending" status
*/
Suite.prototype.pend = function() {
this.markedPending = true;
};
reset() {
/**
* @typedef SuiteResult
* @property {String} id - The unique id of this suite.
* @property {String} description - The description text passed to the {@link describe} that made this suite.
* @property {String} fullName - The full description including all ancestors of this suite.
* @property {String|null} parentSuiteId - The ID of the suite containing this suite, or null if this is not in another describe().
* @property {String} filename - Deprecated. The name of the file the suite was defined in.
* Note: The value may be incorrect if zone.js is installed or
* `describe`/`fdescribe`/`xdescribe` have been replaced with versions that
* don't maintain the same call stack height as the originals. This property
* may be removed in a future version unless there is enough user interest
* in keeping it. See {@link https://github.com/jasmine/jasmine/issues/2065}.
* @property {ExpectationResult[]} failedExpectations - The list of expectations that failed in an {@link afterAll} for this suite.
* @property {ExpectationResult[]} deprecationWarnings - The list of deprecation warnings that occurred on this suite.
* @property {String} status - Once the suite has completed, this string represents the pass/fail status of this suite.
* @property {number} duration - The time in ms for Suite execution, including any before/afterAll, before/afterEach.
* @property {Object} properties - User-supplied properties, if any, that were set using {@link Env#setSuiteProperty}
* @since 2.0.0
*/
this.result = {
id: this.id,
description: this.description,
fullName: this.getFullName(),
parentSuiteId: this.#reportedParentSuiteId,
filename: this.filename,
failedExpectations: [],
deprecationWarnings: [],
duration: null,
properties: null
};
this.markedPending = this.markedExcluding;
this.children.forEach(function(child) {
child.reset();
});
this.reportedDone = false;
}
/*
* Like {@link Suite#pend}, but pending state will survive {@link Spec#reset}
* Useful for fdescribe, xdescribe, where pending state should remain.
*/
Suite.prototype.exclude = function() {
this.pend();
this.markedExcluding = true;
};
removeChildren() {
this.children = [];
}
Suite.prototype.beforeEach = function(fn) {
this.beforeFns.unshift({ ...fn, suite: this });
};
addChild(child) {
this.children.push(child);
}
Suite.prototype.beforeAll = function(fn) {
this.beforeAllFns.push({ ...fn, type: 'beforeAll', suite: this });
};
#status() {
if (this.markedPending) {
return 'pending';
}
Suite.prototype.afterEach = function(fn) {
this.afterFns.unshift({ ...fn, suite: this, type: 'afterEach' });
};
if (this.result.failedExpectations.length > 0) {
return 'failed';
} else {
return 'passed';
}
}
Suite.prototype.afterAll = function(fn) {
this.afterAllFns.unshift({ ...fn, type: 'afterAll' });
};
getResult() {
this.result.status = this.#status();
return this.result;
}
Suite.prototype.startTimer = function() {
this.timer.start();
};
canBeReentered() {
return this.beforeAllFns.length === 0 && this.afterAllFns.length === 0;
}
Suite.prototype.endTimer = function() {
this.result.duration = this.timer.elapsed();
};
sharedUserContext() {
if (!this.sharedContext) {
this.sharedContext = this.parentSuite
? this.parentSuite.clonedSharedUserContext()
: new j$.UserContext();
}
return this.sharedContext;
}
clonedSharedUserContext() {
return j$.UserContext.fromExisting(this.sharedUserContext());
}
handleException() {
if (arguments[0] instanceof j$.errors.ExpectationFailed) {
return;
}
const data = {
matcherName: '',
passed: false,
expected: '',
actual: '',
error: arguments[0]
};
const failedExpectation = j$.buildExpectationResult(data);
if (!this.parentSuite) {
failedExpectation.globalErrorType = 'afterAll';
}
if (this.reportedDone) {
this.onLateError(failedExpectation);
} else {
this.result.failedExpectations.push(failedExpectation);
}
}
onMultipleDone() {
let msg;
// Issue an error. Include the context ourselves and pass
// ignoreRunnable: true, since getting here always means that we've already
// moved on and the current runnable isn't the one that caused the problem.
if (this.parentSuite) {
msg =
"An asynchronous beforeAll or afterAll function called its 'done' " +
'callback more than once.\n' +
'(in suite: ' +
this.getFullName() +
')';
} else {
msg =
'A top-level beforeAll or afterAll function called its ' +
"'done' callback more than once.";
}
this.onLateError(new Error(msg));
}
addExpectationResult() {
if (isFailure(arguments)) {
const data = arguments[1];
const expectationResult = j$.buildExpectationResult(data);
if (this.reportedDone) {
this.onLateError(expectationResult);
} else {
this.result.failedExpectations.push(expectationResult);
// TODO: refactor so that we don't need to override cached status
if (this.result.status) {
this.result.status = 'failed';
}
}
if (this.#throwOnExpectationFailure) {
throw new j$.errors.ExpectationFailed();
}
}
}
addDeprecationWarning(deprecation) {
if (typeof deprecation === 'string') {
deprecation = { message: deprecation };
}
this.result.deprecationWarnings.push(
j$.buildExpectationResult(deprecation)
);
}
hasChildWithDescription(description) {
for (const child of this.children) {
if (child.description === description) {
return true;
}
}
return false;
}
get metadata() {
if (!this.metadata_) {
this.metadata_ = new SuiteMetadata(this);
}
return this.metadata_;
}
}
function removeFns(queueableFns) {
for (const qf of queueableFns) {
@@ -88,248 +278,64 @@ getJasmineRequireObj().Suite = function(j$) {
}
}
Suite.prototype.cleanupBeforeAfter = function() {
if (this.autoCleanClosures) {
removeFns(this.beforeAllFns);
removeFns(this.afterAllFns);
removeFns(this.beforeFns);
removeFns(this.afterFns);
}
};
Suite.prototype.reset = function() {
/**
* @typedef SuiteResult
* @property {String} id - The unique id of this suite.
* @property {String} description - The description text passed to the {@link describe} that made this suite.
* @property {String} fullName - The full description including all ancestors of this suite.
* @property {String|null} parentSuiteId - The ID of the suite containing this suite, or null if this is not in another describe().
* @property {String} filename - The name of the file the suite was defined in.
* Note: The value may be incorrect if zone.js is installed or
* `describe`/`fdescribe`/`xdescribe` have been replaced with versions that
* don't maintain the same call stack height as the originals.
* @property {ExpectationResult[]} failedExpectations - The list of expectations that failed in an {@link afterAll} for this suite.
* @property {ExpectationResult[]} deprecationWarnings - The list of deprecation warnings that occurred on this suite.
* @property {String} status - Once the suite has completed, this string represents the pass/fail status of this suite.
* @property {number} duration - The time in ms for Suite execution, including any before/afterAll, before/afterEach.
* @property {Object} properties - User-supplied properties, if any, that were set using {@link Env#setSuiteProperty}
* @since 2.0.0
*/
this.result = {
id: this.id,
description: this.description,
fullName: this.getFullName(),
parentSuiteId: this.reportedParentSuiteId,
filename: this.filename,
failedExpectations: [],
deprecationWarnings: [],
duration: null,
properties: null
};
this.markedPending = this.markedExcluding;
this.children.forEach(function(child) {
child.reset();
});
this.reportedDone = false;
};
Suite.prototype.removeChildren = function() {
this.children = [];
};
Suite.prototype.addChild = function(child) {
this.children.push(child);
};
Suite.prototype.status = function() {
if (this.markedPending) {
return 'pending';
}
if (this.result.failedExpectations.length > 0) {
return 'failed';
} else {
return 'passed';
}
};
Suite.prototype.canBeReentered = function() {
return this.beforeAllFns.length === 0 && this.afterAllFns.length === 0;
};
Suite.prototype.getResult = function() {
this.result.status = this.status();
return this.result;
};
Suite.prototype.sharedUserContext = function() {
if (!this.sharedContext) {
this.sharedContext = this.parentSuite
? this.parentSuite.clonedSharedUserContext()
: new j$.UserContext();
}
return this.sharedContext;
};
Suite.prototype.clonedSharedUserContext = function() {
return j$.UserContext.fromExisting(this.sharedUserContext());
};
Suite.prototype.handleException = function() {
if (arguments[0] instanceof j$.errors.ExpectationFailed) {
return;
}
const data = {
matcherName: '',
passed: false,
expected: '',
actual: '',
error: arguments[0]
};
const failedExpectation = j$.buildExpectationResult(data);
if (!this.parentSuite) {
failedExpectation.globalErrorType = 'afterAll';
}
if (this.reportedDone) {
this.onLateError(failedExpectation);
} else {
this.result.failedExpectations.push(failedExpectation);
}
};
Suite.prototype.onMultipleDone = function() {
let msg;
// Issue a deprecation. Include the context ourselves and pass
// ignoreRunnable: true, since getting here always means that we've already
// moved on and the current runnable isn't the one that caused the problem.
if (this.parentSuite) {
msg =
"An asynchronous beforeAll or afterAll function called its 'done' " +
'callback more than once.\n' +
'(in suite: ' +
this.getFullName() +
')';
} else {
msg =
'A top-level beforeAll or afterAll function called its ' +
"'done' callback more than once.";
}
this.onLateError(new Error(msg));
};
Suite.prototype.addExpectationResult = function() {
if (isFailure(arguments)) {
const data = arguments[1];
const expectationResult = j$.buildExpectationResult(data);
if (this.reportedDone) {
this.onLateError(expectationResult);
} else {
this.result.failedExpectations.push(expectationResult);
// TODO: refactor so that we don't need to override cached status
if (this.result.status) {
this.result.status = 'failed';
}
}
if (this.throwOnExpectationFailure) {
throw new j$.errors.ExpectationFailed();
}
}
};
Suite.prototype.addDeprecationWarning = function(deprecation) {
if (typeof deprecation === 'string') {
deprecation = { message: deprecation };
}
this.result.deprecationWarnings.push(
j$.buildExpectationResult(deprecation)
);
};
Suite.prototype.hasChildWithDescription = function(description) {
for (const child of this.children) {
if (child.description === description) {
return true;
}
}
return false;
};
Object.defineProperty(Suite.prototype, 'metadata', {
get: function() {
if (!this.metadata_) {
this.metadata_ = new SuiteMetadata(this);
}
return this.metadata_;
}
});
/**
* @interface Suite
* @see Env#topSuite
* @since 2.0.0
*/
function SuiteMetadata(suite) {
this.suite_ = suite;
/**
* The unique ID of this suite.
* @name Suite#id
* @readonly
* @type {string}
* @since 2.0.0
*/
this.id = suite.id;
class SuiteMetadata {
#suite;
/**
* The parent of this suite, or null if this is the top suite.
* @name Suite#parentSuite
* @readonly
* @type {Suite}
*/
this.parentSuite = suite.parentSuite ? suite.parentSuite.metadata : null;
constructor(suite) {
this.#suite = suite;
/**
* The unique ID of this suite.
* @name Suite#id
* @readonly
* @type {string}
* @since 2.0.0
*/
this.id = suite.id;
/**
* The description passed to the {@link describe} that created this suite.
* @name Suite#description
* @readonly
* @type {string}
* @since 2.0.0
*/
this.description = suite.description;
}
/**
* The parent of this suite, or null if this is the top suite.
* @name Suite#parentSuite
* @readonly
* @type {Suite}
*/
this.parentSuite = suite.parentSuite ? suite.parentSuite.metadata : null;
/**
* The full description including all ancestors of this suite.
* @name Suite#getFullName
* @function
* @returns {string}
* @since 2.0.0
*/
SuiteMetadata.prototype.getFullName = function() {
return this.suite_.getFullName();
};
/**
* The suite's children.
* @name Suite#children
* @type {Array.<(Spec|Suite)>}
* @since 2.0.0
*/
Object.defineProperty(SuiteMetadata.prototype, 'children', {
get: function() {
return this.suite_.children.map(child => child.metadata);
/**
* The description passed to the {@link describe} that created this suite.
* @name Suite#description
* @readonly
* @type {string}
* @since 2.0.0
*/
this.description = suite.description;
}
});
/**
* The full description including all ancestors of this suite.
* @name Suite#getFullName
* @function
* @returns {string}
* @since 2.0.0
*/
getFullName() {
return this.#suite.getFullName();
}
/**
* The suite's children.
* @name Suite#children
* @type {Array.<(Spec|Suite)>}
* @since 2.0.0
*/
get children() {
return this.#suite.children.map(child => child.metadata);
}
}
function isFailure(args) {
return !args[0];

View File

@@ -10,8 +10,6 @@ getJasmineRequireObj().SuiteBuilder = function(j$) {
return options.asyncExpectationFactory(actual, suite, 'Spec');
};
this.onLateError_ = options.onLateError;
this.specResultCallback_ = options.specResultCallback;
this.specStarted_ = options.specStarted;
this.nextSuiteId_ = 0;
this.nextSpecId_ = 0;
@@ -247,11 +245,7 @@ getJasmineRequireObj().SuiteBuilder = function(j$) {
expectationFactory: this.expectationFactory_,
asyncExpectationFactory: this.specAsyncExpectationFactory_,
onLateError: this.onLateError_,
resultCallback: (result, next) => {
this.specResultCallback_(spec, result, next);
},
getPath: spec => this.getSpecPath_(spec, suite),
onStart: (spec, next) => this.specStarted_(spec, suite, next),
description: description,
userContext: function() {
return suite.clonedSharedUserContext();

View File

@@ -1,76 +1,59 @@
getJasmineRequireObj().TreeProcessor = function() {
function TreeProcessor(attrs) {
const tree = attrs.tree;
const runnableIds = attrs.runnableIds;
const queueRunnerFactory = attrs.queueRunnerFactory;
const nodeStart = attrs.nodeStart || function() {};
const nodeComplete = attrs.nodeComplete || function() {};
const failSpecWithNoExpectations = !!attrs.failSpecWithNoExpectations;
const orderChildren =
attrs.orderChildren ||
function(node) {
return node.children;
};
const excludeNode =
attrs.excludeNode ||
function(node) {
return false;
};
let stats = { valid: true };
let processed = false;
const defaultMin = Infinity;
const defaultMax = 1 - Infinity;
getJasmineRequireObj().TreeProcessor = function(j$) {
const defaultMin = Infinity;
const defaultMax = 1 - Infinity;
this.processTree = function() {
processNode(tree, true);
processed = true;
return stats;
};
// Transforms the suite tree into an execution tree, which represents the set
// of specs and (possibly interleaved) suites to be run in the order they are
// to be run in.
class TreeProcessor {
#tree;
#runnableIds;
#orderChildren;
#excludeNode;
#stats;
this.execute = async function() {
if (!processed) {
this.processTree();
}
constructor(attrs) {
this.#tree = attrs.tree;
this.#runnableIds = attrs.runnableIds;
if (!stats.valid) {
throw 'invalid order';
}
this.#orderChildren =
attrs.orderChildren ||
function(node) {
return node.children;
};
this.#excludeNode =
attrs.excludeNode ||
function(node) {
return false;
};
}
const childFns = wrapChildren(tree, 0);
processTree() {
this.#stats = {};
this.#processNode(this.#tree, true);
const result = new ExecutionTree(this.#tree, this.#stats);
this.#stats = null;
return result;
}
await new Promise(function(resolve) {
queueRunnerFactory({
queueableFns: childFns,
userContext: tree.sharedUserContext(),
onException: function() {
tree.handleException.apply(tree, arguments);
},
onComplete: resolve,
onMultipleDone: tree.onMultipleDone
? tree.onMultipleDone.bind(tree)
: null
});
});
};
function runnableIndex(id) {
for (let i = 0; i < runnableIds.length; i++) {
if (runnableIds[i] === id) {
#runnableIndex(id) {
for (let i = 0; i < this.#runnableIds.length; i++) {
if (this.#runnableIds[i] === id) {
return i;
}
}
}
function processNode(node, parentExcluded) {
const executableIndex = runnableIndex(node.id);
#processNode(node, parentExcluded) {
const executableIndex = this.#runnableIndex(node.id);
if (executableIndex !== undefined) {
parentExcluded = false;
}
if (!node.children) {
const excluded = parentExcluded || excludeNode(node);
stats[node.id] = {
const excluded = parentExcluded || this.#excludeNode(node);
this.#stats[node.id] = {
excluded: excluded,
willExecute: !excluded && !node.markedPending,
segments: [
@@ -86,178 +69,147 @@ getJasmineRequireObj().TreeProcessor = function() {
} else {
let hasExecutableChild = false;
const orderedChildren = orderChildren(node);
const orderedChildren = this.#orderChildren(node);
for (let i = 0; i < orderedChildren.length; i++) {
const child = orderedChildren[i];
processNode(child, parentExcluded);
if (!stats.valid) {
return;
}
const childStats = stats[child.id];
this.#processNode(child, parentExcluded);
const childStats = this.#stats[child.id];
hasExecutableChild = hasExecutableChild || childStats.willExecute;
}
stats[node.id] = {
this.#stats[node.id] = {
excluded: parentExcluded,
willExecute: hasExecutableChild
};
segmentChildren(node, orderedChildren, stats[node.id], executableIndex);
segmentChildren(node, orderedChildren, this.#stats, executableIndex);
if (!node.canBeReentered() && stats[node.id].segments.length > 1) {
stats = { valid: false };
}
}
}
function startingMin(executableIndex) {
return executableIndex === undefined ? defaultMin : executableIndex;
}
function startingMax(executableIndex) {
return executableIndex === undefined ? defaultMax : executableIndex;
}
function segmentChildren(
node,
orderedChildren,
nodeStats,
executableIndex
) {
let currentSegment = {
index: 0,
owner: node,
nodes: [],
min: startingMin(executableIndex),
max: startingMax(executableIndex)
},
result = [currentSegment],
lastMax = defaultMax,
orderedChildSegments = orderChildSegments(orderedChildren);
function isSegmentBoundary(minIndex) {
return (
lastMax !== defaultMax &&
minIndex !== defaultMin &&
lastMax < minIndex - 1
);
}
for (let i = 0; i < orderedChildSegments.length; i++) {
const childSegment = orderedChildSegments[i],
maxIndex = childSegment.max,
minIndex = childSegment.min;
if (isSegmentBoundary(minIndex)) {
currentSegment = {
index: result.length,
owner: node,
nodes: [],
min: defaultMin,
max: defaultMax
};
result.push(currentSegment);
}
currentSegment.nodes.push(childSegment);
currentSegment.min = Math.min(currentSegment.min, minIndex);
currentSegment.max = Math.max(currentSegment.max, maxIndex);
lastMax = maxIndex;
}
nodeStats.segments = result;
}
function orderChildSegments(children) {
const specifiedOrder = [],
unspecifiedOrder = [];
for (let i = 0; i < children.length; i++) {
const child = children[i],
segments = stats[child.id].segments;
for (let j = 0; j < segments.length; j++) {
const seg = segments[j];
if (seg.min === defaultMin) {
unspecifiedOrder.push(seg);
if (this.#stats[node.id].segments.length > 1) {
if (node.canBeReentered()) {
j$.getEnv().deprecated(
'The specified spec/suite order splits up a suite, running unrelated specs in the middle of it. This will become an error in a future release.'
);
} else {
specifiedOrder.push(seg);
}
}
}
specifiedOrder.sort(function(a, b) {
return a.min - b.min;
});
return specifiedOrder.concat(unspecifiedOrder);
}
function executeNode(node, segmentNumber) {
if (node.children) {
return {
fn: function(done) {
const onStart = {
fn: function(next) {
nodeStart(node, next);
}
};
queueRunnerFactory({
onComplete: function() {
const args = Array.prototype.slice.call(arguments, [0]);
node.cleanupBeforeAfter();
nodeComplete(node, node.getResult(), function() {
done.apply(undefined, args);
});
},
queueableFns: [onStart].concat(wrapChildren(node, segmentNumber)),
userContext: node.sharedUserContext(),
onException: function() {
node.handleException.apply(node, arguments);
},
onMultipleDone: node.onMultipleDone
? node.onMultipleDone.bind(node)
: null
});
}
};
} else {
return {
fn: function(done) {
node.execute(
queueRunnerFactory,
done,
stats[node.id].excluded,
failSpecWithNoExpectations
throw new Error(
'Invalid order: would cause a beforeAll or afterAll to be run multiple times'
);
}
}
}
}
}
class ExecutionTree {
#stats;
constructor(topSuite, stats) {
Object.defineProperty(this, 'topSuite', {
writable: false,
value: topSuite
});
this.#stats = stats;
}
childrenOfTopSuite() {
return this.childrenOfSuiteSegment(this.topSuite, 0);
}
childrenOfSuiteSegment(suite, segmentNumber) {
const segmentChildren = this.#stats[suite.id].segments[segmentNumber]
.nodes;
return segmentChildren.map(function(child) {
if (child.owner.children) {
return { suite: child.owner, segmentNumber: child.index };
} else {
return { spec: child.owner };
}
});
}
isExcluded(node) {
const nodeStats = this.#stats[node.id];
return node.children ? !nodeStats.willExecute : nodeStats.excluded;
}
}
function segmentChildren(node, orderedChildren, stats, executableIndex) {
let currentSegment = {
index: 0,
owner: node,
nodes: [],
min: startingMin(executableIndex),
max: startingMax(executableIndex)
},
result = [currentSegment],
lastMax = defaultMax,
orderedChildSegments = orderChildSegments(orderedChildren, stats);
function isSegmentBoundary(minIndex) {
return (
lastMax !== defaultMax &&
minIndex !== defaultMin &&
lastMax < minIndex - 1
);
}
for (let i = 0; i < orderedChildSegments.length; i++) {
const childSegment = orderedChildSegments[i],
maxIndex = childSegment.max,
minIndex = childSegment.min;
if (isSegmentBoundary(minIndex)) {
currentSegment = {
index: result.length,
owner: node,
nodes: [],
min: defaultMin,
max: defaultMax
};
result.push(currentSegment);
}
currentSegment.nodes.push(childSegment);
currentSegment.min = Math.min(currentSegment.min, minIndex);
currentSegment.max = Math.max(currentSegment.max, maxIndex);
lastMax = maxIndex;
}
stats[node.id].segments = result;
}
function orderChildSegments(children, stats) {
const specifiedOrder = [],
unspecifiedOrder = [];
for (let i = 0; i < children.length; i++) {
const child = children[i],
segments = stats[child.id].segments;
for (let j = 0; j < segments.length; j++) {
const seg = segments[j];
if (seg.min === defaultMin) {
unspecifiedOrder.push(seg);
} else {
specifiedOrder.push(seg);
}
}
}
function wrapChildren(node, segmentNumber) {
const result = [],
segmentChildren = stats[node.id].segments[segmentNumber].nodes;
specifiedOrder.sort(function(a, b) {
return a.min - b.min;
});
for (let i = 0; i < segmentChildren.length; i++) {
result.push(
executeNode(segmentChildren[i].owner, segmentChildren[i].index)
);
}
return specifiedOrder.concat(unspecifiedOrder);
}
if (!stats[node.id].willExecute) {
return result;
}
function startingMin(executableIndex) {
return executableIndex === undefined ? defaultMin : executableIndex;
}
return node.beforeAllFns.concat(result).concat(node.afterAllFns);
}
function startingMax(executableIndex) {
return executableIndex === undefined ? defaultMax : executableIndex;
}
return TreeProcessor;

303
src/core/TreeRunner.js Normal file
View File

@@ -0,0 +1,303 @@
getJasmineRequireObj().TreeRunner = function(j$) {
class TreeRunner {
#executionTree;
#setTimeout;
#globalErrors;
#runableResources;
#reportDispatcher;
#runQueue;
#getConfig;
#currentRunableTracker;
#hasFailures;
constructor(attrs) {
this.#executionTree = attrs.executionTree;
this.#globalErrors = attrs.globalErrors;
this.#setTimeout = attrs.setTimeout || setTimeout.bind(globalThis);
this.#runableResources = attrs.runableResources;
this.#reportDispatcher = attrs.reportDispatcher;
this.#runQueue = attrs.runQueue;
this.#getConfig = attrs.getConfig;
this.#currentRunableTracker = attrs.currentRunableTracker;
}
async execute() {
this.#hasFailures = false;
await new Promise(resolve => {
this.#executeSuiteSegment(this.#executionTree.topSuite, 0, resolve);
});
return { hasFailures: this.#hasFailures };
}
#wrapNodes(nodes) {
return nodes.map(node => {
return {
fn: done => {
if (node.suite) {
this.#executeSuiteSegment(node.suite, node.segmentNumber, done);
} else {
this._executeSpec(node.spec, done);
}
}
};
});
}
// Only exposed for testing.
_executeSpec(spec, specOverallDone) {
const onStart = next => {
this.#currentRunableTracker.setCurrentSpec(spec);
this.#runableResources.initForRunable(spec.id, spec.parentSuiteId);
this.#reportDispatcher.specStarted(spec.result).then(next);
};
const resultCallback = (result, next) => {
this.#specComplete(spec).then(next);
};
const queueableFns = this.#specQueueableFns(
spec,
onStart,
resultCallback
);
this.#runQueue({
isLeaf: true,
queueableFns,
onException: e => spec.handleException(e),
onMultipleDone: () => {
// Issue an erorr. Include the context ourselves and pass
// ignoreRunnable: true, since getting here always means that we've already
// moved on and the current runnable isn't the one that caused the problem.
spec.onLateError(
new Error(
'An asynchronous spec, beforeEach, or afterEach function called its ' +
"'done' callback more than once.\n(in spec: " +
spec.getFullName() +
')'
)
);
},
onComplete: () => {
if (spec.result.status === 'failed') {
specOverallDone(new j$.StopExecutionError('spec failed'));
} else {
specOverallDone();
}
},
userContext: spec.userContext(),
runnableName: spec.getFullName.bind(spec),
SkipPolicy: j$.CompleteOnFirstErrorSkipPolicy
});
}
#specQueueableFns(spec, onStart, resultCallback) {
const config = this.#getConfig();
const excluded = this.#executionTree.isExcluded(spec);
const ba = spec.beforeAndAfterFns();
let fns = [...ba.befores, spec.queueableFn, ...ba.afters];
if (spec.markedPending || excluded === true) {
fns = [];
}
const start = {
fn(done) {
spec.executionStarted();
onStart(done);
}
};
const complete = {
fn(done) {
spec.executionFinished(excluded, config.failSpecWithNoExpectations);
resultCallback(spec.result, done);
},
type: 'specCleanup'
};
fns.unshift(start);
if (config.detectLateRejectionHandling) {
fns.push(this.#lateUnhandledRejectionChecker());
}
fns.push(complete);
return fns;
}
#executeSuiteSegment(suite, segmentNumber, done) {
const isTopSuite = suite === this.#executionTree.topSuite;
const isExcluded = this.#executionTree.isExcluded(suite);
let befores = [];
let afters = [];
if (suite.beforeAllFns.length > 0 && !isExcluded) {
befores = [...suite.beforeAllFns];
if (this.#getConfig().detectLateRejectionHandling) {
befores.push(this.#lateUnhandledRejectionChecker());
}
}
if (suite.afterAllFns.length > 0 && !isExcluded) {
afters = [...suite.afterAllFns];
if (this.#getConfig().detectLateRejectionHandling) {
afters.push(this.#lateUnhandledRejectionChecker());
}
}
const children = isTopSuite
? this.#executionTree.childrenOfTopSuite()
: this.#executionTree.childrenOfSuiteSegment(suite, segmentNumber);
const queueableFns = [
...befores,
...this.#wrapNodes(children),
...afters
];
if (!isTopSuite) {
queueableFns.unshift({
fn: next => {
this.#suiteSegmentStart(suite, next);
}
});
}
this.#runQueue({
onComplete: maybeError => {
this.#suiteSegmentComplete(suite, suite.getResult(), () => {
done(maybeError);
});
},
queueableFns,
userContext: suite.sharedUserContext(),
onException: function() {
suite.handleException.apply(suite, arguments);
},
onMultipleDone: suite.onMultipleDone
? suite.onMultipleDone.bind(suite)
: null,
SkipPolicy: this.#suiteSkipPolicy()
});
}
// Returns a queueable fn that reports any still-unhandled rejections in
// cases where detectLateRejectionHandling is enabled. Should only be called
// when detectLateRejectionHandling is enabled, because the setTimeout
// imposes a significant performance penalty in suites with lots of fast
// specs.
#lateUnhandledRejectionChecker() {
const globalErrors = this.#globalErrors;
return {
fn: done => {
// setTimeout is necessary to trigger rejectionhandled events
this.#setTimeout(function() {
globalErrors.reportUnhandledRejections();
done();
});
}
};
}
#suiteSegmentStart(suite, next) {
this.#currentRunableTracker.pushSuite(suite);
this.#runableResources.initForRunable(suite.id, suite.parentSuite.id);
this.#reportDispatcher.suiteStarted(suite.result).then(next);
suite.startTimer();
}
#suiteSegmentComplete(suite, result, next) {
const isTopSuite = suite === this.#executionTree.topSuite;
if (!isTopSuite) {
if (suite !== this.#currentRunableTracker.currentSuite()) {
throw new Error('Tried to complete the wrong suite');
}
// suite.cleanupBeforeAfter() is conditional because calling it on the
// top suite breaks parallel mode. The top suite is reentered every time
// a runner runs another file, so its before and after fns need to be
// preserved.
suite.cleanupBeforeAfter();
this.#runableResources.clearForRunable(suite.id);
this.#currentRunableTracker.popSuite();
if (result.status === 'failed') {
this.#hasFailures = true;
}
suite.endTimer();
}
const finish = isTopSuite
? next
: () => this.#reportSuiteDone(suite, result, next);
if (suite.hadBeforeAllFailure) {
this.#reportChildrenOfBeforeAllFailure(suite).then(finish);
} else {
finish();
}
}
#reportSuiteDone(suite, result, next) {
suite.reportedDone = true;
this.#reportDispatcher.suiteDone(result).then(next);
}
async #specComplete(spec) {
this.#runableResources.clearForRunable(spec.id);
this.#currentRunableTracker.setCurrentSpec(null);
if (spec.result.status === 'failed') {
this.#hasFailures = true;
}
await this.#reportSpecDone(spec);
}
async #reportSpecDone(spec) {
spec.reportedDone = true;
await this.#reportDispatcher.specDone(spec.result);
}
async #reportChildrenOfBeforeAllFailure(suite) {
for (const child of suite.children) {
if (child instanceof j$.Suite) {
await this.#reportDispatcher.suiteStarted(child.result);
await this.#reportChildrenOfBeforeAllFailure(child);
// Marking the suite passed is consistent with how suites that
// contain failed specs but no suite-level failures are reported.
child.result.status = 'passed';
await this.#reportDispatcher.suiteDone(child.result);
} else {
/* a spec */
await this.#reportDispatcher.specStarted(child.result);
child.addExpectationResult(
false,
{
passed: false,
message:
'Not run because a beforeAll function failed. The ' +
'beforeAll failure will be reported on the suite that ' +
'caused it.'
},
true
);
child.result.status = 'failed';
await this.#reportSpecDone(child);
}
}
}
#suiteSkipPolicy() {
if (this.#getConfig().stopOnSpecFailure) {
return j$.CompleteOnFirstErrorSkipPolicy;
} else {
return j$.SkipAfterBeforeAllErrorPolicy;
}
}
}
return TreeRunner;
};

View File

@@ -2,7 +2,7 @@ getJasmineRequireObj().Anything = function(j$) {
function Anything() {}
Anything.prototype.asymmetricMatch = function(other) {
return !j$.util.isUndefined(other) && other !== null;
return other !== undefined && other !== null;
};
Anything.prototype.jasmineToString = function() {

View File

@@ -73,9 +73,7 @@ getJasmineRequireObj().base = function(j$, jasmineGlobal) {
};
j$.isObject_ = function(value) {
return (
!j$.util.isUndefined(value) && value !== null && j$.isA_('Object', value)
);
return value !== undefined && value !== null && j$.isA_('Object', value);
};
j$.isString_ = function(value) {

View File

@@ -31,10 +31,8 @@ getJasmineRequireObj().DiffBuilder = function(j$) {
const actualCustom = this.prettyPrinter_.customFormat_(actual);
const expectedCustom = this.prettyPrinter_.customFormat_(expected);
const useCustom = !(
j$.util.isUndefined(actualCustom) &&
j$.util.isUndefined(expectedCustom)
);
const useCustom =
actualCustom !== undefined || expectedCustom !== undefined;
if (useCustom) {
const prettyActual = actualCustom || this.prettyPrinter_(actual);

View File

@@ -177,13 +177,13 @@ getJasmineRequireObj().MatchersUtil = function(j$) {
bStack,
diffBuilder
);
if (!j$.util.isUndefined(asymmetricResult)) {
if (asymmetricResult !== undefined) {
return asymmetricResult;
}
for (const tester of this.customTesters_) {
const customTesterResult = tester(a, b);
if (!j$.util.isUndefined(customTesterResult)) {
if (customTesterResult !== undefined) {
if (!customTesterResult) {
diffBuilder.recordMismatch();
}

View File

@@ -43,6 +43,7 @@ var getJasmineRequireObj = (function(jasmineGlobal) {
j$.Clock = jRequire.Clock();
j$.DelayedFunctionScheduler = jRequire.DelayedFunctionScheduler(j$);
j$.Deprecator = jRequire.Deprecator(j$);
j$.Configuration = jRequire.Configuration(j$);
j$.Env = jRequire.Env(j$);
j$.StackTrace = jRequire.StackTrace(j$);
j$.ExceptionFormatter = jRequire.ExceptionFormatter(j$);
@@ -70,6 +71,7 @@ var getJasmineRequireObj = (function(jasmineGlobal) {
j$.reporterEvents = jRequire.reporterEvents(j$);
j$.ReportDispatcher = jRequire.ReportDispatcher(j$);
j$.ParallelReportDispatcher = jRequire.ParallelReportDispatcher(j$);
j$.CurrentRunableTracker = jRequire.CurrentRunableTracker();
j$.RunableResources = jRequire.RunableResources(j$);
j$.Runner = jRequire.Runner(j$);
j$.Spec = jRequire.Spec(j$);
@@ -83,14 +85,27 @@ var getJasmineRequireObj = (function(jasmineGlobal) {
j$.Suite = jRequire.Suite(j$);
j$.SuiteBuilder = jRequire.SuiteBuilder(j$);
j$.Timer = jRequire.Timer();
j$.TreeProcessor = jRequire.TreeProcessor();
j$.TreeProcessor = jRequire.TreeProcessor(j$);
j$.TreeRunner = jRequire.TreeRunner(j$);
j$.version = jRequire.version();
j$.Order = jRequire.Order();
j$.DiffBuilder = jRequire.DiffBuilder(j$);
j$.NullDiffBuilder = jRequire.NullDiffBuilder(j$);
j$.ObjectPath = jRequire.ObjectPath(j$);
j$.MismatchTree = jRequire.MismatchTree(j$);
j$.GlobalErrors = jRequire.GlobalErrors(j$);
// zone.js tries to monkey patch GlobalErrors in a way that is either a
// no-op or causes Jasmine to crash, depending on whether it's done before
// or after env creation. Prevent that.
const GlobalErrors = jRequire.GlobalErrors(j$);
Object.defineProperty(j$, 'GlobalErrors', {
enumerable: true,
configurable: false,
get() {
return GlobalErrors;
},
set() {}
});
j$.Truthy = jRequire.Truthy(j$);
j$.Falsy = jRequire.Falsy(j$);

View File

@@ -168,6 +168,18 @@ getJasmineRequireObj().interface = function(jasmine, env) {
return env.afterAll.apply(env, arguments);
},
/**
* Get a user-defined property as part of the properties field of {@link SpecResult}
* @name getSpecProperty
* @since 5.10.0
* @function
* @param {String} key The name of the property
* @returns {*} The value of the property
*/
getSpecProperty: function(key) {
return env.getSpecProperty(key);
},
/**
* Sets a user-defined property that will be provided to reporters as part of the properties field of {@link SpecResult}
* @name setSpecProperty

View File

@@ -1,10 +1,6 @@
getJasmineRequireObj().util = function(j$) {
const util = {};
util.isUndefined = function(obj) {
return obj === void 0;
};
util.clone = function(obj) {
if (Object.prototype.toString.apply(obj) === '[object Array]') {
return obj.slice();
@@ -52,16 +48,9 @@ getJasmineRequireObj().util = function(j$) {
return Object.prototype.hasOwnProperty.call(obj, key);
};
util.errorWithStack = function errorWithStack() {
// Don't throw and catch. That makes it harder for users to debug their
// code with exception breakpoints, and it's unnecessary since all
// supported environments populate new Error().stack
return new Error();
};
function callerFile() {
const trace = new j$.StackTrace(util.errorWithStack());
return trace.frames[2].file;
const trace = new j$.StackTrace(new Error());
return trace.frames[1].file;
}
util.jasmineFile = (function() {

View File

@@ -47,6 +47,13 @@ jasmineRequire.HtmlReporter = function(j$) {
}
};
/**
* @class HtmlReporter
* @classdesc Displays results and allows re-running individual specs and suites.
* @implements {Reporter}
* @param options Options object. See lib/jasmine-core/boot1.js for details.
* @since 1.2.0
*/
function HtmlReporter(options) {
function config() {
return (options.env && options.env.configuration()) || {};
@@ -64,6 +71,11 @@ jasmineRequire.HtmlReporter = function(j$) {
const deprecationWarnings = [];
const failures = [];
/**
* Initializes the reporter. Should be called before {@link Env#execute}.
* @function
* @name HtmlReporter#initialize
*/
this.initialize = function() {
clearPrior();
htmlReporterMain = createDom(
@@ -523,6 +535,11 @@ jasmineRequire.HtmlReporter = function(j$) {
'a',
{ href: specHref(resultNode.result) },
specDescription
),
createDom(
'span',
{ className: 'jasmine-spec-duration' },
'(' + resultNode.result.duration + 'ms)'
)
)
);

View File

@@ -1,4 +1,6 @@
jasmineRequire.HtmlSpecFilter = function() {
// Legacy HTML spec filter, preserved for backward compatibility with
// boot files that predate HtmlExactSpecFilterV2
function HtmlSpecFilter(options) {
const filterString =
options &&
@@ -6,6 +8,13 @@ jasmineRequire.HtmlSpecFilter = function() {
options.filterString().replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
const filterPattern = new RegExp(filterString);
/**
* Determines whether the spec with the specified name should be executed.
* @name HtmlSpecFilter#matches
* @function
* @param {string} specName The full name of the spec
* @returns {boolean}
*/
this.matches = function(specName) {
return filterPattern.test(specName);
};

View File

@@ -1,36 +1,55 @@
jasmineRequire.QueryString = function() {
function QueryString(options) {
this.navigateWithNewParam = function(key, value) {
options.getWindowLocation().search = this.fullStringWithNewParam(
/**
* Reads and manipulates the query string.
* @since 2.0.0
*/
class QueryString {
#getWindowLocation;
/**
* @param options Object with a getWindowLocation property, which should be
* a function returning the current value of window.location.
*/
constructor(options) {
this.#getWindowLocation = options.getWindowLocation;
}
/**
* Sets the specified query parameter and navigates to the resulting URL.
* @param {string} key
* @param {string} value
*/
navigateWithNewParam(key, value) {
this.#getWindowLocation().search = this.fullStringWithNewParam(
key,
value
);
};
this.fullStringWithNewParam = function(key, value) {
const paramMap = queryStringToParamMap();
paramMap[key] = value;
return toQueryString(paramMap);
};
this.getParam = function(key) {
return queryStringToParamMap()[key];
};
return this;
function toQueryString(paramMap) {
const qStrPairs = [];
for (const prop in paramMap) {
qStrPairs.push(
encodeURIComponent(prop) + '=' + encodeURIComponent(paramMap[prop])
);
}
return '?' + qStrPairs.join('&');
}
function queryStringToParamMap() {
const paramStr = options.getWindowLocation().search.substring(1);
/**
* Returns a new URL based on the current location, with the specified
* query parameter set.
* @param {string} key
* @param {string} value
* @return {string}
*/
fullStringWithNewParam(key, value) {
const paramMap = this.#queryStringToParamMap();
paramMap[key] = value;
return toQueryString(paramMap);
}
/**
* Gets the value of the specified query parameter.
* @param {string} key
* @return {string}
*/
getParam(key) {
return this.#queryStringToParamMap()[key];
}
#queryStringToParamMap() {
const paramStr = this.#getWindowLocation().search.substring(1);
let params = [];
const paramMap = {};
@@ -50,5 +69,15 @@ jasmineRequire.QueryString = function() {
}
}
function toQueryString(paramMap) {
const qStrPairs = [];
for (const prop in paramMap) {
qStrPairs.push(
encodeURIComponent(prop) + '=' + encodeURIComponent(paramMap[prop])
);
}
return '?' + qStrPairs.join('&');
}
return QueryString;
};

View File

@@ -331,6 +331,10 @@ body {
&.jasmine-excluded a:before {
content: $passing-mark + $space;
}
.jasmine-spec-duration {
margin-left: 1em;
}
}
}