228 lines
4.9 KiB
JavaScript
228 lines
4.9 KiB
JavaScript
'use strict';
|
|
const EventEmitter = require('events');
|
|
const path = require('path');
|
|
const Bluebird = require('bluebird');
|
|
const optionChain = require('option-chain');
|
|
const matcher = require('matcher');
|
|
const snapshotManager = require('./snapshot-manager');
|
|
const TestCollection = require('./test-collection');
|
|
const validateTest = require('./validate-test');
|
|
|
|
const chainableMethods = {
|
|
defaults: {
|
|
type: 'test',
|
|
serial: false,
|
|
exclusive: false,
|
|
skipped: false,
|
|
todo: false,
|
|
failing: false,
|
|
callback: false,
|
|
always: false
|
|
},
|
|
chainableMethods: {
|
|
test: {},
|
|
serial: {serial: true},
|
|
before: {type: 'before'},
|
|
after: {type: 'after'},
|
|
skip: {skipped: true},
|
|
todo: {todo: true},
|
|
failing: {failing: true},
|
|
only: {exclusive: true},
|
|
beforeEach: {type: 'beforeEach'},
|
|
afterEach: {type: 'afterEach'},
|
|
cb: {callback: true},
|
|
always: {always: true}
|
|
}
|
|
};
|
|
|
|
function wrapFunction(fn, args) {
|
|
return function (t) {
|
|
return fn.apply(this, [t].concat(args));
|
|
};
|
|
}
|
|
|
|
class Runner extends EventEmitter {
|
|
constructor(options) {
|
|
super();
|
|
|
|
options = options || {};
|
|
|
|
this.file = options.file;
|
|
this.match = options.match || [];
|
|
this.projectDir = options.projectDir;
|
|
this.serial = options.serial;
|
|
this.updateSnapshots = options.updateSnapshots;
|
|
|
|
this.hasStarted = false;
|
|
this.results = [];
|
|
this.snapshots = null;
|
|
this.tests = new TestCollection({
|
|
bail: options.bail,
|
|
failWithoutAssertions: options.failWithoutAssertions,
|
|
compareTestSnapshot: this.compareTestSnapshot.bind(this)
|
|
});
|
|
|
|
this.chain = optionChain(chainableMethods, (opts, args) => {
|
|
let title;
|
|
let fn;
|
|
let macroArgIndex;
|
|
|
|
if (this.hasStarted) {
|
|
throw new Error('All tests and hooks must be declared synchronously in your ' +
|
|
'test file, and cannot be nested within other tests or hooks.');
|
|
}
|
|
|
|
if (typeof args[0] === 'string') {
|
|
title = args[0];
|
|
fn = args[1];
|
|
macroArgIndex = 2;
|
|
} else {
|
|
fn = args[0];
|
|
title = null;
|
|
macroArgIndex = 1;
|
|
}
|
|
|
|
if (this.serial) {
|
|
opts.serial = true;
|
|
}
|
|
|
|
if (args.length > macroArgIndex) {
|
|
args = args.slice(macroArgIndex);
|
|
} else {
|
|
args = null;
|
|
}
|
|
|
|
if (Array.isArray(fn)) {
|
|
fn.forEach(fn => {
|
|
this.addTest(title, opts, fn, args);
|
|
});
|
|
} else {
|
|
this.addTest(title, opts, fn, args);
|
|
}
|
|
});
|
|
}
|
|
|
|
addTest(title, metadata, fn, args) {
|
|
if (args) {
|
|
if (fn.title) {
|
|
title = fn.title.apply(fn, [title || ''].concat(args));
|
|
}
|
|
|
|
fn = wrapFunction(fn, args);
|
|
}
|
|
|
|
if (metadata.type === 'test' && this.match.length > 0) {
|
|
metadata.exclusive = title !== null && matcher([title], this.match).length === 1;
|
|
}
|
|
|
|
const validationError = validateTest(title, fn, metadata);
|
|
if (validationError !== null) {
|
|
throw new TypeError(validationError);
|
|
}
|
|
|
|
this.tests.add({
|
|
metadata,
|
|
fn,
|
|
title
|
|
});
|
|
}
|
|
|
|
addTestResult(result) {
|
|
const test = result.result;
|
|
const props = {
|
|
duration: test.duration,
|
|
title: test.title,
|
|
error: result.reason,
|
|
type: test.metadata.type,
|
|
skip: test.metadata.skipped,
|
|
todo: test.metadata.todo,
|
|
failing: test.metadata.failing
|
|
};
|
|
|
|
this.results.push(result);
|
|
this.emit('test', props);
|
|
}
|
|
|
|
buildStats() {
|
|
const stats = {
|
|
failCount: 0,
|
|
knownFailureCount: 0,
|
|
passCount: 0,
|
|
skipCount: 0,
|
|
testCount: 0,
|
|
todoCount: 0
|
|
};
|
|
|
|
for (const result of this.results) {
|
|
if (!result.passed) {
|
|
// Includes hooks
|
|
stats.failCount++;
|
|
}
|
|
|
|
const metadata = result.result.metadata;
|
|
if (metadata.type === 'test') {
|
|
stats.testCount++;
|
|
|
|
if (metadata.skipped) {
|
|
stats.skipCount++;
|
|
} else if (metadata.todo) {
|
|
stats.todoCount++;
|
|
} else if (result.passed) {
|
|
if (metadata.failing) {
|
|
stats.knownFailureCount++;
|
|
} else {
|
|
stats.passCount++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return stats;
|
|
}
|
|
|
|
compareTestSnapshot(options) {
|
|
if (!this.snapshots) {
|
|
this.snapshots = snapshotManager.load({
|
|
name: path.basename(this.file),
|
|
projectDir: this.projectDir,
|
|
relFile: path.relative(this.projectDir, this.file),
|
|
testDir: path.dirname(this.file),
|
|
updating: this.updateSnapshots
|
|
});
|
|
this.emit('dependency', this.snapshots.snapPath);
|
|
}
|
|
|
|
return this.snapshots.compare(options);
|
|
}
|
|
|
|
saveSnapshotState() {
|
|
if (this.snapshots) {
|
|
const files = this.snapshots.save();
|
|
if (files) {
|
|
this.emit('touched', files);
|
|
}
|
|
} else if (this.updateSnapshots) {
|
|
// TODO: There may be unused snapshot files if no test caused the
|
|
// snapshots to be loaded. Prune them. But not if tests (including hooks!)
|
|
// were skipped. Perhaps emit a warning if this occurs?
|
|
}
|
|
}
|
|
|
|
run(options) {
|
|
if (options.runOnlyExclusive && !this.tests.hasExclusive) {
|
|
return Promise.resolve(null);
|
|
}
|
|
|
|
this.hasStarted = true;
|
|
this.tests.on('test', result => {
|
|
this.addTestResult(result);
|
|
});
|
|
return Bluebird.try(() => this.tests.build().run());
|
|
}
|
|
attributeLeakedError(err) {
|
|
return this.tests.attributeLeakedError(err);
|
|
}
|
|
}
|
|
|
|
module.exports = Runner;
|