micro-test-runner
Version:
A minimal JavaScript test runner.
155 lines (154 loc) • 6.89 kB
JavaScript
class MicroTestRunner {
constructor(candidate) {
this.log = {
name: undefined,
icons: ['✓', '✕'],
severity: 'log'
};
this.args = [];
this.conditions = [];
this.runs = 1;
this.currentRun = 0;
this.performance = {
format: 'none',
measurements: []
};
this.passing = [];
this.result = {
passed: false
};
this.candidate = candidate;
}
get logMessage() {
let performanceMessage = '';
let performanceTable = '';
if (this.result.passed && this.performance.format !== 'none' && this.performance.measurements.length > 0) {
if (this.performance.format === 'table') {
performanceTable = '\n ╭───────┬───────┬───────────────╮\n │ Test │ Run │ Duration (ms) │';
}
const startTimestamp = this.performance.measurements[0][0].start;
const endTimestamp = this.performance.measurements[this.performance.measurements.length - 1][this.performance.measurements[this.performance.measurements.length - 1].length - 1].end;
const testDuration = Number(endTimestamp - startTimestamp);
let averageRunDuration = 0;
let totalRuns = 0;
this.performance.measurements.forEach((test, testIndex) => {
if (this.performance.format === 'table') {
performanceTable += '\n ├───────┼───────┼───────────────┤';
}
test.forEach((run, runIndex) => {
const runDuration = run.end - run.start;
averageRunDuration += runDuration;
if (this.performance.format === 'table') {
performanceTable += `\n │ ${runIndex === 0 ? (testIndex + 1).toString().padEnd(5, ' ') : ' '} │ ${(runIndex + 1).toString().padEnd(5, ' ')} │ ${runDuration.toFixed(3).padStart(13, ' ')} │`;
}
totalRuns++;
});
});
if (this.performance.format === 'table') {
performanceTable += '\n ╰───────┴───────┴───────────────╯';
}
averageRunDuration = Number(averageRunDuration / totalRuns);
performanceMessage = ` in ${testDuration.toFixed(3)}ms${totalRuns > 1 ? ` (x̄ ${averageRunDuration.toFixed(3)}ms per run, over ${totalRuns} runs)` : ''}`;
}
const wrap = typeof this.result.expected === 'string' && /[\r\n]/.test(this.result.expected);
const part1 = `${this.result.passed ? this.log.icons[0] : this.log.icons[1]} ${this.log.name} test ${this.result.passed ? 'passed' : 'failed'}`;
const part2 = `${performanceMessage}`;
const part3 = `${this.result.passed && this.performance.format === 'table' ? ':' : '.'}${performanceTable}`;
const part4 = !this.result.passed && 'expected' in this.result
? `\nExpected:${wrap ? '\n' : ' '}${this.result.expected}`
: '';
const part5 = !this.result.passed && 'received' in this.result
? `\nReceived:${wrap ? '\n' : ' '}${this.result.received}`
: '';
return part1 + part2 + part3 + part4 + part5;
}
logResult() {
if (!this.result.passed) {
if (this.log.severity === 'error') {
throw new Error(this.logMessage);
}
else if (this.log.severity === 'warn') {
console.warn(this.logMessage);
return;
}
}
console.info(this.logMessage);
}
static test(candidate) {
return new MicroTestRunner(candidate);
}
logging(name, severity = 'log', icons, performance = 'none') {
this.log.name = name;
this.log.severity = severity;
if (Array.isArray(icons) && icons.length === 2) {
this.log.icons = icons;
}
if (globalThis.performance) {
this.performance.format = performance;
}
if (typeof performance === 'string') {
this.performance.format = performance;
}
return this;
}
context(context) {
this.candidateContext = context;
return this;
}
times(number) {
this.runs = Math.max(Math.ceil(number), 1);
return this;
}
with(...args) {
this.args.push(args);
return this;
}
async expect(...conditions) {
this.conditions = conditions;
if (!this.args.length) {
this.args.push([]);
}
let halt = false;
for (const [index, argumentGroup] of this.args.entries()) {
this.currentRun = 0;
this.performance.measurements.push([]);
while (this.currentRun < this.runs) {
try {
let runDuration = undefined;
if (this.performance.format !== 'none') {
this.performance.measurements[index].push({ start: performance.now(), end: 0 });
}
const runResult = await Promise.resolve(this.candidate.apply(this.candidateContext, argumentGroup));
if (this.performance.format !== 'none') {
this.performance.measurements[index][this.currentRun].end = performance.now();
runDuration = this.performance.measurements[index][this.currentRun].end - this.performance.measurements[index][this.currentRun].start;
}
const condition = this.conditions[Math.min(index, this.conditions.length - 1)];
const pass = typeof condition === 'function' ? condition(runResult, this.currentRun, runDuration) : runResult === condition;
this.passing.push(pass);
if (!pass) {
halt = true;
this.result.received = runResult;
if (typeof condition !== 'function') {
this.result.expected = condition;
}
}
}
catch (error) {
console.warn('MicroTestRunner: Run failed with error:\n', error);
}
this.currentRun++;
if (halt)
break;
}
if (halt)
break;
}
this.result.passed = !this.passing.includes(false);
if (this.log.name) {
this.logResult();
}
return this.result.passed;
}
}
export default MicroTestRunner.test;