Compare commits

...

68 Commits

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

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

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

fixes #2068
2025-07-14 16:55:05 -07:00
Steve Gravrock
84f78c1435 Split GlobalErrors into portable and platform-specific parts 2025-07-12 13:59:19 -07:00
Steve Gravrock
ff476b1982 Unify error dispatching between browser and node 2025-07-12 13:56:58 -07:00
Steve Gravrock
d53d2ff3eb Convert GlobalErrors to an ES6 class 2025-07-12 13:56:50 -07:00
Steve Gravrock
adfbd00c75 Refactor mocking in GlobalErrorsSpec 2025-07-12 13:56:48 -07:00
Steve Gravrock
495e5fcd50 Backfill integration tests for unhandled promise rejections 2025-07-11 21:36:30 -07:00
Steve Gravrock
bc2aa7be25 Start breaking up integration/EnvSpec.js 2025-07-11 07:39:39 -07:00
Steve Gravrock
af04599bb5 Relaxed timeout on flaky test 2025-07-09 06:56:08 -07:00
Steve Gravrock
21db6ec0e3 Removed unnecessary errorWithStack helper 2025-06-22 12:49:26 -07:00
Steve Gravrock
2d07b3e6d7 Removed protections against user code redefining undefined
Jasmine hasn't even run on platforms that allowed redefining undefined
since 2.x.
2025-06-22 12:23:18 -07:00
Steve Gravrock
6891789ed2 Don't test on Node versions before 18.20.5
18.20.5 is the oldest version supported by current selenium-webdriver.
Also, many dev dependencies require at least 18.18.0.
2025-06-14 10:22:03 -07:00
Steve Gravrock
7a3d3c9360 Removed shelljs dev dependency 2025-06-14 09:05:12 -07:00
Steve Gravrock
1b2922e008 Don't hardcode temp dir in buildStandaloneDist 2025-06-14 09:05:12 -07:00
Steve Gravrock
bd8d23f2a7 Removed rimraf dev dependency 2025-06-14 09:05:06 -07:00
Steve Gravrock
de26763868 CI: remove special case for Chrome 2025-06-09 15:05:50 -07:00
Steve Gravrock
f4be08b657 Bump version to 5.8.0 2025-06-06 17:34:09 -07:00
Steve Gravrock
50ef882a1a Merge branch 'gh1886-spy-args-deep-clone' of https://github.com/evanwalsh/jasmine
Merges #2062 from @evanwaslh
Fixes #1886
2025-06-05 06:54:37 -07:00
Steve Gravrock
c1cd5c6291 Use custom object formatters in spy strategy mismatch errors 2025-06-05 05:46:29 -07:00
Steve Gravrock
63ed2b3948 Include function names in pretty printer output
This helps make matcher errors and spy strategy mismatch errors easier
to understand in cases where the difference involves expecting one
function but getting a different one.
2025-06-04 18:37:44 -07:00
Steve Gravrock
0183acc682 Fix diff building when only one side has a custom object formatter
Fixes #2061
2025-06-04 18:04:40 -07:00
Steve Gravrock
e15819c0dd Test aginast Node 24 2025-05-27 17:32:39 -07:00
Evan Walsh
f694194b2b Allow passing a function to saveArgumentsByValue to customize how arguments are saved
For instance, pass `structuredClone` to do a deep clone.

Fixes https://github.com/jasmine/jasmine/issues/1886
2025-05-27 15:43:21 -04:00
Steve Gravrock
94c00886a6 Merge branch 'setimmedate' of https://github.com/atscott/jasmine
Merges #2058 from @atscott
2025-05-03 10:00:41 -07:00
Andrew Scott
6a7c0e6368 perf(clock): use setImmediate for autoTick macrotask in Node
When called within an I/O cycle, `setImmediate` is generally faster because it
is designed to execute immediately after the current I/O event completes,
whereas `setTimeout(0)` gets placed in the timers queue and might be subject to delays.

> The main advantage to using setImmediate() over setTimeout() is setImmediate()
> will always be executed before any timers if scheduled within an I/O cycle,
> independently of how many timers are present.

* https://nodejs.org/en/learn/asynchronous-work/event-loop-timers-and-nexttick#setimmediate-vs-settimeout
* https://nodejs.org/en/learn/asynchronous-work/event-loop-timers-and-nexttick#poll
* https://nodejs.org/en/learn/asynchronous-work/understanding-setimmediate
2025-04-30 14:10:39 -07:00
70 changed files with 5940 additions and 4038 deletions

View File

@@ -4,6 +4,10 @@
version: 2.1
executors:
node24:
docker:
- image: cimg/node:24.0.0
working_directory: ~/workspace
node22:
docker:
- image: cimg/node:22.0.0
@@ -14,7 +18,7 @@ executors:
working_directory: ~/workspace
node18:
docker:
- image: cimg/node:18.0.0
- image: cimg/node:18.20.5
working_directory: ~/workspace
jobs:
@@ -100,6 +104,9 @@ workflows:
push:
jobs:
- build:
executor: node24
name: build_node_24
- build:
executor: node22
name: build_node_22
@@ -125,10 +132,10 @@ workflows:
requires:
- build_node_18
- test_parallel:
executor: node18
name: test_parallel_node_18
executor: node24
name: test_parallel_node_24
requires:
- build_node_18
- build_node_24
- test_parallel:
executor: node22
name: test_parallel_node_22
@@ -139,6 +146,11 @@ workflows:
name: test_parallel_node_20
requires:
- build_node_20
- test_parallel:
executor: node18
name: test_parallel_node_18
requires:
- build_node_18
- test_browsers:
requires:
- build_node_18

View File

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

View File

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

View File

