Compare commits

..

32 Commits

Author SHA1 Message Date
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
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
32 changed files with 3275 additions and 2171 deletions
+1 -1
View File
@@ -30,7 +30,7 @@ Microsoft Edge) as well as Node.
| Environment | Supported versions | | Environment | Supported versions |
|-------------------|----------------------------------| |-------------------|----------------------------------|
| Node | 18.20.5+*, 20, 22, 24 | | Node | 18.20.5+*, 20, 22, 24 |
| Safari | 15*, 16*, 17* | | Safari | 16*, 17* |
| Chrome | Evergreen | | Chrome | Evergreen |
| Firefox | Evergreen, 102*, 115*, 128*, 140 | | Firefox | Evergreen, 102*, 115*, 128*, 140 |
| Edge | Evergreen | | Edge | Evergreen |
+74 -24
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 HtmlReporter(options) {
function config() { function config() {
return (options.env && options.env.configuration()) || {}; return (options.env && options.env.configuration()) || {};
@@ -98,6 +105,11 @@ jasmineRequire.HtmlReporter = function(j$) {
const deprecationWarnings = []; const deprecationWarnings = [];
const failures = []; const failures = [];
/**
* Initializes the reporter. Should be called before {@link Env#execute}.
* @function
* @name HtmlReporter#initialize
*/
this.initialize = function() { this.initialize = function() {
clearPrior(); clearPrior();
htmlReporterMain = createDom( htmlReporterMain = createDom(
@@ -881,6 +893,8 @@ jasmineRequire.HtmlReporter = function(j$) {
}; };
jasmineRequire.HtmlSpecFilter = function() { jasmineRequire.HtmlSpecFilter = function() {
// Legacy HTML spec filter, preserved for backward compatibility with
// boot files that predate HtmlExactSpecFilterV2
function HtmlSpecFilter(options) { function HtmlSpecFilter(options) {
const filterString = const filterString =
options && options &&
@@ -888,6 +902,13 @@ jasmineRequire.HtmlSpecFilter = function() {
options.filterString().replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); options.filterString().replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
const filterPattern = new RegExp(filterString); 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) { this.matches = function(specName) {
return filterPattern.test(specName); return filterPattern.test(specName);
}; };
@@ -921,38 +942,57 @@ jasmineRequire.ResultsNode = function() {
}; };
jasmineRequire.QueryString = function() { jasmineRequire.QueryString = function() {
function QueryString(options) { /**
this.navigateWithNewParam = function(key, value) { * Reads and manipulates the query string.
options.getWindowLocation().search = this.fullStringWithNewParam( * @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, key,
value value
); );
}; }
this.fullStringWithNewParam = function(key, value) { /**
const paramMap = queryStringToParamMap(); * 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; paramMap[key] = value;
return toQueryString(paramMap); 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); * 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 = []; let params = [];
const paramMap = {}; const paramMap = {};
@@ -972,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; return QueryString;
}; };
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -1,7 +1,7 @@
{ {
"name": "jasmine-core", "name": "jasmine-core",
"license": "MIT", "license": "MIT",
"version": "5.10.0", "version": "5.12.1",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/jasmine/jasmine.git" "url": "https://github.com/jasmine/jasmine.git"
@@ -52,7 +52,7 @@
"sass": "^1.58.3" "sass": "^1.58.3"
}, },
"browserslist": [ "browserslist": [
"Safari >= 15", "Safari >= 16",
"Firefox >= 102", "Firefox >= 102",
"last 2 Chrome versions", "last 2 Chrome versions",
"last 2 Edge versions" "last 2 Edge versions"
+2 -1
View File
@@ -2,7 +2,8 @@
## New Features ## New Features
* Optionally detect late promise rejections and don't report them as errors * 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 This is off by default because it comes with a performance cost. It can be
enabled by setting the `detectLateRejectionHandling` config property to true. enabled by setting the `detectLateRejectionHandling` config property to true.
* Add `getSpecProperty` to retrieve data that was set with `setSpecProperty`. * Add `getSpecProperty` to retrieve data that was set with `setSpecProperty`.
+66
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
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
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)_
+100
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)_
+116
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)_
+11 -4
View File
@@ -28,13 +28,20 @@ failfile=`mktemp -t jasmine-results.XXXXXX` || exit 1
run_browser chrome latest run_browser chrome latest
run_browser firefox latest run_browser firefox latest
run_browser firefox 140 if [ "$1" = "--not-actually-all" ]; then
run_browser firefox 128 echo "SKIPPED: firefox 140" >> "$passfile"
run_browser firefox 115 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 firefox 102
run_browser safari 17 run_browser safari 17
run_browser safari 16 run_browser safari 16
run_browser safari 15
run_browser MicrosoftEdge latest run_browser MicrosoftEdge latest
echo echo
+16 -8
View File
@@ -242,7 +242,7 @@ describe('Clock', function() {
expect(fakeGlobal.clearInterval).toBe(replacedClearInterval); 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'), const fakeSetTimeout = jasmine.createSpy('global setTimeout'),
fakeClearTimeout = jasmine.createSpy('global clearTimeout'), fakeClearTimeout = jasmine.createSpy('global clearTimeout'),
fakeSetInterval = jasmine.createSpy('global setInterval'), fakeSetInterval = jasmine.createSpy('global setInterval'),
@@ -408,7 +408,8 @@ describe('Clock', function() {
expect(delayedFunctionScheduler.scheduleFunction).not.toHaveBeenCalled(); expect(delayedFunctionScheduler.scheduleFunction).not.toHaveBeenCalled();
}); });
it('schedules the delayed function (via setTimeout) with the fake timer', function() { describe('setTimeout', function() {
it('schedules the delayed function with the fake timer', function() {
const fakeSetTimeout = jasmine.createSpy('setTimeout'), const fakeSetTimeout = jasmine.createSpy('setTimeout'),
scheduleFunction = jasmine.createSpy('scheduleFunction'), scheduleFunction = jasmine.createSpy('scheduleFunction'),
delayedFunctionScheduler = { scheduleFunction: scheduleFunction }, delayedFunctionScheduler = { scheduleFunction: scheduleFunction },
@@ -481,7 +482,9 @@ describe('Clock', function() {
expect(timeout.constructor.name).toEqual('FakeTimeout'); expect(timeout.constructor.name).toEqual('FakeTimeout');
} }
}); });
});
describe('clearTimeout', function() {
it('clears the scheduled function with the scheduler', function() { it('clears the scheduled function with the scheduler', function() {
const fakeClearTimeout = jasmine.createSpy('clearTimeout'), const fakeClearTimeout = jasmine.createSpy('clearTimeout'),
delayedFunctionScheduler = jasmine.createSpyObj( delayedFunctionScheduler = jasmine.createSpyObj(
@@ -506,11 +509,13 @@ describe('Clock', function() {
clock.clearTimeout(123); clock.clearTimeout(123);
expect(fakeClearTimeout).not.toHaveBeenCalled(); expect(fakeClearTimeout).not.toHaveBeenCalled();
expect(delayedFunctionScheduler.removeFunctionWithId).toHaveBeenCalledWith( expect(
123 delayedFunctionScheduler.removeFunctionWithId
); ).toHaveBeenCalledWith(123);
});
}); });
describe('setInterval', function() {
it('schedules the delayed function with the fake timer', function() { it('schedules the delayed function with the fake timer', function() {
const fakeSetInterval = jasmine.createSpy('setInterval'), const fakeSetInterval = jasmine.createSpy('setInterval'),
scheduleFunction = jasmine.createSpy('scheduleFunction'), scheduleFunction = jasmine.createSpy('scheduleFunction'),
@@ -585,7 +590,9 @@ describe('Clock', function() {
expect(interval.constructor.name).toEqual('FakeTimeout'); expect(interval.constructor.name).toEqual('FakeTimeout');
} }
}); });
});
describe('clearInterval', function() {
it('clears the scheduled function with the scheduler', function() { it('clears the scheduled function with the scheduler', function() {
const clearInterval = jasmine.createSpy('clearInterval'), const clearInterval = jasmine.createSpy('clearInterval'),
delayedFunctionScheduler = jasmine.createSpyObj( delayedFunctionScheduler = jasmine.createSpyObj(
@@ -610,9 +617,10 @@ describe('Clock', function() {
clock.clearInterval(123); clock.clearInterval(123);
expect(clearInterval).not.toHaveBeenCalled(); expect(clearInterval).not.toHaveBeenCalled();
expect(delayedFunctionScheduler.removeFunctionWithId).toHaveBeenCalledWith( expect(
123 delayedFunctionScheduler.removeFunctionWithId
); ).toHaveBeenCalledWith(123);
});
}); });
it('gives you a friendly reminder if the Clock is not installed and you tick', function() { it('gives you a friendly reminder if the Clock is not installed and you tick', function() {
+134
View File
@@ -0,0 +1,134 @@
describe('Configuration', function() {
const standardBooleanKeys = [
'random',
'stopOnSpecFailure',
'stopSpecOnExpectationFailure',
'failSpecWithNoExpectations',
'hideDisabled',
'autoCleanClosures',
'forbidDuplicateNames',
'detectLateRejectionHandling'
];
const allKeys = [
...standardBooleanKeys,
'seed',
'specFilter',
'verboseDeprecations'
];
Object.freeze(standardBooleanKeys);
Object.freeze(allKeys);
it('provides defaults', function() {
const subject = new jasmineUnderTest.Configuration();
expect(subject.random).toEqual(true);
expect(subject.seed).toBeNull();
expect(subject.stopOnSpecFailure).toEqual(false);
expect(subject.stopSpecOnExpectationFailure).toEqual(false);
expect(subject.failSpecWithNoExpectations).toEqual(false);
expect(subject.specFilter).toEqual(jasmine.any(Function));
expect(subject.specFilter()).toEqual(true);
expect(subject.hideDisabled).toEqual(false);
expect(subject.autoCleanClosures).toEqual(true);
expect(subject.forbidDuplicateNames).toEqual(false);
expect(subject.verboseDeprecations).toEqual(false);
expect(subject.detectLateRejectionHandling).toEqual(false);
});
describe('copy()', function() {
it('returns a copy of the configuration as a plain old JS object', function() {
const subject = new jasmineUnderTest.Configuration();
const copy = subject.copy();
expect(copy.constructor.name).toEqual('Object');
expect(new Set(Object.keys(copy))).toEqual(new Set(allKeys));
for (const k of allKeys) {
expect(copy[k]).toEqual(subject[k]);
}
});
});
describe('update()', function() {
it('does not update properties that are absent from the parameter', function() {
const subject = new jasmineUnderTest.Configuration();
const originalValues = subject.copy();
subject.update({});
expect(subject.copy()).toEqual(originalValues);
});
function booleanPropertyBehavior(key) {
it('does not update the property if the specified value is undefined', function() {
const subject = new jasmineUnderTest.Configuration();
const orig = subject[key];
subject.update({ [key]: undefined });
expect(subject[key]).toEqual(orig);
});
it('updates the property if the specified value is not undefined', function() {
const subject = new jasmineUnderTest.Configuration();
const orig = subject[key];
subject.update({ [key]: !orig });
expect(subject[key]).toEqual(!orig);
subject.update({ [key]: orig });
expect(subject[key]).toEqual(orig);
});
}
for (const k of standardBooleanKeys) {
describe(k, function() {
booleanPropertyBehavior(k);
});
}
// TODO: in the next major release, treat verboseDeprecations like other booleans
it('sets verboseDeprecations when present', function() {
const subject = new jasmineUnderTest.Configuration();
const orig = subject.verboseDeprecations;
subject.update({ verboseDeprecations: !orig });
expect(subject.verboseDeprecations).toEqual(!orig);
subject.update({ verboseDeprecations: orig });
expect(subject.verboseDeprecations).toEqual(orig);
// For backwards compatibility, explicitly setting to undefined should
// work. Undefined isn't officially valid but gets treated like false.
subject.update({ verboseDeprecations: undefined });
expect(subject.verboseDeprecations).toBeUndefined();
});
it('sets specFilter when truthy', function() {
const subject = new jasmineUnderTest.Configuration();
const orig = subject.specFilter;
subject.update({ specFilter: undefined });
expect(subject.specFilter).toBe(orig);
subject.update({ specFilter: false });
expect(subject.specFilter).toBe(orig);
function newSpecFilter() {}
subject.update({ specFilter: newSpecFilter });
expect(subject.specFilter).toBe(newSpecFilter);
});
it('sets seed when not undefined', function() {
const subject = new jasmineUnderTest.Configuration();
subject.update({ seed: undefined });
expect(subject.seed).toBeNull();
subject.update({ seed: 1234 });
expect(subject.seed).toEqual(1234);
subject.update({ seed: null });
expect(subject.seed).toBeNull();
});
});
});
+19 -26
View File
@@ -33,18 +33,6 @@ describe('Spec', function() {
expect(jasmineUnderTest.Spec.isPendingSpecException(void 0)).toBe(false); expect(jasmineUnderTest.Spec.isPendingSpecException(void 0)).toBe(false);
}); });
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');
});
describe('#executionFinished', function() { describe('#executionFinished', function() {
it('removes the fn if autoCleanClosures is true', function() { it('removes the fn if autoCleanClosures is true', function() {
const spec = new jasmineUnderTest.Spec({ const spec = new jasmineUnderTest.Spec({
@@ -116,32 +104,25 @@ describe('Spec', function() {
}); });
}); });
it('#status returns passing by default', function() { describe('status', function() {
const spec = new jasmineUnderTest.Spec({ it('is "passed" by default', function() {
queueableFn: { fn: jasmine.createSpy('spec body') }
});
expect(spec.status()).toBe('passed');
});
describe('#status', function() {
it('returns "passed"" by default', function() {
const spec = new jasmineUnderTest.Spec({ const spec = new jasmineUnderTest.Spec({
queueableFn: { fn: () => {} } queueableFn: { fn: () => {} }
}); });
expect(spec.status()).toBe('passed'); expect(spec.getResult().status).toBe('passed');
}); });
it('returns "passed"" if all expectations passed', function() { it('is "passed" if all expectations passed', function() {
const spec = new jasmineUnderTest.Spec({ const spec = new jasmineUnderTest.Spec({
queueableFn: { fn: () => {} } queueableFn: { fn: () => {} }
}); });
spec.addExpectationResult(true, {}); spec.addExpectationResult(true, {});
expect(spec.status()).toBe('passed'); expect(spec.getResult().status).toBe('passed');
}); });
it('returns "failed" if any expectation failed', function() { it('is "failed" if any expectation failed', function() {
const spec = new jasmineUnderTest.Spec({ const spec = new jasmineUnderTest.Spec({
queueableFn: { fn: () => {} } queueableFn: { fn: () => {} }
}); });
@@ -149,7 +130,19 @@ describe('Spec', function() {
spec.addExpectationResult(true, {}); spec.addExpectationResult(true, {});
spec.addExpectationResult(false, {}); spec.addExpectationResult(false, {});
expect(spec.status()).toBe('failed'); 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');
}); });
}); });
+2 -2
View File
@@ -115,7 +115,7 @@ describe('Suite', function() {
const suite = new jasmineUnderTest.Suite({}); const suite = new jasmineUnderTest.Suite({});
suite.addExpectationResult(false, {}); suite.addExpectationResult(false, {});
expect(suite.status()).toBe('failed'); expect(suite.getResult().status).toBe('failed');
}); });
it('retrieves a result with updated status', function() { it('retrieves a result with updated status', function() {
@@ -140,7 +140,7 @@ describe('Suite', function() {
suite.addExpectationResult(false, { message: 'failed' }); suite.addExpectationResult(false, { message: 'failed' });
}).toThrowError(jasmineUnderTest.errors.ExpectationFailed); }).toThrowError(jasmineUnderTest.errors.ExpectationFailed);
expect(suite.status()).toBe('failed'); expect(suite.getResult().status).toBe('failed');
expect(suite.result.failedExpectations).toEqual([ expect(suite.result.failedExpectations).toEqual([
jasmine.objectContaining({ message: 'failed' }) jasmine.objectContaining({ message: 'failed' })
]); ]);
+282 -107
View File
@@ -114,7 +114,6 @@ describe('TreeRunner', function() {
expect(specRunQueueArgs.queueableFns[1]).toEqual(queueableFn); expect(specRunQueueArgs.queueableFns[1]).toEqual(queueableFn);
queueableFn.fn(); queueableFn.fn();
expect(spec.status()).toEqual('pending');
expect(spec.getResult().status).toEqual('pending'); expect(spec.getResult().status).toEqual('pending');
expect(spec.getResult().pendingReason).toEqual(''); expect(spec.getResult().pendingReason).toEqual('');
}); });
@@ -136,7 +135,6 @@ describe('TreeRunner', function() {
expect(specRunQueueArgs.queueableFns[1]).toEqual(queueableFn); expect(specRunQueueArgs.queueableFns[1]).toEqual(queueableFn);
queueableFn.fn(); queueableFn.fn();
expect(spec.status()).toEqual('pending');
expect(spec.getResult().status).toEqual('pending'); expect(spec.getResult().status).toEqual('pending');
expect(spec.getResult().pendingReason).toEqual('some reason'); expect(spec.getResult().pendingReason).toEqual('some reason');
}); });
@@ -163,111 +161,6 @@ describe('TreeRunner', function() {
expect(spec.executionFinished).toHaveBeenCalledWith(false, true); expect(spec.executionFinished).toHaveBeenCalledWith(false, true);
await expectAsync(executePromise).toBePending(); await expectAsync(executePromise).toBePending();
}); });
describe('Late promise rejection handling', function() {
it('is enabled 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
);
});
});
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
};
}
}); });
describe('Suite execution', function() { describe('Suite execution', function() {
@@ -516,6 +409,288 @@ describe('TreeRunner', function() {
}); });
}); });
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() { function mockReportDispatcher() {
const reportDispatcher = jasmine.createSpyObj( const reportDispatcher = jasmine.createSpyObj(
'reportDispatcher', 'reportDispatcher',
+185 -78
View File
@@ -1600,6 +1600,16 @@ describe('Env integration', function() {
suiteFullNameToId[e.fullName] = e.id; 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.addReporter(reporter);
env.it('a top level spec', function() {}); env.it('a top level spec', function() {});
@@ -1636,110 +1646,179 @@ describe('Env integration', function() {
expect(reporter.specStarted.calls.count()).toBe(6); expect(reporter.specStarted.calls.count()).toBe(6);
expect(reporter.specDone.calls.count()).toBe(6); expect(reporter.specDone.calls.count()).toBe(6);
expect(reporter.specStarted).toHaveBeenCalledWith( const baseSpecEvent = {
jasmine.objectContaining({ passedExpectations: [],
description: 'a top level spec', failedExpectations: [],
parentSuiteId: null deprecationWarnings: [],
}) pendingReason: '',
); duration: null,
expect(reporter.specDone).toHaveBeenCalledWith( properties: null,
jasmine.objectContaining({ debugLogs: null,
description: 'a top level spec', id: jasmine.any(String),
status: 'passed', filename: jasmine.any(String)
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']
})
);
expect(reporter.specStarted).toHaveBeenCalledWith( expect(reporter.specStarted.calls.argsFor(0)[0]).toEqual({
jasmine.objectContaining({ ...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.calls.argsFor(2)[0]).toEqual({
...baseSpecEvent,
description: "with an x'ed spec", description: "with an x'ed spec",
parentSuiteId: suiteFullNameToId['A Suite with a nested suite'] 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).toHaveBeenCalledWith( });
jasmine.objectContaining({ expect(reporter.specDone.calls.argsFor(2)[0]).toEqual({
...baseSpecEvent,
description: "with an x'ed spec", description: "with an x'ed spec",
fullName: "A Suite with a nested suite with an x'ed spec",
status: 'pending', status: 'pending',
parentSuiteId: suiteFullNameToId['A Suite with a nested suite'] parentSuiteId: suiteFullNameToId['A Suite with a nested suite'],
}) pendingReason: 'Temporarily disabled with xit',
); duration: jasmine.any(Number)
});
expect(reporter.specStarted).toHaveBeenCalledWith( expect(reporter.specStarted.calls.argsFor(3)[0]).toEqual({
jasmine.objectContaining({ ...baseSpecEvent,
description: 'with a spec', description: 'with a spec',
fullName: 'A Suite with a nested suite with a spec',
parentSuiteId: suiteFullNameToId['A Suite with a nested suite'] parentSuiteId: suiteFullNameToId['A Suite with a nested suite']
}) });
); expect(reporter.specDone.calls.argsFor(3)[0]).toEqual({
expect(reporter.specDone).toHaveBeenCalledWith( ...baseSpecEvent,
jasmine.objectContaining({
description: 'with a spec', description: 'with a spec',
fullName: 'A Suite with a nested suite with a spec',
status: 'failed', status: 'failed',
parentSuiteId: suiteFullNameToId['A Suite with a nested suite'] 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.specStarted).toHaveBeenCalledWith( expect(reporter.specStarted.calls.argsFor(4)[0]).toEqual({
jasmine.objectContaining({ ...baseSpecEvent,
description: 'is pending', description: 'is pending',
parentSuiteId: fullName: 'A Suite with only non-executable specs is pending',
suiteFullNameToId['A Suite with only non-executable specs'] parentSuiteId: suiteFullNameToId['A Suite with only non-executable specs']
}) });
); expect(reporter.specDone.calls.argsFor(4)[0]).toEqual({
expect(reporter.specDone).toHaveBeenCalledWith( ...baseSpecEvent,
jasmine.objectContaining({
description: 'is pending', description: 'is pending',
status: 'pending', status: 'pending',
fullName: 'A Suite with only non-executable specs is pending',
parentSuiteId: parentSuiteId:
suiteFullNameToId['A Suite with only non-executable specs'] suiteFullNameToId['A Suite with only non-executable specs'],
}) duration: jasmine.any(Number)
); });
expect(reporter.suiteStarted).toHaveBeenCalledWith( expect(reporter.specStarted.calls.argsFor(5)[0]).toEqual({
jasmine.objectContaining({ ...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)
});
expect(reporter.suiteStarted.calls.count()).toBe(3);
expect(reporter.suiteDone.calls.count()).toBe(3);
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', description: 'A Suite',
fullName: 'A Suite',
parentSuiteId: null parentSuiteId: null
}) });
); expect(reporter.suiteDone.calls.argsFor(2)[0]).toEqual({
expect(reporter.suiteDone).toHaveBeenCalledWith( ...baseSuiteEvent,
jasmine.objectContaining({
description: 'A Suite', description: 'A Suite',
fullName: 'A Suite',
status: 'passed', status: 'passed',
parentSuiteId: null parentSuiteId: null,
}) duration: jasmine.any(Number)
); });
expect(reporter.suiteStarted).toHaveBeenCalledWith( expect(reporter.suiteStarted.calls.argsFor(1)[0]).toEqual({
jasmine.objectContaining({ ...baseSuiteEvent,
description: 'with a nested suite', description: 'with a nested suite',
fullName: 'A Suite with a nested suite',
parentSuiteId: suiteFullNameToId['A Suite'] parentSuiteId: suiteFullNameToId['A Suite']
}) });
); expect(reporter.suiteDone.calls.argsFor(0)[0]).toEqual({
expect(reporter.suiteDone).toHaveBeenCalledWith( ...baseSuiteEvent,
jasmine.objectContaining({
description: 'with a nested suite', description: 'with a nested suite',
status: 'passed', 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'] parentSuiteId: suiteFullNameToId['A Suite']
}) });
); expect(reporter.suiteDone.calls.argsFor(1)[0]).toEqual({
...baseSuiteEvent,
const suiteDone = reporter.suiteDone.calls.argsFor(0)[0]; description: 'with only non-executable specs',
expect(typeof suiteDone.duration).toBe('number'); status: 'passed',
fullName: 'A Suite with only non-executable specs',
const suiteResult = reporter.suiteStarted.calls.argsFor(0)[0]; parentSuiteId: suiteFullNameToId['A Suite'],
expect(suiteResult.description).toEqual('A Suite'); duration: jasmine.any(Number)
});
}); });
it('reports focused specs and suites as expected', async function() { it('reports focused specs and suites as expected', async function() {
@@ -2198,6 +2277,34 @@ describe('Env integration', function() {
await env.execute(); 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() { it('throws an exception if you try to create a spy outside of a runnable', async function() {
const obj = { fn: function() {} }; const obj = { fn: function() {} };
let exception; let exception;
@@ -809,7 +809,6 @@ describe('Global error handling (integration)', function() {
describe('When the detectLateRejectionHandling config option is set', function() { describe('When the detectLateRejectionHandling config option is set', function() {
describe('When the unhandled rejection event has a promise', function() { describe('When the unhandled rejection event has a promise', function() {
it('reports the rejection unless a corresponding rejection handled event occurs', async function() {
function makeEvent(suffix) { function makeEvent(suffix) {
const reason = `rejection ${suffix}`; const reason = `rejection ${suffix}`;
const promise = Promise.reject(reason); const promise = Promise.reject(reason);
@@ -817,7 +816,10 @@ describe('Global error handling (integration)', function() {
return { reason, promise }; return { reason, promise };
} }
const global = { let global, reporter;
beforeEach(function() {
global = {
...browserEventMethods(), ...browserEventMethods(),
setTimeout: function(fn, delay) { setTimeout: function(fn, delay) {
return setTimeout(fn, delay); return setTimeout(fn, delay);
@@ -833,13 +835,17 @@ describe('Global error handling (integration)', function() {
env.cleanup_(); env.cleanup_();
env = new jasmineUnderTest.Env(); env = new jasmineUnderTest.Env();
env.configure({ detectLateRejectionHandling: true }); env.configure({ detectLateRejectionHandling: true });
const reporter = jasmine.createSpyObj('fakeReporter', [
reporter = jasmine.createSpyObj('fakeReporter', [
'specDone', 'specDone',
'suiteDone' 'suiteDone'
]); ]);
env.addReporter(reporter); 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.describe('A suite', function() {
env.it('fails', function(specDone) { env.it('fails', function(specDone) {
setTimeout(function() { setTimeout(function() {
@@ -873,6 +879,91 @@ describe('Global error handling (integration)', function() {
}); });
}); });
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() { describe("When the unhandled rejection event doesn't have a promise", function() {
it('reports the rejection', async function() { it('reports the rejection', async function() {
const global = { const global = {
@@ -920,6 +1011,43 @@ describe('Global error handling (integration)', function() {
}); });
}); });
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() { describe('#spyOnGlobalErrorsAsync', function() {
const leftInstalledMessage = const leftInstalledMessage =
'Global error spy was not uninstalled. ' + 'Global error spy was not uninstalled. ' +
+12
View File
@@ -470,22 +470,30 @@ describe('Matchers (Integration)', function() {
}); });
describe('toBeNullish', function() { describe('toBeNullish', function() {
describe('with undefined', function() {
verifyPasses(function(env) { verifyPasses(function(env) {
env.expect(undefined).toBeNullish(); env.expect(undefined).toBeNullish();
}); });
});
describe('with null', function() {
verifyPasses(function(env) { verifyPasses(function(env) {
env.expect(null).toBeNullish(); env.expect(null).toBeNullish();
}); });
});
describe('with a truthy value', function() {
verifyFails(function(env) { verifyFails(function(env) {
env.expect(1).toBeNullish(); env.expect(1).toBeNullish();
}); });
});
describe('with a non-null falsy value', function() {
verifyFails(function(env) { verifyFails(function(env) {
env.expect('').toBeNullish(); env.expect('').toBeNullish();
}); });
}); });
});
describe('toContain', function() { describe('toContain', function() {
verifyPasses(function(env) { verifyPasses(function(env) {
@@ -665,15 +673,19 @@ describe('Matchers (Integration)', function() {
env.expect(spyObj).toHaveSpyInteractions(); env.expect(spyObj).toHaveSpyInteractions();
}); });
describe('with no methods called', function() {
verifyFails(function(env) { verifyFails(function(env) {
env.expect(spyObj).toHaveSpyInteractions(); env.expect(spyObj).toHaveSpyInteractions();
}); });
});
describe('with only non-spy methods called', function() {
verifyFails(function(env) { verifyFails(function(env) {
spyObj.otherMethod(); spyObj.otherMethod();
env.expect(spyObj).toHaveSpyInteractions(); env.expect(spyObj).toHaveSpyInteractions();
}); });
}); });
});
describe('toHaveNoOtherSpyInteractions', function() { describe('toHaveNoOtherSpyInteractions', function() {
let spyObj; let spyObj;
+22 -18
View File
@@ -281,6 +281,7 @@ describe('toThrowError', function() {
); );
}); });
describe('with a string', function() {
it('fails if thrown is a type of Error and the expected is a different Error', function() { it('fails if thrown is a type of Error and the expected is a different Error', function() {
const matchersUtil = { const matchersUtil = {
equals: jasmine.createSpy('delegated-equal').and.returnValue(false), equals: jasmine.createSpy('delegated-equal').and.returnValue(false),
@@ -298,6 +299,27 @@ describe('toThrowError', function() {
"Expected function to throw TypeError with message 'bar', but it threw TypeError with message 'foo'." "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() { 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() {
const matchersUtil = { const matchersUtil = {
@@ -316,22 +338,4 @@ describe('toThrowError', function() {
'Expected function not to throw TypeError with a message matching /foo/.' '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'."
);
});
}); });
+3
View File
@@ -26,6 +26,9 @@ module.exports = {
'helpers/defineJasmineUnderTest.js', 'helpers/defineJasmineUnderTest.js',
'helpers/resetEnv.js' 'helpers/resetEnv.js'
], ],
env: {
forbidDuplicateNames: true
},
random: true, random: true,
browser: { browser: {
name: process.env.JASMINE_BROWSER || 'firefox', name: process.env.JASMINE_BROWSER || 'firefox',
+3
View File
@@ -12,5 +12,8 @@
"helpers/nodeDefineJasmineUnderTest.js", "helpers/nodeDefineJasmineUnderTest.js",
"helpers/resetEnv.js" "helpers/resetEnv.js"
], ],
"env": {
"forbidDuplicateNames": true
},
"random": true "random": true
} }
+186
View File
@@ -0,0 +1,186 @@
getJasmineRequireObj().Configuration = function(j$) {
/**
* This represents the available options to configure Jasmine.
* Options that are not provided will use their default values.
* @see Env#configure
* @interface Configuration
* @since 3.3.0
*/
const defaultConfig = {
/**
* Whether to randomize spec execution order
* @name Configuration#random
* @since 3.3.0
* @type Boolean
* @default true
*/
random: true,
/**
* Seed to use as the basis of randomization.
* Null causes the seed to be determined randomly at the start of execution.
* @name Configuration#seed
* @since 3.3.0
* @type (number|string)
* @default null
*/
seed: null,
/**
* Whether to stop execution of the suite after the first spec failure
*
* <p>In parallel mode, `stopOnSpecFailure` works on a "best effort"
* basis. Jasmine will stop execution as soon as practical after a failure
* but it might not be immediate.</p>
* @name Configuration#stopOnSpecFailure
* @since 3.9.0
* @type Boolean
* @default false
*/
stopOnSpecFailure: false,
/**
* Whether to fail the spec if it ran no expectations. By default
* a spec that ran no expectations is reported as passed. Setting this
* to true will report such spec as a failure.
* @name Configuration#failSpecWithNoExpectations
* @since 3.5.0
* @type Boolean
* @default false
*/
failSpecWithNoExpectations: false,
/**
* Whether to cause specs to only have one expectation failure.
* @name Configuration#stopSpecOnExpectationFailure
* @since 3.3.0
* @type Boolean
* @default false
*/
stopSpecOnExpectationFailure: false,
/**
* A function that takes a spec and returns true if it should be executed
* or false if it should be skipped.
* @callback SpecFilter
* @param {Spec} spec - The spec that the filter is being applied to.
* @return boolean
*/
/**
* Function to use to filter specs
* @name Configuration#specFilter
* @since 3.3.0
* @type SpecFilter
* @default A function that always returns true.
*/
specFilter: function() {
return true;
},
/**
* Whether reporters should hide disabled specs from their output.
* Currently only supported by Jasmine's HTMLReporter
* @name Configuration#hideDisabled
* @since 3.3.0
* @type Boolean
* @default false
*/
hideDisabled: false,
/**
* Clean closures when a suite is done running (done by clearing the stored function reference).
* This prevents memory leaks, but you won't be able to run jasmine multiple times.
* @name Configuration#autoCleanClosures
* @since 3.10.0
* @type boolean
* @default true
*/
autoCleanClosures: true,
/**
* Whether to forbid duplicate spec or suite names. If set to true, using
* the same name multiple times in the same immediate parent suite is an
* error.
* @name Configuration#forbidDuplicateNames
* @type boolean
* @default false
*/
forbidDuplicateNames: false,
/**
* Whether to issue warnings for certain deprecated functionality
* every time it's used. If not set or set to false, deprecation warnings
* for methods that tend to be called frequently will be issued only once
* or otherwise throttled to prevent the suite output from being flooded
* with warnings.
* @name Configuration#verboseDeprecations
* @since 3.6.0
* @type Boolean
* @default false
*/
verboseDeprecations: false,
/**
* Whether to detect late promise rejection handling during spec
* execution. If this option is enabled, a promise rejection that triggers
* the JavaScript runtime's unhandled rejection event will not be treated
* as an error as long as it's handled before the spec finishes.
*
* This option is off by default because it imposes a performance penalty.
* @name Configuration#detectLateRejectionHandling
* @since 5.10.0
* @type Boolean
* @default false
*/
detectLateRejectionHandling: false
};
Object.freeze(defaultConfig);
class Configuration {
#values;
constructor() {
this.#values = { ...defaultConfig };
for (const k of Object.keys(defaultConfig)) {
Object.defineProperty(this, k, {
enumerable: true,
get() {
return this.#values[k];
}
});
}
}
copy() {
return { ...this.#values };
}
update(changes) {
const booleanProps = [
'random',
'failSpecWithNoExpectations',
'hideDisabled',
'stopOnSpecFailure',
'stopSpecOnExpectationFailure',
'autoCleanClosures',
'forbidDuplicateNames',
'detectLateRejectionHandling'
];
for (const k of booleanProps) {
if (typeof changes[k] !== 'undefined') {
this.#values[k] = changes[k];
}
}
if (changes.specFilter) {
this.#values.specFilter = changes.specFilter;
}
// 0 and null are valid values, so a truthiness check wouldn't work
if (typeof changes.seed !== 'undefined') {
this.#values.seed = changes.seed;
}
// TODO: in the next major release, make verboseDeprecations work like
// other boolean properties.
if (changes.hasOwnProperty('verboseDeprecations')) {
this.#values.verboseDeprecations = changes.verboseDeprecations;
}
}
}
return Configuration;
};
+21 -170
View File
@@ -7,12 +7,12 @@ getJasmineRequireObj().Env = function(j$) {
* calling {@link jasmine.getEnv}. * calling {@link jasmine.getEnv}.
* @hideconstructor * @hideconstructor
*/ */
function Env(options) { function Env(envOptions) {
options = options || {}; envOptions = envOptions || {};
const self = this; const self = this;
const GlobalErrors = options.GlobalErrors || j$.GlobalErrors; const GlobalErrors = envOptions.GlobalErrors || j$.GlobalErrors;
const global = options.global || j$.getGlobal(); const global = envOptions.global || j$.getGlobal();
const realSetTimeout = global.setTimeout; const realSetTimeout = global.setTimeout;
const realClearTimeout = global.clearTimeout; const realClearTimeout = global.clearTimeout;
@@ -31,13 +31,22 @@ getJasmineRequireObj().Env = function(j$) {
// before it's set to detect load-time errors in browsers // before it's set to detect load-time errors in browsers
() => this.configuration() () => this.configuration()
); );
const installGlobalErrors = (function() { const { installGlobalErrors, uninstallGlobalErrors } = (function() {
let installed = false; let installed = false;
return function() {
return {
installGlobalErrors() {
if (!installed) { if (!installed) {
globalErrors.install(); globalErrors.install();
installed = true; installed = true;
} }
},
uninstallGlobalErrors() {
if (installed) {
globalErrors.uninstall();
installed = false;
}
}
}; };
})(); })();
@@ -54,134 +63,9 @@ getJasmineRequireObj().Env = function(j$) {
let runner; let runner;
let parallelLoadingState = null; // 'specs', 'helpers', or null for non-parallel let parallelLoadingState = null; // 'specs', 'helpers', or null for non-parallel
/** const config = new j$.Configuration();
* 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 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,
/** if (!envOptions.suppressLoadErrors) {
* 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
};
if (!options.suppressLoadErrors) {
installGlobalErrors(); installGlobalErrors();
globalErrors.pushListener(function loadtimeErrorHandler(error, event) { globalErrors.pushListener(function loadtimeErrorHandler(error, event) {
topSuite.result.failedExpectations.push({ topSuite.result.failedExpectations.push({
@@ -202,42 +86,15 @@ getJasmineRequireObj().Env = function(j$) {
* @argument {Configuration} configuration * @argument {Configuration} configuration
* @function * @function
*/ */
this.configure = function(configuration) { this.configure = function(changes) {
if (parallelLoadingState) { if (parallelLoadingState) {
throw new Error( throw new Error(
'Jasmine cannot be configured via Env in parallel mode' 'Jasmine cannot be configured via Env in parallel mode'
); );
} }
const booleanProps = [ config.update(changes);
'random',
'failSpecWithNoExpectations',
'hideDisabled',
'stopOnSpecFailure',
'stopSpecOnExpectationFailure',
'autoCleanClosures',
'forbidDuplicateNames',
'detectLateRejectionHandling'
];
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); deprecator.verboseDeprecations(config.verboseDeprecations);
}
}; };
/** /**
@@ -248,11 +105,7 @@ getJasmineRequireObj().Env = function(j$) {
* @returns {Configuration} * @returns {Configuration}
*/ */
this.configuration = function() { this.configuration = function() {
const result = {}; return config.copy();
for (const property in config) {
result[property] = config[property];
}
return result;
}; };
this.setDefaultSpyStrategy = function(defaultStrategyFn) { this.setDefaultSpyStrategy = function(defaultStrategyFn) {
@@ -960,9 +813,7 @@ getJasmineRequireObj().Env = function(j$) {
}; };
this.cleanup_ = function() { this.cleanup_ = function() {
if (globalErrors) { uninstallGlobalErrors();
globalErrors.uninstall();
}
}; };
} }
+6
View File
@@ -103,6 +103,12 @@ getJasmineRequireObj().Runner = function(j$) {
// In parallel mode, the jasmineStarted event is separately dispatched // In parallel mode, the jasmineStarted event is separately dispatched
// by jasmine-npm. This event only reaches reporters in non-parallel. // by jasmine-npm. This event only reaches reporters in non-parallel.
totalSpecsDefined, 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, order: order,
parallel: false parallel: false
}); });
+74 -80
View File
@@ -1,5 +1,11 @@
getJasmineRequireObj().Spec = function(j$) { getJasmineRequireObj().Spec = function(j$) {
function Spec(attrs) { class Spec {
#autoCleanClosures;
#throwOnExpectationFailure;
#timer;
#metadata;
constructor(attrs) {
this.expectationFactory = attrs.expectationFactory; this.expectationFactory = attrs.expectationFactory;
this.asyncExpectationFactory = attrs.asyncExpectationFactory; this.asyncExpectationFactory = attrs.asyncExpectationFactory;
this.id = attrs.id; this.id = attrs.id;
@@ -17,21 +23,17 @@ getJasmineRequireObj().Spec = function(j$) {
function() { function() {
return {}; return {};
}; };
this.autoCleanClosures =
attrs.autoCleanClosures === undefined ? true : !!attrs.autoCleanClosures;
this.getPath = function() { this.getPath = function() {
return attrs.getPath ? attrs.getPath(this) : []; return attrs.getPath ? attrs.getPath(this) : [];
}; };
this.#autoCleanClosures =
attrs.autoCleanClosures === undefined
? true
: !!attrs.autoCleanClosures;
this.onLateError = attrs.onLateError || function() {}; this.onLateError = attrs.onLateError || function() {};
this.catchingExceptions = this.#throwOnExpectationFailure = !!attrs.throwOnExpectationFailure;
attrs.catchingExceptions || this.#timer = attrs.timer || new j$.Timer();
function() {
return true;
};
this.throwOnExpectationFailure = !!attrs.throwOnExpectationFailure;
this.timer = attrs.timer || new j$.Timer();
if (!this.queueableFn.fn) { if (!this.queueableFn.fn) {
this.exclude(); this.exclude();
@@ -40,7 +42,7 @@ getJasmineRequireObj().Spec = function(j$) {
this.reset(); this.reset();
} }
Spec.prototype.addExpectationResult = function(passed, data, isError) { addExpectationResult(passed, data, isError) {
const expectationResult = j$.buildExpectationResult(data); const expectationResult = j$.buildExpectationResult(data);
if (passed) { if (passed) {
@@ -57,40 +59,40 @@ getJasmineRequireObj().Spec = function(j$) {
} }
} }
if (this.throwOnExpectationFailure && !isError) { if (this.#throwOnExpectationFailure && !isError) {
throw new j$.errors.ExpectationFailed(); throw new j$.errors.ExpectationFailed();
} }
} }
}; }
Spec.prototype.getSpecProperty = function(key) { getSpecProperty(key) {
this.result.properties = this.result.properties || {}; this.result.properties = this.result.properties || {};
return this.result.properties[key]; return this.result.properties[key];
}; }
Spec.prototype.setSpecProperty = function(key, value) { setSpecProperty(key, value) {
this.result.properties = this.result.properties || {}; this.result.properties = this.result.properties || {};
this.result.properties[key] = value; this.result.properties[key] = value;
}; }
Spec.prototype.executionStarted = function() { executionStarted() {
this.timer.start(); this.#timer.start();
}; }
Spec.prototype.executionFinished = function(excluded, failSpecWithNoExp) { executionFinished(excluded, failSpecWithNoExp) {
if (this.autoCleanClosures) { if (this.#autoCleanClosures) {
this.queueableFn.fn = null; this.queueableFn.fn = null;
} }
this.result.status = this.status(excluded, failSpecWithNoExp); this.result.status = this.#status(excluded, failSpecWithNoExp);
this.result.duration = this.timer.elapsed(); this.result.duration = this.#timer.elapsed();
if (this.result.status !== 'failed') { if (this.result.status !== 'failed') {
this.result.debugLogs = null; this.result.debugLogs = null;
} }
}; }
Spec.prototype.reset = function() { reset() {
/** /**
* @typedef SpecResult * @typedef SpecResult
* @property {String} id - The unique id of this spec. * @property {String} id - The unique id of this spec.
@@ -129,9 +131,9 @@ getJasmineRequireObj().Spec = function(j$) {
}; };
this.markedPending = this.markedExcluding; this.markedPending = this.markedExcluding;
this.reportedDone = false; this.reportedDone = false;
}; }
Spec.prototype.handleException = function handleException(e) { handleException(e) {
if (Spec.isPendingSpecException(e)) { if (Spec.isPendingSpecException(e)) {
this.pend(extractCustomPendingMessage(e)); this.pend(extractCustomPendingMessage(e));
return; return;
@@ -152,40 +154,33 @@ getJasmineRequireObj().Spec = function(j$) {
}, },
true true
); );
}; }
/* pend(message) {
* Marks state as pending
* @param {string} [message] An optional reason message
*/
Spec.prototype.pend = function(message) {
this.markedPending = true; this.markedPending = true;
if (message) { if (message) {
this.result.pendingReason = message; this.result.pendingReason = message;
} }
}; }
/* // Like pend(), but pending state will survive reset().
* Like {@link Spec#pend}, but pending state will survive {@link Spec#reset} // Useful for fit, xit, where pending state remains.
* Useful for fit, xit, where pending state remains. exclude(message) {
* @param {string} [message] An optional reason message
*/
Spec.prototype.exclude = function(message) {
this.markedExcluding = true; this.markedExcluding = true;
if (this.message) { if (this.message) {
this.excludeMessage = message; this.excludeMessage = message;
} }
this.pend(message); this.pend(message);
}; }
// TODO: ensure that all access to result goes through .getResult() // TODO: ensure that all access to result goes through .getResult()
// so that the status is correct. // so that the status is correct.
Spec.prototype.getResult = function() { getResult() {
this.result.status = this.status(); this.result.status = this.#status();
return this.result; return this.result;
}; }
Spec.prototype.status = function(excluded, failSpecWithNoExpectations) { #status(excluded, failSpecWithNoExpectations) {
if (excluded === true) { if (excluded === true) {
return 'excluded'; return 'excluded';
} }
@@ -205,22 +200,22 @@ getJasmineRequireObj().Spec = function(j$) {
} }
return 'passed'; return 'passed';
}; }
Spec.prototype.getFullName = function() { getFullName() {
return this.getPath().join(' '); return this.getPath().join(' ');
}; }
Spec.prototype.addDeprecationWarning = function(deprecation) { addDeprecationWarning(deprecation) {
if (typeof deprecation === 'string') { if (typeof deprecation === 'string') {
deprecation = { message: deprecation }; deprecation = { message: deprecation };
} }
this.result.deprecationWarnings.push( this.result.deprecationWarnings.push(
j$.buildExpectationResult(deprecation) j$.buildExpectationResult(deprecation)
); );
}; }
Spec.prototype.debugLog = function(msg) { debugLog(msg) {
if (!this.result.debugLogs) { if (!this.result.debugLogs) {
this.result.debugLogs = []; this.result.debugLogs = [];
} }
@@ -233,42 +228,22 @@ getJasmineRequireObj().Spec = function(j$) {
*/ */
this.result.debugLogs.push({ this.result.debugLogs.push({
message: msg, message: msg,
timestamp: this.timer.elapsed() 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 * @interface Spec
* @see Configuration#specFilter * @see Configuration#specFilter
* @since 2.0.0 * @since 2.0.0
*/ */
Object.defineProperty(Spec.prototype, 'metadata', { get metadata() {
// NOTE: Although most of jasmine-core only exposes these metadata objects, // NOTE: Although most of jasmine-core only exposes these metadata objects,
// actual Spec instances are still passed to Configuration#specFilter. Until // actual Spec instances are still passed to Configuration#specFilter. Until
// that is fixed, it's important to make sure that all metadata properties // that is fixed, it's important to make sure that all metadata properties
// also exist in compatible form on the underlying Spec. // also exist in compatible form on the underlying Spec.
get: function() { if (!this.#metadata) {
if (!this.metadata_) { this.#metadata = {
this.metadata_ = {
/** /**
* The unique ID of this spec. * The unique ID of this spec.
* @name Spec#id * @name Spec#id
@@ -307,9 +282,28 @@ getJasmineRequireObj().Spec = function(j$) {
}; };
} }
return this.metadata_; return this.#metadata;
} }
}); }
const extractCustomPendingMessage = function(e) {
const fullMessage = e.toString(),
boilerplateStart = fullMessage.indexOf(Spec.pendingSpecExceptionMessage),
boilerplateEnd =
boilerplateStart + Spec.pendingSpecExceptionMessage.length;
return fullMessage.slice(boilerplateEnd);
};
Spec.pendingSpecExceptionMessage = '=> marked Pending';
Spec.isPendingSpecException = function(e) {
return !!(
e &&
e.toString &&
e.toString().indexOf(Spec.pendingSpecExceptionMessage) !== -1
);
};
return Spec; return Spec;
}; };
+94 -90
View File
@@ -1,34 +1,41 @@
getJasmineRequireObj().Suite = function(j$) { getJasmineRequireObj().Suite = function(j$) {
function Suite(attrs) { class Suite {
this.env = attrs.env; #reportedParentSuiteId;
#throwOnExpectationFailure;
#autoCleanClosures;
#timer;
constructor(attrs) {
this.id = attrs.id; this.id = attrs.id;
this.parentSuite = attrs.parentSuite; this.parentSuite = attrs.parentSuite;
this.description = attrs.description; this.description = attrs.description;
this.reportedParentSuiteId = attrs.reportedParentSuiteId;
this.filename = attrs.filename; this.filename = attrs.filename;
this.expectationFactory = attrs.expectationFactory; this.expectationFactory = attrs.expectationFactory;
this.asyncExpectationFactory = attrs.asyncExpectationFactory; this.asyncExpectationFactory = attrs.asyncExpectationFactory;
this.throwOnExpectationFailure = !!attrs.throwOnExpectationFailure;
this.autoCleanClosures =
attrs.autoCleanClosures === undefined ? true : !!attrs.autoCleanClosures;
this.onLateError = attrs.onLateError || function() {}; 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.beforeFns = []; this.beforeFns = [];
this.afterFns = []; this.afterFns = [];
this.beforeAllFns = []; this.beforeAllFns = [];
this.afterAllFns = []; this.afterAllFns = [];
this.timer = attrs.timer || new j$.Timer();
this.children = []; this.children = [];
this.reset(); this.reset();
} }
Suite.prototype.setSuiteProperty = function(key, value) { setSuiteProperty(key, value) {
this.result.properties = this.result.properties || {}; this.result.properties = this.result.properties || {};
this.result.properties[key] = value; this.result.properties[key] = value;
}; }
Suite.prototype.getFullName = function() { getFullName() {
const fullName = []; const fullName = [];
for ( for (
let parentSuite = this; let parentSuite = this;
@@ -40,64 +47,54 @@ getJasmineRequireObj().Suite = function(j$) {
} }
} }
return fullName.join(' '); return fullName.join(' ');
}; }
/* // Mark the suite with "pending" status
* Mark the suite with "pending" status pend() {
*/
Suite.prototype.pend = function() {
this.markedPending = true; this.markedPending = true;
}; }
/* // Like pend(), but pending state will survive reset().
* Like {@link Suite#pend}, but pending state will survive {@link Spec#reset} // Useful for fdescribe, xdescribe, where pending state should remain.
* Useful for fdescribe, xdescribe, where pending state should remain. exclude() {
*/
Suite.prototype.exclude = function() {
this.pend(); this.pend();
this.markedExcluding = true; this.markedExcluding = true;
}; }
Suite.prototype.beforeEach = function(fn) { beforeEach(fn) {
this.beforeFns.unshift({ ...fn, suite: this }); this.beforeFns.unshift({ ...fn, suite: this });
}; }
Suite.prototype.beforeAll = function(fn) { beforeAll(fn) {
this.beforeAllFns.push({ ...fn, type: 'beforeAll', suite: this }); this.beforeAllFns.push({ ...fn, type: 'beforeAll', suite: this });
}; }
Suite.prototype.afterEach = function(fn) { afterEach(fn) {
this.afterFns.unshift({ ...fn, suite: this, type: 'afterEach' }); this.afterFns.unshift({ ...fn, suite: this, type: 'afterEach' });
}; }
Suite.prototype.afterAll = function(fn) { afterAll(fn) {
this.afterAllFns.unshift({ ...fn, type: 'afterAll' }); this.afterAllFns.unshift({ ...fn, type: 'afterAll' });
};
Suite.prototype.startTimer = function() {
this.timer.start();
};
Suite.prototype.endTimer = function() {
this.result.duration = this.timer.elapsed();
};
function removeFns(queueableFns) {
for (const qf of queueableFns) {
qf.fn = null;
}
} }
Suite.prototype.cleanupBeforeAfter = function() { startTimer() {
if (this.autoCleanClosures) { this.#timer.start();
}
endTimer() {
this.result.duration = this.#timer.elapsed();
}
cleanupBeforeAfter() {
if (this.#autoCleanClosures) {
removeFns(this.beforeAllFns); removeFns(this.beforeAllFns);
removeFns(this.afterAllFns); removeFns(this.afterAllFns);
removeFns(this.beforeFns); removeFns(this.beforeFns);
removeFns(this.afterFns); removeFns(this.afterFns);
} }
}; }
Suite.prototype.reset = function() { reset() {
/** /**
* @typedef SuiteResult * @typedef SuiteResult
* @property {String} id - The unique id of this suite. * @property {String} id - The unique id of this suite.
@@ -121,7 +118,7 @@ getJasmineRequireObj().Suite = function(j$) {
id: this.id, id: this.id,
description: this.description, description: this.description,
fullName: this.getFullName(), fullName: this.getFullName(),
parentSuiteId: this.reportedParentSuiteId, parentSuiteId: this.#reportedParentSuiteId,
filename: this.filename, filename: this.filename,
failedExpectations: [], failedExpectations: [],
deprecationWarnings: [], deprecationWarnings: [],
@@ -133,17 +130,17 @@ getJasmineRequireObj().Suite = function(j$) {
child.reset(); child.reset();
}); });
this.reportedDone = false; this.reportedDone = false;
}; }
Suite.prototype.removeChildren = function() { removeChildren() {
this.children = []; this.children = [];
}; }
Suite.prototype.addChild = function(child) { addChild(child) {
this.children.push(child); this.children.push(child);
}; }
Suite.prototype.status = function() { #status() {
if (this.markedPending) { if (this.markedPending) {
return 'pending'; return 'pending';
} }
@@ -153,18 +150,18 @@ getJasmineRequireObj().Suite = function(j$) {
} else { } else {
return 'passed'; return 'passed';
} }
}; }
Suite.prototype.canBeReentered = function() { getResult() {
return this.beforeAllFns.length === 0 && this.afterAllFns.length === 0; this.result.status = this.#status();
};
Suite.prototype.getResult = function() {
this.result.status = this.status();
return this.result; return this.result;
}; }
Suite.prototype.sharedUserContext = function() { canBeReentered() {
return this.beforeAllFns.length === 0 && this.afterAllFns.length === 0;
}
sharedUserContext() {
if (!this.sharedContext) { if (!this.sharedContext) {
this.sharedContext = this.parentSuite this.sharedContext = this.parentSuite
? this.parentSuite.clonedSharedUserContext() ? this.parentSuite.clonedSharedUserContext()
@@ -172,13 +169,13 @@ getJasmineRequireObj().Suite = function(j$) {
} }
return this.sharedContext; return this.sharedContext;
}; }
Suite.prototype.clonedSharedUserContext = function() { clonedSharedUserContext() {
return j$.UserContext.fromExisting(this.sharedUserContext()); return j$.UserContext.fromExisting(this.sharedUserContext());
}; }
Suite.prototype.handleException = function() { handleException() {
if (arguments[0] instanceof j$.errors.ExpectationFailed) { if (arguments[0] instanceof j$.errors.ExpectationFailed) {
return; return;
} }
@@ -201,12 +198,12 @@ getJasmineRequireObj().Suite = function(j$) {
} else { } else {
this.result.failedExpectations.push(failedExpectation); this.result.failedExpectations.push(failedExpectation);
} }
}; }
Suite.prototype.onMultipleDone = function() { onMultipleDone() {
let msg; let msg;
// Issue a deprecation. Include the context ourselves and pass // Issue an error. Include the context ourselves and pass
// ignoreRunnable: true, since getting here always means that we've already // 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. // moved on and the current runnable isn't the one that caused the problem.
if (this.parentSuite) { if (this.parentSuite) {
@@ -223,9 +220,9 @@ getJasmineRequireObj().Suite = function(j$) {
} }
this.onLateError(new Error(msg)); this.onLateError(new Error(msg));
}; }
Suite.prototype.addExpectationResult = function() { addExpectationResult() {
if (isFailure(arguments)) { if (isFailure(arguments)) {
const data = arguments[1]; const data = arguments[1];
const expectationResult = j$.buildExpectationResult(data); const expectationResult = j$.buildExpectationResult(data);
@@ -241,22 +238,22 @@ getJasmineRequireObj().Suite = function(j$) {
} }
} }
if (this.throwOnExpectationFailure) { if (this.#throwOnExpectationFailure) {
throw new j$.errors.ExpectationFailed(); throw new j$.errors.ExpectationFailed();
} }
} }
}; }
Suite.prototype.addDeprecationWarning = function(deprecation) { addDeprecationWarning(deprecation) {
if (typeof deprecation === 'string') { if (typeof deprecation === 'string') {
deprecation = { message: deprecation }; deprecation = { message: deprecation };
} }
this.result.deprecationWarnings.push( this.result.deprecationWarnings.push(
j$.buildExpectationResult(deprecation) j$.buildExpectationResult(deprecation)
); );
}; }
Suite.prototype.hasChildWithDescription = function(description) { hasChildWithDescription(description) {
for (const child of this.children) { for (const child of this.children) {
if (child.description === description) { if (child.description === description) {
return true; return true;
@@ -264,25 +261,33 @@ getJasmineRequireObj().Suite = function(j$) {
} }
return false; return false;
}; }
Object.defineProperty(Suite.prototype, 'metadata', { get metadata() {
get: function() {
if (!this.metadata_) { if (!this.metadata_) {
this.metadata_ = new SuiteMetadata(this); this.metadata_ = new SuiteMetadata(this);
} }
return this.metadata_; return this.metadata_;
} }
}); }
function removeFns(queueableFns) {
for (const qf of queueableFns) {
qf.fn = null;
}
}
/** /**
* @interface Suite * @interface Suite
* @see Env#topSuite * @see Env#topSuite
* @since 2.0.0 * @since 2.0.0
*/ */
function SuiteMetadata(suite) { class SuiteMetadata {
this.suite_ = suite; #suite;
constructor(suite) {
this.#suite = suite;
/** /**
* The unique ID of this suite. * The unique ID of this suite.
* @name Suite#id * @name Suite#id
@@ -317,9 +322,9 @@ getJasmineRequireObj().Suite = function(j$) {
* @returns {string} * @returns {string}
* @since 2.0.0 * @since 2.0.0
*/ */
SuiteMetadata.prototype.getFullName = function() { getFullName() {
return this.suite_.getFullName(); return this.#suite.getFullName();
}; }
/** /**
* The suite's children. * The suite's children.
@@ -327,11 +332,10 @@ getJasmineRequireObj().Suite = function(j$) {
* @type {Array.<(Spec|Suite)>} * @type {Array.<(Spec|Suite)>}
* @since 2.0.0 * @since 2.0.0
*/ */
Object.defineProperty(SuiteMetadata.prototype, 'children', { get children() {
get: function() { return this.#suite.children.map(child => child.metadata);
return this.suite_.children.map(child => child.metadata); }
} }
});
function isFailure(args) { function isFailure(args) {
return !args[0]; return !args[0];
+73 -70
View File
@@ -23,34 +23,9 @@ getJasmineRequireObj().TreeRunner = function(j$) {
async execute() { async execute() {
this.#hasFailures = false; this.#hasFailures = false;
const topSuite = this.#executionTree.topSuite;
const wrappedChildren = this.#wrapNodes(
this.#executionTree.childrenOfTopSuite()
);
const queueableFns = this.#addBeforeAndAfterAlls(
topSuite,
wrappedChildren
);
await new Promise(resolve => { await new Promise(resolve => {
this.#runQueue({ this.#executeSuiteSegment(this.#executionTree.topSuite, 0, resolve);
queueableFns,
userContext: this.#executionTree.topSuite.sharedUserContext(),
onException: function() {
topSuite.handleException.apply(topSuite, arguments);
}.bind(this),
onComplete: resolve,
onMultipleDone: topSuite.onMultipleDone
? topSuite.onMultipleDone.bind(topSuite)
: null,
SkipPolicy: this.#suiteSkipPolicy()
}); });
});
if (topSuite.hadBeforeAllFailure) {
await this.#reportChildrenOfBeforeAllFailure(topSuite);
}
return { hasFailures: this.#hasFailures }; return { hasFailures: this.#hasFailures };
} }
@@ -72,7 +47,10 @@ getJasmineRequireObj().TreeRunner = function(j$) {
_executeSpec(spec, specOverallDone) { _executeSpec(spec, specOverallDone) {
const onStart = next => { const onStart = next => {
this.#currentRunableTracker.setCurrentSpec(spec); this.#currentRunableTracker.setCurrentSpec(spec);
this.#runableResources.initForRunable(spec.id, spec.parentSuiteId); this.#runableResources.initForRunable(
spec.id,
spec.parentSuiteId || this.#executionTree.topSuite.id
);
this.#reportDispatcher.specStarted(spec.result).then(next); this.#reportDispatcher.specStarted(spec.result).then(next);
}; };
const resultCallback = (result, next) => { const resultCallback = (result, next) => {
@@ -142,19 +120,7 @@ getJasmineRequireObj().TreeRunner = function(j$) {
fns.unshift(start); fns.unshift(start);
if (config.detectLateRejectionHandling) { if (config.detectLateRejectionHandling) {
// Conditional because the setTimeout imposes a significant performance fns.push(this.#lateUnhandledRejectionChecker());
// penalty in suites with lots of fast specs.
const globalErrors = this.#globalErrors;
fns.push({
fn: done => {
// setTimeout is necessary to trigger rejectionhandled events
// TODO: let clearStack know about this so it doesn't do redundant setTimeouts
this.#setTimeout(function() {
globalErrors.reportUnhandledRejections();
done();
});
}
});
} }
fns.push(complete); fns.push(complete);
@@ -162,28 +128,48 @@ getJasmineRequireObj().TreeRunner = function(j$) {
} }
#executeSuiteSegment(suite, segmentNumber, done) { #executeSuiteSegment(suite, segmentNumber, done) {
const wrappedChildren = this.#wrapNodes( const isTopSuite = suite === this.#executionTree.topSuite;
this.#executionTree.childrenOfSuiteSegment(suite, segmentNumber) const isExcluded = this.#executionTree.isExcluded(suite);
); let befores = [];
const onStart = { 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 => { fn: next => {
this.#suiteSegmentStart(suite, next); this.#suiteSegmentStart(suite, next);
} }
}; });
const queueableFns = [ }
onStart,
...this.#addBeforeAndAfterAlls(suite, wrappedChildren)
];
this.#runQueue({ this.#runQueue({
// TODO: if onComplete always takes 0-1 arguments (and it probably does) onComplete: maybeError => {
// then it can be switched to an arrow fn with a named arg.
onComplete: function() {
const args = Array.prototype.slice.call(arguments, [0]);
this.#suiteSegmentComplete(suite, suite.getResult(), () => { this.#suiteSegmentComplete(suite, suite.getResult(), () => {
done.apply(undefined, args); done(maybeError);
}); });
}.bind(this), },
queueableFns, queueableFns,
userContext: suite.sharedUserContext(), userContext: suite.sharedUserContext(),
onException: function() { onException: function() {
@@ -196,6 +182,24 @@ getJasmineRequireObj().TreeRunner = function(j$) {
}); });
} }
// 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) { #suiteSegmentStart(suite, next) {
this.#currentRunableTracker.pushSuite(suite); this.#currentRunableTracker.pushSuite(suite);
this.#runableResources.initForRunable(suite.id, suite.parentSuite.id); this.#runableResources.initForRunable(suite.id, suite.parentSuite.id);
@@ -204,12 +208,18 @@ getJasmineRequireObj().TreeRunner = function(j$) {
} }
#suiteSegmentComplete(suite, result, next) { #suiteSegmentComplete(suite, result, next) {
suite.cleanupBeforeAfter(); const isTopSuite = suite === this.#executionTree.topSuite;
if (!isTopSuite) {
if (suite !== this.#currentRunableTracker.currentSuite()) { if (suite !== this.#currentRunableTracker.currentSuite()) {
throw new Error('Tried to complete the wrong suite'); 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.#runableResources.clearForRunable(suite.id);
this.#currentRunableTracker.popSuite(); this.#currentRunableTracker.popSuite();
@@ -217,13 +227,16 @@ getJasmineRequireObj().TreeRunner = function(j$) {
this.#hasFailures = true; this.#hasFailures = true;
} }
suite.endTimer(); suite.endTimer();
}
const finish = isTopSuite
? next
: () => this.#reportSuiteDone(suite, result, next);
if (suite.hadBeforeAllFailure) { if (suite.hadBeforeAllFailure) {
this.#reportChildrenOfBeforeAllFailure(suite).then(() => { this.#reportChildrenOfBeforeAllFailure(suite).then(finish);
this.#reportSuiteDone(suite, result, next);
});
} else { } else {
this.#reportSuiteDone(suite, result, next); finish();
} }
} }
@@ -280,16 +293,6 @@ getJasmineRequireObj().TreeRunner = function(j$) {
} }
} }
#addBeforeAndAfterAlls(suite, wrappedChildren) {
if (this.#executionTree.isExcluded(suite)) {
return wrappedChildren;
}
return suite.beforeAllFns
.concat(wrappedChildren)
.concat(suite.afterAllFns);
}
#suiteSkipPolicy() { #suiteSkipPolicy() {
if (this.#getConfig().stopOnSpecFailure) { if (this.#getConfig().stopOnSpecFailure) {
return j$.CompleteOnFirstErrorSkipPolicy; return j$.CompleteOnFirstErrorSkipPolicy;
+1
View File
@@ -43,6 +43,7 @@ var getJasmineRequireObj = (function(jasmineGlobal) {
j$.Clock = jRequire.Clock(); j$.Clock = jRequire.Clock();
j$.DelayedFunctionScheduler = jRequire.DelayedFunctionScheduler(j$); j$.DelayedFunctionScheduler = jRequire.DelayedFunctionScheduler(j$);
j$.Deprecator = jRequire.Deprecator(j$); j$.Deprecator = jRequire.Deprecator(j$);
j$.Configuration = jRequire.Configuration(j$);
j$.Env = jRequire.Env(j$); j$.Env = jRequire.Env(j$);
j$.StackTrace = jRequire.StackTrace(j$); j$.StackTrace = jRequire.StackTrace(j$);
j$.ExceptionFormatter = jRequire.ExceptionFormatter(j$); j$.ExceptionFormatter = jRequire.ExceptionFormatter(j$);
+12
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 HtmlReporter(options) {
function config() { function config() {
return (options.env && options.env.configuration()) || {}; return (options.env && options.env.configuration()) || {};
@@ -64,6 +71,11 @@ jasmineRequire.HtmlReporter = function(j$) {
const deprecationWarnings = []; const deprecationWarnings = [];
const failures = []; const failures = [];
/**
* Initializes the reporter. Should be called before {@link Env#execute}.
* @function
* @name HtmlReporter#initialize
*/
this.initialize = function() { this.initialize = function() {
clearPrior(); clearPrior();
htmlReporterMain = createDom( htmlReporterMain = createDom(
+9
View File
@@ -1,4 +1,6 @@
jasmineRequire.HtmlSpecFilter = function() { jasmineRequire.HtmlSpecFilter = function() {
// Legacy HTML spec filter, preserved for backward compatibility with
// boot files that predate HtmlExactSpecFilterV2
function HtmlSpecFilter(options) { function HtmlSpecFilter(options) {
const filterString = const filterString =
options && options &&
@@ -6,6 +8,13 @@ jasmineRequire.HtmlSpecFilter = function() {
options.filterString().replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&'); options.filterString().replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
const filterPattern = new RegExp(filterString); 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) { this.matches = function(specName) {
return filterPattern.test(specName); return filterPattern.test(specName);
}; };
+53 -24
View File
@@ -1,36 +1,55 @@
jasmineRequire.QueryString = function() { jasmineRequire.QueryString = function() {
function QueryString(options) { /**
this.navigateWithNewParam = function(key, value) { * Reads and manipulates the query string.
options.getWindowLocation().search = this.fullStringWithNewParam( * @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, key,
value value
); );
}; }
this.fullStringWithNewParam = function(key, value) { /**
const paramMap = queryStringToParamMap(); * 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; paramMap[key] = value;
return toQueryString(paramMap); 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); * 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 = []; let params = [];
const paramMap = {}; 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; return QueryString;
}; };