@oazmi/kitchensink
Version:
a collection of personal utility functions
140 lines (139 loc) • 5.41 kB
JavaScript
/** the following shim (polyfill) defines a simple standalone `Deno.Test`-like replacement for non-deno environments.
*
* the only two things implemented here are:
* - `test: Deno.Test` function.
* - `step: Deno.TestContext` interface, which is passed onto your test-function that you feed to `Deno.test`.
*
* > [!note]
* > make sure that your test-function's body is side-effect free if you will be using `async` test functions in parallel.
* > otherwise, always perform a top-level await on all `Deno.test`s.
* > deno itself waits for all async tests to end their execution, so we don't have to write `await` before every `Deno.test`.
* > however, you don't get that luxury in the browser or other runtimes.
* > thus, you should **always** `await` your calls to {@link test | `Deno.test`} if you wish to use this shim/polyfill.
* >
* > @example
* > ```diff
* > // adapting a deno-native test file to become browser compatible:
* > - Deno.test("my test", (t): Promise<void> => {
* > + await Deno.test("my test", (t): Promise<void> => {
* > // do the test
* > })
* > ```
*
* @module
*/
import * as dntShim from "./_dnt.shims.js";
import { noop } from "./alias.js";
import { isFunction, isString } from "./struct.js";
export const subtestResults = Symbol();
let anon_test_counter = 0;
const parseTestArgs = (arg1, arg2, arg3) => {
if (arg3 !== undefined) {
return parseTestArgs({ ...arg2, name: arg1, fn: arg3 });
}
if (arg2 !== undefined) {
const options = isString(arg1) ? { name: arg1 } : arg1;
return parseTestArgs({ ...options, fn: arg2 });
}
const definition = isFunction(arg1)
? { name: `anon-test-${anon_test_counter++}`, fn: arg1 }
: arg1;
if (!isFunction(definition.fn)) {
console.warn(`the test \"${definition.name}\" is missing a test-function, so we're attaching a dummy one.`);
definition.fn = noop;
}
return definition;
};
const parseStepTestArgs = (arg1, arg2, arg3) => {
return (arg3 === undefined && arg2 === undefined && isFunction(arg1))
? parseTestArgs({ name: `anon-test-step-${anon_test_counter++}`, fn: arg1 })
: parseTestArgs(arg1, arg2, arg3);
};
const createTestContext = (definition, parent) => {
const t = {
name: definition.name,
origin: "unknown",
parent: parent,
logger: parent?.logger ?? console,
[subtestResults]: [],
async step(arg1, arg2, arg3) {
const definition = parseStepTestArgs(arg1, arg2, arg3), { name, ignore } = definition;
if (ignore) {
return true;
}
const t_sub_test = createTestContext(definition, t);
// run the nested step function and pass the `t_sub_test` context to it.
try {
await definition.fn(t_sub_test);
}
catch (err) {
t[subtestResults].push({ name, passed: false, error: err });
t_sub_test.logger.error(`[sub-test-FAILED]: ${name}`);
t_sub_test.logger.error(err);
return false;
}
t[subtestResults].push({ name, passed: true });
return true;
},
};
return t;
};
/** this function mimics the behavior of `Deno.test` so that it can be used in non-deno environments.
*
* ```ts
* import { assertEquals } from "jsr:@std/assert"
*
* await test("testing test", async (t) => {
* await t.step("non-problematic steps should have no problems!", () => {
* assertEquals(1 + 1, 2)
* })
*
* await t.step("erroneous steps should be reported!", (t2) => {
* // silencing the logger for this step.
* t2.logger = { error: () => undefined, log: () => undefined }
* assertEquals("the earth is flat", "the earth is an oblate ellipsoid")
* })
*
* const [result_1, result_2] = t[subtestResults]
* assertEquals(result_1.passed, true)
* assertEquals(result_1.error, undefined)
* assertEquals(result_2.passed, false)
* assertEquals(result_2.error instanceof Error, true)
*
* // popping away the failing result, so that the main test itself does not fail.
* t[subtestResults].pop()
*
* // TODO: add more test cases, such as teting multi-depth steps, etc...
* })
* ```
*/
export const test = async (arg1, arg2, arg3) => {
const definition = parseTestArgs(arg1, arg2, arg3), t = createTestContext(definition);
try {
// running the main test function, and passing the context to it.
await definition.fn(t);
const failed_tests = t[subtestResults].filter((info) => (!info.passed)), failed_tests_len = failed_tests.length;
if (failed_tests_len > 0) {
throw Error(`[sub-tests ]: ${failed_tests_len} sub-tests have failed.`);
}
t.logger.log(`[ test-passed]: ${definition.name}`);
return true;
}
catch (err) {
t.logger.error(`[ test-FAILED]: ${definition.name}`);
t.logger.error(err);
return false;
}
};
/** injects the {@link test} function to the global `Deno` variable's `test` property, if it is not already defined.
*
* @returns the polyfilled {@link test} function.
*/
export const injectTestShim = () => {
if (typeof Deno === "undefined") {
dntShim.dntGlobalThis.Deno = {};
}
if (Deno.test === undefined) {
Object.assign(Deno, { test });
}
};