lib/Suite.js
const path = require('path');
const Testable = require('./Testable');
const Outcomes = require('./Outcomes');
const Options = require('./Options');
const HookSet = require('./HookSet');
const Locals = require('./Locals');
const { appendAll, findFiles, loadModule } = require('./utils');
const defaults = {
pattern: /^[\w-]+\.test\.(?:js|cjs|mjs)$/,
directory: path.resolve('test'),
};
class Suite extends Testable {
constructor(name, initial = {}) {
super(name);
this._testables = [];
this._options = new Options({ defaults, initial });
this._suiteHooks = new HookSet();
this._testHooks = new HookSet();
}
get pending() {
return this._testables.length === 0;
}
get numberOfTests() {
return this._testables.reduce((numberOfTests, testable) => {
return numberOfTests + testable.numberOfTests;
}, 0);
}
get numberOfPasses() {
return this._testables.reduce((numberOfPasses, testable) => {
return numberOfPasses + testable.numberOfPasses;
}, 0);
}
get numberOfFailures() {
return this._testables.reduce((numberOfFailures, testable) => {
return numberOfFailures + testable.numberOfFailures;
}, 0);
}
get numberOfSkipped() {
return this._testables.reduce((numberOfSkipped, testable) => {
return numberOfSkipped + testable.numberOfSkipped;
}, 0);
}
async discover(runtime = {}) {
const runtimeOptions = new Options({ runtime });
const options = this._options.apply(runtimeOptions);
const files = findFiles(options.export());
for (let i = 0; i < files.length; i++) {
const filePath = files[i];
const testable = await loadModule(filePath);
this.add(testable);
}
return this;
}
hasExclusiveTests() {
return this._testables.some((testable) => testable._shouldRun(false));
}
hasFailures() {
return this._testables.some((testable) => testable.failed);
}
before(...additions) {
this._suiteHooks.addBefores(...additions);
return this;
}
beforeEach(...additions) {
this._testHooks.addBefores(...additions);
return this;
}
after(...additions) {
this._suiteHooks.addAfters(...additions);
return this;
}
afterEach(...additions) {
this._testHooks.addAfters(...additions);
return this;
}
add(...additions) {
this._testables = appendAll(this._testables, additions);
return this;
}
_finalise(parent, offset = 1, inheritedTestHooks = [], locals = new Locals()) {
const suite = new Suite(this._name, this._options.initial);
suite._parent = parent;
suite._suiteHooks = this._suiteHooks._finalise(suite);
suite._locals = locals;
return this._testables.reduce((suite, testable) => {
if (!testable?.initialised) throw new Error(`Suite ${this.name} was initialised something other than a suite or test`);
const locals = suite._locals.child();
const testHooks = inheritedTestHooks.concat(this._testHooks);
const finalised = testable._finalise(suite, offset + suite.numberOfTests, testHooks, locals);
return suite.add(finalised);
}, suite);
}
async run(reporter, propagatedOptions, force = true) {
const suiteReporter = reporter.withSuite(this);
const options = this._options.apply(propagatedOptions);
this._start();
try {
const testables = this._testables.filter((testable) => testable._shouldRun(force));
await this._runAll(testables, suiteReporter, options, force);
} catch (error) {
this._fail(error);
} finally {
this._finish(options);
}
}
async _runAll(testables, reporter, options, force) {
try {
if (this._shouldRunHooks(options, testables)) await this._suiteHooks.runBefores(options);
if (this.skipped) options.bequeath({ skip: true, reason: this.reason });
for (let i = 0; i < testables.length; i++) {
await this._runTestable(testables[i], reporter, options, force);
}
} catch (error) {
this._fail(error);
} finally {
this._run = true;
await this._suiteHooks.runAfters(options);
}
}
async _runTestable(testable, reporter, options, force) {
await testable.run(reporter, options, testable._shouldForce(force));
if (testable.failed && options.get('abort')) options.bequeath({ skip: true });
}
_shouldRunHooks(options, testables) {
return !options.get('skip') && testables.length;
}
_shouldRun(force) {
return force || this.exclusive || this.hasExclusiveTests();
}
_shouldForce(force) {
return force || (this.exclusive && !this.hasExclusiveTests());
}
_finish(options) {
if (this.result) {
// Do Nothing
} else if (options.skip) {
this.result = Outcomes.SKIPPED;
} else {
this.result = this.hasFailures() ? Outcomes.FAILED : Outcomes.PASSED;
}
super._finish();
}
_decorateApi(api) {
return { ...api, suite: this._getApi() };
}
}
module.exports = Suite;