deno/ext/node/polyfills/testing.ts
Divy Srivastava 71d1384a89
Some checks are pending
ci / lint debug macos-x86_64 (push) Blocked by required conditions
ci / lint debug windows-x86_64 (push) Blocked by required conditions
ci / test debug linux-x86_64 (push) Blocked by required conditions
ci / test release linux-x86_64 (push) Blocked by required conditions
ci / test debug macos-x86_64 (push) Blocked by required conditions
ci / test release macos-x86_64 (push) Blocked by required conditions
ci / test debug windows-x86_64 (push) Blocked by required conditions
ci / test release windows-x86_64 (push) Blocked by required conditions
ci / build wasm32 (push) Blocked by required conditions
ci / publish canary (push) Blocked by required conditions
ci / pre-build (push) Waiting to run
ci / test debug linux-aarch64 (push) Blocked by required conditions
ci / test release linux-aarch64 (push) Blocked by required conditions
ci / test debug macos-aarch64 (push) Blocked by required conditions
ci / test release macos-aarch64 (push) Blocked by required conditions
ci / bench release linux-x86_64 (push) Blocked by required conditions
ci / lint debug linux-x86_64 (push) Blocked by required conditions
fix(ext/node): include assert.ok in node:test (#29383)
2025-05-20 04:05:19 +00:00

453 lines
11 KiB
TypeScript

// Copyright 2018-2025 the Deno authors. MIT license.
import { primordials } from "ext:core/mod.js";
const {
PromisePrototypeThen,
ArrayPrototypePush,
ArrayPrototypeForEach,
SafePromiseAll,
TypeError,
SafeArrayIterator,
SafePromisePrototypeFinally,
Symbol,
} = primordials;
import { notImplemented } from "ext:deno_node/_utils.ts";
import assert from "node:assert";
const methodsToCopy = [
"deepEqual",
"deepStrictEqual",
"doesNotMatch",
"doesNotReject",
"doesNotThrow",
"equal",
"fail",
"ifError",
"match",
"notDeepEqual",
"notDeepStrictEqual",
"notEqual",
"notStrictEqual",
"partialDeepStrictEqual",
"rejects",
"strictEqual",
"throws",
"ok",
];
/** `assert` object available via t.assert */
let assertObject = undefined;
function getAssertObject() {
if (assertObject === undefined) {
assertObject = { __proto__: null };
ArrayPrototypeForEach(methodsToCopy, (method) => {
assertObject[method] = assert[method];
});
}
return assertObject;
}
export function run() {
notImplemented("test.run");
}
function noop() {}
const skippedSymbol = Symbol("skipped");
class NodeTestContext {
#denoContext: Deno.TestContext;
#afterHooks: (() => void)[] = [];
#beforeHooks: (() => void)[] = [];
#parent: NodeTestContext | undefined;
#skipped = false;
constructor(t: Deno.TestContext, parent: NodeTestContext | undefined) {
this.#denoContext = t;
this.#parent = parent;
}
get [skippedSymbol]() {
return this.#skipped || (this.#parent?.[skippedSymbol] ?? false);
}
get assert() {
return getAssertObject();
}
get signal() {
notImplemented("test.TestContext.signal");
return null;
}
get name() {
notImplemented("test.TestContext.name");
return null;
}
diagnostic(message) {
// deno-lint-ignore no-console
console.log("DIAGNOSTIC:", message);
}
get mock() {
notImplemented("test.TestContext.mock");
return null;
}
runOnly() {
notImplemented("test.TestContext.runOnly");
return null;
}
skip() {
this.#skipped = true;
return null;
}
todo() {
this.#skipped = true;
return null;
}
test(name, options, fn) {
const prepared = prepareOptions(name, options, fn, {});
// deno-lint-ignore no-this-alias
const parentContext = this;
const after = async () => {
for (const hook of new SafeArrayIterator(this.#afterHooks)) {
await hook();
}
};
const before = async () => {
for (const hook of new SafeArrayIterator(this.#beforeHooks)) {
await hook();
}
};
return PromisePrototypeThen(
this.#denoContext.step({
name: prepared.name,
fn: async (denoTestContext) => {
const newNodeTextContext = new NodeTestContext(
denoTestContext,
parentContext,
);
try {
await before();
await prepared.fn(newNodeTextContext);
await after();
} catch (err) {
if (!newNodeTextContext[skippedSymbol]) {
throw err;
}
try {
await after();
} catch { /* ignore, test is already failing */ }
}
},
ignore: prepared.options.todo || prepared.options.skip,
sanitizeExit: false,
sanitizeOps: false,
sanitizeResources: false,
}),
() => undefined,
);
}
before(fn, _options) {
if (typeof fn !== "function") {
throw new TypeError("before() requires a function");
}
ArrayPrototypePush(this.#beforeHooks, fn);
}
after(fn, _options) {
if (typeof fn !== "function") {
throw new TypeError("after() requires a function");
}
ArrayPrototypePush(this.#afterHooks, fn);
}
beforeEach(_fn, _options) {
notImplemented("test.TestContext.beforeEach");
}
afterEach(_fn, _options) {
notImplemented("test.TestContext.afterEach");
}
}
let currentSuite: TestSuite | null = null;
class TestSuite {
#denoTestContext: Deno.TestContext;
steps: Promise<boolean>[] = [];
constructor(t: Deno.TestContext) {
this.#denoTestContext = t;
}
addTest(name, options, fn, overrides) {
const prepared = prepareOptions(name, options, fn, overrides);
const step = this.#denoTestContext.step({
name: prepared.name,
fn: async (denoTestContext) => {
const newNodeTextContext = new NodeTestContext(
denoTestContext,
undefined,
);
try {
return await prepared.fn(newNodeTextContext);
} catch (err) {
if (newNodeTextContext[skippedSymbol]) {
return undefined;
} else {
throw err;
}
}
},
ignore: prepared.options.todo || prepared.options.skip,
sanitizeExit: false,
sanitizeOps: false,
sanitizeResources: false,
});
ArrayPrototypePush(this.steps, step);
}
addSuite(name, options, fn, overrides) {
const prepared = prepareOptions(name, options, fn, overrides);
// deno-lint-ignore prefer-primordials
const { promise, resolve } = Promise.withResolvers();
const step = this.#denoTestContext.step({
name: prepared.name,
fn: wrapSuiteFn(prepared.fn, resolve),
ignore: prepared.options.todo || prepared.options.skip,
sanitizeExit: false,
sanitizeOps: false,
sanitizeResources: false,
});
ArrayPrototypePush(this.steps, step);
return promise;
}
}
function prepareOptions(name, options, fn, overrides) {
if (typeof name === "function") {
fn = name;
} else if (name !== null && typeof name === "object") {
fn = options;
options = name;
} else if (typeof options === "function") {
fn = options;
}
if (options === null || typeof options !== "object") {
options = {};
}
const finalOptions = { ...options, ...overrides };
// TODO(bartlomieju): these options are currently not handled
// const { concurrency, timeout, signal } = finalOptions;
if (typeof fn !== "function") {
fn = noop;
}
if (typeof name !== "string" || name === "") {
name = fn.name || "<anonymous>";
}
return { fn, options: finalOptions, name };
}
function wrapTestFn(fn, resolve) {
return async function (t) {
const nodeTestContext = new NodeTestContext(t, undefined);
try {
await fn(nodeTestContext);
} catch (err) {
if (!nodeTestContext[skippedSymbol]) {
throw err;
}
} finally {
resolve();
}
};
}
function prepareDenoTest(name, options, fn, overrides) {
const prepared = prepareOptions(name, options, fn, overrides);
// TODO(iuioiua): Update once there's a primordial for `Promise.withResolvers()`.
// deno-lint-ignore prefer-primordials
const { promise, resolve } = Promise.withResolvers();
const denoTestOptions = {
name: prepared.name,
fn: wrapTestFn(prepared.fn, resolve),
only: prepared.options.only,
ignore: prepared.options.todo || prepared.options.skip,
sanitizeExit: false,
sanitizeOps: false,
sanitizeResources: false,
};
Deno.test(denoTestOptions);
return promise;
}
function wrapSuiteFn(fn, resolve) {
return function (t) {
const prevSuite = currentSuite;
const suite = currentSuite = new TestSuite(t);
try {
fn();
} finally {
currentSuite = prevSuite;
}
return SafePromisePrototypeFinally(SafePromiseAll(suite.steps), resolve);
};
}
function prepareDenoTestForSuite(name, options, fn, overrides) {
const prepared = prepareOptions(name, options, fn, overrides);
// deno-lint-ignore prefer-primordials
const { promise, resolve } = Promise.withResolvers();
const denoTestOptions = {
name: prepared.name,
fn: wrapSuiteFn(prepared.fn, resolve),
only: prepared.options.only,
ignore: prepared.options.todo || prepared.options.skip,
sanitizeExit: false,
sanitizeOps: false,
sanitizeResources: false,
};
Deno.test(denoTestOptions);
return promise;
}
export function test(name, options, fn, overrides) {
if (currentSuite) {
return currentSuite.addTest(name, options, fn, overrides);
}
return prepareDenoTest(name, options, fn, overrides);
}
test.skip = function skip(name, options, fn) {
return test(name, options, fn, { skip: true });
};
test.todo = function todo(name, options, fn) {
return test(name, options, fn, { todo: true });
};
test.only = function only(name, options, fn) {
return test(name, options, fn, { only: true });
};
export function describe(name, options, fn) {
return suite(name, options, fn, {});
}
describe.skip = function skip(name, options, fn) {
return suite.skip(name, options, fn);
};
describe.todo = function todo(name, options, fn) {
return suite.todo(name, options, fn);
};
describe.only = function only(name, options, fn) {
return suite.only(name, options, fn);
};
export function suite(name, options, fn, overrides) {
if (currentSuite) {
return currentSuite.addSuite(name, options, fn, overrides);
}
return prepareDenoTestForSuite(name, options, fn, overrides);
}
suite.skip = function skip(name, options, fn) {
return suite(name, options, fn, { skip: true });
};
suite.todo = function todo(name, options, fn) {
return suite(name, options, fn, { todo: true });
};
suite.only = function only(name, options, fn) {
return suite(name, options, fn, { only: true });
};
export function it(name, options, fn) {
return test(name, options, fn, {});
}
it.skip = function skip(name, options, fn) {
return test.skip(name, options, fn);
};
it.todo = function todo(name, options, fn) {
return test.todo(name, options, fn);
};
it.only = function only(name, options, fn) {
return test.only(name, options, fn);
};
export function before() {
notImplemented("test.before");
}
export function after() {
notImplemented("test.after");
}
export function beforeEach() {
notImplemented("test.beforeEach");
}
export function afterEach() {
notImplemented("test.afterEach");
}
test.it = it;
test.describe = describe;
test.suite = suite;
export const mock = {
fn: () => {
notImplemented("test.mock.fn");
},
getter: () => {
notImplemented("test.mock.getter");
},
method: () => {
notImplemented("test.mock.method");
},
reset: () => {
notImplemented("test.mock.reset");
},
restoreAll: () => {
notImplemented("test.mock.restoreAll");
},
setter: () => {
notImplemented("test.mock.setter");
},
timers: {
enable: () => {
notImplemented("test.mock.timers.enable");
},
reset: () => {
notImplemented("test.mock.timers.reset");
},
tick: () => {
notImplemented("test.mock.timers.tick");
},
runAll: () => {
notImplemented("test.mock.timers.runAll");
},
},
};
test.test = test;
export default test;