Compare commits

...

14 Commits

Author SHA1 Message Date
Steve Gravrock
481f1e7c5c Bump version to 4.5.0 2022-10-29 14:48:32 -07:00
Steve Gravrock
5e650953cd Added Safari 16 to supported browsers 2022-10-22 13:08:10 -07:00
Steve Gravrock
87f9ab29df Fixed the jsdoc types of SuiteResult and SpecResult ids 2022-10-19 17:20:24 -07:00
Steve Gravrock
b831e81074 Include inner exceptions in stack traces 2022-09-24 12:12:21 -07:00
Steve Gravrock
4c13c2b00b Replaced var with const in API doc examples 2022-09-24 10:12:22 -07:00
Steve Gravrock
4cd190b232 Merge branch 'internal-async' 2022-09-18 13:31:43 -07:00
Steve Gravrock
d4025999b7 Report exceptions thrown by a describe before any it calls
Previously, these were masked by the "describe with no children" error.
Now they're reported as suite level errors on an empty suite.
2022-09-17 13:24:45 -07:00
Steve Gravrock
44f331f43d Updated the style of the examples
* const/let instead of var
* classes
* pass our own eslint checks
2022-09-17 12:00:20 -07:00
Steve Gravrock
59848ca151 Coerce the random string to a seed before sending it to reporters
This fixes an error in HTMLReporter when the configured seed is a
number rather than a string, which has been allowed since 3.8.0
2022-09-03 12:36:35 -07:00
Steve Gravrock
c14bfe3e5f Updated release process doc
* Fixed description of patch releases
* Moved -npm release docmentation to that repo
* Refer to -npm specifically rather than "binding libraries" generally,
  now that we only have one of those that versions in lockstep with core.
2022-09-03 11:02:13 -07:00
Steve Gravrock
b3d9435dbb Convreted TreeProcessor to async/await 2022-08-22 17:04:44 -07:00
Steve Gravrock
fec8dd37b0 Merge branch 'main' into internal-async 2022-08-22 10:46:03 -07:00
Steve Gravrock
5f3475342e Re-added missing JasmineStartedInfo jsdoc 2022-08-06 10:53:28 -07:00
Steve Gravrock
21f25972bb Converted ReportDispatcher to promises 2022-07-01 17:25:22 -07:00
29 changed files with 549 additions and 378 deletions

View File

@@ -33,7 +33,7 @@ Microsoft Edge) as well as Node.
| Environment | Supported versions |
|-------------------|--------------------|
| Node | 12.17+, 14, 16, 18 |
| Safari | 14-15 |
| Safari | 14-16 |
| Chrome | Evergreen |
| Firefox | Evergreen, 91 |
| Edge | Evergreen |

View File

@@ -18,12 +18,11 @@ copied to `jasmine.js` when the distribution is built. When releasing a new
version, update `package.json` with the new version and `npm run build` to
update the gem version number.
Note that Jasmine should only use the "patch" version number in the following cases:
Note that Jasmine should only use the "patch" version number if the new release
contains only bug fixes.
* Changes related to packaging for a specific binding library (npm or browser-runner)
* Fixes for regressions.
When jasmine-core revs its major or minor version, the binding libraries should also rev to that version.
When `jasmine-core` revs its major or minor version, the `jasmine` NPM package
should also rev to that version.
## Release
@@ -61,20 +60,13 @@ for instructions.
1. `rake release[${version}]` to copy the current edge docs to the new version
1. Commit and push.
### Release the binding libraries
### Release the `jasmine` NPM package
#### NPM
See <https://github.com/jasmine/jasmine-npm/blob/main/RELEASE.md>.
1. Create release notes using Anchorman as above
1. In `package.json`, update both the package version and the jasmine-core dependency version
1. Commit and push.
1. Wait for Circle CI to go green again.
1. `grunt release `. (Note: This will publish the package by running `npm publish`.)
### Publish the GitHub release
### Finally
For each of the above GitHub repos:
1. Visit the releases page and find the tag just published.
1. Paste in a link to the correct release notes for this release. The link should reference the blob and tag correctly, and the markdown file for the notes.
1. If it is a pre-release, mark it as such.
1. For core, attach the standalone zipfile.
2. Paste in a link to the correct release notes for this release.
3. If it is a pre-release, mark it as such.
4. Attach the standalone zipfile.

View File

