Compare commits

...

83 Commits

Author SHA1 Message Date
Steve Gravrock
9cf9b856b0 Bump version to 5.13.0
Some checks failed
Test in latest available Safari / build (push) Has been cancelled
2025-12-01 17:25:03 -08:00
Steve Gravrock
db6c142afd Copy 6.0.0-beta.0 release notes from branch 2025-11-28 11:49:05 -08:00
Steve Gravrock
1e691b2470 Prettier 2025-11-27 06:53:27 -08:00
Steve Gravrock
c5555dd8cc Better debug logging for spec that occasionally fails in FF 2025-11-27 06:30:09 -08:00
Steve Gravrock
78c14f81a8 Copy 6.0.0-alpha.2 release notes from branch 2025-11-15 14:40:39 -08:00
Steve Gravrock
56e2832ebe Add manual and cron triggers to Safari build 2025-11-11 09:13:34 -08:00
Steve Gravrock
9a9d3994da Add Safari 26 to supported browsers 2025-11-03 07:37:11 -08:00
Steve Gravrock
ff9feb29d3 Configurable spec/suite filename detection
* Adds extraItStackFrames and extraDescribeStackFrames config properties.
* Un-deprecates the filename properties of reporter events.
* Fixes #2065.
2025-11-01 14:17:01 -07:00
Steve Gravrock
fee7e6e64e Merge branch 'jonahd-g-main'
* Adds jasmine.allOf asymmetric equality tester
* Merges #2087 from @jonahd-g
* Fixes #2083
2025-11-01 09:01:53 -07:00
Steve Gravrock
18d4d38655 Fix version number in 5.12.1 release notes 2025-10-29 19:55:00 -07:00
Steve Gravrock
53e9bc68d2 Bump version to 5.12.1 2025-10-29 19:53:34 -07:00
Steve Gravrock
2be50e1b87 Merge branch 'bonkevin-fix-custom-matcher'
* Fixes custom matchers in top-level specs
* Merges #2088 from @bonkevin
2025-10-29 19:44:06 -07:00
bonkevin
27a1257b6d fix: unavailable custom matchers on top-it 2025-10-29 13:04:10 -04:00
Jonah Bron
75658e0566 jasmine.allOf AsymmetricEqualityTester
New asymmetric equality tester that accepts a variable number of arguments, and will pass if all of them evaluate as being equal to the input value.
Includes unit tests
2025-10-27 10:10:16 -07:00
Steve Gravrock
9a67c4e24d Copy 6.0.0-alpha.1 release notes from branch 2025-10-18 13:05:29 -07:00
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
66 changed files with 7353 additions and 4326 deletions

View File

@@ -1,5 +1,5 @@
# Run tests against supported Node versions, and (except for pull requests)
# against supported browsers.
# against supported browsers that are available on Saucelabs.
version: 2.1
@@ -93,7 +93,7 @@ jobs:
export SAUCE_TUNNEL_NAME=$CIRCLE_WORKFLOW_JOB_ID
scripts/start-sauce-connect
set +o errexit
scripts/run-all-browsers
scripts/run-sauce-browsers
exitcode=$?
set -o errexit
scripts/stop-sauce-connect

23
.github/workflows/safari.yml vendored Normal file
View File

@@ -0,0 +1,23 @@
name: Test in latest available Safari
on:
push:
pull_request:
workflow_dispatch:
schedule:
- cron: '0 0 * * *'
jobs:
build:
runs-on: macos-latest
steps:
- name: Report Safari version
run: osascript -e 'get version of application "Safari"'
- uses: actions/checkout@v4
- name: Use Node.js 22.x
uses: actions/setup-node@v4
with:
node-version: 22.x
- run: npm install
- run: npm run build
- run: JASMINE_BROWSER=safari npm run ci

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.5+*, 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*, 26* |
| 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

