Files
jasmine/src/core/Suite.js
2025-11-03 17:22:11 -08:00

403 lines
12 KiB
JavaScript

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