forked from rosa/hakurei
TODO: implement skipping from TestController
This commit is contained in:
@@ -53,28 +53,44 @@ function checkDuplicates(parent: string, names: { name: string }[]) {
|
||||
}
|
||||
}
|
||||
|
||||
class FailNowSentinel {}
|
||||
export type TestState = "success" | "failure" | "skip";
|
||||
|
||||
class AbortSentinel {}
|
||||
|
||||
export class TestController {
|
||||
#state: TestState;
|
||||
logs: string[];
|
||||
#failed: boolean;
|
||||
|
||||
constructor() {
|
||||
this.#state = "success";
|
||||
this.logs = [];
|
||||
this.#failed = false;
|
||||
}
|
||||
|
||||
getState(): TestState {
|
||||
return this.#state;
|
||||
}
|
||||
|
||||
fail() {
|
||||
this.#failed = true;
|
||||
this.#state = "failure";
|
||||
}
|
||||
|
||||
failed(): boolean {
|
||||
return this.#failed;
|
||||
return this.#state === "failure";
|
||||
}
|
||||
|
||||
failNow(): never {
|
||||
this.fail();
|
||||
throw new FailNowSentinel();
|
||||
throw new AbortSentinel();
|
||||
}
|
||||
|
||||
skip(message?: string): never {
|
||||
if (message != null) this.log(message);
|
||||
if (this.#state !== "failure") this.#state = "skip";
|
||||
throw new AbortSentinel();
|
||||
}
|
||||
|
||||
skipped(): boolean {
|
||||
return this.#state === "skip";
|
||||
}
|
||||
|
||||
log(message: string) {
|
||||
@@ -96,7 +112,7 @@ export class TestController {
|
||||
// Execution
|
||||
|
||||
export interface TestResult {
|
||||
success: boolean;
|
||||
state: TestState;
|
||||
logs: string[];
|
||||
}
|
||||
|
||||
@@ -110,11 +126,11 @@ function runTests(reporter: Reporter, parents: string[], node: TestTree) {
|
||||
try {
|
||||
node.test(controller);
|
||||
} catch (e) {
|
||||
if (!(e instanceof FailNowSentinel)) {
|
||||
if (!(e instanceof AbortSentinel)) {
|
||||
controller.error(extractExceptionString(e));
|
||||
}
|
||||
}
|
||||
reporter.update(path, { success: !controller.failed(), logs: controller.logs });
|
||||
reporter.update(path, { state: controller.getState(), logs: controller.logs });
|
||||
}
|
||||
|
||||
function extractExceptionString(e: any): string {
|
||||
@@ -170,14 +186,16 @@ const SEP = " ❯ ";
|
||||
export class StreamReporter implements Reporter {
|
||||
stream: Stream;
|
||||
verbose: boolean;
|
||||
#successes: ({ path: string[] } & TestResult)[];
|
||||
#failures: ({ path: string[] } & TestResult)[];
|
||||
counts: { successes: number, failures: number };
|
||||
#skips: ({ path: string[] } & TestResult)[];
|
||||
|
||||
constructor(stream: Stream, verbose: boolean = false) {
|
||||
this.stream = stream;
|
||||
this.verbose = verbose;
|
||||
this.#successes = [];
|
||||
this.#failures = [];
|
||||
this.counts = { successes: 0, failures: 0 };
|
||||
this.#skips = [];
|
||||
}
|
||||
|
||||
register(suites: TestGroup[]) {}
|
||||
@@ -185,31 +203,52 @@ export class StreamReporter implements Reporter {
|
||||
update(path: string[], result: TestResult) {
|
||||
if (path.length === 0) throw new RangeError("path is empty");
|
||||
const pathStr = path.join(SEP);
|
||||
if (result.success) {
|
||||
this.counts.successes++;
|
||||
switch (result.state) {
|
||||
case "success":
|
||||
this.#successes.push({ path, ...result });
|
||||
if (this.verbose) this.stream.writeln(`✅️ ${pathStr}`);
|
||||
} else {
|
||||
this.counts.failures++;
|
||||
this.stream.writeln(`⚠️ ${pathStr}`);
|
||||
break;
|
||||
case "failure":
|
||||
this.#failures.push({ path, ...result });
|
||||
this.stream.writeln(`⚠️ ${pathStr}`);
|
||||
break;
|
||||
case "skip":
|
||||
this.#skips.push({ path, ...result });
|
||||
this.stream.writeln(`⏭️ ${pathStr}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
finalize() {
|
||||
if (this.verbose) this.#displaySection("successes", this.#successes, true);
|
||||
this.#displaySection("failures", this.#failures);
|
||||
this.#displaySection("skips", this.#skips);
|
||||
this.stream.writeln("");
|
||||
this.stream.writeln(
|
||||
`${this.#successes.length} succeeded, ${this.#failures.length} failed`
|
||||
+ (this.#skips.length ? `, ${this.#skips.length} skipped` : "")
|
||||
);
|
||||
}
|
||||
|
||||
#displaySection(name: string, data: ({ path: string[] } & TestResult)[], ignoreEmpty: boolean = false) {
|
||||
if (!data.length) return;
|
||||
|
||||
// Transform [{ path: ["a", "b", "c"] }, { path: ["a", "b", "d"] }]
|
||||
// into { "a ❯ b": ["c", "d"] }.
|
||||
let pathMap = new Map<string, ({ name: string } & TestResult)[]>();
|
||||
for (const f of this.#failures) {
|
||||
const key = f.path.slice(0, -1).join(SEP);
|
||||
for (const t of data) {
|
||||
const key = t.path.slice(0, -1).join(SEP);
|
||||
if (!pathMap.has(key)) pathMap.set(key, []);
|
||||
pathMap.get(key).push({ name: f.path.at(-1), ...f });
|
||||
pathMap.get(key).push({ name: t.path.at(-1), ...t });
|
||||
}
|
||||
|
||||
this.stream.writeln("");
|
||||
this.stream.writeln("FAILURES");
|
||||
this.stream.writeln("========");
|
||||
this.stream.writeln(name.toUpperCase());
|
||||
this.stream.writeln("=".repeat(name.length));
|
||||
|
||||
for (const [path, tests] of pathMap) {
|
||||
for (let [path, tests] of pathMap) {
|
||||
if (ignoreEmpty) tests = tests.filter((t) => t.logs.length);
|
||||
if (tests.length === 0) continue;
|
||||
if (tests.length === 1) {
|
||||
this.#writeOutput(tests[0], path ? `${path}${SEP}` : "", false);
|
||||
} else {
|
||||
@@ -217,10 +256,6 @@ export class StreamReporter implements Reporter {
|
||||
for (const t of tests) this.#writeOutput(t, " - ", true);
|
||||
}
|
||||
}
|
||||
|
||||
this.stream.writeln("");
|
||||
const { successes, failures } = this.counts;
|
||||
this.stream.writeln(`${successes} succeeded, ${failures} failed`);
|
||||
}
|
||||
|
||||
#writeOutput(test: { name: string } & TestResult, prefix: string, nested: boolean) {
|
||||
@@ -246,7 +281,10 @@ export class DOMReporter implements Reporter {
|
||||
|
||||
update(path: string[], result: TestResult) {
|
||||
if (path.length === 0) throw new RangeError("path is empty");
|
||||
const counter = document.getElementById(result.success ? "successes" : "failures");
|
||||
if (result.state === "skip") {
|
||||
document.getElementById("skip-counter-text").classList.remove("hidden");
|
||||
}
|
||||
const counter = document.getElementById(`${result.state}-counter`);
|
||||
counter.innerText = (Number(counter.innerText) + 1).toString();
|
||||
let parent = document.getElementById("root");
|
||||
for (const node of path) {
|
||||
@@ -267,9 +305,10 @@ export class DOMReporter implements Reporter {
|
||||
child.appendChild(summary);
|
||||
parent.appendChild(child);
|
||||
}
|
||||
if (!result.success) {
|
||||
child.open = true;
|
||||
child.classList.add("failure");
|
||||
if (result.state === "failure") child.open = true;
|
||||
if (!child.classList.contains("failure") &&
|
||||
(result.state !== "success" || !child.classList.contains("skip"))) {
|
||||
child.classList.add(result.state);
|
||||
}
|
||||
parent = child;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user