@@ -28,9 +28,18 @@ should also rev to that version.
When ready to release - specs are all green and the stories are done:
1. Update the release notes in `release_notes` - use the Anchorman gem to generate the markdown file and edit accordingly. Include a list of supported environments.
1. Update the version in `package.json`
1. Run `npm run build`.
1. Update the release notes in `release_notes` - use the Anchorman gem to
generate the Markdown file and edit accordingly. Include a list of supported
environments. Get that information from these places:
* For Node, see .circleci/config.yml or the README.
* For Firefox ESR and Safari <=17, see scripts/run-sauce-browsers or the README.
* For evergreen browsers, trigger a Circle CI run and check the
[Saucelabs dashboard](https://app.saucelabs.com/dashboard/tests?ownerId=90a771d55857492da3bd5251a2d92457&ownerType=user&ownerName=jasmine-js&start=last7days)
once it's finished.
* For Safari >17, trigger the [Safari action](https://github.com/jasmine/jasmine/actions/workflows/safari.yml)
and get the version from the output.
2. Update the version in `package.json`
3. Run `npm run build`.
### Commit and push core changes

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.9.0",
"version": "5.13.0",
"repository": {
"type": "git",
"url": "https://github.com/jasmine/jasmine.git"
@@ -52,7 +52,7 @@
"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)_

29
release_notes/5.12.1.md Normal file
View File

@@ -0,0 +1,29 @@
Jasmine Core 5.12.1 Release Notes
## Bug fixes
* Fix custom matchers in top-level specs
* Merges [#2088](https://github.com/jasmine/jasmine/pull/2088) from @bonkevin
## 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, 144* |
| Edge | 141* |
\* 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)_

44
release_notes/5.13.0.md Normal file
View File

@@ -0,0 +1,44 @@
# Jasmine Core 5.13.0 Release Notes
## Changes to supported environments
Safari 26 is now supported on a best-effort basis.
Due to the limited availability of Safari 18 and later on free CI services,
Safari support in future jasmine-core versions will be limited to:
* Best-effort support for the latest Safari version available on GitHub Actions,
which may change at any time.
* Best-effort support for Safari 16 and 17 for as long as it remains practical.
## New Features
* New `extraItStackFrames` and `extraDescribeStackFrames` config options to fix
the filename properties of reporter events in configurations that wrap
`it`/`describe`, such as zone.js. The `filename` properties of reporter events
are no longer deprecated.
* `jasmine.allOf` asymmetric equality tester
* Merges [#2087](https://github.com/jasmine/jasmine/issues/2083) from @jonahd-g
* Fixes [#2083](https://github.com/jasmine/jasmine/pull/2087)
## Supported environments
This version has been tested in the following environments.
| Environment | Supported versions |
|-------------|--------------------------------|
| Node | 18.20.5**, 20, 22, 24 |
| Safari** | 16, 17, 26.1 |
| Chrome | 142* |
| Firefox | 102**, 115**, 128**, 140, 145* |
| Edge | 142* |
\* 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

@@ -0,0 +1,116 @@
# Jasmine Core 6.0.0-alpha.1 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.1`,
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.1`.
## Breaking changes
### Changes that affect reporters
* Irrelevant properties such as `status` and `failedExpectations` are omitted
from [the event passed to suiteStarted](https://jasmine.github.io/api/6.0.0-alpha.1/global.html#SuiteStartedEvent).
This change 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.
### Changes that affect browser boot files
* The `createElement` and `createTextNode` options of `HtmlReporter` are ignored.
`HtmlReporter` now unconditionally uses `document.createElement` and
`document.createTextNode`.
### Changes that affect spec writing
* HTML reporters cache configuration throughout each run. Configuration changes
made while specs are running will not affect reporter behavior.
* Global error spies always receive a single argument. Previously, the browser
error event was passed as the second argument.
## New features
* A new `HtmlReporterV2` with several improvements over the old `HtmlReporter`:
* Clicking a spec/suite link does exact filtering rather than a substring
match.
* The old dots are replaced with a progress bar. This improves usability with
large suites and fixes an accessibility problem.
* Details of failed specs are displayed as soon as each spec finishes.
* Initialization and wire-up in boot files are much simpler.
If you're using jasmine-browser-runner or copying boot1.js from the standalone
distribution, you'll automatically get the new reporter. If you maintain your
own boot files, you'll get the old reporter unless you update your boot1.js
to match the one that's in this package.
The new reporter produces `spec` query string parameters that are different
from those created by the old reporter. If you use non-Jasmine software that
interprets the `spec` parameter, such as karma-jasmine, you may not be able to
adopt `HtmlReporterV2` unlesss it's updated.
* Use `globalThis` to determine the global object during initialization
This makes jasmine-core more tolerant of buggy bundlers or loaders that
cause `this` to be undefined in the global context.
## Deprecations
* Warn if jasmine-core is loaded as an ES module in a browser.
This is an untested and unsupported configuration that has been known to cause
problems in the past.
* Deprecated `HtmlReporter` and `HtmlSpecFilter` in favor of `HtmlReporterV2`.
## Documentation improvements
* Improved API reference documentation for APIs that are used from browser boot
files.
## Internal improvements
* Removed remaining code that supported suite re-entry.
* Encapsulated suite and spec result and status management.
* Adopted strict mode throughout the codebase.
* Decomposed `HtmlReporter` into components and converted to ES6 classes.
* Made global error handling more uniform between browsers and Node.
## Supported environments
This version has been tested in the following environments.
| Environment | Supported versions |
|-------------------|--------------------------------|
| Node | 20, 22, 24 |
| Safari | 16**, 17** |
| Chrome | 141* |
| Firefox | 102**, 115**, 128**, 140, 143* |
| Edge | 141* |
\* 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,90 @@
# Jasmine Core 6.0.0-alpha.2 Release Notes
This is a pre-release, intended to offer a preview of upcoming 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.2`,
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.2`.
## Changes to supported environments
Safari 26 is now supported on a best-effort basis.†
Due to the limited availability of Safari 18 and later on free CI services,
Safari support in future jasmine-core versions will be limited to:
* Best-effort support for the latest Safari version available on GitHub Actions,
which may change at any time.
* Best-effort support for Safari 16 and 17 for as long as it remains practical.
## Bug Fixes
* Fix custom matchers in top-level specs††
* Merges [#2088](https://github.com/jasmine/jasmine/pull/2088) from @bonkevin
## New features
* Larger body font size in HTML reporters
* New Performance tab in HtmlReporterV2 shows metrics and a list of the slowest
specs.
* Experimental `safariYieldStrategy: "time"` config option, which may make
Jasmine run significantly faster in Safari and similar browsers. So far, this
option has not been tested on a wide variety of workloads. Feedback is
appreciated.
* New `extraItStackFrames` and `extraDescribeStackFrames` config options to fix
the filename properties of reporter events in configurations that wrap
`it`/`describe`, such as zone.js.†
* `jasmine.allOf asymmetric` equality tester†
* Merges [#2087](https://github.com/jasmine/jasmine/pull/2087) from @jonahd-g
* Fixes [#2083](https://github.com/jasmine/jasmine/issues/2083)
* Re-add support for partial spec name filtering via `spec` query parameter
* Fixes [#2085](https://github.com/jasmine/jasmine/issues/2085).
* Require spec/suite property keys to be strings, not just anything that's
cloneable and serializable. This matches the existing API reference
documentation.
## Documentation improvements
* Fix HtmlReporterV2 ctor example
## Internal Improvements
* Remove code to support browsers that don't have MessageChannel. Jasmine hasn't
run in any such browsers since 2.x.
† Also likely to be included in a future 5.x release.<br>
†† Also released in 5.12.1.
## Supported environments
This version has been tested in the following environments.
| Environment | Supported versions |
|-------------------|--------------------------------|
| Node | 20, 22, 24 |
| Safari | 16**, 17**, 26** |
| Chrome | 142* |
| Firefox | 102**, 115**, 128**, 140, 145* |
| Edge | 142* |
\* 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,74 @@
# Jasmine Core 6.0.0-beta.0 Release Notes
This is a pre-release, intended to offer a preview of upcoming 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-beta.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-beta.0`.
## Breaking changes
* boot1.js no longer adds jsApiReporter to the env.
* HtmlReporterV2 initialization and boot1.js have been simplified. If you
maintain your own boot file, update it to match the boot1.js included in this
package.
## New features
* Statically exposed pretty printer as jasmine.pp().
## Bug fixes
* Fixed HtmlReporterV2 progress bar when running a subset of specs.
## Deprecations
* jsApiReporter is deprecated.
* Detect monkey patching and emit a deprecation warning.
## Documentation improvements
* Documented the set of possible spec statuses.
## Internal improvements
* Replaced isArray helper with native Array.isArray
## Supported environments
This version has been tested in the following environments.
| Environment | Supported versions |
|-------------------|--------------------------------|
| Node | 20, 22, 24 |
| Safari | 16**, 17**, 26.1** |
| Chrome | 142* |
| Firefox | 102**, 115**, 128**, 140, 145* |
| Edge | 142* |
\* 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,5 +1,10 @@
#!/bin/sh
# Run tests in supported browsers that are available on Saucelabs.
# Note: The latest Safari version is tested via GitHub Actions because Saucelabs
# only makes it available to paid enterprise accounts. See
# .github/workflows/safari.yml.
run_browser() {
browser=$1
version=$2
@@ -28,12 +33,20 @@ failfile=`mktemp -t jasmine-results.XXXXXX` || exit 1
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() {

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,158 @@
describe('Configuration', function() {
const standardBooleanKeys = [
'random',
'stopOnSpecFailure',
'stopSpecOnExpectationFailure',
'failSpecWithNoExpectations',
'hideDisabled',
'autoCleanClosures',
'forbidDuplicateNames',
'detectLateRejectionHandling'
];
const allKeys = [
...standardBooleanKeys,
'seed',
'specFilter',
'verboseDeprecations',
'extraItStackFrames',
'extraDescribeStackFrames'
];
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);
expect(subject.extraItStackFrames).toEqual(0);
expect(subject.extraDescribeStackFrames).toEqual(0);
});
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();
});
it('sets extraItStackFrames when not undefined', function() {
const subject = new jasmineUnderTest.Configuration();
subject.update({ extraItStackFrames: undefined });
expect(subject.extraItStackFrames).toEqual(0);
subject.update({ extraItStackFrames: 100000 });
expect(subject.extraItStackFrames).toEqual(100000);
});
it('sets extraDescribeStackFrames when not undefined', function() {
const subject = new jasmineUnderTest.Configuration();
subject.update({ extraDescribeStackFrames: undefined });
expect(subject.extraDescribeStackFrames).toEqual(0);
subject.update({ extraDescribeStackFrames: 100000 });
expect(subject.extraDescribeStackFrames).toEqual(100000);
});
});
});

View File

