@travetto/test
Version:
Declarative test framework
143 lines (127 loc) • 4.6 kB
text/typescript
import { Class, Runtime, classConstruct, describeFunction, asFull } from '@travetto/runtime';
import { MetadataRegistry } from '@travetto/registry';
import { SuiteConfig } from '../model/suite.ts';
import { TestConfig, TestRun } from '../model/test.ts';
/**
* Test Suite registry
*/
class $SuiteRegistry extends MetadataRegistry<SuiteConfig, TestConfig> {
/**
* Find all valid tests (ignoring abstract)
*/
getValidClasses(): Class[] {
return this.getClasses().filter(c => !describeFunction(c).abstract);
}
createPending(cls: Class): Partial<SuiteConfig> {
const lines = describeFunction(cls)?.lines;
return {
class: cls,
classId: cls.Ⲑid,
tags: [],
import: Runtime.getImport(cls),
lineStart: lines?.[0],
lineEnd: lines?.[1],
tests: [],
beforeAll: [],
beforeEach: [],
afterAll: [],
afterEach: []
};
}
override createPendingField(cls: Class, fn: Function): Partial<TestConfig> {
const lines = describeFunction(cls)?.methods?.[fn.name].lines;
return {
class: cls,
tags: [],
import: Runtime.getImport(cls),
lineStart: lines?.[0],
lineEnd: lines?.[1],
lineBodyStart: lines?.[2],
methodName: fn.name
};
}
/**
* Add a new phase listeners
*/
registerPendingListener<T>(cls: Class<T>, listener: Function, phase: 'beforeAll' | 'beforeEach' | 'afterAll' | 'afterEach'): void {
const suiteConfig = this.getOrCreatePending(cls);
suiteConfig[phase]!.push(listener);
}
/**
* On finalize, collapse state with super classes to create
* a full projection of all listeners and tests.
*/
onInstallFinalize<T>(cls: Class<T>): SuiteConfig {
const config = asFull(this.getOrCreatePending(cls));
const tests = [...this.pendingFields.get(cls.Ⲑid)!.values()];
const parent = this.getParentClass(cls);
if (parent && this.has(parent)) {
const pConf = this.get(parent);
config.afterAll.push(...pConf.afterAll);
config.beforeAll.push(...pConf.beforeAll);
config.afterEach.push(...pConf.afterEach);
config.beforeEach.push(...pConf.beforeEach);
tests.push(...[...pConf.tests.values()].map(t => ({
...t,
sourceImport: pConf.import,
class: cls
})));
}
config.instance = classConstruct(config.class);
config.tests = tests!.map(x => asFull(x));
config.description ||= config.classId;
for (const t of config.tests) {
t.classId = config.classId;
t.import = config.import;
t.tags = [...t.tags!, ...config.tags!];
}
return config;
}
/**
* Get run parameters from provided input
*/
getSuiteTests(run: TestRun): { suite: SuiteConfig, tests: TestConfig[] }[] {
const clsId = run.classId;
const imp = run.import;
const methodNames = run.methodNames ?? [];
if (clsId && /^\d+$/.test(clsId)) { // If we only have a line number
const line = parseInt(clsId, 10);
const suites = this.getValidClasses()
.filter(cls => Runtime.getImport(cls) === imp)
.map(x => this.get(x)).filter(x => !x.skip);
const suite = suites.find(x => line >= x.lineStart && line <= x.lineEnd);
if (suite) {
const test = suite.tests.find(x => line >= x.lineStart && line <= x.lineEnd);
return test ? [{ suite, tests: [test] }] : [{ suite, tests: suite.tests }];
} else {
return suites.map(x => ({ suite: x, tests: x.tests }));
}
} else { // Else lookup directly
if (methodNames.length) {
const cls = this.getValidClasses().find(x => x.Ⲑid === clsId)!;
const suite = this.get(cls);
const tests = suite.tests.filter(x => methodNames.includes(x.methodName))!;
return [{ suite, tests }];
} else if (clsId) {
const cls = this.getValidClasses().find(x => x.Ⲑid === clsId)!;
const suite = this.get(cls);
return suite ? [{ suite, tests: suite.tests }] : [];
} else {
const suites = this.getValidClasses()
.map(x => this.get(x))
.filter(x => !describeFunction(x.class).abstract); // Do not run abstract suites
return suites.map(x => ({ suite: x, tests: x.tests }));
}
}
}
/**
* Find a test configuration given class and optionally a method
*/
getByClassAndMethod(cls: Class, method: Function): TestConfig | undefined {
if (this.has(cls)) {
const conf = this.get(cls);
return conf.tests.find(x => x.methodName === method.name);
}
}
}
export const SuiteRegistry = new $SuiteRegistry();