@@ -1,24 +1,24 @@
function Player() {
}
Player.prototype.play = function(song) {
this.currentlyPlayingSong = song;
this.isPlaying = true;
};
Player.prototype.pause = function() {
this.isPlaying = false;
};
Player.prototype.resume = function() {
if (this.isPlaying) {
throw new Error("song is already playing");
class Player {
play(song) {
this.currentlyPlayingSong = song;
this.isPlaying = true;
}
this.isPlaying = true;
};
pause() {
this.isPlaying = false;
}
Player.prototype.makeFavorite = function() {
this.currentlyPlayingSong.persistFavoriteStatus(true);
};
resume() {
if (this.isPlaying) {
throw new Error('song is already playing');
}
this.isPlaying = true;
}
makeFavorite() {
this.currentlyPlayingSong.persistFavoriteStatus(true);
}
}
module.exports = Player;

View File

@@ -1,9 +1,8 @@
function Song() {
class Song {
persistFavoriteStatus(value) {
// something complicated
throw new Error('not yet implemented');
}
}
Song.prototype.persistFavoriteStatus = function(value) {
// something complicated
throw new Error("not yet implemented");
};
module.exports = Song;

View File

@@ -3,11 +3,11 @@ beforeEach(function () {
toBePlaying: function () {
return {
compare: function (actual, expected) {
var player = actual;
const player = actual;
return {
pass: player.currentlyPlayingSong === expected && player.isPlaying
}
};
}
};
}

View File

@@ -1,36 +1,37 @@
describe("Player", function() {
var Player = require('../../lib/jasmine_examples/Player');
var Song = require('../../lib/jasmine_examples/Song');
var player;
var song;
const Player = require('../../lib/jasmine_examples/Player');
const Song = require('../../lib/jasmine_examples/Song');
describe('Player', function() {
let player;
let song;
beforeEach(function() {
player = new Player();
song = new Song();
});
it("should be able to play a Song", function() {
it('should be able to play a Song', function() {
player.play(song);
expect(player.currentlyPlayingSong).toEqual(song);
//demonstrates use of custom matcher
// demonstrates use of custom matcher
expect(player).toBePlaying(song);
});
describe("when song has been paused", function() {
describe('when song has been paused', function() {
beforeEach(function() {
player.play(song);
player.pause();
});
it("should indicate that the song is currently paused", function() {
it('should indicate that the song is currently paused', function() {
expect(player.isPlaying).toBeFalsy();
// demonstrates use of 'not' with a custom matcher
expect(player).not.toBePlaying(song);
});
it("should be possible to resume", function() {
it('should be possible to resume', function() {
player.resume();
expect(player.isPlaying).toBeTruthy();
expect(player.currentlyPlayingSong).toEqual(song);
@@ -38,7 +39,7 @@ describe("Player", function() {
});
// demonstrates use of spies to intercept and test method calls
it("tells the current song if the user has made it a favorite", function() {
it('tells the current song if the user has made it a favorite', function() {
spyOn(song, 'persistFavoriteStatus');
player.play(song);
@@ -48,13 +49,13 @@ describe("Player", function() {
});
//demonstrates use of expected exceptions
describe("#resume", function() {
it("should throw an exception if song is already playing", function() {
describe('#resume', function() {
it('should throw an exception if song is already playing', function() {
player.play(song);
expect(function() {
player.resume();
}).toThrowError("song is already playing");
}).toThrowError('song is already playing');
});
});
});

View File

@@ -1,34 +1,34 @@
describe("Player", function() {
var player;
var song;
describe('Player', function() {
let player;
let song;
beforeEach(function() {
player = new Player();
song = new Song();
});
it("should be able to play a Song", function() {
it('should be able to play a Song', function() {
player.play(song);
expect(player.currentlyPlayingSong).toEqual(song);
//demonstrates use of custom matcher
// demonstrates use of custom matcher
expect(player).toBePlaying(song);
});
describe("when song has been paused", function() {
describe('when song has been paused', function() {
beforeEach(function() {
player.play(song);
player.pause();
});
it("should indicate that the song is currently paused", function() {
it('should indicate that the song is currently paused', function() {
expect(player.isPlaying).toBeFalsy();
// demonstrates use of 'not' with a custom matcher
expect(player).not.toBePlaying(song);
});
it("should be possible to resume", function() {
it('should be possible to resume', function() {
player.resume();
expect(player.isPlaying).toBeTruthy();
expect(player.currentlyPlayingSong).toEqual(song);
@@ -36,7 +36,7 @@ describe("Player", function() {
});
// demonstrates use of spies to intercept and test method calls
it("tells the current song if the user has made it a favorite", function() {
it('tells the current song if the user has made it a favorite', function() {
spyOn(song, 'persistFavoriteStatus');
player.play(song);
@@ -46,13 +46,13 @@ describe("Player", function() {
});
//demonstrates use of expected exceptions
describe("#resume", function() {
it("should throw an exception if song is already playing", function() {
describe('#resume', function() {
it('should throw an exception if song is already playing', function() {
player.play(song);
expect(function() {
player.resume();
}).toThrowError("song is already playing");
}).toThrowError('song is already playing');
});
});
});

View File

@@ -3,7 +3,7 @@ beforeEach(function () {
toBePlaying: function () {
return {
compare: function (actual, expected) {
var player = actual;
const player = actual;
return {
pass: player.currentlyPlayingSong === expected && player.isPlaying

View File

@@ -1,22 +1,22 @@
function Player() {
}
Player.prototype.play = function(song) {
this.currentlyPlayingSong = song;
this.isPlaying = true;
};
Player.prototype.pause = function() {
this.isPlaying = false;
};
Player.prototype.resume = function() {
if (this.isPlaying) {
throw new Error("song is already playing");
class Player {
play(song) {
this.currentlyPlayingSong = song;
this.isPlaying = true;
}
this.isPlaying = true;
};
pause() {
this.isPlaying = false;
}
Player.prototype.makeFavorite = function() {
this.currentlyPlayingSong.persistFavoriteStatus(true);
};
resume() {
if (this.isPlaying) {
throw new Error('song is already playing');
}
this.isPlaying = true;
}
makeFavorite() {
this.currentlyPlayingSong.persistFavoriteStatus(true);
}
}

View File

@@ -1,7 +1,6 @@
function Song() {
class Song {
persistFavoriteStatus(value) {
// something complicated
throw new Error('not yet implemented');
}
}
Song.prototype.persistFavoriteStatus = function(value) {
// something complicated
throw new Error("not yet implemented");
};

View File

@@ -776,7 +776,7 @@ getJasmineRequireObj().Spec = function(j$) {
/**
* @typedef SpecResult
* @property {Int} id - The unique id of this spec.
* @property {String} id - The unique id of this spec.
* @property {String} description - The description passed to the {@link it} that created this spec.
* @property {String} fullName - The full description including all ancestors of this spec.
* @property {Expectation[]} failedExpectations - The list of expectations that failed during execution of this spec.
@@ -1843,12 +1843,12 @@ getJasmineRequireObj().Env = function(j$) {
function specStarted(spec, suite, next) {
runner.currentSpec = spec;
runableResources.initForRunable(spec.id, suite.id);
reporter.specStarted(spec.result, next);
reporter.specStarted(spec.result).then(next);
}
function reportSpecDone(spec, result, next) {
spec.reportedDone = true;
reporter.specDone(result, next);
reporter.specDone(result).then(next);
}
this.it = function(description, fn, timeout) {
@@ -2868,7 +2868,8 @@ getJasmineRequireObj().clearStack = function(j$) {
SAFARI ||
j$.util.isUndefined(global.MessageChannel) /* tests */
) {
// queueMicrotask is dramatically faster than MessageChannel in Safari.
// queueMicrotask is dramatically faster than MessageChannel in Safari,
// at least through version 16.
// Some of our own integration tests provide a mock queueMicrotask in all
// environments because it's simpler to mock than MessageChannel.
return browserQueueMicrotaskImpl(global);
@@ -3494,18 +3495,38 @@ getJasmineRequireObj().ExceptionFormatter = function(j$) {
return null;
}
const stackTrace = new j$.StackTrace(error);
const lines = filterJasmine(stackTrace);
let result = '';
const lines = this.stack_(error, {
messageHandling: omitMessage ? 'omit' : undefined
});
return lines.join('\n');
};
if (stackTrace.message && !omitMessage) {
// messageHandling can be falsy (unspecified), 'omit', or 'require'
this.stack_ = function(error, { messageHandling }) {
let lines = formatProperties(error).split('\n');
if (lines[lines.length - 1] === '') {
lines.pop();
}
const stackTrace = new j$.StackTrace(error);
lines = lines.concat(filterJasmine(stackTrace));
if (messageHandling === 'require') {
lines.unshift(stackTrace.message || 'Error: ' + error.message);
} else if (messageHandling !== 'omit' && stackTrace.message) {
lines.unshift(stackTrace.message);
}
result += formatProperties(error);
result += lines.join('\n');
if (error.cause) {
const substack = this.stack_(error.cause, {
messageHandling: 'require'
});
substack[0] = 'Caused by: ' + substack[0];
lines = lines.concat(substack);
}
return result;
return lines;
};
function filterJasmine(stackTrace) {
@@ -5241,7 +5262,7 @@ getJasmineRequireObj().MatchersUtil = function(j$) {
* };
* }
*
* var actual = {
* const actual = {
* n: 2,
* otherFields: "don't care"
* };
@@ -6387,7 +6408,7 @@ getJasmineRequireObj().toHaveClass = function(j$) {
* @since 3.0.0
* @param {Object} expected - The class name to test for
* @example
* var el = document.createElement('div');
* const el = document.createElement('div');
* el.className = 'foo bar baz';
* expect(el).toHaveClass('bar');
*/
@@ -7719,7 +7740,7 @@ getJasmineRequireObj().ReportDispatcher = function(j$) {
for (const method of dispatchedMethods) {
this[method] = (function(m) {
return function() {
dispatch(m, arguments);
return dispatch(m, arguments);
};
})(method);
}
@@ -7745,25 +7766,25 @@ getJasmineRequireObj().ReportDispatcher = function(j$) {
if (reporters.length === 0 && fallbackReporter !== null) {
reporters.push(fallbackReporter);
}
const onComplete = args[args.length - 1];
args = Array.from(args).splice(0, args.length - 1);
const fns = [];
for (const reporter of reporters) {
addFn(fns, reporter, method, args);
}
queueRunnerFactory({
queueableFns: fns,
onComplete: onComplete,
isReporter: true,
onMultipleDone: function() {
onLateError(
new Error(
"An asynchronous reporter callback called its 'done' callback " +
'more than once.'
)
);
}
return new Promise(function(resolve) {
queueRunnerFactory({
queueableFns: fns,
onComplete: resolve,
isReporter: true,
onMultipleDone: function() {
onLateError(
new Error(
"An asynchronous reporter callback called its 'done' callback " +
'more than once.'
)
);
}
});
});
}
@@ -8419,7 +8440,6 @@ getJasmineRequireObj().Runner = function(j$) {
this.executedBefore_ = true;
this.hasFailures = false;
const totalSpecsDefined = this.totalSpecsDefined_();
const focusedRunables = this.focusedRunables_();
const config = this.getConfig_();
@@ -8433,7 +8453,7 @@ getJasmineRequireObj().Runner = function(j$) {
const order = new j$.Order({
random: config.random,
seed: config.seed
seed: j$.isNumber_(config.seed) ? config.seed + '' : config.seed
});
const processor = new j$.TreeProcessor({
@@ -8458,7 +8478,7 @@ getJasmineRequireObj().Runner = function(j$) {
nodeStart: (suite, next) => {
this.currentlyExecutingSuites_.push(suite);
this.runableResources_.initForRunable(suite.id, suite.parentSuite.id);
this.reporter_.suiteStarted(suite.result, next);
this.reporter_.suiteStarted(suite.result).then(next);
suite.startTimer();
},
nodeComplete: (suite, result, next) => {
@@ -8496,106 +8516,97 @@ getJasmineRequireObj().Runner = function(j$) {
);
}
return this.execute2_(runablesToRun, order, processor);
}
async execute2_(runablesToRun, order, processor) {
const totalSpecsDefined = this.totalSpecsDefined_();
this.runableResources_.initForRunable(this.topSuite_.id);
const jasmineTimer = new j$.Timer();
jasmineTimer.start();
return new Promise(resolve => {
/**
* Information passed to the {@link Reporter#jasmineStarted} event.
* @typedef JasmineStartedInfo
* @property {Int} totalSpecsDefined - The total number of specs defined in this suite.
* @property {Order} order - Information about the ordering (random or not) of this execution of the suite.
* @since 2.0.0
*/
this.reporter_.jasmineStarted(
{
totalSpecsDefined,
order: order
},
() => {
this.currentlyExecutingSuites_.push(this.topSuite_);
processor.execute(() => {
(async () => {
if (this.topSuite_.hadBeforeAllFailure) {
await this.reportChildrenOfBeforeAllFailure_(this.topSuite_);
}
this.runableResources_.clearForRunable(this.topSuite_.id);
this.currentlyExecutingSuites_.pop();
let overallStatus, incompleteReason;
if (
this.hasFailures ||
this.topSuite_.result.failedExpectations.length > 0
) {
overallStatus = 'failed';
} else if (focusedRunables.length > 0) {
overallStatus = 'incomplete';
incompleteReason = 'fit() or fdescribe() was found';
} else if (totalSpecsDefined === 0) {
overallStatus = 'incomplete';
incompleteReason = 'No specs found';
} else {
overallStatus = 'passed';
}
/**
* Information passed to the {@link Reporter#jasmineDone} event.
* @typedef JasmineDoneInfo
* @property {OverallStatus} overallStatus - The overall result of the suite: 'passed', 'failed', or 'incomplete'.
* @property {Int} totalTime - The total time (in ms) that it took to execute the suite
* @property {IncompleteReason} incompleteReason - Explanation of why the suite was incomplete.
* @property {Order} order - Information about the ordering (random or not) of this execution of the suite.
* @property {Expectation[]} failedExpectations - List of expectations that failed in an {@link afterAll} at the global level.
* @property {Expectation[]} deprecationWarnings - List of deprecation warnings that occurred at the global level.
* @since 2.4.0
*/
const jasmineDoneInfo = {
overallStatus: overallStatus,
totalTime: jasmineTimer.elapsed(),
incompleteReason: incompleteReason,
order: order,
failedExpectations: this.topSuite_.result.failedExpectations,
deprecationWarnings: this.topSuite_.result.deprecationWarnings
};
this.topSuite_.reportedDone = true;
this.reporter_.jasmineDone(jasmineDoneInfo, function() {
resolve(jasmineDoneInfo);
});
})();
});
}
);
/**
* Information passed to the {@link Reporter#jasmineStarted} event.
* @typedef JasmineStartedInfo
* @property {Int} totalSpecsDefined - The total number of specs defined in this suite.
* @property {Order} order - Information about the ordering (random or not) of this execution of the suite.
* @since 2.0.0
*/
await this.reporter_.jasmineStarted({
totalSpecsDefined,
order: order
});
this.currentlyExecutingSuites_.push(this.topSuite_);
await processor.execute();
if (this.topSuite_.hadBeforeAllFailure) {
await this.reportChildrenOfBeforeAllFailure_(this.topSuite_);
}
this.runableResources_.clearForRunable(this.topSuite_.id);
this.currentlyExecutingSuites_.pop();
let overallStatus, incompleteReason;
if (
this.hasFailures ||
this.topSuite_.result.failedExpectations.length > 0
) {
overallStatus = 'failed';
} else if (this.focusedRunables_().length > 0) {
overallStatus = 'incomplete';
incompleteReason = 'fit() or fdescribe() was found';
} else if (totalSpecsDefined === 0) {
overallStatus = 'incomplete';
incompleteReason = 'No specs found';
} else {
overallStatus = 'passed';
}
/**
* Information passed to the {@link Reporter#jasmineDone} event.
* @typedef JasmineDoneInfo
* @property {OverallStatus} overallStatus - The overall result of the suite: 'passed', 'failed', or 'incomplete'.
* @property {Int} totalTime - The total time (in ms) that it took to execute the suite
* @property {IncompleteReason} incompleteReason - Explanation of why the suite was incomplete.
* @property {Order} order - Information about the ordering (random or not) of this execution of the suite.
* @property {Expectation[]} failedExpectations - List of expectations that failed in an {@link afterAll} at the global level.
* @property {Expectation[]} deprecationWarnings - List of deprecation warnings that occurred at the global level.
* @since 2.4.0
*/
const jasmineDoneInfo = {
overallStatus: overallStatus,
totalTime: jasmineTimer.elapsed(),
incompleteReason: incompleteReason,
order: order,
failedExpectations: this.topSuite_.result.failedExpectations,
deprecationWarnings: this.topSuite_.result.deprecationWarnings
};
this.topSuite_.reportedDone = true;
await this.reporter_.jasmineDone(jasmineDoneInfo);
return jasmineDoneInfo;
}
reportSuiteDone_(suite, result, next) {
suite.reportedDone = true;
this.reporter_.suiteDone(result, next);
this.reporter_.suiteDone(result).then(next);
}
async reportChildrenOfBeforeAllFailure_(suite) {
for (const child of suite.children) {
if (child instanceof j$.Suite) {
await new Promise(resolve => {
this.reporter_.suiteStarted(child.result, resolve);
});
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 new Promise(resolve => {
this.reporter_.suiteDone(child.result, resolve);
});
await this.reporter_.suiteDone(child.result);
} else {
/* a spec */
await new Promise(resolve => {
this.reporter_.specStarted(child.result, resolve);
});
await this.reporter_.specStarted(child.result);
child.addExpectationResult(
false,
@@ -9621,7 +9632,7 @@ getJasmineRequireObj().Suite = function(j$) {
Suite.prototype.reset = function() {
/**
* @typedef SuiteResult
* @property {Int} id - The unique id of this suite.
* @property {String} id - The unique id of this suite.
* @property {String} description - The description text passed to the {@link describe} that made this suite.
* @property {String} fullName - The full description including all ancestors of this suite.
* @property {Expectation[]} failedExpectations - The list of expectations that failed in an {@link afterAll} for this suite.
@@ -9872,11 +9883,6 @@ getJasmineRequireObj().SuiteBuilder = function(j$) {
suite.exclude();
}
this.addSpecsToSuite_(suite, definitionFn);
if (suite.parentSuite && !suite.children.length) {
throw new Error(
`describe with no children (describe() or it()): ${suite.getFullName()}`
);
}
return suite;
}
@@ -10023,11 +10029,19 @@ getJasmineRequireObj().SuiteBuilder = function(j$) {
const parentSuite = this.currentDeclarationSuite_;
parentSuite.addChild(suite);
this.currentDeclarationSuite_ = suite;
let threw = false;
try {
definitionFn();
} catch (e) {
suite.handleException(e);
threw = true;
}
if (suite.parentSuite && !suite.children.length && !threw) {
throw new Error(
`describe with no children (describe() or it()): ${suite.getFullName()}`
);
}
this.currentDeclarationSuite_ = parentSuite;
@@ -10195,7 +10209,7 @@ getJasmineRequireObj().TreeProcessor = function() {
return stats;
};
this.execute = function(done) {
this.execute = async function() {
if (!processed) {
this.processTree();
}
@@ -10206,16 +10220,18 @@ getJasmineRequireObj().TreeProcessor = function() {
const childFns = wrapChildren(tree, 0);
queueRunnerFactory({
queueableFns: childFns,
userContext: tree.sharedUserContext(),
onException: function() {
tree.handleException.apply(tree, arguments);
},
onComplete: done,
onMultipleDone: tree.onMultipleDone
? tree.onMultipleDone.bind(tree)
: null
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
});
});
};

View File

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

40
release_notes/4.5.0.md Normal file
View File

@@ -0,0 +1,40 @@
# Jasmine 4.5.0 Release Notes
## New Features
* Added Safari 16 to supported browsers
* Include inner exceptions in stack traces
## Bug Fixes
* Report exceptions thrown by a describe before any it calls
* Coerce the random string to a seed before sending it to reporters
* This fixes an error in HTMLReporter when the configured seed is a
number rather than a string, which has been allowed since 3.8.0
## Documentation updates
* Fixed the jsdoc types of SuiteResult and SpecResult ids
* Replaced var with const in API doc examples
* Updated the style of the examples that are included in jasmine-core
## Internal improvements
* Converted TreeProcessor to async/await
* Converted ReportDispatcher to promises
## Supported environments
jasmine-core 4.5.0 has been tested in the following environments.
| Environment | Supported versions |
|-------------------|--------------------|
| Node | 12.17+, 14, 16, 18 |
| Safari | 14-16 |
| Chrome | 107 |
| Firefox | 91, 102, 106 |
| Edge | 106 |
------
_Release Notes generated with _[Anchorman](http://github.com/infews/anchorman)_

View File

@@ -27,6 +27,7 @@ run_browser chrome latest
run_browser firefox latest
run_browser firefox 102
run_browser firefox 91
run_browser safari 16
run_browser safari 15
run_browser safari 14
run_browser MicrosoftEdge latest

View File

@@ -256,5 +256,51 @@ describe('ExceptionFormatter', function() {
expect(result).not.toContain('an error');
});
});
describe('In environments that support the cause property of Errors', function() {
beforeEach(function() {
const inner = new Error('inner');
const outer = new Error('outer', { cause: inner });
if (!outer.cause) {
// Currently: Node 12, Node 14, Safari 14
pending('Environment does not support error cause');
}
});
it('recursively includes the cause in the stack trace in this environment', function() {
const subject = new jasmineUnderTest.ExceptionFormatter();
const rootCause = new Error('root cause');
const proximateCause = new Error('proximate cause', {
cause: rootCause
});
const symptom = new Error('symptom', { cause: proximateCause });
const lines = subject.stack(symptom).split('\n');
// Not all environments include the message in the stack trace.
const hasRootMessage = lines[0].indexOf('symptom') !== -1;
const firstSymptomStackIx = hasRootMessage ? 1 : 0;
expect(lines[firstSymptomStackIx])
.withContext('first symptom stack frame')
.toContain('ExceptionFormatterSpec.js');
const proximateCauseMsgIx = lines.indexOf(
'Caused by: Error: proximate cause'
);
expect(proximateCauseMsgIx)
.withContext('index of proximate cause message')
.toBeGreaterThan(firstSymptomStackIx);
expect(lines[proximateCauseMsgIx + 1])
.withContext('first proximate cause stack frame')
.toContain('ExceptionFormatterSpec.js');
const rootCauseMsgIx = lines.indexOf('Caused by: Error: root cause');
expect(rootCauseMsgIx)
.withContext('index of root cause message')
.toBeGreaterThan(proximateCauseMsgIx + 1);
expect(lines[rootCauseMsgIx + 1])
.withContext('first root cause stack frame')
.toContain('ExceptionFormatterSpec.js');
});
});
});
});

View File

@@ -18,13 +18,12 @@ describe('ReportDispatcher', function() {
queueRunnerFactory
),
reporter = jasmine.createSpyObj('reporter', ['foo', 'bar']),
anotherReporter = jasmine.createSpyObj('reporter', ['foo', 'bar']),
completeCallback = jasmine.createSpy('complete');
anotherReporter = jasmine.createSpyObj('reporter', ['foo', 'bar']);
dispatcher.addReporter(reporter);
dispatcher.addReporter(anotherReporter);
dispatcher.foo(123, 456, completeCallback);
dispatcher.foo(123, 456);
expect(queueRunnerFactory).toHaveBeenCalledWith(
jasmine.objectContaining({
@@ -47,7 +46,7 @@ describe('ReportDispatcher', function() {
queueRunnerFactory.calls.reset();
dispatcher.bar('a', 'b', completeCallback);
dispatcher.bar('a', 'b');
expect(queueRunnerFactory).toHaveBeenCalledWith(
jasmine.objectContaining({
@@ -91,11 +90,10 @@ describe('ReportDispatcher', function() {
['foo', 'bar'],
queueRunnerFactory
),
reporter = jasmine.createSpyObj('reporter', ['foo', 'bar']),
completeCallback = jasmine.createSpy('complete');
reporter = jasmine.createSpyObj('reporter', ['foo', 'bar']);
dispatcher.provideFallbackReporter(reporter);
dispatcher.foo(123, 456, completeCallback);
dispatcher.foo(123, 456);
expect(queueRunnerFactory).toHaveBeenCalledWith(
jasmine.objectContaining({
@@ -116,12 +114,11 @@ describe('ReportDispatcher', function() {
queueRunnerFactory
),
reporter = jasmine.createSpyObj('reporter', ['foo', 'bar']),
fallbackReporter = jasmine.createSpyObj('otherReporter', ['foo', 'bar']),
completeCallback = jasmine.createSpy('complete');
fallbackReporter = jasmine.createSpyObj('otherReporter', ['foo', 'bar']);
dispatcher.provideFallbackReporter(fallbackReporter);
dispatcher.addReporter(reporter);
dispatcher.foo(123, 456, completeCallback);
dispatcher.foo(123, 456);
expect(queueRunnerFactory).toHaveBeenCalledWith(
jasmine.objectContaining({
@@ -143,11 +140,10 @@ describe('ReportDispatcher', function() {
queueRunnerFactory
),
reporter1 = jasmine.createSpyObj('reporter1', ['foo', 'bar']),
reporter2 = jasmine.createSpyObj('reporter2', ['foo', 'bar']),
completeCallback = jasmine.createSpy('complete');
reporter2 = jasmine.createSpyObj('reporter2', ['foo', 'bar']);
dispatcher.addReporter(reporter1);
dispatcher.foo(123, completeCallback);
dispatcher.foo(123);
expect(queueRunnerFactory).toHaveBeenCalledWith(
jasmine.objectContaining({
queueableFns: [{ fn: jasmine.any(Function) }],
@@ -161,7 +157,7 @@ describe('ReportDispatcher', function() {
dispatcher.clearReporters();
dispatcher.addReporter(reporter2);
dispatcher.bar(456, completeCallback);
dispatcher.bar(456);
expect(queueRunnerFactory).toHaveBeenCalledWith(
jasmine.objectContaining({

View File

@@ -274,7 +274,7 @@ describe('TreeProcessor', function() {
expect(result.valid).toBe(true);
});
it('runs a single leaf', function() {
it('runs a single leaf', async function() {
const leaf = new Leaf(),
node = new Node({ children: [leaf], userContext: { root: 'context' } }),
queueRunner = jasmine.createSpy('queueRunner'),
@@ -282,25 +282,27 @@ describe('TreeProcessor', function() {
tree: node,
runnableIds: [leaf.id],
queueRunnerFactory: queueRunner
}),
treeComplete = jasmine.createSpy('treeComplete');
});
processor.execute(treeComplete);
const promise = processor.execute();
expect(queueRunner).toHaveBeenCalledWith({
onComplete: treeComplete,
onComplete: jasmine.any(Function),
onException: jasmine.any(Function),
userContext: { root: 'context' },
queueableFns: [{ fn: jasmine.any(Function) }],
onMultipleDone: null
});
queueRunner.calls.mostRecent().args[0].queueableFns[0].fn('foo');
const queueRunnerArgs = queueRunner.calls.mostRecent().args[0];
queueRunnerArgs.queueableFns[0].fn('foo');
expect(leaf.execute).toHaveBeenCalledWith(queueRunner, 'foo', false, false);
queueRunnerArgs.onComplete();
await expectAsync(promise).toBeResolvedTo(undefined);
});
it('runs a node with no children', function() {
it('runs a node with no children', async function() {
const node = new Node({ userContext: { node: 'context' } }),
root = new Node({ children: [node], userContext: { root: 'context' } }),
nodeStart = jasmine.createSpy('nodeStart'),
@@ -313,21 +315,20 @@ describe('TreeProcessor', function() {
nodeComplete: nodeComplete,
queueRunnerFactory: queueRunner
}),
treeComplete = jasmine.createSpy('treeComplete'),
nodeDone = jasmine.createSpy('nodeDone');
processor.execute(treeComplete);
const promise = processor.execute();
expect(queueRunner).toHaveBeenCalledWith({
onComplete: treeComplete,
onComplete: jasmine.any(Function),
onException: jasmine.any(Function),
userContext: { root: 'context' },
queueableFns: [{ fn: jasmine.any(Function) }],
onMultipleDone: null
});
queueRunner.calls.mostRecent().args[0].queueableFns[0].fn(nodeDone);
const queueRunnerArgs = queueRunner.calls.mostRecent().args[0];
queueRunnerArgs.queueableFns[0].fn(nodeDone);
expect(queueRunner).toHaveBeenCalledWith({
onComplete: jasmine.any(Function),
onMultipleDone: null,
@@ -348,6 +349,9 @@ describe('TreeProcessor', function() {
{ my: 'result' },
jasmine.any(Function)
);
queueRunnerArgs.onComplete();
await expectAsync(promise).toBeResolvedTo(undefined);
});
it('runs a node with children', function() {

View File

@@ -2026,6 +2026,29 @@ describe('Env integration', function() {
expect(doneArg.order.seed).toEqual('123456');
});
it('coerces the random seed to a string if it is a number', async function() {
const reporter = jasmine.createSpyObj('fakeReporter', [
'jasmineStarted',
'jasmineDone',
'suiteStarted',
'suiteDone',
'specStarted',
'specDone'
]);
env.configure({ random: true, seed: 123456 });
env.addReporter(reporter);
env.configure({ random: true });
await env.execute();
expect(reporter.jasmineStarted).toHaveBeenCalled();
const startedArg = reporter.jasmineStarted.calls.argsFor(0)[0];
expect(startedArg.order.seed).toEqual('123456');
const doneArg = reporter.jasmineDone.calls.argsFor(0)[0];
expect(doneArg.order.seed).toEqual('123456');
});
it('should report pending spec messages', async function() {
const reporter = jasmine.createSpyObj('fakeReporter', ['specDone']);
@@ -3943,6 +3966,44 @@ describe('Env integration', function() {
});
});
it('reports a suite level error when a describe fn throws', async function() {
const reporter = jasmine.createSpyObj('reporter', ['suiteDone']);
env.addReporter(reporter);
env.describe('throws before defining specs', function() {
throw new Error('nope');
});
env.describe('throws after defining specs', function() {
env.it('is a spec');
throw new Error('nope');
});
await env.execute();
expect(reporter.suiteDone).toHaveBeenCalledWith(
jasmine.objectContaining({
fullName: 'throws after defining specs',
failedExpectations: [
jasmine.objectContaining({
message: jasmine.stringContaining('Error: nope')
})
]
})
);
expect(reporter.suiteDone).toHaveBeenCalledWith(
jasmine.objectContaining({
fullName: 'throws after defining specs',
failedExpectations: [
jasmine.objectContaining({
message: jasmine.stringContaining('Error: nope')
})
]
})
);
});
function browserEventMethods() {
return {
addEventListener() {},

View File

@@ -80,7 +80,8 @@ getJasmineRequireObj().clearStack = function(j$) {
SAFARI ||
j$.util.isUndefined(global.MessageChannel) /* tests */
) {
// queueMicrotask is dramatically faster than MessageChannel in Safari.
// queueMicrotask is dramatically faster than MessageChannel in Safari,
// at least through version 16.
// Some of our own integration tests provide a mock queueMicrotask in all
// environments because it's simpler to mock than MessageChannel.
return browserQueueMicrotaskImpl(global);

View File

@@ -701,12 +701,12 @@ getJasmineRequireObj().Env = function(j$) {
function specStarted(spec, suite, next) {
runner.currentSpec = spec;
runableResources.initForRunable(spec.id, suite.id);
reporter.specStarted(spec.result, next);
reporter.specStarted(spec.result).then(next);
}
function reportSpecDone(spec, result, next) {
spec.reportedDone = true;
reporter.specDone(result, next);
reporter.specDone(result).then(next);
}
this.it = function(description, fn, timeout) {

View File

@@ -44,18 +44,38 @@ getJasmineRequireObj().ExceptionFormatter = function(j$) {
return null;
}
const stackTrace = new j$.StackTrace(error);
const lines = filterJasmine(stackTrace);
let result = '';
const lines = this.stack_(error, {
messageHandling: omitMessage ? 'omit' : undefined
});
return lines.join('\n');
};
if (stackTrace.message && !omitMessage) {
// messageHandling can be falsy (unspecified), 'omit', or 'require'
this.stack_ = function(error, { messageHandling }) {
let lines = formatProperties(error).split('\n');
if (lines[lines.length - 1] === '') {
lines.pop();
}
const stackTrace = new j$.StackTrace(error);
lines = lines.concat(filterJasmine(stackTrace));
if (messageHandling === 'require') {
lines.unshift(stackTrace.message || 'Error: ' + error.message);
} else if (messageHandling !== 'omit' && stackTrace.message) {
lines.unshift(stackTrace.message);
}
result += formatProperties(error);
result += lines.join('\n');
if (error.cause) {
const substack = this.stack_(error.cause, {
messageHandling: 'require'
});
substack[0] = 'Caused by: ' + substack[0];
lines = lines.concat(substack);
}
return result;
return lines;
};
function filterJasmine(stackTrace) {

View File

@@ -5,7 +5,7 @@ getJasmineRequireObj().ReportDispatcher = function(j$) {
for (const method of dispatchedMethods) {
this[method] = (function(m) {
return function() {
dispatch(m, arguments);
return dispatch(m, arguments);
};
})(method);
}
@@ -31,25 +31,25 @@ getJasmineRequireObj().ReportDispatcher = function(j$) {
if (reporters.length === 0 && fallbackReporter !== null) {
reporters.push(fallbackReporter);
}
const onComplete = args[args.length - 1];
args = Array.from(args).splice(0, args.length - 1);
const fns = [];
for (const reporter of reporters) {
addFn(fns, reporter, method, args);
}
queueRunnerFactory({
queueableFns: fns,
onComplete: onComplete,
isReporter: true,
onMultipleDone: function() {
onLateError(
new Error(
"An asynchronous reporter callback called its 'done' callback " +
'more than once.'
)
);
}
return new Promise(function(resolve) {
queueRunnerFactory({
queueableFns: fns,
onComplete: resolve,
isReporter: true,
onMultipleDone: function() {
onLateError(
new Error(
"An asynchronous reporter callback called its 'done' callback " +
'more than once.'
)
);
}
});
});
}

View File

@@ -37,7 +37,6 @@ getJasmineRequireObj().Runner = function(j$) {
this.executedBefore_ = true;
this.hasFailures = false;
const totalSpecsDefined = this.totalSpecsDefined_();
const focusedRunables = this.focusedRunables_();
const config = this.getConfig_();
@@ -51,7 +50,7 @@ getJasmineRequireObj().Runner = function(j$) {
const order = new j$.Order({
random: config.random,
seed: config.seed
seed: j$.isNumber_(config.seed) ? config.seed + '' : config.seed
});
const processor = new j$.TreeProcessor({
@@ -76,7 +75,7 @@ getJasmineRequireObj().Runner = function(j$) {
nodeStart: (suite, next) => {
this.currentlyExecutingSuites_.push(suite);
this.runableResources_.initForRunable(suite.id, suite.parentSuite.id);
this.reporter_.suiteStarted(suite.result, next);
this.reporter_.suiteStarted(suite.result).then(next);
suite.startTimer();
},
nodeComplete: (suite, result, next) => {
@@ -114,106 +113,97 @@ getJasmineRequireObj().Runner = function(j$) {
);
}
return this.execute2_(runablesToRun, order, processor);
}
async execute2_(runablesToRun, order, processor) {
const totalSpecsDefined = this.totalSpecsDefined_();
this.runableResources_.initForRunable(this.topSuite_.id);
const jasmineTimer = new j$.Timer();
jasmineTimer.start();
return new Promise(resolve => {
/**
* Information passed to the {@link Reporter#jasmineStarted} event.
* @typedef JasmineStartedInfo
* @property {Int} totalSpecsDefined - The total number of specs defined in this suite.
* @property {Order} order - Information about the ordering (random or not) of this execution of the suite.
* @since 2.0.0
*/
this.reporter_.jasmineStarted(
{
totalSpecsDefined,
order: order
},
() => {
this.currentlyExecutingSuites_.push(this.topSuite_);
processor.execute(() => {
(async () => {
if (this.topSuite_.hadBeforeAllFailure) {
await this.reportChildrenOfBeforeAllFailure_(this.topSuite_);
}
this.runableResources_.clearForRunable(this.topSuite_.id);
this.currentlyExecutingSuites_.pop();
let overallStatus, incompleteReason;
if (
this.hasFailures ||
this.topSuite_.result.failedExpectations.length > 0
) {
overallStatus = 'failed';
} else if (focusedRunables.length > 0) {
overallStatus = 'incomplete';
incompleteReason = 'fit() or fdescribe() was found';
} else if (totalSpecsDefined === 0) {
overallStatus = 'incomplete';
incompleteReason = 'No specs found';
} else {
overallStatus = 'passed';
}
/**
* Information passed to the {@link Reporter#jasmineDone} event.
* @typedef JasmineDoneInfo
* @property {OverallStatus} overallStatus - The overall result of the suite: 'passed', 'failed', or 'incomplete'.
* @property {Int} totalTime - The total time (in ms) that it took to execute the suite
* @property {IncompleteReason} incompleteReason - Explanation of why the suite was incomplete.
* @property {Order} order - Information about the ordering (random or not) of this execution of the suite.
* @property {Expectation[]} failedExpectations - List of expectations that failed in an {@link afterAll} at the global level.
* @property {Expectation[]} deprecationWarnings - List of deprecation warnings that occurred at the global level.
* @since 2.4.0
*/
const jasmineDoneInfo = {
overallStatus: overallStatus,
totalTime: jasmineTimer.elapsed(),
incompleteReason: incompleteReason,
order: order,
failedExpectations: this.topSuite_.result.failedExpectations,
deprecationWarnings: this.topSuite_.result.deprecationWarnings
};
this.topSuite_.reportedDone = true;
this.reporter_.jasmineDone(jasmineDoneInfo, function() {
resolve(jasmineDoneInfo);
});
})();
});
}
);
/**
* Information passed to the {@link Reporter#jasmineStarted} event.
* @typedef JasmineStartedInfo
* @property {Int} totalSpecsDefined - The total number of specs defined in this suite.
* @property {Order} order - Information about the ordering (random or not) of this execution of the suite.
* @since 2.0.0
*/
await this.reporter_.jasmineStarted({
totalSpecsDefined,
order: order
});
this.currentlyExecutingSuites_.push(this.topSuite_);
await processor.execute();
if (this.topSuite_.hadBeforeAllFailure) {
await this.reportChildrenOfBeforeAllFailure_(this.topSuite_);
}
this.runableResources_.clearForRunable(this.topSuite_.id);
this.currentlyExecutingSuites_.pop();
let overallStatus, incompleteReason;
if (
this.hasFailures ||
this.topSuite_.result.failedExpectations.length > 0
) {
overallStatus = 'failed';
} else if (this.focusedRunables_().length > 0) {
overallStatus = 'incomplete';
incompleteReason = 'fit() or fdescribe() was found';
} else if (totalSpecsDefined === 0) {
overallStatus = 'incomplete';
incompleteReason = 'No specs found';
} else {
overallStatus = 'passed';
}
/**
* Information passed to the {@link Reporter#jasmineDone} event.
* @typedef JasmineDoneInfo
* @property {OverallStatus} overallStatus - The overall result of the suite: 'passed', 'failed', or 'incomplete'.
* @property {Int} totalTime - The total time (in ms) that it took to execute the suite
* @property {IncompleteReason} incompleteReason - Explanation of why the suite was incomplete.
* @property {Order} order - Information about the ordering (random or not) of this execution of the suite.
* @property {Expectation[]} failedExpectations - List of expectations that failed in an {@link afterAll} at the global level.
* @property {Expectation[]} deprecationWarnings - List of deprecation warnings that occurred at the global level.
* @since 2.4.0
*/
const jasmineDoneInfo = {
overallStatus: overallStatus,
totalTime: jasmineTimer.elapsed(),
incompleteReason: incompleteReason,
order: order,
failedExpectations: this.topSuite_.result.failedExpectations,
deprecationWarnings: this.topSuite_.result.deprecationWarnings
};
this.topSuite_.reportedDone = true;
await this.reporter_.jasmineDone(jasmineDoneInfo);
return jasmineDoneInfo;
}
reportSuiteDone_(suite, result, next) {
suite.reportedDone = true;
this.reporter_.suiteDone(result, next);
this.reporter_.suiteDone(result).then(next);
}
async reportChildrenOfBeforeAllFailure_(suite) {
for (const child of suite.children) {
if (child instanceof j$.Suite) {
await new Promise(resolve => {
this.reporter_.suiteStarted(child.result, resolve);
});
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 new Promise(resolve => {
this.reporter_.suiteDone(child.result, resolve);
});
await this.reporter_.suiteDone(child.result);
} else {
/* a spec */
await new Promise(resolve => {
this.reporter_.specStarted(child.result, resolve);
});
await this.reporter_.specStarted(child.result);
child.addExpectationResult(
false,

View File

@@ -39,7 +39,7 @@ getJasmineRequireObj().Spec = function(j$) {
/**
* @typedef SpecResult
* @property {Int} id - The unique id of this spec.
* @property {String} id - The unique id of this spec.
* @property {String} description - The description passed to the {@link it} that created this spec.
* @property {String} fullName - The full description including all ancestors of this spec.
* @property {Expectation[]} failedExpectations - The list of expectations that failed during execution of this spec.

View File

@@ -106,7 +106,7 @@ getJasmineRequireObj().Suite = function(j$) {
Suite.prototype.reset = function() {
/**
* @typedef SuiteResult
* @property {Int} id - The unique id of this suite.
* @property {String} id - The unique id of this suite.
* @property {String} description - The description text passed to the {@link describe} that made this suite.
* @property {String} fullName - The full description including all ancestors of this suite.
* @property {Expectation[]} failedExpectations - The list of expectations that failed in an {@link afterAll} for this suite.

View File

@@ -32,11 +32,6 @@ getJasmineRequireObj().SuiteBuilder = function(j$) {
suite.exclude();
}
this.addSpecsToSuite_(suite, definitionFn);
if (suite.parentSuite && !suite.children.length) {
throw new Error(
`describe with no children (describe() or it()): ${suite.getFullName()}`
);
}
return suite;
}
@@ -183,11 +178,19 @@ getJasmineRequireObj().SuiteBuilder = function(j$) {
const parentSuite = this.currentDeclarationSuite_;
parentSuite.addChild(suite);
this.currentDeclarationSuite_ = suite;
let threw = false;
try {
definitionFn();
} catch (e) {
suite.handleException(e);
threw = true;
}
if (suite.parentSuite && !suite.children.length && !threw) {
throw new Error(
`describe with no children (describe() or it()): ${suite.getFullName()}`
);
}
this.currentDeclarationSuite_ = parentSuite;

View File

@@ -27,7 +27,7 @@ getJasmineRequireObj().TreeProcessor = function() {
return stats;
};
this.execute = function(done) {
this.execute = async function() {
if (!processed) {
this.processTree();
}
@@ -38,16 +38,18 @@ getJasmineRequireObj().TreeProcessor = function() {
const childFns = wrapChildren(tree, 0);
queueRunnerFactory({
queueableFns: childFns,
userContext: tree.sharedUserContext(),
onException: function() {
tree.handleException.apply(tree, arguments);
},
onComplete: done,
onMultipleDone: tree.onMultipleDone
? tree.onMultipleDone.bind(tree)
: null
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
});
});
};

View File

@@ -652,7 +652,7 @@ getJasmineRequireObj().MatchersUtil = function(j$) {
* };
* }
*
* var actual = {
* const actual = {
* n: 2,
* otherFields: "don't care"
* };

View File

@@ -6,7 +6,7 @@ getJasmineRequireObj().toHaveClass = function(j$) {
* @since 3.0.0
* @param {Object} expected - The class name to test for
* @example
* var el = document.createElement('div');
* const el = document.createElement('div');
* el.className = 'foo bar baz';
* expect(el).toHaveClass('bar');
*/