@@ -1,4 +1,3 @@
// TODO: Fix these unit tests!
describe('Env', function() {
let env;
beforeEach(function() {
@@ -95,7 +94,7 @@ describe('Env', function() {
});
});
it('accepts its own current configureation', function() {
it('accepts its own current configuration', function() {
env.configure(env.configuration());
});
@@ -198,6 +197,29 @@ describe('Env', function() {
expect(innerSuite.parentSuite).toBe(suite);
expect(spec.getFullName()).toEqual('outer suite inner suite a spec');
});
it('sets the caller filename correctly when extraDescribeStackFrames is not set', function() {
// IIFE is used to match the stack depth when global describe() is called
const suite = (function() {
return env[methodName]('a suite', function() {
env.it('a spec');
});
})();
expect(suite.filename).toMatch(/EnvSpec\.js$/);
});
it('sets the caller filename correctly when extraDescribeStackFrames is set', function() {
env.configure({ extraDescribeStackFrames: 2 });
// IIFE is used to match the stack depth when global describe() is called
const suite = (function() {
return specHelpers.callerFilenameShim(function() {
return env[methodName]('a suite', function() {
env.it('a spec');
});
});
})();
expect(suite.filename).toMatch(/EnvSpec\.js$/);
});
}
describe('#describe', function() {
@@ -300,6 +322,25 @@ describe('Env', function() {
.not.toEqual('');
expect(spec.pend).toBeFalsy();
});
it('sets the caller filename correctly when extraItStackFrames is not set', function() {
// IIFE is used to match the stack depth when global it() is called
const spec = (function() {
return env[methodName]('a spec', function() {});
})();
expect(spec.filename).toMatch(/EnvSpec\.js$/);
});
it('sets the caller filename correctly when extraItStackFrames is set', function() {
env.configure({ extraItStackFrames: 2 });
// IIFE is used to match the stack depth when global it() is called
const spec = (function() {
return specHelpers.callerFilenameShim(function() {
return env[methodName]('a spec', function() {});
});
})();
expect(spec.filename).toMatch(/EnvSpec\.js$/);
});
}
describe('#it', function() {
@@ -625,9 +666,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 +685,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

@@ -2,7 +2,10 @@ describe('GlobalErrors', function() {
it('calls the added handler on error', function() {
const globals = browserGlobals();
const handler = jasmine.createSpy('errorHandler');
const errors = new jasmineUnderTest.GlobalErrors(globals.global);
const errors = new jasmineUnderTest.GlobalErrors(
globals.global,
() => ({})
);
errors.install();
errors.pushListener(handler);
@@ -19,7 +22,10 @@ describe('GlobalErrors', function() {
it('is not affected by overriding global.onerror', function() {
const globals = browserGlobals();
const handler = jasmine.createSpy('errorHandler');
const errors = new jasmineUnderTest.GlobalErrors(globals.global);
const errors = new jasmineUnderTest.GlobalErrors(
globals.global,
() => ({})
);
errors.install();
errors.pushListener(handler);
@@ -39,7 +45,10 @@ describe('GlobalErrors', function() {
const globals = browserGlobals();
const handler1 = jasmine.createSpy('errorHandler1');
const handler2 = jasmine.createSpy('errorHandler2');
const errors = new jasmineUnderTest.GlobalErrors(globals.global);
const errors = new jasmineUnderTest.GlobalErrors(
globals.global,
() => ({})
);
errors.install();
errors.pushListener(handler1);
@@ -59,7 +68,10 @@ describe('GlobalErrors', function() {
const globals = browserGlobals();
const handler1 = jasmine.createSpy('errorHandler1');
const handler2 = jasmine.createSpy('errorHandler2');
const errors = new jasmineUnderTest.GlobalErrors(globals.global);
const errors = new jasmineUnderTest.GlobalErrors(
globals.global,
() => ({})
);
errors.install();
errors.pushListener(handler1);
@@ -86,7 +98,10 @@ describe('GlobalErrors', function() {
it('uninstalls itself', function() {
const globals = browserGlobals();
const errors = new jasmineUnderTest.GlobalErrors(globals.global);
const errors = new jasmineUnderTest.GlobalErrors(
globals.global,
() => ({})
);
function unrelatedListener() {}
errors.install();
@@ -98,7 +113,10 @@ describe('GlobalErrors', function() {
it('rethrows the original error when there is no handler', function() {
const globals = browserGlobals();
const errors = new jasmineUnderTest.GlobalErrors(globals.global);
const errors = new jasmineUnderTest.GlobalErrors(
globals.global,
() => ({})
);
const originalError = new Error('nope');
errors.install();
@@ -114,7 +132,10 @@ describe('GlobalErrors', function() {
it('reports uncaught exceptions in node.js', function() {
const globals = nodeGlobals();
const errors = new jasmineUnderTest.GlobalErrors(globals.global);
const errors = new jasmineUnderTest.GlobalErrors(
globals.global,
() => ({})
);
const handler = jasmine.createSpy('errorHandler');
function originalHandler() {}
globals.listeners.uncaughtException = [originalHandler];
@@ -144,7 +165,10 @@ describe('GlobalErrors', function() {
describe('Reporting unhandled promise rejections in node.js', function() {
it('reports rejections with `Error` reasons', function() {
const globals = nodeGlobals();
const errors = new jasmineUnderTest.GlobalErrors(globals.global);
const errors = new jasmineUnderTest.GlobalErrors(
globals.global,
() => ({})
);
const handler = jasmine.createSpy('errorHandler');
function originalHandler() {}
globals.listeners.unhandledRejection = [originalHandler];
@@ -173,7 +197,10 @@ describe('GlobalErrors', function() {
it('reports rejections with non-`Error` reasons', function() {
const globals = nodeGlobals();
const errors = new jasmineUnderTest.GlobalErrors(globals.global);
const errors = new jasmineUnderTest.GlobalErrors(
globals.global,
() => ({})
);
const handler = jasmine.createSpy('errorHandler');
errors.install();
@@ -193,7 +220,10 @@ describe('GlobalErrors', function() {
it('reports rejections with no reason provided', function() {
const globals = nodeGlobals();
const errors = new jasmineUnderTest.GlobalErrors(globals.global);
const errors = new jasmineUnderTest.GlobalErrors(
globals.global,
() => ({})
);
const handler = jasmine.createSpy('errorHandler');
errors.install();
@@ -210,12 +240,149 @@ describe('GlobalErrors', function() {
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 globals = browserGlobals();
const errors = new jasmineUnderTest.GlobalErrors(globals.global);
const errors = new jasmineUnderTest.GlobalErrors(
globals.global,
() => ({})
);
errors.install();
expect(globals.listeners.unhandledrejection).toEqual([
@@ -229,7 +396,10 @@ describe('GlobalErrors', function() {
it('reports rejections whose reason is a string', function() {
const globals = browserGlobals();
const handler = jasmine.createSpy('errorHandler');
const errors = new jasmineUnderTest.GlobalErrors(globals.global);
const errors = new jasmineUnderTest.GlobalErrors(
globals.global,
() => ({})
);
errors.install();
errors.pushListener(handler);
@@ -246,7 +416,10 @@ describe('GlobalErrors', function() {
it('reports rejections whose reason is an Error', function() {
const globals = browserGlobals();
const handler = jasmine.createSpy('errorHandler');
const errors = new jasmineUnderTest.GlobalErrors(globals.global);
const errors = new jasmineUnderTest.GlobalErrors(
globals.global,
() => ({})
);
errors.install();
errors.pushListener(handler);
@@ -264,12 +437,129 @@ 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 globals = nodeGlobals();
const errors = new jasmineUnderTest.GlobalErrors(globals.global);
const errors = new jasmineUnderTest.GlobalErrors(
globals.global,
() => ({})
);
const handler = jasmine.createSpy('errorHandler');
errors.install();
@@ -285,7 +575,10 @@ describe('GlobalErrors', function() {
it('substitutes a descriptive message when the error is falsy', function() {
const globals = nodeGlobals();
const errors = new jasmineUnderTest.GlobalErrors(globals.global);
const errors = new jasmineUnderTest.GlobalErrors(
globals.global,
() => ({})
);
const handler = jasmine.createSpy('errorHandler');
errors.install();
@@ -306,7 +599,10 @@ describe('GlobalErrors', function() {
const handler0 = jasmine.createSpy('handler0');
const handler1 = jasmine.createSpy('handler1');
const overrideHandler = jasmine.createSpy('overrideHandler');
const errors = new jasmineUnderTest.GlobalErrors(globals.global);
const errors = new jasmineUnderTest.GlobalErrors(
globals.global,
() => ({})
);
errors.install();
errors.pushListener(handler0);
@@ -331,7 +627,10 @@ describe('GlobalErrors', function() {
const handler0 = jasmine.createSpy('handler0');
const handler1 = jasmine.createSpy('handler1');
const overrideHandler = jasmine.createSpy('overrideHandler');
const errors = new jasmineUnderTest.GlobalErrors(globals.global);
const errors = new jasmineUnderTest.GlobalErrors(
globals.global,
() => ({})
);
errors.install();
errors.pushListener(handler0);
@@ -356,7 +655,10 @@ describe('GlobalErrors', function() {
const globals = browserGlobals();
const handler = jasmine.createSpy('handler');
const overrideHandler = jasmine.createSpy('overrideHandler');
const errors = new jasmineUnderTest.GlobalErrors(globals.global);
const errors = new jasmineUnderTest.GlobalErrors(
globals.global,
() => ({})
);
errors.install();
errors.pushListener(handler);
@@ -381,7 +683,10 @@ describe('GlobalErrors', function() {
const handler0 = jasmine.createSpy('handler0');
const handler1 = jasmine.createSpy('handler1');
const overrideHandler = jasmine.createSpy('overrideHandler');
const errors = new jasmineUnderTest.GlobalErrors(globals.global);
const errors = new jasmineUnderTest.GlobalErrors(
globals.global,
() => ({})
);
errors.install();
errors.pushListener(handler0);
@@ -424,7 +729,11 @@ describe('GlobalErrors', function() {
});
function browserGlobals() {
const listeners = { error: [], unhandledrejection: [] };
const listeners = {
error: [],
unhandledrejection: [],
rejectionhandled: []
};
return {
listeners,
global: {
@@ -441,7 +750,11 @@ describe('GlobalErrors', function() {
}
function nodeGlobals() {
const listeners = { uncaughtException: [], unhandledRejection: [] };
const listeners = {
uncaughtException: [],
unhandledRejection: [],
rejectionHandled: []
};
return {
listeners,
global: {
@@ -465,13 +778,13 @@ describe('GlobalErrors', function() {
};
}
function dispatchEvent(listeners, eventName, event) {
function dispatchEvent(listeners, eventName, ...args) {
expect(listeners[eventName].length)
.withContext(`number of ${eventName} listeners`)
.toBeGreaterThan(0);
for (const l of listeners[eventName]) {
l(event);
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

@@ -0,0 +1,63 @@
describe('AllOf', function() {
it('matches a single value', function() {
const matchersUtil = new jasmineUnderTest.MatchersUtil();
const allOf = new jasmineUnderTest.AllOf('foo');
expect(allOf.asymmetricMatch('foo', matchersUtil)).toBeTrue();
});
it('matches a single matcher', function() {
const matchersUtil = new jasmineUnderTest.MatchersUtil();
const allOf = new jasmineUnderTest.AllOf(
new jasmineUnderTest.StringContaining('oo')
);
expect(allOf.asymmetricMatch('foo', matchersUtil)).toBeTrue();
});
it('matches multiple matchers', function() {
const matchersUtil = new jasmineUnderTest.MatchersUtil();
const allOf = new jasmineUnderTest.AllOf(
new jasmineUnderTest.StringContaining('o'),
new jasmineUnderTest.StringContaining('f')
);
expect(allOf.asymmetricMatch('foo', matchersUtil)).toBeTrue();
});
it('does not match when value does not match', function() {
const matchersUtil = new jasmineUnderTest.MatchersUtil();
const allOf = new jasmineUnderTest.AllOf('bar');
expect(allOf.asymmetricMatch('foo', matchersUtil)).toBeFalse();
});
it('does not match when any matchers fail', function() {
const matchersUtil = new jasmineUnderTest.MatchersUtil();
const allOf = new jasmineUnderTest.AllOf(
new jasmineUnderTest.StringContaining('o'),
new jasmineUnderTest.StringContaining('x')
);
expect(allOf.asymmetricMatch('foo', matchersUtil)).toBeFalse();
});
it('jasmineToStrings itself', function() {
const matcher = new jasmineUnderTest.AllOf('o');
const pp = jasmine.createSpy('pp').and.returnValue('sample');
expect(matcher.jasmineToString(pp)).toEqual('<jasmine.allOf(sample)>');
expect(pp).toHaveBeenCalledWith(['o']);
});
describe('when called without an argument', function() {
it('tells the user to pass a constructor argument', function() {
expect(function() {
new jasmineUnderTest.AllOf();
}).toThrowError(
TypeError,
'jasmine.allOf() expects at least one argument to be passed.'
);
});
});
});

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;

View File

@@ -1600,6 +1600,16 @@ describe('Env integration', function() {
suiteFullNameToId[e.fullName] = e.id;
});
// Clone args to work around Jasmine mutating the result after passing it
// to the reporter event.
// TODO: remove this once Jasmine no longer does that
const clone = structuredClone.bind(globalThis);
reporter.specStarted.calls.saveArgumentsByValue(clone);
reporter.specDone.calls.saveArgumentsByValue(clone);
reporter.specStarted.calls.saveArgumentsByValue(clone);
reporter.suiteDone.calls.saveArgumentsByValue(clone);
env.configure({ random: false });
env.addReporter(reporter);
env.it('a top level spec', function() {});
@@ -1636,110 +1646,179 @@ describe('Env integration', function() {
expect(reporter.specStarted.calls.count()).toBe(6);
expect(reporter.specDone.calls.count()).toBe(6);
expect(reporter.specStarted).toHaveBeenCalledWith(
jasmine.objectContaining({
description: 'a top level spec',
parentSuiteId: null
})
);
expect(reporter.specDone).toHaveBeenCalledWith(
jasmine.objectContaining({
description: 'a top level spec',
status: 'passed',
parentSuiteId: null
})
);
expect(reporter.specStarted).toHaveBeenCalledWith(
jasmine.objectContaining({
description: 'with a spec',
parentSuiteId: suiteFullNameToId['A Suite']
})
);
expect(reporter.specDone).toHaveBeenCalledWith(
jasmine.objectContaining({
description: 'with a spec',
status: 'passed',
parentSuiteId: suiteFullNameToId['A Suite']
})
);
const baseSpecEvent = {
passedExpectations: [],
failedExpectations: [],
deprecationWarnings: [],
pendingReason: '',
duration: null,
properties: null,
debugLogs: null,
id: jasmine.any(String),
filename: jasmine.any(String)
};
expect(reporter.specStarted).toHaveBeenCalledWith(
jasmine.objectContaining({
description: "with an x'ed spec",
parentSuiteId: suiteFullNameToId['A Suite with a nested suite']
})
);
expect(reporter.specDone).toHaveBeenCalledWith(
jasmine.objectContaining({
description: "with an x'ed spec",
status: 'pending',
parentSuiteId: suiteFullNameToId['A Suite with a nested suite']
})
);
expect(reporter.specStarted.calls.argsFor(0)[0]).toEqual({
...baseSpecEvent,
description: 'a top level spec',
fullName: 'a top level spec',
parentSuiteId: null
});
expect(reporter.specDone.calls.argsFor(0)[0]).toEqual({
...baseSpecEvent,
description: 'a top level spec',
fullName: 'a top level spec',
status: 'passed',
parentSuiteId: null,
duration: jasmine.any(Number)
});
expect(reporter.specStarted.calls.argsFor(1)[0]).toEqual({
...baseSpecEvent,
description: 'with a spec',
fullName: 'A Suite with a spec',
parentSuiteId: suiteFullNameToId['A Suite']
});
expect(reporter.specDone.calls.argsFor(1)[0]).toEqual({
...baseSpecEvent,
description: 'with a spec',
fullName: 'A Suite with a spec',
status: 'passed',
parentSuiteId: suiteFullNameToId['A Suite'],
passedExpectations: [
{ matcherName: 'toBe', message: 'Passed.', stack: '', passed: true }
],
duration: jasmine.any(Number)
});
expect(reporter.specStarted).toHaveBeenCalledWith(
jasmine.objectContaining({
description: 'with a spec',
parentSuiteId: suiteFullNameToId['A Suite with a nested suite']
})
);
expect(reporter.specDone).toHaveBeenCalledWith(
jasmine.objectContaining({
description: 'with a spec',
status: 'failed',
parentSuiteId: suiteFullNameToId['A Suite with a nested suite']
})
);
expect(reporter.specStarted.calls.argsFor(2)[0]).toEqual({
...baseSpecEvent,
description: "with an x'ed spec",
fullName: "A Suite with a nested suite with an x'ed spec",
parentSuiteId: suiteFullNameToId['A Suite with a nested suite'],
pendingReason: 'Temporarily disabled with xit'
});
expect(reporter.specDone.calls.argsFor(2)[0]).toEqual({
...baseSpecEvent,
description: "with an x'ed spec",
fullName: "A Suite with a nested suite with an x'ed spec",
status: 'pending',
parentSuiteId: suiteFullNameToId['A Suite with a nested suite'],
pendingReason: 'Temporarily disabled with xit',
duration: jasmine.any(Number)
});
expect(reporter.specStarted).toHaveBeenCalledWith(
jasmine.objectContaining({
description: 'is pending',
parentSuiteId:
suiteFullNameToId['A Suite with only non-executable specs']
})
);
expect(reporter.specDone).toHaveBeenCalledWith(
jasmine.objectContaining({
description: 'is pending',
status: 'pending',
parentSuiteId:
suiteFullNameToId['A Suite with only non-executable specs']
})
);
expect(reporter.specStarted.calls.argsFor(3)[0]).toEqual({
...baseSpecEvent,
description: 'with a spec',
fullName: 'A Suite with a nested suite with a spec',
parentSuiteId: suiteFullNameToId['A Suite with a nested suite']
});
expect(reporter.specDone.calls.argsFor(3)[0]).toEqual({
...baseSpecEvent,
description: 'with a spec',
fullName: 'A Suite with a nested suite with a spec',
status: 'failed',
parentSuiteId: suiteFullNameToId['A Suite with a nested suite'],
failedExpectations: [
jasmine.objectContaining({
matcherName: 'toBe',
message: 'Expected true to be false.'
})
],
duration: jasmine.any(Number)
});
expect(reporter.suiteStarted).toHaveBeenCalledWith(
jasmine.objectContaining({
description: 'A Suite',
parentSuiteId: null
})
);
expect(reporter.suiteDone).toHaveBeenCalledWith(
jasmine.objectContaining({
description: 'A Suite',
status: 'passed',
parentSuiteId: null
})
);
expect(reporter.specStarted.calls.argsFor(4)[0]).toEqual({
...baseSpecEvent,
description: 'is pending',
fullName: 'A Suite with only non-executable specs is pending',
parentSuiteId: suiteFullNameToId['A Suite with only non-executable specs']
});
expect(reporter.specDone.calls.argsFor(4)[0]).toEqual({
...baseSpecEvent,
description: 'is pending',
status: 'pending',
fullName: 'A Suite with only non-executable specs is pending',
parentSuiteId:
suiteFullNameToId['A Suite with only non-executable specs'],
duration: jasmine.any(Number)
});
expect(reporter.suiteStarted).toHaveBeenCalledWith(
jasmine.objectContaining({
description: 'with a nested suite',
parentSuiteId: suiteFullNameToId['A Suite']
})
);
expect(reporter.suiteDone).toHaveBeenCalledWith(
jasmine.objectContaining({
description: 'with a nested suite',
status: 'passed',
parentSuiteId: suiteFullNameToId['A Suite']
})
);
expect(reporter.specStarted.calls.argsFor(5)[0]).toEqual({
...baseSpecEvent,
description: 'is xed',
fullName: 'A Suite with only non-executable specs is xed',
parentSuiteId:
suiteFullNameToId['A Suite with only non-executable specs'],
pendingReason: 'Temporarily disabled with xit'
});
expect(reporter.specDone.calls.argsFor(5)[0]).toEqual({
...baseSpecEvent,
description: 'is xed',
status: 'pending',
fullName: 'A Suite with only non-executable specs is xed',
parentSuiteId:
suiteFullNameToId['A Suite with only non-executable specs'],
pendingReason: 'Temporarily disabled with xit',
duration: jasmine.any(Number)
});
const suiteDone = reporter.suiteDone.calls.argsFor(0)[0];
expect(typeof suiteDone.duration).toBe('number');
expect(reporter.suiteStarted.calls.count()).toBe(3);
expect(reporter.suiteDone.calls.count()).toBe(3);
const suiteResult = reporter.suiteStarted.calls.argsFor(0)[0];
expect(suiteResult.description).toEqual('A Suite');
const baseSuiteEvent = {
id: jasmine.any(String),
filename: jasmine.any(String),
failedExpectations: [],
deprecationWarnings: [],
duration: null,
properties: null
};
expect(reporter.suiteStarted.calls.argsFor(0)[0]).toEqual({
...baseSuiteEvent,
description: 'A Suite',
fullName: 'A Suite',
parentSuiteId: null
});
expect(reporter.suiteDone.calls.argsFor(2)[0]).toEqual({
...baseSuiteEvent,
description: 'A Suite',
fullName: 'A Suite',
status: 'passed',
parentSuiteId: null,
duration: jasmine.any(Number)
});
expect(reporter.suiteStarted.calls.argsFor(1)[0]).toEqual({
...baseSuiteEvent,
description: 'with a nested suite',
fullName: 'A Suite with a nested suite',
parentSuiteId: suiteFullNameToId['A Suite']
});
expect(reporter.suiteDone.calls.argsFor(0)[0]).toEqual({
...baseSuiteEvent,
description: 'with a nested suite',
status: 'passed',
fullName: 'A Suite with a nested suite',
parentSuiteId: suiteFullNameToId['A Suite'],
duration: jasmine.any(Number)
});
expect(reporter.suiteStarted.calls.argsFor(2)[0]).toEqual({
...baseSuiteEvent,
description: 'with only non-executable specs',
fullName: 'A Suite with only non-executable specs',
parentSuiteId: suiteFullNameToId['A Suite']
});
expect(reporter.suiteDone.calls.argsFor(1)[0]).toEqual({
...baseSuiteEvent,
description: 'with only non-executable specs',
status: 'passed',
fullName: 'A Suite with only non-executable specs',
parentSuiteId: suiteFullNameToId['A Suite'],
duration: jasmine.any(Number)
});
});
it('reports focused specs and suites as expected', async function() {
@@ -2198,6 +2277,34 @@ describe('Env integration', function() {
await env.execute();
});
it('Custom matchers set in top-level beforeAll should be available to all specs and suites', async function() {
const matchers = {
toFoo: function() {}
};
env.beforeAll(function() {
env.addMatchers(matchers);
});
env.describe('suite - top-level', function() {
env.it('has access to the custom matcher', function() {
expect(env.expect().toFoo).toBeDefined();
});
env.describe('suite - nested', function() {
env.it('has access to the custom matcher', function() {
expect(env.expect().toFoo).toBeDefined();
});
});
});
env.it('spec - top-level - has access to the custom matcher', function() {
expect(env.expect().toFoo).toBeDefined();
});
await env.execute();
});
it('throws an exception if you try to create a spy outside of a runnable', async function() {
const obj = { fn: function() {} };
let exception;
@@ -2262,17 +2369,54 @@ describe('Env integration', function() {
);
});
it('throws an exception if you try to getSpecProperty outside of a spec', async function() {
const env = new jasmineUnderTest.Env();
let exception;
env.describe('a suite', function() {
env.it('a spec');
try {
env.getSpecProperty('a prop');
} catch (e) {
exception = e;
}
env.it('has a test', function() {});
});
await env.execute();
expect(exception.message).toBe(
"'getSpecProperty' was used when there was no current spec"
);
});
it('reports test properties on specs', async function() {
const env = new jasmineUnderTest.Env(),
reporter = jasmine.createSpyObj('reporter', ['suiteDone', 'specDone']);
reporter.specDone.and.callFake(function(e) {
expect(e.properties).toEqual({ a: 'Bee' });
expect(e.properties).toEqual({
fromBeforeEach: 'Pie',
fromSpecInnerCallback: 'Bee',
willChangeInAfterEach: 2,
fromAfterEach: 'Cheese'
});
});
env.addReporter(reporter);
env.beforeEach(function() {
env.setSpecProperty('fromBeforeEach', 'Pie');
});
env.afterEach(function() {
env.setSpecProperty(
'willChangeInAfterEach',
env.getSpecProperty('willChangeInAfterEach') + 1
);
env.setSpecProperty('fromAfterEach', 'Cheese');
});
env.it('calls setSpecProperty', function() {
env.setSpecProperty('a', 'Bee');
env.setSpecProperty('fromSpecInnerCallback', 'Bee');
env.setSpecProperty('willChangeInAfterEach', 1);
});
await env.execute();
@@ -3697,7 +3841,7 @@ describe('Env integration', function() {
function browserEventMethods() {
return {
listeners_: { error: [], unhandledrejection: [] },
listeners_: { error: [], unhandledrejection: [], rejectionhandled: [] },
addEventListener(eventName, listener) {
this.listeners_[eventName].push(listener);
},

View File

@@ -88,10 +88,14 @@ describe('Global error handling (integration)', function() {
const globalErrors = new jasmineUnderTest.GlobalErrors(global);
const onerror = jasmine.createSpy('onerror');
globalErrors.pushListener(onerror);
spyOn(jasmineUnderTest, 'GlobalErrors').and.returnValue(globalErrors);
env.cleanup_();
env = new jasmineUnderTest.Env({ suppressLoadErrors: true });
env = new jasmineUnderTest.Env({
suppressLoadErrors: true,
GlobalErrors: function() {
return globalErrors;
}
});
const reporter = jasmine.createSpyObj('reporter', [
'jasmineDone',
'suiteDone',
@@ -802,6 +806,246 @@ describe('Global error handling (integration)', function() {
);
}
});
describe('When the detectLateRejectionHandling config option is set', function() {
describe('When the unhandled rejection event has a promise', function() {
function makeEvent(suffix) {
const reason = `rejection ${suffix}`;
const promise = Promise.reject(reason);
promise.catch(() => {});
return { reason, promise };
}
let global, reporter;
beforeEach(function() {
global = {
...browserEventMethods(),
setTimeout: function(fn, delay) {
return setTimeout(fn, delay);
},
clearTimeout: function(fn, delay) {
clearTimeout(fn, delay);
},
queueMicrotask: function(fn) {
queueMicrotask(fn);
}
};
spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global);
env.cleanup_();
env = new jasmineUnderTest.Env();
env.configure({ detectLateRejectionHandling: true });
reporter = jasmine.createSpyObj('fakeReporter', [
'specDone',
'suiteDone'
]);
env.addReporter(reporter);
});
describe('During spec execution', function() {
it('reports the rejection unless a corresponding rejection handled event occurs', async function() {
env.describe('A suite', function() {
env.it('fails', function(specDone) {
setTimeout(function() {
const events = ['spec 1', 'spec 2'].map(makeEvent);
for (const e of events) {
dispatchErrorEvent(global, 'unhandledrejection', e);
}
dispatchErrorEvent(global, 'rejectionhandled', events[0]);
specDone();
});
});
});
await env.execute();
expect(reporter.specDone).toHaveBeenCalledWith(
jasmine.objectContaining({
fullName: 'A suite fails',
failedExpectations: [
// Only the second rejection should be reported, since the first
// one was eventually handled.
jasmine.objectContaining({
message:
'Unhandled promise rejection: rejection spec 2 thrown'
})
]
})
);
});
});
describe('During beforeAll execution', function() {
it('reports the rejection unless a corresponding rejection handled event occurs by the end of the beforeAll', async function() {
env.describe('A suite', function() {
let events;
env.beforeAll(function(beforeAllDone) {
setTimeout(function() {
events = ['suite 1', 'suite 2'].map(makeEvent);
for (const e of events) {
dispatchErrorEvent(global, 'unhandledrejection', e);
}
dispatchErrorEvent(global, 'rejectionhandled', events[0]);
beforeAllDone();
});
});
env.it('is a spec', function(specDone) {
setTimeout(function() {
// Should not prevent the second rejection from being reported
dispatchErrorEvent(global, 'rejectionhandled', events[1]);
specDone();
});
});
});
await env.execute();
expect(reporter.suiteDone).toHaveBeenCalledWith(
jasmine.objectContaining({
fullName: 'A suite',
failedExpectations: [
// Only the second rejection should be reported, since the first
// one was eventually handled.
jasmine.objectContaining({
message:
'Unhandled promise rejection: rejection suite 2 thrown'
})
]
})
);
});
});
describe('During afterAll execution', function() {
it('reports the rejection unless a corresponding rejection handled event occurs by the end of the afterAll', async function() {
env.describe('A suite', function() {
let events;
env.afterAll(function(beforeAllDone) {
setTimeout(function() {
events = ['suite 1', 'suite 2'].map(makeEvent);
for (const e of events) {
dispatchErrorEvent(global, 'unhandledrejection', e);
}
dispatchErrorEvent(global, 'rejectionhandled', events[0]);
beforeAllDone();
});
});
env.it('is a spec', function() {});
});
await env.execute();
expect(reporter.suiteDone).toHaveBeenCalledWith(
jasmine.objectContaining({
fullName: 'A suite',
failedExpectations: [
// Only the second rejection should be reported, since the first
// one was eventually handled.
jasmine.objectContaining({
message:
'Unhandled promise rejection: rejection suite 2 thrown'
})
]
})
);
});
});
});
describe("When the unhandled rejection event doesn't have a promise", function() {
it('reports the rejection', async function() {
const global = {
...browserEventMethods(),
setTimeout: function(fn, delay) {
return setTimeout(fn, delay);
},
clearTimeout: function(fn, delay) {
clearTimeout(fn, delay);
},
queueMicrotask: function(fn) {
queueMicrotask(fn);
}
};
spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global);
env.cleanup_();
env = new jasmineUnderTest.Env();
env.configure({ detectLateRejectionHandling: true });
const reporter = jasmine.createSpyObj('fakeReporter', [
'specDone',
'suiteDone'
]);
env.addReporter(reporter);
env.describe('A suite', function() {
env.it('fails', function(specDone) {
setTimeout(function() {
dispatchErrorEvent(global, 'unhandledrejection', {
reason: 'fail'
});
specDone();
});
});
});
await env.execute();
expect(reporter.specDone).toHaveFailedExpectationsForRunnable(
'A suite fails',
['Unhandled promise rejection: fail thrown']
);
});
});
});
});
it('works when the suite is run multiple times', async function() {
const global = {
...browserEventMethods(),
setTimeout: function(fn, delay) {
return setTimeout(fn, delay);
},
clearTimeout: function(fn, delay) {
clearTimeout(fn, delay);
},
queueMicrotask: function(fn) {
queueMicrotask(fn);
}
};
spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global);
env.cleanup_();
env = new jasmineUnderTest.Env();
env.configure({ autoCleanClosures: false });
const reporter = jasmine.createSpyObj('fakeReporter', ['specDone']);
env.addReporter(reporter);
env.it('fails', function(specDone) {
setTimeout(function() {
dispatchErrorEvent(global, 'error', { error: 'fail' });
specDone();
});
});
await env.execute();
reporter.specDone.calls.reset();
await env.execute();
expect(reporter.specDone).toHaveFailedExpectationsForRunnable('fails', [
'fail thrown'
]);
});
describe('#spyOnGlobalErrorsAsync', function() {
@@ -1147,7 +1391,7 @@ describe('Global error handling (integration)', function() {
function browserEventMethods() {
return {
listeners_: { error: [], unhandledrejection: [] },
listeners_: { error: [], unhandledrejection: [], rejectionhandled: [] },
addEventListener(eventName, listener) {
this.listeners_[eventName].push(listener);
},

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 = {
@@ -783,7 +768,22 @@ describe('matchersUtil', function() {
a2[0] = 1;
const diffBuilder = new jasmineUnderTest.DiffBuilder();
expect(matchersUtil.equals(a1, a2, diffBuilder)).toBe(false);
jasmine.debugLog('Diff: ' + diffBuilder.getMessage());
jasmine.debugLog(
'a1 keys: ' +
jasmine.basicPrettyPrinter_(
jasmineUnderTest.MatchersUtil.keys(a1)
)
);
jasmine.debugLog(
'a2 keys: ' +
jasmine.basicPrettyPrinter_(
jasmineUnderTest.MatchersUtil.keys(a2)
)
);
jasmine.debugLog('a1 length:' + a1.length);
jasmine.debugLog('a2 length:' + a2.length);
jasmine.debugLog('a1[0]: ' + a1[0]);
jasmine.debugLog('a2[0]: ' + a2[0]);
}
);

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

@@ -0,0 +1,5 @@
(function() {
specHelpers.callerFilenameShim = function(fn) {
return fn();
};
})();

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

@@ -23,9 +23,13 @@ module.exports = {
'helpers/BrowserFlags.js',
'helpers/domHelpers.js',
'helpers/integrationMatchers.js',
'helpers/callerFilenameShim.js',
'helpers/defineJasmineUnderTest.js',
'helpers/resetEnv.js'
],
env: {
forbidDuplicateNames: true
},
random: true,
browser: {
name: process.env.JASMINE_BROWSER || 'firefox',

View File

@@ -8,9 +8,13 @@
"helpers/init.js",
"helpers/domHelpers.js",
"helpers/integrationMatchers.js",
"helpers/callerFilenameShim.js",
"helpers/overrideConsoleLogForCircleCi.js",
"helpers/nodeDefineJasmineUnderTest.js",
"helpers/resetEnv.js"
],
"env": {
"forbidDuplicateNames": true
},
"random": true
}

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

@@ -0,0 +1,219 @@
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,
/**
* The number of extra stack frames inserted by a wrapper around {@link it}
* or by some other local modification. Jasmine uses this to determine the
* filename for {@link SpecStartedEvent} and {@link SpecDoneEvent}.
* @name Configuration#extraItStackFrames
* @since 5.13.0
* @type number
* @default 0
*/
extraItStackFrames: 0,
/**
* The number of extra stack frames inserted by a wrapper around
* {@link describe} or by some other local modification. Jasmine uses this
* to determine the filename for {@link SpecStartedEvent} and
* {@link SpecDoneEvent}.
* @name Configuration#extraDescribeStackFrames
* @since 5.13.0
* @type number
* @default 0
*/
extraDescribeStackFrames: 0
};
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;
}
// 0 is a valid value for both of these, so a truthiness check wouldn't work
if (typeof changes.extraItStackFrames !== 'undefined') {
this.#values.extraItStackFrames = changes.extraItStackFrames;
}
if (typeof changes.extraDescribeStackFrames !== 'undefined') {
this.#values.extraDescribeStackFrames =
changes.extraDescribeStackFrames;
}
}
}
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

@@ -1,4 +1,6 @@
getJasmineRequireObj().Env = function(j$) {
const DEFAULT_IT_DESCRIBE_STACK_DEPTH = 3;
/**
* @class Env
* @since 2.0.0
@@ -7,11 +9,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 +27,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 +60,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 +88,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 +107,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 +310,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 +332,7 @@ getJasmineRequireObj().Env = function(j$) {
expectationFactory,
asyncExpectationFactory,
onLateError: recordLateError,
specResultCallback,
specStarted,
queueRunnerFactory
runQueue
});
topSuite = suiteBuilder.topSuite;
const deprecator = new j$.Deprecator(topSuite);
@@ -481,11 +355,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 +369,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 +436,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 +448,7 @@ getJasmineRequireObj().Env = function(j$) {
* @see custom_reporter
*/
this.provideFallbackReporter = function(reporterToAdd) {
reporter.provideFallbackReporter(reporterToAdd);
reportDispatcher.provideFallbackReporter(reporterToAdd);
};
/**
@@ -587,7 +462,7 @@ getJasmineRequireObj().Env = function(j$) {
throw new Error('Reporters cannot be removed via Env in parallel mode');
}
reporter.clearReporters();
reportDispatcher.clearReporters();
};
/**
@@ -720,14 +595,14 @@ getJasmineRequireObj().Env = function(j$) {
this.describe = function(description, definitionFn) {
ensureIsNotNested('describe');
const filename = callerCallerFilename();
const filename = indirectCallerFilename(describeStackDepth());
return suiteBuilder.describe(description, definitionFn, filename)
.metadata;
};
this.xdescribe = function(description, definitionFn) {
ensureIsNotNested('xdescribe');
const filename = callerCallerFilename();
const filename = indirectCallerFilename(describeStackDepth());
return suiteBuilder.xdescribe(description, definitionFn, filename)
.metadata;
};
@@ -735,52 +610,58 @@ getJasmineRequireObj().Env = function(j$) {
this.fdescribe = function(description, definitionFn) {
ensureIsNotNested('fdescribe');
ensureNonParallel('fdescribe');
const filename = callerCallerFilename();
const filename = indirectCallerFilename(describeStackDepth());
return suiteBuilder.fdescribe(description, definitionFn, filename)
.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();
const filename = indirectCallerFilename(itStackDepth());
return suiteBuilder.it(description, fn, timeout, filename).metadata;
};
this.xit = function(description, fn, timeout) {
ensureIsNotNested('xit');
const filename = callerCallerFilename();
const filename = indirectCallerFilename(itStackDepth());
return suiteBuilder.xit(description, fn, timeout, filename).metadata;
};
this.fit = function(description, fn, timeout) {
ensureIsNotNested('fit');
ensureNonParallel('fit');
const filename = callerCallerFilename();
const filename = indirectCallerFilename(itStackDepth());
return suiteBuilder.fit(description, fn, timeout, filename).metadata;
};
function itStackDepth() {
return DEFAULT_IT_DESCRIBE_STACK_DEPTH + config.extraItStackFrames;
}
function describeStackDepth() {
return DEFAULT_IT_DESCRIBE_STACK_DEPTH + config.extraDescribeStackFrames;
}
/**
* 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,17 +823,16 @@ getJasmineRequireObj().Env = function(j$) {
};
this.cleanup_ = function() {
if (globalErrors) {
globalErrors.uninstall();
}
uninstallGlobalErrors();
};
}
function callerCallerFilename() {
function indirectCallerFilename(depth) {
const frames = new j$.StackTrace(new Error()).frames;
// frames[3] should always exist except in Jasmine's own tests, which bypass
// the global it/describe layer, but don't crash if it doesn't.
return frames[3] && frames[3].file;
// The specified frame should always exist except in Jasmine's own tests,
// which bypass the global it/describe layer, but could be absent in case
// of misconfiguration. Don't crash if it's absent.
return frames[depth] && frames[depth].file;
}
return Env;

View File

@@ -1,27 +1,35 @@
getJasmineRequireObj().GlobalErrors = function(j$) {
class GlobalErrors {
#getConfig;
#adapter;
#handlers;
#overrideHandler;
#onRemoveOverrideHandler;
#pendingUnhandledRejections;
constructor(global) {
constructor(global, getConfig) {
global = global || j$.getGlobal();
const dispatchError = this.#dispatchError.bind(this);
this.#getConfig = getConfig;
this.#pendingUnhandledRejections = new Map();
this.#handlers = [];
this.#overrideHandler = null;
this.#onRemoveOverrideHandler = null;
const dispatch = {
onUncaughtException: this.#onUncaughtException.bind(this),
onUnhandledRejection: this.#onUnhandledRejection.bind(this),
onRejectionHandled: this.#onRejectionHandled.bind(this)
};
if (
global.process &&
global.process.listeners &&
j$.isFunction_(global.process.on)
) {
this.#adapter = new NodeAdapter(global, dispatchError);
this.#adapter = new NodeAdapter(global, dispatch);
} else {
this.#adapter = new BrowserAdapter(global, dispatchError);
this.#adapter = new BrowserAdapter(global, dispatch);
}
this.#handlers = [];
this.#overrideHandler = null;
this.#onRemoveOverrideHandler = null;
}
install() {
@@ -69,6 +77,41 @@ getJasmineRequireObj().GlobalErrors = function(j$) {
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) {
@@ -89,24 +132,29 @@ getJasmineRequireObj().GlobalErrors = function(j$) {
class BrowserAdapter {
#global;
#dispatchError;
#dispatch;
#onError;
#onUnhandledRejection;
#onRejectionHandled;
constructor(global, dispatchError) {
constructor(global, dispatch) {
this.#global = global;
this.#dispatchError = dispatchError;
this.#onError = event => this.#dispatchError(event.error, event);
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() {
@@ -115,50 +163,55 @@ getJasmineRequireObj().GlobalErrors = function(j$) {
'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)) {
event.reason.jasmineMessage =
'Unhandled promise rejection: ' + event.reason;
this.#dispatchError(event.reason, event);
reason = event.reason;
reason.jasmineMessage = jasmineMessage;
} else {
this.#dispatchError(
'Unhandled promise rejection: ' + event.reason,
event
);
reason = jasmineMessage;
}
this.#dispatch.onUnhandledRejection(reason, event.promise, event);
}
#rejectionHandledHandler(event) {
this.#dispatch.onRejectionHandled(event.promise);
}
}
class NodeAdapter {
#global;
#dispatchError;
#dispatch;
#originalHandlers;
#jasmineHandlers;
#onError;
#onUnhandledRejection;
constructor(global, dispatchError) {
constructor(global, dispatch) {
this.#global = global;
this.#dispatchError = dispatchError;
this.#dispatch = dispatch;
this.#jasmineHandlers = {};
this.#originalHandlers = {};
this.#onError = error =>
this.#eventHandler(error, 'uncaughtException', 'Uncaught exception');
this.#onUnhandledRejection = error =>
this.#eventHandler(
error,
'unhandledRejection',
'Unhandled promise rejection'
);
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('uncaughtException', this.onError);
this.#installHandler('unhandledRejection', this.onUnhandledRejection);
this.#installHandler(
'rejectionHandled',
this.#dispatch.onRejectionHandled
);
}
uninstall() {
@@ -190,31 +243,49 @@ getJasmineRequireObj().GlobalErrors = function(j$) {
this.#global.process.on(errorType, handler);
}
#eventHandler(error, errorType, jasmineMessage) {
#augmentError(error, isUnhandledRejection) {
let jasmineMessagePrefix;
if (isUnhandledRejection) {
jasmineMessagePrefix = 'Unhandled promise rejection';
} else {
jasmineMessagePrefix = 'Uncaught exception';
}
if (j$.isError_(error)) {
error.jasmineMessage = jasmineMessage + ': ' + error;
error.jasmineMessage = jasmineMessagePrefix + ': ' + error;
return error;
} else {
let substituteMsg;
if (error) {
substituteMsg = jasmineMessage + ': ' + error;
substituteMsg = jasmineMessagePrefix + ': ' + error;
} else {
substituteMsg = jasmineMessage + ' with no error or message';
substituteMsg = jasmineMessagePrefix + ' with no error or message';
}
if (errorType === 'unhandledRejection') {
if (isUnhandledRejection) {
substituteMsg +=
'\n' +
'(Tip: to get a useful stack trace, use ' +
'Promise.reject(new Error(...)) instead of Promise.reject(' +
'Promise.reject(n' +
'ew Error(...)) instead of Promise.reject(' +
(error ? '...' : '') +
').)';
}
error = new Error(substituteMsg);
return new Error(substituteMsg);
}
}
this.#dispatchError(error);
onError(error) {
error = this.#augmentError(error, false);
this.#dispatch.onUncaughtException(error);
}
onUnhandledRejection(reason, promise) {
reason = this.#augmentError(reason, true);
this.#dispatch.onUnhandledRejection(reason, promise);
}
}

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,322 +1,248 @@
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 - 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. You can fix that by setting
* {@link Configuration#extraItStackFrames}.
* @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 - 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.
* @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
@@ -351,13 +277,45 @@ getJasmineRequireObj().Spec = function(j$) {
* @returns {Array.<string>}
* @since 5.7.0
*/
getPath: this.getPath.bind(this)
getPath: this.getPath.bind(this),
/**
* 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. You can fix that by setting
* {@link Configuration#extraItStackFrames}.
* @name Spec#filename
* @readonly
* @type {string}
* @since 5.13.0
*/
filename: this.filename
};
}
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

@@ -1,86 +1,275 @@
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 - 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. You can fix
* that by setting {@link Configuration#extraDescribeStackFrames}.
* @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,250 +277,77 @@ 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 - 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;
};
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 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 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 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. You
* can fix that by setting {@link Configuration#extraItStackFrames}.
* @name Suite#filename
* @readonly
* @type {string}
* @since 5.13.0
*/
this.filename = suite.filename;
}
});
/**
* 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;

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

@@ -0,0 +1,306 @@
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.#executionTree.topSuite.id
);
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

@@ -0,0 +1,27 @@
getJasmineRequireObj().AllOf = function(j$) {
function AllOf() {
const expectedValues = Array.from(arguments);
if (expectedValues.length === 0) {
throw new TypeError(
'jasmine.allOf() expects at least one argument to be passed.'
);
}
this.expectedValues = expectedValues;
}
AllOf.prototype.asymmetricMatch = function(other, matchersUtil) {
for (const expectedValue of this.expectedValues) {
if (!matchersUtil.equals(other, expectedValue)) {
return false;
}
}
return true;
};
AllOf.prototype.jasmineToString = function(pp) {
return '<jasmine.allOf(' + pp(this.expectedValues) + ')>';
};
return AllOf;
};

View File

@@ -221,6 +221,19 @@ getJasmineRequireObj().base = function(j$, jasmineGlobal) {
);
};
/**
* Get an {@link AsymmetricEqualityTester} that will succeed if the actual
* value being compared matches every provided equality tester.
* @name asymmetricEqualityTesters.allOf
* @emittedName jasmine.allOf
* @since 5.13.0
* @function
* @param {...*} arguments - The asymmetric equality checkers to compare.
*/
j$.allOf = function() {
return new j$.AllOf(...arguments);
};
/**
* Get an {@link AsymmetricEqualityTester} that will succeed if the actual
* value being compared is an instance of the specified class/constructor.

View File

@@ -35,6 +35,7 @@ var getJasmineRequireObj = (function(jasmineGlobal) {
j$.util = jRequire.util(j$);
j$.errors = jRequire.errors();
j$.formatErrorMsg = jRequire.formatErrorMsg();
j$.AllOf = jRequire.AllOf(j$);
j$.Any = jRequire.Any(j$);
j$.Anything = jRequire.Anything(j$);
j$.CallTracker = jRequire.CallTracker(j$);
@@ -43,6 +44,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 +72,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 +86,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

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