feat(testrunner): composable and bindable attributes and modifiers (#1547)

This allows to make things like `fit`, `skip()` and soon even `dit` be implemented externally.
This commit is contained in:
Dmitry Gozman 2020-03-25 22:42:09 -07:00 committed by GitHub
parent c468e92f41
commit 81bd8de00b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23

View file

@ -24,7 +24,7 @@ const {SourceMapSupport} = require('./SourceMapSupport');
const debug = require('debug'); const debug = require('debug');
const {getCallerLocation} = require('./utils'); const {getCallerLocation} = require('./utils');
const INFINITE_TIMEOUT = 2147483647; const INFINITE_TIMEOUT = 100000000;
const readFileAsync = util.promisify(fs.readFile.bind(fs)); const readFileAsync = util.promisify(fs.readFile.bind(fs));
@ -451,15 +451,21 @@ class TestPass {
} }
} }
function specBuilder(defaultTimeout, modifiers, attributes, action) { // TODO: merge spec with Test/Suite.
let mode = TestMode.Run; function createSpec(name, callback) {
let expectation = TestExpectation.Ok; let timeout = INFINITE_TIMEOUT;
let repeat = 1; let repeat = 1;
let timeout = defaultTimeout; let expectation = TestExpectation.Ok;
let mode = TestMode.Run;
const spec = { const spec = {
Modes: { ...TestMode }, Modes: { ...TestMode },
Expectations: { ...TestExpectation }, Expectations: { ...TestExpectation },
name() {
return name;
},
callback() {
return callback;
},
mode() { mode() {
return mode; return mode;
}, },
@ -473,7 +479,7 @@ function specBuilder(defaultTimeout, modifiers, attributes, action) {
setExpectations(e) { setExpectations(e) {
if (Array.isArray(e)) { if (Array.isArray(e)) {
if (e.length > 1) if (e.length > 1)
throw new Error(''); throw new Error('Only a single expectation is currently supported');
e = e[0]; e = e[0];
} }
expectation = e; expectation = e;
@ -489,32 +495,9 @@ function specBuilder(defaultTimeout, modifiers, attributes, action) {
}, },
setRepeat(r) { setRepeat(r) {
repeat = r; repeat = r;
} },
}; };
return spec;
const func = (...args) => {
for (let i = 0; i < repeat; ++i)
action(mode, expectation, timeout, ...args);
mode = TestMode.Run;
expectation = TestExpectation.Ok;
repeat = 1;
timeout = defaultTimeout;
};
for (const { name, callback } of modifiers) {
func[name] = (...args) => {
callback(spec, ...args);
return func;
};
}
for (const { name, callback } of attributes) {
Object.defineProperty(func, name, {
get: () => {
callback(spec);
return func;
}
});
}
return func;
} }
class TestRunner extends EventEmitter { class TestRunner extends EventEmitter {
@ -536,8 +519,8 @@ class TestRunner extends EventEmitter {
this._timeout = timeout === 0 ? INFINITE_TIMEOUT : timeout; this._timeout = timeout === 0 ? INFINITE_TIMEOUT : timeout;
this._parallel = parallel; this._parallel = parallel;
this._breakOnFailure = breakOnFailure; this._breakOnFailure = breakOnFailure;
this._modifiers = []; this._modifiers = new Map();
this._attributes = []; this._attributes = new Map();
if (MAJOR_NODEJS_VERSION >= 8 && disableTimeoutWhenInspectorIsEnabled) { if (MAJOR_NODEJS_VERSION >= 8 && disableTimeoutWhenInspectorIsEnabled) {
if (inspector.url()) { if (inspector.url()) {
@ -554,72 +537,104 @@ class TestRunner extends EventEmitter {
this.afterAll = this._addHook.bind(this, 'afterAll'); this.afterAll = this._addHook.bind(this, 'afterAll');
this.afterEach = this._addHook.bind(this, 'afterEach'); this.afterEach = this._addHook.bind(this, 'afterEach');
this._modifiers.push({ name: 'skip', callback: (t, condition) => condition && t.setMode(t.Modes.Skip) }); this.describe = this._specBuilder([], true);
this._modifiers.push({ name: 'fail', callback: (t, condition) => condition && t.setExpectations(t.Expectations.Fail) }); this.it = this._specBuilder([], false);
this._modifiers.push({ name: 'slow', callback: (t, condition) => condition && t.setTimeout(t.timeout() * 3) });
this._modifiers.push({ name: 'repeat', callback: (t, count) => t.setRepeat(count) }); this._attributes.set('debug', t => {
this._attributes.push({ name: 'focus', callback: t => t.setMode(t.Modes.Focus) }); t.setMode(t.Modes.Focus);
this._buildSpecs(); t.setTimeout(INFINITE_TIMEOUT);
const N = t.callback().toString().split('\n').length;
const location = getCallerLocation(__filename);
for (let line = 0; line < N; ++line)
this._debuggerLogBreakpointLines.set(location.filePath, line + location.lineNumber);
});
this._modifiers.set('skip', (t, condition) => condition && t.setMode(t.Modes.Skip));
this._modifiers.set('fail', (t, condition) => condition && t.setExpectations(t.Expectations.Fail));
this._modifiers.set('slow', (t, condition) => condition && t.setTimeout(t.timeout() * 3));
this._modifiers.set('repeat', (t, count) => t.setRepeat(count));
this._attributes.set('focus', t => t.setMode(t.Modes.Focus));
this.fdescribe = this.describe.focus;
this.xdescribe = this.describe.skip(true);
this.fit = this.it.focus;
this.xit = this.it.skip(true);
this.dit = this.it.debug;
} }
_buildSpecs() { _specBuilder(callbacks, isSuite) {
this.describe = specBuilder(this._timeout, this._modifiers, this._attributes, (mode, expectation, timeout, ...args) => this._addSuite(mode, expectation, ...args)); return new Proxy(() => {}, {
this.fdescribe = specBuilder(this._timeout, this._modifiers, this._attributes, (mode, expectation, timeout, ...args) => this._addSuite(TestMode.Focus, expectation, ...args)); apply: (target, thisArg, [name, callback]) => {
this.xdescribe = specBuilder(this._timeout, this._modifiers, this._attributes, (mode, expectation, timeout, ...args) => this._addSuite(TestMode.Skip, expectation, ...args)); const spec = createSpec(name, callback);
this.it = specBuilder(this._timeout, this._modifiers, this._attributes, (mode, expectation, timeout, name, callback) => this._addTest(name, callback, mode, expectation, timeout)); spec.setTimeout(this._timeout);
this.fit = specBuilder(this._timeout, this._modifiers, this._attributes, (mode, expectation, timeout, name, callback) => this._addTest(name, callback, TestMode.Focus, expectation, timeout)); for (const { callback, args } of callbacks)
this.xit = specBuilder(this._timeout, this._modifiers, this._attributes, (mode, expectation, timeout, name, callback) => this._addTest(name, callback, TestMode.Skip, expectation, timeout)); callback(spec, ...args);
this.dit = specBuilder(this._timeout, this._modifiers, this._attributes, (mode, expectation, timeout, name, callback) => { if (isSuite)
const test = this._addTest(name, callback, TestMode.Focus, expectation, INFINITE_TIMEOUT); this._addSuite(spec, []);
const N = callback.toString().split('\n').length; else
for (let i = 0; i < N; ++i) this._addTest(spec);
this._debuggerLogBreakpointLines.set(test.location.filePath, i + test.location.lineNumber); },
get: (obj, prop) => {
if (this._modifiers.has(prop))
return (...args) => this._specBuilder([...callbacks, { callback: this._modifiers.get(prop), args }], isSuite);
if (this._attributes.has(prop))
return this._specBuilder([...callbacks, { callback: this._attributes.get(prop), args: [] }], isSuite);
return obj[prop];
},
}); });
} }
_buildSpec() {
}
modifier(name, callback) { modifier(name, callback) {
this._modifiers.push({ name, callback }); this._modifiers.set(name, callback);
this._buildSpecs();
} }
attribute(name, callback) { attribute(name, callback) {
this._attributes.push({ name, callback }); this._attributes.set(name, callback);
this._buildSpecs();
} }
loadTests(module, ...args) { loadTests(module, ...args) {
if (typeof module.describe === 'function') if (typeof module.describe === 'function') {
this._addSuite(TestMode.Run, TestExpectation.Ok, '', module.describe, ...args); const spec = createSpec('', module.describe);
if (typeof module.fdescribe === 'function') spec.setMode(spec.Modes.Run);
this._addSuite(TestMode.Focus, TestExpectation.Ok, '', module.fdescribe, ...args); this._addSuite(spec, args);
if (typeof module.xdescribe === 'function') }
this._addSuite(TestMode.Skip, TestExpectation.Ok, '', module.xdescribe, ...args); if (typeof module.fdescribe === 'function') {
} const spec = createSpec('', module.fdescribe);
spec.setMode(spec.Modes.Focus);
_addTest(name, callback, mode, expectation, timeout) { this._addSuite(spec, args);
for (let suite = this._currentSuite; suite; suite = suite.parentSuite) { }
if (suite.expectation === TestExpectation.Fail) if (typeof module.xdescribe === 'function') {
expectation = TestExpectation.Fail; const spec = createSpec('', module.xdescribe);
if (suite.declaredMode === TestMode.Skip) spec.setMode(spec.Modes.Skip);
mode = TestMode.Skip; this._addSuite(spec, args);
} }
const test = new Test(this._currentSuite, name, callback, mode, expectation, timeout);
this._currentSuite.children.push(test);
this._tests.push(test);
return test;
} }
_addSuite(mode, expectation, name, callback, ...args) { _addTest(spec) {
const oldSuite = this._currentSuite; for (let i = 0; i < spec.repeat(); i++) {
const suite = new Suite(this._currentSuite, name, mode, expectation); let expectation = spec.expectations()[0];
this._suites.push(suite); let mode = spec.mode();
this._currentSuite.children.push(suite); for (let suite = this._currentSuite; suite; suite = suite.parentSuite) {
this._currentSuite = suite; if (suite.expectation === TestExpectation.Fail)
callback(...args); expectation = TestExpectation.Fail;
this._currentSuite = oldSuite; if (suite.declaredMode === TestMode.Skip)
mode = TestMode.Skip;
}
const test = new Test(this._currentSuite, spec.name(), spec.callback(), mode, expectation, spec.timeout());
this._currentSuite.children.push(test);
this._tests.push(test);
}
}
_addSuite(spec, args) {
for (let i = 0; i < spec.repeat(); i++) {
const oldSuite = this._currentSuite;
const suite = new Suite(this._currentSuite, spec.name(), spec.mode(), spec.expectations()[0]);
this._suites.push(suite);
this._currentSuite.children.push(suite);
this._currentSuite = suite;
spec.callback()(...args);
this._currentSuite = oldSuite;
}
} }
_addHook(hookName, callback) { _addHook(hookName, callback) {