@@ -557,6 +557,11 @@ jasmineRequire.HtmlReporter = function(j$) {
'a',
{ href: specHref(resultNode.result) },
specDescription
),
createDom(
'span',
{ className: 'jasmine-spec-duration' },
'(' + resultNode.result.duration + 'ms)'
)
)
);

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
{
"name": "jasmine-core",
"license": "MIT",
"version": "5.7.1",
"version": "5.10.0",
"repository": {
"type": "git",
"url": "https://github.com/jasmine/jasmine.git"
@@ -49,9 +49,7 @@
"jasmine-browser-runner": "github:jasmine/jasmine-browser-runner",
"jsdom": "^26.0.0",
"prettier": "1.17.1",
"rimraf": "^5.0.10",
"sass": "^1.58.3",
"shelljs": "^0.9.2"
"sass": "^1.58.3"
},
"browserslist": [
"Safari >= 15",

53
release_notes/5.10.0.md Normal file
View File

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

44
release_notes/5.8.0.md Normal file
View File

@@ -0,0 +1,44 @@
# Jasmine Core 5.8.0 Release Notes
## New Features
* Allow passing a function to `saveArgumentsByValue` to customize how arguments
are saved
* Merges [#2062](https://github.com/jasmine/jasmine/pull/2062) from @evanwaslh
* Fixes [#1886](https://github.com/jasmine/jasmine/issues/1886)
* Use custom object formatters in spy strategy mismatch errors
* Include function names in pretty printer output
* Improve performance of autoTick in Node
* Merges [#2058](https://github.com/jasmine/jasmine/pull/2058) from @atscott
## Bug Fixes
* Fix diff building when only one side has a custom object formatter
* Fixes [#2061](https://github.com/jasmine/jasmine/issues/2061)
## Documentation improvements
* Added Node 24 to supported environments
## Supported environments
This version has been tested in the following environments.
| Environment | Supported versions |
|-------------------|-------------------------|
| Node | 18**, 20, 22, 24 |
| Safari | 15**, 16**, 17** |
| Chrome | 137* |
| Firefox | 102**, 115**, 128, 139* |
| Edge | 137* |
\* Evergreen browser. Each version of Jasmine is tested against the latest
version available at release time.<br>
\** Supported on a best-effort basis. Support for these versions may be dropped
if it becomes impractical, and bugs affecting only these versions may not be
treated as release blockers.
------
_Release Notes generated with _[Anchorman](http://github.com/infews/anchorman)_

56
release_notes/5.9.0.md Normal file
View File

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

View File

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

View File

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

View File

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

View File

@@ -134,6 +134,42 @@ describe('CallTracker', function() {
expect(callTracker.mostRecent().args[1]).toEqual(arrayArg);
});
it('allows object arguments to be deep cloned', function() {
const callTracker = new jasmineUnderTest.CallTracker();
callTracker.saveArgumentsByValue(args => JSON.parse(JSON.stringify(args)));
const objectArg = { foo: { bar: { baz: ['qux'] } } },
arrayArg = ['foo', 'bar'];
callTracker.track({
object: {},
args: [objectArg, arrayArg, false, undefined, null, NaN, '', 0, 1.0]
});
objectArg.foo.bar.baz.push('quux');
expect(callTracker.mostRecent().args[0]).not.toBe(objectArg);
expect(callTracker.mostRecent().args[0]).not.toEqual(objectArg);
expect(callTracker.mostRecent().args[0]).toEqual({
foo: { bar: { baz: ['qux'] } }
});
expect(callTracker.mostRecent().args[1]).not.toBe(arrayArg);
expect(callTracker.mostRecent().args[1]).toEqual(arrayArg);
});
it('can take any function to transform arguments when saving by value', function() {
const callTracker = new jasmineUnderTest.CallTracker();
callTracker.saveArgumentsByValue(JSON.stringify);
const objectArg = { foo: { bar: { baz: ['qux'] } } },
arrayArg = ['foo', 'bar'],
args = [objectArg, arrayArg, false, undefined, null, NaN, '', 0, 1.0];
callTracker.track({ object: {}, args });
expect(callTracker.mostRecent().args).toEqual(JSON.stringify(args));
});
it('saves primitive arguments by value', function() {
const callTracker = new jasmineUnderTest.CallTracker(),
args = [undefined, null, false, '', /\s/, 0, 1.2, NaN];

View File

@@ -718,6 +718,20 @@ describe('Clock (acceptance)', function() {
clock.uninstall();
});
it('flushes microtask queue between macrotasks', async () => {
const log = [];
await new Promise(r => clock.setTimeout(r, 10)).then(() => {
log.push(1);
Promise.resolve().then(() => log.push(2));
Promise.resolve().then(() => log.push(3));
});
await new Promise(r => clock.setTimeout(r, 10)).then(() => {
log.push(4);
Promise.resolve().then(() => log.push(5));
});
expect(log).toEqual([1, 2, 3, 4, 5]);
});
it('can run setTimeouts/setIntervals asynchronously', function() {
const recurring = jasmine.createSpy('recurring'),
fn1 = jasmine.createSpy('fn1'),

View File

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

View File

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

View File

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

View File

@@ -164,7 +164,7 @@ describe('PrettyPrinter', function() {
"Object({ foo: 'bar', baz: 3, nullValue: null, undefinedValue: undefined })"
);
expect(pp({ foo: function() {}, bar: [1, 2, 3] })).toEqual(
'Object({ foo: Function, bar: [ 1, 2, 3 ] })'
"Object({ foo: Function 'foo', bar: [ 1, 2, 3 ] })"
);
});
@@ -450,7 +450,7 @@ describe('PrettyPrinter', function() {
};
expect(pp(objFromOtherContext)).toEqual(
"Object({ foo: 'bar', toString: Function })"
"Object({ foo: 'bar', toString: Function 'toString' })"
);
});
@@ -477,6 +477,17 @@ describe('PrettyPrinter', function() {
expect(pp(a)).toEqual('<anonymous>({ })');
});
it('stringifies functions with names', function() {
const pp = jasmineUnderTest.makePrettyPrinter();
expect(pp(foo)).toEqual("Function 'foo'");
function foo() {}
});
it('stringifies functions without names', function() {
const pp = jasmineUnderTest.makePrettyPrinter();
expect(pp(function() {})).toEqual('Function');
});
it('should handle objects with null prototype', function() {
const pp = jasmineUnderTest.makePrettyPrinter();
const obj = Object.create(null);

View File

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

View File

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

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

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

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

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

View File

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

View File

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

View File

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

View File

@@ -429,350 +429,6 @@ describe('Env integration', function() {
]);
});
describe('Handling async errors', function() {
it('routes async errors to a running spec', 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();
const reporter = jasmine.createSpyObj('fakeReporter', [
'specDone',
'suiteDone'
]);
env.addReporter(reporter);
env.describe('A suite', function() {
env.it('fails', function(specDone) {
setTimeout(function() {
dispatchErrorEvent(global, { error: 'fail' });
specDone();
});
});
});
await env.execute();
expect(reporter.specDone).toHaveFailedExpectationsForRunnable(
'A suite fails',
['fail thrown']
);
});
describe('When the running spec has reported specDone', function() {
it('routes async errors to an ancestor suite', async function() {
const global = {
...browserEventMethods(),
setTimeout: function(fn, delay) {
return setTimeout(fn, delay);
},
clearTimeout: function(fn) {
clearTimeout(fn);
},
queueMicrotask: function(fn) {
queueMicrotask(fn);
}
};
spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global);
const realClearStack = jasmineUnderTest.getClearStack(global);
const clearStackCallbacks = {};
let clearStackCallCount = 0;
spyOn(jasmineUnderTest, 'getClearStack').and.returnValue(function(fn) {
clearStackCallCount++;
if (clearStackCallbacks[clearStackCallCount]) {
clearStackCallbacks[clearStackCallCount]();
}
realClearStack(fn);
});
env.cleanup_();
env = new jasmineUnderTest.Env();
let suiteErrors = [];
env.addReporter({
suiteDone: function(result) {
const messages = result.failedExpectations.map(e => e.message);
suiteErrors = suiteErrors.concat(messages);
},
specDone: function() {
clearStackCallbacks[clearStackCallCount + 1] = function() {
dispatchErrorEvent(global, {
error: 'fail at the end of the reporter queue'
});
};
clearStackCallbacks[clearStackCallCount + 2] = function() {
dispatchErrorEvent(global, {
error: 'fail at the end of the spec queue'
});
};
}
});
env.describe('A suite', function() {
env.it('is finishing when the failure occurs', function() {});
});
await env.execute();
expect(suiteErrors).toEqual([
'fail at the end of the reporter queue thrown',
'fail at the end of the spec queue thrown'
]);
});
});
it('routes async errors to a running suite', 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();
const reporter = jasmine.createSpyObj('fakeReporter', [
'specDone',
'suiteDone'
]);
env.addReporter(reporter);
env.fdescribe('A suite', function() {
env.it('fails', function(specDone) {
setTimeout(function() {
specDone();
queueMicrotask(function() {
queueMicrotask(function() {
dispatchErrorEvent(global, { error: 'fail' });
});
});
});
});
});
env.describe('Ignored', function() {
env.it('is not run', function() {});
});
await env.execute();
expect(reporter.specDone).not.toHaveFailedExpectationsForRunnable(
'A suite fails',
['fail thrown']
);
expect(reporter.suiteDone).toHaveFailedExpectationsForRunnable(
'A suite',
['fail thrown']
);
});
describe('When the running suite has reported suiteDone', function() {
it('routes async errors to an ancestor suite', 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);
const realClearStack = jasmineUnderTest.getClearStack(global);
const clearStackCallbacks = {};
let clearStackCallCount = 0;
spyOn(jasmineUnderTest, 'getClearStack').and.returnValue(function(fn) {
clearStackCallCount++;
if (clearStackCallbacks[clearStackCallCount]) {
clearStackCallbacks[clearStackCallCount]();
}
realClearStack(fn);
});
env.cleanup_();
env = new jasmineUnderTest.Env();
let suiteErrors = [];
env.addReporter({
suiteDone: function(result) {
const messages = result.failedExpectations.map(e => e.message);
suiteErrors = suiteErrors.concat(messages);
if (result.description === 'A nested suite') {
clearStackCallbacks[clearStackCallCount + 1] = function() {
dispatchErrorEvent(global, {
error: 'fail at the end of the reporter queue'
});
};
clearStackCallbacks[clearStackCallCount + 2] = function() {
dispatchErrorEvent(global, {
error: 'fail at the end of the suite queue'
});
};
}
}
});
env.describe('A suite', function() {
env.describe('A nested suite', function() {
env.it('a spec', function() {});
});
});
await env.execute();
expect(suiteErrors).toEqual([
'fail at the end of the reporter queue thrown',
'fail at the end of the suite queue thrown'
]);
});
});
describe('When the env has started reporting jasmineDone', function() {
it('logs the error to the console', 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();
spyOn(console, 'error');
env.addReporter({
jasmineDone: function() {
dispatchErrorEvent(global, { error: 'a very late error' });
}
});
env.it('a spec', function() {});
await env.execute();
/* eslint-disable-next-line no-console */
expect(console.error).toHaveBeenCalledWith(
'Jasmine received a result after the suite finished:'
);
/* eslint-disable-next-line no-console */
expect(console.error).toHaveBeenCalledWith(
jasmine.objectContaining({
message: 'a very late error thrown',
globalErrorType: 'afterAll'
})
);
});
});
it('routes all errors that occur during stack clearing somewhere', async function() {
const global = {
...browserEventMethods(),
setTimeout: function(fn, delay) {
return setTimeout(fn, delay);
},
clearTimeout: function(fn) {
clearTimeout(fn);
},
queueMicrotask: function(fn) {
queueMicrotask(fn);
}
};
spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global);
const realClearStack = jasmineUnderTest.getClearStack(global);
let clearStackCallCount = 0;
let jasmineDone = false;
const expectedErrors = [];
const expectedErrorsAfterJasmineDone = [];
spyOn(jasmineUnderTest, 'getClearStack').and.returnValue(function(fn) {
clearStackCallCount++;
const msg = `Error in clearStack #${clearStackCallCount}`;
if (jasmineDone) {
expectedErrorsAfterJasmineDone.push(`${msg} thrown`);
} else {
expectedErrors.push(`${msg} thrown`);
}
dispatchErrorEvent(global, { error: msg });
realClearStack(fn);
});
spyOn(console, 'error');
env.cleanup_();
env = new jasmineUnderTest.Env();
const receivedErrors = [];
function logErrors(event) {
for (const failure of event.failedExpectations) {
receivedErrors.push(failure.message);
}
}
env.addReporter({
specDone: logErrors,
suiteDone: logErrors,
jasmineDone: function(event) {
jasmineDone = true;
logErrors(event);
}
});
env.describe('A suite', function() {
env.it('is finishing when the failure occurs', function() {});
});
await env.execute();
expect(receivedErrors.length).toEqual(expectedErrors.length);
for (const e of expectedErrors) {
expect(receivedErrors).toContain(e);
}
for (const message of expectedErrorsAfterJasmineDone) {
/* eslint-disable-next-line no-console */
expect(console.error).toHaveBeenCalledWith(
jasmine.objectContaining({ message })
);
}
});
});
it('reports multiple calls to done in the top suite as errors', async function() {
const reporter = jasmine.createSpyObj('fakeReporter', ['jasmineDone']);
const message =
@@ -1524,7 +1180,7 @@ describe('Env integration', function() {
env = new jasmineUnderTest.Env({
global: {
setTimeout: function(cb, t) {
const stack = jasmine.util.errorWithStack().stack;
const stack = new Error().stack;
if (stack.indexOf('ClearStack') >= 0) {
return realSetTimeout(cb, t);
} else {
@@ -2606,17 +2262,54 @@ describe('Env integration', function() {
);
});
it('throws an exception if you try to getSpecProperty outside of a spec', async function() {
const env = new jasmineUnderTest.Env();
let exception;
env.describe('a suite', function() {
env.it('a spec');
try {
env.getSpecProperty('a prop');
} catch (e) {
exception = e;
}
env.it('has a test', function() {});
});
await env.execute();
expect(exception.message).toBe(
"'getSpecProperty' was used when there was no current spec"
);
});
it('reports test properties on specs', async function() {
const env = new jasmineUnderTest.Env(),
reporter = jasmine.createSpyObj('reporter', ['suiteDone', 'specDone']);
reporter.specDone.and.callFake(function(e) {
expect(e.properties).toEqual({ a: 'Bee' });
expect(e.properties).toEqual({
fromBeforeEach: 'Pie',
fromSpecInnerCallback: 'Bee',
willChangeInAfterEach: 2,
fromAfterEach: 'Cheese'
});
});
env.addReporter(reporter);
env.beforeEach(function() {
env.setSpecProperty('fromBeforeEach', 'Pie');
});
env.afterEach(function() {
env.setSpecProperty(
'willChangeInAfterEach',
env.getSpecProperty('willChangeInAfterEach') + 1
);
env.setSpecProperty('fromAfterEach', 'Cheese');
});
env.it('calls setSpecProperty', function() {
env.setSpecProperty('a', 'Bee');
env.setSpecProperty('fromSpecInnerCallback', 'Bee');
env.setSpecProperty('willChangeInAfterEach', 1);
});
await env.execute();
@@ -2697,7 +2390,7 @@ describe('Env integration', function() {
setTimeout(function() {
throw new Error('suite');
}, 1);
}, 10);
}, 50);
env.it('spec', function() {});
});
@@ -2710,7 +2403,7 @@ describe('Env integration', function() {
throw new Error('spec');
}, 1);
},
10
50
);
});
@@ -2840,104 +2533,6 @@ describe('Env integration', function() {
);
});
it('reports errors that occur during loading', 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);
},
onerror: function() {}
};
spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global);
env.cleanup_();
env = new jasmineUnderTest.Env();
const reporter = jasmine.createSpyObj('reporter', [
'jasmineDone',
'suiteDone',
'specDone'
]);
env.addReporter(reporter);
dispatchErrorEvent(global, {
message: 'Uncaught SyntaxError: Unexpected end of input',
error: undefined,
filename: 'borkenSpec.js',
lineno: 42
});
const error = new Error('ENOCHEESE');
dispatchErrorEvent(global, { error });
await env.execute();
const e = reporter.jasmineDone.calls.argsFor(0)[0];
expect(e.failedExpectations).toEqual([
{
passed: false,
globalErrorType: 'load',
message: 'Uncaught SyntaxError: Unexpected end of input',
stack: undefined,
filename: 'borkenSpec.js',
lineno: 42
},
{
passed: false,
globalErrorType: 'load',
message: 'ENOCHEESE',
stack: error.stack,
filename: undefined,
lineno: undefined
}
]);
});
describe('If suppressLoadErrors: true was passed', function() {
it('does not install a global error handler during loading', async function() {
const originalOnerror = jasmine.createSpy('original onerror');
const global = {
...browserEventMethods(),
setTimeout: function(fn, delay) {
return setTimeout(fn, delay);
},
clearTimeout: function(fn, delay) {
clearTimeout(fn, delay);
},
queueMicrotask: function(fn) {
queueMicrotask(fn);
},
onerror: originalOnerror
};
spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global);
const globalErrors = new jasmineUnderTest.GlobalErrors(global);
const onerror = jasmine.createSpy('onerror');
globalErrors.pushListener(onerror);
spyOn(jasmineUnderTest, 'GlobalErrors').and.returnValue(globalErrors);
env.cleanup_();
env = new jasmineUnderTest.Env({ suppressLoadErrors: true });
const reporter = jasmine.createSpyObj('reporter', [
'jasmineDone',
'suiteDone',
'specDone'
]);
env.addReporter(reporter);
global.onerror('Uncaught Error: ENOCHEESE');
await env.execute();
const e = reporter.jasmineDone.calls.argsFor(0)[0];
expect(e.failedExpectations).toEqual([]);
expect(originalOnerror).toHaveBeenCalledWith('Uncaught Error: ENOCHEESE');
});
});
describe('Overall status in the jasmineDone event', function() {
describe('When everything passes', function() {
it('is "passed"', async function() {
@@ -3855,345 +3450,33 @@ describe('Env integration', function() {
expect(failedExpectations).toEqual([]);
});
describe('#spyOnGlobalErrorsAsync', function() {
const leftInstalledMessage =
'Global error spy was not uninstalled. ' +
'(Did you forget to await the return value of spyOnGlobalErrorsAsync?)';
function resultForRunable(reporterSpy, fullName) {
const match = reporterSpy.calls.all().find(function(call) {
return call.args[0].fullName === fullName;
});
if (!match) {
throw new Error(`No result for runable "${fullName}"`);
}
return match.args[0];
}
it('allows global errors to be suppressed and spied on', async function() {
env.it('a passing spec', async function() {
await env.spyOnGlobalErrorsAsync(async spy => {
setTimeout(() => {
throw new Error('nope');
});
await new Promise(resolve => setTimeout(resolve));
env.expect(spy).toHaveBeenCalledWith(new Error('nope'));
});
});
env.it('a failing spec', async function() {
await env.spyOnGlobalErrorsAsync(async spy => {
setTimeout(() => {
throw new Error('yep');
});
await new Promise(resolve => setTimeout(resolve));
env.expect(spy).toHaveBeenCalledWith(new Error('nope'));
});
});
const reporter = jasmine.createSpyObj('reporter', ['specDone']);
env.addReporter(reporter);
await jasmine.spyOnGlobalErrorsAsync(async function(globalErrorSpy) {
await env.execute();
if (isBrowser) {
// Verify that there were no unexpected errors
expect(globalErrorSpy).toHaveBeenCalledTimes(2);
expect(globalErrorSpy).toHaveBeenCalledWith(new Error('nope'));
expect(globalErrorSpy).toHaveBeenCalledWith(new Error('yep'));
it('uses custom object formatters in spy strategy argument mismatch errors', async function() {
env.it('a spec', function() {
env.addCustomObjectFormatter(function(value) {
if (typeof value === 'string') {
return 'custom:' + value;
}
});
const passingResult = resultForRunable(
reporter.specDone,
'a passing spec'
);
expect(passingResult.status).toEqual('passed');
expect(passingResult.failedExpectations).toEqual([]);
const failingResult = resultForRunable(
reporter.specDone,
'a failing spec'
);
expect(failingResult.status).toEqual('failed');
expect(failingResult.failedExpectations[0].message).toMatch(
/Expected \$\[0] = Error: yep to equal Error: nope\./
);
const spy = env
.createSpy('foo')
.withArgs('x')
.and.returnValue('');
spy('y');
});
it('cleans up if the global error spy is left installed in a beforeAll', async function() {
env.configure({ random: false });
env.describe('Suite 1', function() {
env.beforeAll(async function() {
env.spyOnGlobalErrorsAsync(function() {
// Never resolves
return new Promise(() => {});
});
});
env.it('a spec', function() {});
});
env.describe('Suite 2', function() {
env.it('a spec', async function() {
setTimeout(function() {
throw new Error('should fail the spec');
});
await new Promise(resolve => setTimeout(resolve));
});
});
const reporter = jasmine.createSpyObj('reporter', [
'specDone',
'suiteDone'
]);
env.addReporter(reporter);
await jasmine.spyOnGlobalErrorsAsync(async function(globalErrorSpy) {
await env.execute();
if (isBrowser) {
// Verify that there were no unexpected errors
expect(globalErrorSpy).toHaveBeenCalledTimes(1);
expect(globalErrorSpy).toHaveBeenCalledWith(
new Error('should fail the spec')
);
}
});
const suiteResult = resultForRunable(reporter.suiteDone, 'Suite 1');
expect(suiteResult.status).toEqual('failed');
expect(suiteResult.failedExpectations.length).toEqual(1);
expect(suiteResult.failedExpectations[0].message).toEqual(
leftInstalledMessage
);
const specResult = resultForRunable(reporter.specDone, 'Suite 2 a spec');
expect(specResult.status).toEqual('failed');
expect(specResult.failedExpectations.length).toEqual(1);
expect(specResult.failedExpectations[0].message).toMatch(
/Error: should fail the spec/
);
let failedExpectations;
env.addReporter({
specDone: r => (failedExpectations = r.failedExpectations)
});
it('cleans up if the global error spy is left installed in an afterAll', async function() {
env.configure({ random: false });
env.describe('Suite 1', function() {
env.afterAll(async function() {
env.spyOnGlobalErrorsAsync(function() {
// Never resolves
return new Promise(() => {});
});
});
env.it('a spec', function() {});
});
env.describe('Suite 2', function() {
env.it('a spec', async function() {
setTimeout(function() {
throw new Error('should fail the spec');
});
await new Promise(resolve => setTimeout(resolve));
});
});
const reporter = jasmine.createSpyObj('reporter', [
'specDone',
'suiteDone'
]);
env.addReporter(reporter);
await jasmine.spyOnGlobalErrorsAsync(async function(globalErrorSpy) {
await env.execute();
if (isBrowser) {
// Verify that there were no unexpected errors
expect(globalErrorSpy).toHaveBeenCalledTimes(1);
expect(globalErrorSpy).toHaveBeenCalledWith(
new Error('should fail the spec')
);
}
});
expect(reporter.suiteDone).toHaveFailedExpectationsForRunnable(
'Suite 1',
[leftInstalledMessage]
);
const suiteResult = resultForRunable(reporter.suiteDone, 'Suite 1');
expect(suiteResult.status).toEqual('failed');
expect(suiteResult.failedExpectations.length).toEqual(1);
expect(suiteResult.failedExpectations[0].message).toEqual(
leftInstalledMessage
);
const specResult = resultForRunable(reporter.specDone, 'Suite 2 a spec');
expect(specResult.status).toEqual('failed');
expect(specResult.failedExpectations.length).toEqual(1);
expect(specResult.failedExpectations[0].message).toMatch(
/Error: should fail the spec/
);
});
it('cleans up if the global error spy is left installed in a beforeEach', async function() {
env.configure({ random: false });
env.describe('Suite 1', function() {
env.beforeEach(async function() {
env.spyOnGlobalErrorsAsync(function() {
// Never resolves
return new Promise(() => {});
});
});
env.it('a spec', function() {});
});
env.describe('Suite 2', function() {
env.it('a spec', async function() {
setTimeout(function() {
throw new Error('should fail the spec');
});
await new Promise(resolve => setTimeout(resolve));
});
});
const reporter = jasmine.createSpyObj('reporter', [
'specDone',
'suiteDone'
]);
env.addReporter(reporter);
await jasmine.spyOnGlobalErrorsAsync(async function(globalErrorSpy) {
await env.execute();
if (isBrowser) {
// Verify that there were no unexpected errors
expect(globalErrorSpy).toHaveBeenCalledTimes(1);
expect(globalErrorSpy).toHaveBeenCalledWith(
new Error('should fail the spec')
);
}
});
const spec1Result = resultForRunable(reporter.specDone, 'Suite 1 a spec');
expect(spec1Result.status).toEqual('failed');
expect(spec1Result.failedExpectations.length).toEqual(1);
expect(spec1Result.failedExpectations[0].message).toEqual(
leftInstalledMessage
);
const spec2Result = resultForRunable(reporter.specDone, 'Suite 2 a spec');
expect(spec2Result.status).toEqual('failed');
expect(spec2Result.failedExpectations.length).toEqual(1);
expect(spec2Result.failedExpectations[0].message).toMatch(
/Error: should fail the spec/
);
});
it('cleans up if the global error spy is left installed in an it', async function() {
env.configure({ random: false });
env.it('spec 1', async function() {
env.spyOnGlobalErrorsAsync(function() {
// Never resolves
return new Promise(() => {});
});
});
env.it('spec 2', async function() {
setTimeout(function() {
throw new Error('should fail the spec');
});
await new Promise(resolve => setTimeout(resolve));
});
const reporter = jasmine.createSpyObj('reporter', ['specDone']);
env.addReporter(reporter);
await jasmine.spyOnGlobalErrorsAsync(async function(globalErrorSpy) {
await env.execute();
if (isBrowser) {
// Verify that there were no unexpected errors
expect(globalErrorSpy).toHaveBeenCalledTimes(1);
expect(globalErrorSpy).toHaveBeenCalledWith(
new Error('should fail the spec')
);
}
});
const spec1Result = resultForRunable(reporter.specDone, 'spec 1');
expect(spec1Result.status).toEqual('failed');
expect(spec1Result.failedExpectations.length).toEqual(1);
expect(spec1Result.failedExpectations[0].message).toEqual(
leftInstalledMessage
);
const spec2Result = resultForRunable(reporter.specDone, 'spec 2');
expect(spec2Result.status).toEqual('failed');
expect(spec2Result.failedExpectations.length).toEqual(1);
expect(spec2Result.failedExpectations[0].message).toMatch(
/Error: should fail the spec/
);
});
it('cleans up if the global error spy is left installed in an afterEach', async function() {
env.configure({ random: false });
env.describe('Suite 1', function() {
env.afterEach(async function() {
env.spyOnGlobalErrorsAsync(function() {
// Never resolves
return new Promise(() => {});
});
});
env.it('a spec', function() {});
});
env.describe('Suite 2', function() {
env.it('a spec', async function() {
setTimeout(function() {
throw new Error('should fail the spec');
});
await new Promise(resolve => setTimeout(resolve));
});
});
const reporter = jasmine.createSpyObj('reporter', [
'specDone',
'suiteDone'
]);
env.addReporter(reporter);
await jasmine.spyOnGlobalErrorsAsync(async function(globalErrorSpy) {
await env.execute();
if (isBrowser) {
// Verify that there were no unexpected errors
expect(globalErrorSpy).toHaveBeenCalledTimes(1);
expect(globalErrorSpy).toHaveBeenCalledWith(
new Error('should fail the spec')
);
}
});
const spec1Result = resultForRunable(reporter.specDone, 'Suite 1 a spec');
expect(spec1Result.status).toEqual('failed');
expect(spec1Result.failedExpectations.length).toEqual(1);
expect(spec1Result.failedExpectations[0].message).toEqual(
leftInstalledMessage
);
const spec2Result = resultForRunable(reporter.specDone, 'Suite 2 a spec');
expect(spec2Result.status).toEqual('failed');
expect(spec2Result.failedExpectations.length).toEqual(1);
expect(spec2Result.failedExpectations[0].message).toMatch(
/Error: should fail the spec/
);
});
await env.execute();
expect(failedExpectations).toEqual([
jasmine.objectContaining({
message: jasmine.stringContaining(
'received a call with arguments [ custom:y ]'
)
})
]);
});
it('reports a suite level error when a describe fn throws', async function() {
@@ -4451,7 +3734,7 @@ describe('Env integration', function() {
function browserEventMethods() {
return {
listeners_: { error: [], unhandledrejection: [] },
listeners_: { error: [], unhandledrejection: [], rejectionhandled: [] },
addEventListener(eventName, listener) {
this.listeners_[eventName].push(listener);
},

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -161,6 +161,63 @@ describe('DiffBuilder', function() {
expect(diffBuilder.getMessage()).toEqual(expectedMsg);
});
it('handles cases where only the expected has a custom object formatter', function() {
const formatter = function(x) {
if (typeof x === 'number') {
return '[number:' + x + ']';
}
};
const prettyPrinter = jasmineUnderTest.makePrettyPrinter([formatter]);
const diffBuilder = new jasmineUnderTest.DiffBuilder({
prettyPrinter: prettyPrinter
});
diffBuilder.setRoots('five', 4);
diffBuilder.recordMismatch();
expect(diffBuilder.getMessage()).toEqual(
"Expected 'five' to equal [number:4]."
);
});
it('handles cases where only the actual has a custom object formatter', function() {
const formatter = function(x) {
if (typeof x === 'number') {
return '[number:' + x + ']';
}
};
const prettyPrinter = jasmineUnderTest.makePrettyPrinter([formatter]);
const diffBuilder = new jasmineUnderTest.DiffBuilder({
prettyPrinter: prettyPrinter
});
diffBuilder.setRoots(5, 'four');
diffBuilder.recordMismatch();
expect(diffBuilder.getMessage()).toEqual(
"Expected [number:5] to equal 'four'."
);
});
it('handles complex cases where only one side has a custom object formatter', function() {
const formatter = function(x) {
if (typeof x === 'number') {
return '[number:' + x + ']';
}
};
const prettyPrinter = jasmineUnderTest.makePrettyPrinter([formatter]);
const diffBuilder = new jasmineUnderTest.DiffBuilder({
prettyPrinter: prettyPrinter
});
diffBuilder.setRoots(5, { foo: 'bar', fnord: { graults: ['wombat'] } });
diffBuilder.recordMismatch();
expect(diffBuilder.getMessage()).toEqual(
"Expected [number:5] to equal Object({ foo: 'bar', fnord: Object({ graults: [ 'wombat' ] }) })."
);
});
it('builds diffs involving asymmetric equality testers that implement valuesForDiff_ at the root', function() {
const prettyPrinter = jasmineUnderTest.makePrettyPrinter([]),
diffBuilder = new jasmineUnderTest.DiffBuilder({

View File

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

View File

@@ -458,9 +458,9 @@ describe('toEqual', function() {
});
it('reports mismatches between Functions', function() {
const actual = { x: function() {} },
expected = { x: function() {} },
message = 'Expected $.x = Function to equal Function.';
const actual = { x: function() {} };
const expected = { x: function() {} };
const message = "Expected $.x = Function 'x' to equal Function 'x'.";
expect(compareEquals(actual, expected).message).toEqual(message);
});

View File

@@ -82,7 +82,7 @@ describe('toThrowError', function() {
it('passes if thrown is an instanceof Error regardless of global that contains its constructor', function() {
if (isNotRunningInBrowser()) {
return;
pending('This test only runs in browsers.');
}
const matcher = jasmineUnderTest.matchers.toThrowError();

View File

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

View File

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

View File

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

View File

@@ -9,7 +9,7 @@ getJasmineRequireObj().CallTracker = function(j$) {
this.track = function(context) {
if (opts.cloneArgs) {
context.args = j$.util.cloneArgs(context.args);
context.args = opts.argsCloner(context.args);
}
calls.push(context);
};
@@ -117,13 +117,15 @@ getJasmineRequireObj().CallTracker = function(j$) {
};
/**
* Set this spy to do a shallow clone of arguments passed to each invocation.
* Set this spy to do a clone of arguments passed to each invocation.
* @name Spy#calls#saveArgumentsByValue
* @since 2.5.0
* @param {Function} [argsCloner] A function to use to clone the arguments. Defaults to a shallow cloning function.
* @function
*/
this.saveArgumentsByValue = function() {
this.saveArgumentsByValue = function(argsCloner = j$.util.cloneArgs) {
opts.cloneArgs = true;
opts.argsCloner = argsCloner;
};
this.unverifiedCount = function() {

View File

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

View File

@@ -226,6 +226,12 @@ callbacks to execute _before_ running the next one.
//
// @return {!Promise<undefined>}
async function newMacrotask() {
if (NODE_JS) {
// setImmediate is generally faster than setTimeout in Node
// https://nodejs.org/en/learn/asynchronous-work/event-loop-timers-and-nexttick#setimmediate-vs-settimeout
return new Promise(resolve => void setImmediate(resolve));
}
// MessageChannel ensures that setTimeout is not throttled to 4ms.
// https://developer.mozilla.org/en-US/docs/Web/API/setTimeout#reasons_for_delays_longer_than_specified
// https://stackblitz.com/edit/stackblitz-starters-qtlpcc

View File

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

View File

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

View File

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

View File

@@ -11,6 +11,7 @@ getJasmineRequireObj().Env = function(j$) {
options = options || {};
const self = this;
const GlobalErrors = options.GlobalErrors || j$.GlobalErrors;
const global = options.global || j$.getGlobal();
const realSetTimeout = global.setTimeout;
@@ -24,7 +25,12 @@ getJasmineRequireObj().Env = function(j$) {
new j$.MockDate(global)
);
const globalErrors = new j$.GlobalErrors();
const globalErrors = new GlobalErrors(
undefined,
// Configuration is late-bound because GlobalErrors needs to be constructed
// before it's set to detect load-time errors in browsers
() => this.configuration()
);
const installGlobalErrors = (function() {
let installed = false;
return function() {
@@ -43,7 +49,7 @@ getJasmineRequireObj().Env = function(j$) {
globalErrors
});
let reporter;
let reportDispatcher;
let topSuite;
let runner;
let parallelLoadingState = null; // 'specs', 'helpers', or null for non-parallel
@@ -121,7 +127,7 @@ getJasmineRequireObj().Env = function(j$) {
return true;
},
/**
* Whether or not reporters should hide disabled specs from their output.
* Whether reporters should hide disabled specs from their output.
* Currently only supported by Jasmine's HTMLReporter
* @name Configuration#hideDisabled
* @since 3.3.0
@@ -148,17 +154,31 @@ getJasmineRequireObj().Env = function(j$) {
*/
forbidDuplicateNames: false,
/**
* Whether or not to issue warnings for certain deprecated functionality
* 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 to prevent the suite output from being flooded
* 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
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
};
if (!options.suppressLoadErrors) {
@@ -196,7 +216,8 @@ getJasmineRequireObj().Env = function(j$) {
'stopOnSpecFailure',
'stopSpecOnExpectationFailure',
'autoCleanClosures',
'forbidDuplicateNames'
'forbidDuplicateNames',
'detectLateRejectionHandling'
];
booleanProps.forEach(function(prop) {
@@ -434,7 +455,7 @@ getJasmineRequireObj().Env = function(j$) {
deprecator.addDeprecationWarning(runable, deprecation, options);
};
function queueRunnerFactory(options) {
function runQueue(options) {
options.clearStack = options.clearStack || clearStack;
options.timeout = {
setTimeout: realSetTimeout,
@@ -456,9 +477,7 @@ getJasmineRequireObj().Env = function(j$) {
expectationFactory,
asyncExpectationFactory,
onLateError: recordLateError,
specResultCallback,
specStarted,
queueRunnerFactory
runQueue
});
topSuite = suiteBuilder.topSuite;
const deprecator = new j$.Deprecator(topSuite);
@@ -481,11 +500,11 @@ getJasmineRequireObj().Env = function(j$) {
* @interface Reporter
* @see custom_reporter
*/
reporter = new j$.ReportDispatcher(
reportDispatcher = new j$.ReportDispatcher(
j$.reporterEvents,
function(options) {
options.SkipPolicy = j$.NeverSkipPolicy;
return queueRunnerFactory(options);
return runQueue(options);
},
recordLateError
);
@@ -495,10 +514,11 @@ getJasmineRequireObj().Env = function(j$) {
totalSpecsDefined: () => suiteBuilder.totalSpecsDefined,
focusedRunables: () => suiteBuilder.focusedRunables,
runableResources,
reporter,
queueRunnerFactory,
getConfig: () => config,
reportSpecDone
reportDispatcher,
runQueue,
TreeProcessor: j$.TreeProcessor,
globalErrors,
getConfig: () => config
});
this.setParallelLoadingState = function(state) {
@@ -561,7 +581,7 @@ getJasmineRequireObj().Env = function(j$) {
throw new Error('Reporters cannot be added via Env in parallel mode');
}
reporter.addReporter(reporterToAdd);
reportDispatcher.addReporter(reporterToAdd);
};
/**
@@ -573,7 +593,7 @@ getJasmineRequireObj().Env = function(j$) {
* @see custom_reporter
*/
this.provideFallbackReporter = function(reporterToAdd) {
reporter.provideFallbackReporter(reporterToAdd);
reportDispatcher.provideFallbackReporter(reporterToAdd);
};
/**
@@ -587,7 +607,7 @@ getJasmineRequireObj().Env = function(j$) {
throw new Error('Reporters cannot be removed via Env in parallel mode');
}
reporter.clearReporters();
reportDispatcher.clearReporters();
};
/**
@@ -740,28 +760,6 @@ getJasmineRequireObj().Env = function(j$) {
.metadata;
};
function specResultCallback(spec, result, next) {
runableResources.clearForRunable(spec.id);
runner.currentSpec = null;
if (result.status === 'failed') {
runner.hasFailures = true;
}
reportSpecDone(spec, result, next);
}
function specStarted(spec, suite, next) {
runner.currentSpec = spec;
runableResources.initForRunable(spec.id, suite.id);
reporter.specStarted(spec.result).then(next);
}
function reportSpecDone(spec, result, next) {
spec.reportedDone = true;
reporter.specDone(result).then(next);
}
this.it = function(description, fn, timeout) {
ensureIsNotNested('it');
const filename = callerCallerFilename();
@@ -781,6 +779,26 @@ getJasmineRequireObj().Env = function(j$) {
return suiteBuilder.fit(description, fn, timeout, filename).metadata;
};
/**
* Get a user-defined property as part of the properties field of {@link SpecResult}
* @name Env#getSpecProperty
* @since 5.10.0
* @function
* @param {String} key The name of the property
* @returns {*} The value of the property
*/
this.getSpecProperty = function(key) {
if (
!runner.currentRunable() ||
runner.currentRunable() == runner.currentSuite()
) {
throw new Error(
"'getSpecProperty' was used when there was no current spec"
);
}
return runner.currentRunable().getSpecProperty(key);
};
/**
* Sets a user-defined property that will be provided to reporters as part of the properties field of {@link SpecResult}
* @name Env#setSpecProperty

View File

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

View File

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

View File

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

View File

@@ -16,7 +16,7 @@ getJasmineRequireObj().makePrettyPrinter = function(j$) {
if (customFormatResult) {
this.emitScalar(customFormatResult);
} else if (j$.util.isUndefined(value)) {
} else if (value === undefined) {
this.emitScalar('undefined');
} else if (value === null) {
this.emitScalar('null');
@@ -35,7 +35,11 @@ getJasmineRequireObj().makePrettyPrinter = function(j$) {
} else if (value instanceof RegExp) {
this.emitScalar(value.toString());
} else if (typeof value === 'function') {
this.emitScalar('Function');
if (value.name) {
this.emitScalar(`Function '${value.name}'`);
} else {
this.emitScalar('Function');
}
} else if (j$.isDomNode(value)) {
if (value.tagName) {
this.emitDomElement(value);

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,6 @@ getJasmineRequireObj().Spec = function(j$) {
function Spec(attrs) {
this.expectationFactory = attrs.expectationFactory;
this.asyncExpectationFactory = attrs.asyncExpectationFactory;
this.resultCallback = attrs.resultCallback || function() {};
this.id = attrs.id;
this.filename = attrs.filename;
this.parentSuiteId = attrs.parentSuiteId;
@@ -18,7 +17,6 @@ getJasmineRequireObj().Spec = function(j$) {
function() {
return {};
};
this.onStart = attrs.onStart || function() {};
this.autoCleanClosures =
attrs.autoCleanClosures === undefined ? true : !!attrs.autoCleanClosures;
@@ -65,79 +63,31 @@ getJasmineRequireObj().Spec = function(j$) {
}
};
Spec.prototype.getSpecProperty = function(key) {
this.result.properties = this.result.properties || {};
return this.result.properties[key];
};
Spec.prototype.setSpecProperty = function(key, value) {
this.result.properties = this.result.properties || {};
this.result.properties[key] = value;
};
Spec.prototype.execute = function(
queueRunnerFactory,
onComplete,
excluded,
failSpecWithNoExp
) {
const onStart = {
fn: done => {
this.timer.start();
this.onStart(this, done);
}
};
Spec.prototype.executionStarted = function() {
this.timer.start();
};
const complete = {
fn: done => {
if (this.autoCleanClosures) {
this.queueableFn.fn = null;
}
this.result.status = this.status(excluded, failSpecWithNoExp);
this.result.duration = this.timer.elapsed();
if (this.result.status !== 'failed') {
this.result.debugLogs = null;
}
this.resultCallback(this.result, done);
},
type: 'specCleanup'
};
const fns = this.beforeAndAfterFns();
const runnerConfig = {
isLeaf: true,
queueableFns: [...fns.befores, this.queueableFn, ...fns.afters],
onException: e => this.handleException(e),
onMultipleDone: () => {
// Issue a deprecation. Include the context ourselves and pass
// ignoreRunnable: true, since getting here always means that we've already
// moved on and the current runnable isn't the one that caused the problem.
this.onLateError(
new Error(
'An asynchronous spec, beforeEach, or afterEach function called its ' +
"'done' callback more than once.\n(in spec: " +
this.getFullName() +
')'
)
);
},
onComplete: () => {
if (this.result.status === 'failed') {
onComplete(new j$.StopExecutionError('spec failed'));
} else {
onComplete();
}
},
userContext: this.userContext(),
runnableName: this.getFullName.bind(this)
};
if (this.markedPending || excluded === true) {
runnerConfig.queueableFns = [];
Spec.prototype.executionFinished = function(excluded, failSpecWithNoExp) {
if (this.autoCleanClosures) {
this.queueableFn.fn = null;
}
runnerConfig.queueableFns.unshift(onStart);
runnerConfig.queueableFns.push(complete);
this.result.status = this.status(excluded, failSpecWithNoExp);
this.result.duration = this.timer.elapsed();
queueRunnerFactory(runnerConfig);
if (this.result.status !== 'failed') {
this.result.debugLogs = null;
}
};
Spec.prototype.reset = function() {
@@ -147,10 +97,12 @@ getJasmineRequireObj().Spec = function(j$) {
* @property {String} description - The description passed to the {@link it} that created this spec.
* @property {String} fullName - The full description including all ancestors of this spec.
* @property {String|null} parentSuiteId - The ID of the suite containing this spec, or null if this spec is not in a describe().
* @property {String} filename - The name of the file the spec was defined in.
* @property {String} filename - Deprecated. The name of the file the spec was defined in.
* Note: The value may be incorrect if zone.js is installed or
* `it`/`fit`/`xit` have been replaced with versions that don't maintain the
* same call stack height as the originals.
* same call stack height as the originals. This property may be removed in
* a future version unless there is enough user interest in keeping it.
* See {@link https://github.com/jasmine/jasmine/issues/2065}.
* @property {ExpectationResult[]} failedExpectations - The list of expectations that failed during execution of this spec.
* @property {ExpectationResult[]} passedExpectations - The list of expectations that passed during execution of this spec.
* @property {ExpectationResult[]} deprecationWarnings - The list of deprecation warnings that occurred during execution this spec.
@@ -226,6 +178,8 @@ getJasmineRequireObj().Spec = function(j$) {
this.pend(message);
};
// TODO: ensure that all access to result goes through .getResult()
// so that the status is correct.
Spec.prototype.getResult = function() {
this.result.status = this.status();
return this.result;

View File

@@ -161,7 +161,7 @@ getJasmineRequireObj().Spy = function(j$) {
"Spy '" +
strategyArgs.name +
"' received a call with arguments " +
j$.basicPrettyPrinter_(Array.prototype.slice.call(args)) +
matchersUtil.pp(Array.prototype.slice.call(args)) +
' but all configured strategies specify other arguments.'
);
} else {

View File

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

View File

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

View File

@@ -104,10 +104,12 @@ getJasmineRequireObj().Suite = function(j$) {
* @property {String} description - The description text passed to the {@link describe} that made this suite.
* @property {String} fullName - The full description including all ancestors of this suite.
* @property {String|null} parentSuiteId - The ID of the suite containing this suite, or null if this is not in another describe().
* @property {String} filename - The name of the file the suite was defined in.
* @property {String} filename - Deprecated. The name of the file the suite was defined in.
* Note: The value may be incorrect if zone.js is installed or
* `describe`/`fdescribe`/`xdescribe` have been replaced with versions that
* don't maintain the same call stack height as the originals.
* don't maintain the same call stack height as the originals. This property
* may be removed in a future version unless there is enough user interest
* in keeping it. See {@link https://github.com/jasmine/jasmine/issues/2065}.
* @property {ExpectationResult[]} failedExpectations - The list of expectations that failed in an {@link afterAll} for this suite.
* @property {ExpectationResult[]} deprecationWarnings - The list of deprecation warnings that occurred on this suite.
* @property {String} status - Once the suite has completed, this string represents the pass/fail status of this suite.

View File

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

View File

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

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

@@ -0,0 +1,303 @@
getJasmineRequireObj().TreeRunner = function(j$) {
class TreeRunner {
#executionTree;
#setTimeout;
#globalErrors;
#runableResources;
#reportDispatcher;
#runQueue;
#getConfig;
#currentRunableTracker;
#hasFailures;
constructor(attrs) {
this.#executionTree = attrs.executionTree;
this.#globalErrors = attrs.globalErrors;
this.#setTimeout = attrs.setTimeout || setTimeout.bind(globalThis);
this.#runableResources = attrs.runableResources;
this.#reportDispatcher = attrs.reportDispatcher;
this.#runQueue = attrs.runQueue;
this.#getConfig = attrs.getConfig;
this.#currentRunableTracker = attrs.currentRunableTracker;
}
async execute() {
this.#hasFailures = false;
const topSuite = this.#executionTree.topSuite;
const wrappedChildren = this.#wrapNodes(
this.#executionTree.childrenOfTopSuite()
);
const queueableFns = this.#addBeforeAndAfterAlls(
topSuite,
wrappedChildren
);
await new Promise(resolve => {
this.#runQueue({
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 };
}
#wrapNodes(nodes) {
return nodes.map(node => {
return {
fn: done => {
if (node.suite) {
this.#executeSuiteSegment(node.suite, node.segmentNumber, done);
} else {
this._executeSpec(node.spec, done);
}
}
};
});
}
// Only exposed for testing.
_executeSpec(spec, specOverallDone) {
const onStart = next => {
this.#currentRunableTracker.setCurrentSpec(spec);
this.#runableResources.initForRunable(spec.id, spec.parentSuiteId);
this.#reportDispatcher.specStarted(spec.result).then(next);
};
const resultCallback = (result, next) => {
this.#specComplete(spec).then(next);
};
const queueableFns = this.#specQueueableFns(
spec,
onStart,
resultCallback
);
this.#runQueue({
isLeaf: true,
queueableFns,
onException: e => spec.handleException(e),
onMultipleDone: () => {
// Issue an erorr. Include the context ourselves and pass
// ignoreRunnable: true, since getting here always means that we've already
// moved on and the current runnable isn't the one that caused the problem.
spec.onLateError(
new Error(
'An asynchronous spec, beforeEach, or afterEach function called its ' +
"'done' callback more than once.\n(in spec: " +
spec.getFullName() +
')'
)
);
},
onComplete: () => {
if (spec.result.status === 'failed') {
specOverallDone(new j$.StopExecutionError('spec failed'));
} else {
specOverallDone();
}
},
userContext: spec.userContext(),
runnableName: spec.getFullName.bind(spec),
SkipPolicy: j$.CompleteOnFirstErrorSkipPolicy
});
}
#specQueueableFns(spec, onStart, resultCallback) {
const config = this.#getConfig();
const excluded = this.#executionTree.isExcluded(spec);
const ba = spec.beforeAndAfterFns();
let fns = [...ba.befores, spec.queueableFn, ...ba.afters];
if (spec.markedPending || excluded === true) {
fns = [];
}
const start = {
fn(done) {
spec.executionStarted();
onStart(done);
}
};
const complete = {
fn(done) {
spec.executionFinished(excluded, config.failSpecWithNoExpectations);
resultCallback(spec.result, done);
},
type: 'specCleanup'
};
fns.unshift(start);
if (config.detectLateRejectionHandling) {
// Conditional because the setTimeout imposes a significant performance
// 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);
return fns;
}
#executeSuiteSegment(suite, segmentNumber, done) {
const wrappedChildren = this.#wrapNodes(
this.#executionTree.childrenOfSuiteSegment(suite, segmentNumber)
);
const onStart = {
fn: next => {
this.#suiteSegmentStart(suite, next);
}
};
const queueableFns = [
onStart,
...this.#addBeforeAndAfterAlls(suite, wrappedChildren)
];
this.#runQueue({
// TODO: if onComplete always takes 0-1 arguments (and it probably does)
// 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(), () => {
done.apply(undefined, args);
});
}.bind(this),
queueableFns,
userContext: suite.sharedUserContext(),
onException: function() {
suite.handleException.apply(suite, arguments);
},
onMultipleDone: suite.onMultipleDone
? suite.onMultipleDone.bind(suite)
: null,
SkipPolicy: this.#suiteSkipPolicy()
});
}
#suiteSegmentStart(suite, next) {
this.#currentRunableTracker.pushSuite(suite);
this.#runableResources.initForRunable(suite.id, suite.parentSuite.id);
this.#reportDispatcher.suiteStarted(suite.result).then(next);
suite.startTimer();
}
#suiteSegmentComplete(suite, result, next) {
suite.cleanupBeforeAfter();
if (suite !== this.#currentRunableTracker.currentSuite()) {
throw new Error('Tried to complete the wrong suite');
}
this.#runableResources.clearForRunable(suite.id);
this.#currentRunableTracker.popSuite();
if (result.status === 'failed') {
this.#hasFailures = true;
}
suite.endTimer();
if (suite.hadBeforeAllFailure) {
this.#reportChildrenOfBeforeAllFailure(suite).then(() => {
this.#reportSuiteDone(suite, result, next);
});
} else {
this.#reportSuiteDone(suite, result, next);
}
}
#reportSuiteDone(suite, result, next) {
suite.reportedDone = true;
this.#reportDispatcher.suiteDone(result).then(next);
}
async #specComplete(spec) {
this.#runableResources.clearForRunable(spec.id);
this.#currentRunableTracker.setCurrentSpec(null);
if (spec.result.status === 'failed') {
this.#hasFailures = true;
}
await this.#reportSpecDone(spec);
}
async #reportSpecDone(spec) {
spec.reportedDone = true;
await this.#reportDispatcher.specDone(spec.result);
}
async #reportChildrenOfBeforeAllFailure(suite) {
for (const child of suite.children) {
if (child instanceof j$.Suite) {
await this.#reportDispatcher.suiteStarted(child.result);
await this.#reportChildrenOfBeforeAllFailure(child);
// Marking the suite passed is consistent with how suites that
// contain failed specs but no suite-level failures are reported.
child.result.status = 'passed';
await this.#reportDispatcher.suiteDone(child.result);
} else {
/* a spec */
await this.#reportDispatcher.specStarted(child.result);
child.addExpectationResult(
false,
{
passed: false,
message:
'Not run because a beforeAll function failed. The ' +
'beforeAll failure will be reported on the suite that ' +
'caused it.'
},
true
);
child.result.status = 'failed';
await this.#reportSpecDone(child);
}
}
}
#addBeforeAndAfterAlls(suite, wrappedChildren) {
if (this.#executionTree.isExcluded(suite)) {
return wrappedChildren;
}
return suite.beforeAllFns
.concat(wrappedChildren)
.concat(suite.afterAllFns);
}
#suiteSkipPolicy() {
if (this.#getConfig().stopOnSpecFailure) {
return j$.CompleteOnFirstErrorSkipPolicy;
} else {
return j$.SkipAfterBeforeAllErrorPolicy;
}
}
}
return TreeRunner;
};

View File

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

View File

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

View File

@@ -31,13 +31,14 @@ getJasmineRequireObj().DiffBuilder = function(j$) {
const actualCustom = this.prettyPrinter_.customFormat_(actual);
const expectedCustom = this.prettyPrinter_.customFormat_(expected);
const useCustom = !(
j$.util.isUndefined(actualCustom) &&
j$.util.isUndefined(expectedCustom)
);
const useCustom =
actualCustom !== undefined || expectedCustom !== undefined;
if (useCustom) {
messages.push(wrapPrettyPrinted(actualCustom, expectedCustom, path));
const prettyActual = actualCustom || this.prettyPrinter_(actual);
const prettyExpected =
expectedCustom || this.prettyPrinter_(expected);
messages.push(wrapPrettyPrinted(prettyActual, prettyExpected, path));
return false; // don't recurse further
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -523,6 +523,11 @@ jasmineRequire.HtmlReporter = function(j$) {
'a',
{ href: specHref(resultNode.result) },
specDescription
),
createDom(
'span',
{ className: 'jasmine-spec-duration' },
'(' + resultNode.result.duration + 'ms)'
)
)
);

View File

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