2022-12-22 13:00:03 +00:00
|
|
|
export class AssertionError extends Error {
|
|
|
|
public readonly actual: string;
|
|
|
|
public readonly expected: string;
|
2022-12-27 13:26:00 +00:00
|
|
|
public readonly title: string | undefined;
|
2022-12-22 13:00:03 +00:00
|
|
|
|
2022-12-27 13:26:00 +00:00
|
|
|
constructor(message: string, actual: any, expected: any, title?: string) {
|
2022-12-22 13:00:03 +00:00
|
|
|
super(message);
|
|
|
|
|
|
|
|
this.actual = JSON.stringify(actual);
|
|
|
|
this.expected = JSON.stringify(expected);
|
2022-12-27 13:26:00 +00:00
|
|
|
this.title = title;
|
2022-12-22 13:00:03 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Test execution context with assertions. */
|
|
|
|
export class TestContext {
|
|
|
|
/** Assert strict equality with `===`. */
|
2022-12-27 13:26:00 +00:00
|
|
|
equals<T>(actual: T, expected: T, title?: string): void {
|
2022-12-22 13:00:03 +00:00
|
|
|
if (actual === expected) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-12-27 13:26:00 +00:00
|
|
|
throw new AssertionError(
|
|
|
|
"Failed equals assertion",
|
|
|
|
actual,
|
|
|
|
expected,
|
|
|
|
title,
|
|
|
|
);
|
2022-12-22 13:00:03 +00:00
|
|
|
}
|
2022-12-27 19:16:54 +00:00
|
|
|
|
2022-12-27 20:03:27 +00:00
|
|
|
/** Assert that the value is false. */
|
|
|
|
false(actual: boolean, title?: string): void {
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-boolean-literal-compare
|
|
|
|
if (actual === false) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
throw new AssertionError("Failed false assertion", actual, false, title);
|
|
|
|
}
|
|
|
|
|
2022-12-27 19:16:54 +00:00
|
|
|
/** Assert that the value is true. */
|
|
|
|
true(actual: boolean, title?: string): void {
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unnecessary-boolean-literal-compare
|
|
|
|
if (actual === true) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
throw new AssertionError("Failed true assertion", actual, true, title);
|
|
|
|
}
|
2022-12-22 13:00:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/** Special options for test cases. */
|
|
|
|
export type TestOptions = {
|
|
|
|
skip?: boolean;
|
|
|
|
};
|
|
|
|
|
|
|
|
/** A test case. */
|
|
|
|
export class Test {
|
|
|
|
constructor(
|
|
|
|
public name: string,
|
|
|
|
public fn: (test: TestContext) => Promise<void>,
|
|
|
|
public options?: TestOptions,
|
|
|
|
) {}
|
|
|
|
|
|
|
|
/** Run the test and return its result. */
|
|
|
|
async run(test: TestContext): Promise<Result> {
|
|
|
|
const data: ResultData = {
|
|
|
|
duration: undefined,
|
|
|
|
error: undefined,
|
|
|
|
name: this.name,
|
|
|
|
status: "unknown",
|
|
|
|
};
|
|
|
|
|
|
|
|
const start = performance.now();
|
|
|
|
try {
|
|
|
|
if (this.options?.skip) {
|
|
|
|
data.status = "skipped";
|
|
|
|
} else {
|
|
|
|
await this.fn(test);
|
|
|
|
data.duration = performance.now() - start;
|
|
|
|
data.status = "passed";
|
|
|
|
}
|
|
|
|
} catch (_error: unknown) {
|
|
|
|
data.duration = performance.now() - start;
|
|
|
|
if (_error instanceof AssertionError) {
|
|
|
|
data.error = _error;
|
|
|
|
} else {
|
|
|
|
throw _error;
|
|
|
|
}
|
|
|
|
|
|
|
|
data.status = "failed";
|
|
|
|
}
|
|
|
|
|
|
|
|
return new Result(data);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Data created by a test case. */
|
|
|
|
type ResultData = {
|
|
|
|
duration: number | undefined;
|
|
|
|
error: AssertionError | undefined;
|
|
|
|
name: string;
|
|
|
|
status: "failed" | "passed" | "skipped" | "unknown";
|
|
|
|
};
|
|
|
|
|
|
|
|
/** The result of a test case. */
|
|
|
|
export class Result implements ResultData {
|
|
|
|
public duration: ResultData["duration"];
|
|
|
|
public error: ResultData["error"];
|
|
|
|
public name: ResultData["name"];
|
|
|
|
public status: ResultData["status"];
|
|
|
|
|
|
|
|
constructor(data: ResultData) {
|
|
|
|
this.duration = data.duration;
|
|
|
|
this.error = data.error;
|
|
|
|
this.name = data.name;
|
|
|
|
this.status = data.status;
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Get the color associated with a result status. */
|
|
|
|
statusColor(): string {
|
|
|
|
if (this.status === "failed") {
|
|
|
|
return "red";
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.status === "passed") {
|
|
|
|
return "green";
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.status === "skipped") {
|
|
|
|
return "yellow";
|
|
|
|
}
|
|
|
|
|
|
|
|
return "white";
|
|
|
|
}
|
|
|
|
|
|
|
|
/** Print the result to the console. */
|
|
|
|
display(): void {
|
|
|
|
const bold = "font-weight: bold;";
|
|
|
|
const styles = [bold, `color: ${this.statusColor()}; ${bold}`];
|
|
|
|
|
|
|
|
let message = `- %c${this.name} %c${this.status}`;
|
|
|
|
if (this.duration !== undefined && this.duration > 1) {
|
|
|
|
message += ` %c${this.duration}ms`;
|
|
|
|
styles.push("color: white;");
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.error !== undefined) {
|
|
|
|
message += `\n %c${this.error.message}`;
|
2022-12-27 13:26:00 +00:00
|
|
|
message += this.error.title === undefined ? "" : `: ${this.error.title}`;
|
2022-12-22 13:00:03 +00:00
|
|
|
message += `\n | Actual: ${this.error.actual}`;
|
|
|
|
message += `\n | Expected: ${this.error.expected}`;
|
|
|
|
styles.push("color: pink;");
|
|
|
|
}
|
|
|
|
|
|
|
|
console.log(message, ...styles);
|
|
|
|
}
|
|
|
|
}
|