Initial support for node:test (#18140)

This commit is contained in:
Ashcon Partovi
2025-03-19 11:49:00 -07:00
committed by GitHub
parent 21a42a0dee
commit 6e1f1c4da7
39 changed files with 1456 additions and 397 deletions

View File

@@ -174,7 +174,7 @@ Some methods are not optimized yet.
### [`node:test`](https://nodejs.org/api/test.html)
🔴 Not implemented. Use [`bun:test`](https://bun.sh/docs/cli/test) instead.
🟡 Partly implemented. Missing mocks, snapshots, timers. Use [`bun:test`](https://bun.sh/docs/cli/test) instead.
### [`node:trace_events`](https://nodejs.org/api/tracing.html)

View File

@@ -256,8 +256,10 @@ async function runTests() {
for (const testPath of tests) {
const absoluteTestPath = join(testsPath, testPath);
const title = relative(cwd, absoluteTestPath).replaceAll(sep, "/");
if (isNodeParallelTest(testPath)) {
const runWithBunTest = title.includes("needs-test") || readFileSync(absoluteTestPath, "utf-8").includes('bun:test');
if (isNodeTest(testPath)) {
const testContent = readFileSync(absoluteTestPath, "utf-8");
const runWithBunTest =
title.includes("needs-test") || testContent.includes("bun:test") || testContent.includes("node:test");
const subcommand = runWithBunTest ? "test" : "run";
await runTest(title, async () => {
const { ok, error, stdout } = await spawnBun(execPath, {
@@ -870,19 +872,26 @@ function isJavaScriptTest(path) {
}
/**
* @param {string} testPath
* @param {string} path
* @returns {boolean}
*/
function isNodeParallelTest(testPath) {
return testPath.replaceAll(sep, "/").includes("js/node/test/parallel/");
function isNodeTest(path) {
// Do not run node tests on macOS x64 in CI
// TODO: Unclear why we decided to do this?
if (isCI && isMacOS && isX64) {
return false;
}
const unixPath = path.replaceAll(sep, "/");
return unixPath.includes("js/node/test/parallel/") || unixPath.includes("js/node/test/sequential/");
}
/**
* @param {string} testPath
* @param {string} path
* @returns {boolean}
*/
function isNodeSequentialTest(testPath) {
return testPath.replaceAll(sep, "/").includes("js/node/test/sequential/");
function isClusterTest(path) {
const unixPath = path.replaceAll(sep, "/");
return unixPath.includes("js/node/cluster/test-") && unixPath.endsWith(".ts");
}
/**
@@ -890,21 +899,17 @@ function isNodeSequentialTest(testPath) {
* @returns {boolean}
*/
function isTest(path) {
if (isNodeParallelTest(path) && targetDoesRunNodeTests()) return true;
if (isNodeSequentialTest(path) && targetDoesRunNodeTests()) return true;
if (path.replaceAll(sep, "/").startsWith("js/node/cluster/test-") && path.endsWith(".ts")) return true;
return isTestStrict(path);
return isNodeTest(path) || isClusterTest(path) ? true : isTestStrict(path);
}
/**
* @param {string} path
* @returns {boolean}
*/
function isTestStrict(path) {
return isJavaScript(path) && /\.test|spec\./.test(basename(path));
}
function targetDoesRunNodeTests() {
if (isMacOS && isX64) return false;
return true;
}
/**
* @param {string} path
* @returns {boolean}

View File

@@ -78,27 +78,31 @@ static constexpr ASCIILiteral builtinModuleNamesSortedLength[] = {
"inspector/promises"_s,
"_stream_passthrough"_s,
"diagnostics_channel"_s,
"node:test"_s,
};
namespace Bun {
bool isBuiltinModule(const String& namePossiblyWithNodePrefix)
{
// First check the original name as-is
for (auto& builtinModule : builtinModuleNamesSortedLength) {
if (namePossiblyWithNodePrefix == builtinModule)
return true;
}
// If no match found and the name has a "node:" prefix, try without the prefix
String name = namePossiblyWithNodePrefix;
if (name.startsWith("node:"_s)) {
name = name.substringSharingImpl(5);
// bun doesn't have `node:test` as of writing, but this makes sure that
// `node:module` is compatible (`test/parallel/test-module-isBuiltin.js`)
if (name == "test"_s) {
return true;
// Check again with the prefix removed
for (auto& builtinModule : builtinModuleNamesSortedLength) {
if (name == builtinModule)
return true;
}
}
for (auto& builtinModule : builtinModuleNamesSortedLength) {
if (name == builtinModule)
return true;
}
return false;
}

View File

@@ -1781,7 +1781,7 @@ pub const ModuleLoader = struct {
.already_bundled = true,
.hash = 0,
.bytecode_cache = if (bytecode_slice.len > 0) bytecode_slice.ptr else null,
.bytecode_cache_size = if (bytecode_slice.len > 0) bytecode_slice.len else 0,
.bytecode_cache_size = bytecode_slice.len,
.is_commonjs_module = parse_result.already_bundled.isCommonJS(),
};
}
@@ -2902,7 +2902,8 @@ pub const HardcodedModule = enum {
.{ "stream/promises", .{ .path = "stream/promises" } },
.{ "stream/web", .{ .path = "stream/web" } },
.{ "string_decoder", .{ .path = "string_decoder" } },
// .{ "test", .{ .path = "test" } },
// Node.js does not support "test", only "node:test"
// .{ "test", .{ .path = "node:test" } },
.{ "timers", .{ .path = "timers" } },
.{ "timers/promises", .{ .path = "timers/promises" } },
.{ "tls", .{ .path = "tls" } },

View File

@@ -115,6 +115,7 @@ static constexpr ASCIILiteral builtinModuleNames[] = {
"worker_threads"_s,
"ws"_s,
"zlib"_s,
"node:test"_s,
};
template<std::size_t N, class T> consteval std::size_t countof(T (&)[N])

View File

@@ -75,6 +75,7 @@ const builtinModules = [
"wasi",
"worker_threads",
"zlib",
"node:test",
];
export default {

View File

@@ -1,38 +1,621 @@
// Hardcoded module "node:test"
// This follows the Node.js API as described in: https://nodejs.org/api/test.html
const { throwNotImplemented } = require("internal/shared");
const { jest } = Bun;
const { kEmptyObject, throwNotImplemented } = require("internal/shared");
const Readable = require("internal/streams/readable");
function suite() {
throwNotImplemented("node:test", 5090, "bun:test in available in the interim.");
const kDefaultName = "<anonymous>";
const kDefaultFunction = () => {};
const kDefaultOptions = kEmptyObject;
const kDefaultFilePath = callerSourceOrigin();
function run(...args: unknown[]) {
throwNotImplemented("run()", 5090, "Use `bun:test` in the interim.");
}
function test() {
throwNotImplemented("node:test", 5090, "bun:test in available in the interim.");
function mock(...args: unknown[]) {
throwNotImplemented("mock()", 5090, "Use `bun:test` in the interim.");
}
function before() {
throwNotImplemented("node:test", 5090, "bun:test in available in the interim.");
/**
* @link https://nodejs.org/api/test.html#class-mockfunctioncontext
*/
class MockFunctionContext {
constructor() {
throwNotImplemented("new MockFunctionContext()", 5090, "Use `bun:test` in the interim.");
}
get calls() {
throwNotImplemented("calls()", 5090, "Use `bun:test` in the interim.");
return undefined;
}
callCount() {
throwNotImplemented("callCount()", 5090, "Use `bun:test` in the interim.");
return 0;
}
mockImplementation(fn: Function) {
throwNotImplemented("mockImplementation()", 5090, "Use `bun:test` in the interim.");
return undefined;
}
mockImplementationOnce(fn: Function, onCall?: unknown) {
throwNotImplemented("mockImplementationOnce()", 5090, "Use `bun:test` in the interim.");
return undefined;
}
resetCalls() {
throwNotImplemented("resetCalls()", 5090, "Use `bun:test` in the interim.");
}
restore() {
throwNotImplemented("restore()", 5090, "Use `bun:test` in the interim.");
}
}
function after() {
throwNotImplemented("node:test", 5090, "bun:test in available in the interim.");
/**
* @link https://nodejs.org/api/test.html#class-mockmodulecontext
*/
class MockModuleContext {
constructor() {
throwNotImplemented("new MockModuleContext()", 5090, "Use `bun:test` in the interim.");
}
restore() {
throwNotImplemented("restore()", 5090, "Use `bun:test` in the interim.");
}
}
function beforeEach() {
throwNotImplemented("node:test", 5090, "bun:test in available in the interim.");
/**
* @link https://nodejs.org/api/test.html#class-mocktracker
*/
class MockTracker {
constructor() {
throwNotImplemented("new MockTracker()", 5090, "Use `bun:test` in the interim.");
}
fn(original: unknown, implementation: unknown, options: unknown) {
throwNotImplemented("fn()", 5090, "Use `bun:test` in the interim.");
return undefined;
}
method(object: unknown, methodName: unknown, implementation: unknown, options: unknown) {
throwNotImplemented("method()", 5090, "Use `bun:test` in the interim.");
return undefined;
}
getter(original: unknown, implementation: unknown, options: unknown) {
throwNotImplemented("getter()", 5090, "Use `bun:test` in the interim.");
return undefined;
}
setter(original: unknown, implementation: unknown, options: unknown) {
throwNotImplemented("setter()", 5090, "Use `bun:test` in the interim.");
return undefined;
}
module(specifier: unknown, options: unknown) {
throwNotImplemented("module()", 5090, "Use `bun:test` in the interim.");
return undefined;
}
reset() {
throwNotImplemented("reset()", 5090, "Use `bun:test` in the interim.");
}
restoreAll() {
throwNotImplemented("restoreAll()", 5090, "Use `bun:test` in the interim.");
}
}
function afterEach() {
throwNotImplemented("node:test", 5090, "bun:test in available in the interim.");
class MockTimers {
constructor() {
throwNotImplemented("new MockTimers()", 5090, "Use `bun:test` in the interim.");
}
enable(options: unknown) {
throwNotImplemented("enable()", 5090, "Use `bun:test` in the interim.");
}
reset() {
throwNotImplemented("reset()", 5090, "Use `bun:test` in the interim.");
}
tick(milliseconds: unknown) {
throwNotImplemented("tick()", 5090, "Use `bun:test` in the interim.");
}
runAll() {
throwNotImplemented("runAll()", 5090, "Use `bun:test` in the interim.");
}
setTime(milliseconds: unknown) {
throwNotImplemented("setTime()", 5090, "Use `bun:test` in the interim.");
}
[Symbol.dispose]() {
this.reset();
}
}
export default {
suite,
test,
describe: suite,
it: test,
before,
after,
beforeEach,
afterEach,
/**
* @link https://nodejs.org/api/test.html#class-testsstream
*/
class TestsStream extends Readable {
constructor() {
super();
throwNotImplemented("new TestsStream()", 5090, "Use `bun:test` in the interim.");
}
}
function fileSnapshot(value: unknown, path: string, options: { serializers?: Function[] } = kEmptyObject) {
throwNotImplemented("fileSnapshot()", 5090, "Use `bun:test` in the interim.");
}
function snapshot(value: unknown, options: { serializers?: Function[] } = kEmptyObject) {
throwNotImplemented("snapshot()", 5090, "Use `bun:test` in the interim.");
}
function register(name: string, fn: Function) {
throwNotImplemented("register()", 5090, "Use `bun:test` in the interim.");
}
const assert = {
...require("node:assert"),
fileSnapshot,
snapshot,
// register,
};
// Delete deprecated methods on assert (required to pass node's tests)
delete assert.AssertionError;
delete assert.CallTracker;
delete assert.strict;
/**
* @link https://nodejs.org/api/test.html#class-suitecontext
*/
class SuiteContext {
#name: string | undefined;
#filePath: string | undefined;
#abortController?: AbortController;
constructor(name: string | undefined, filePath: string | undefined) {
this.#name = name;
this.#filePath = filePath || kDefaultFilePath;
}
get name(): string {
return this.#name!;
}
get filePath(): string {
return this.#filePath!;
}
get signal(): AbortSignal {
if (this.#abortController === undefined) {
this.#abortController = new AbortController();
}
return this.#abortController.signal;
}
}
/**
* @link https://nodejs.org/api/test.html#class-testcontext
*/
class TestContext {
#insideTest: boolean;
#name: string | undefined;
#filePath: string | undefined;
#parent?: TestContext;
#abortController?: AbortController;
constructor(
insideTest: boolean,
name: string | undefined,
filePath: string | undefined,
parent: TestContext | undefined,
) {
this.#insideTest = insideTest;
this.#name = name;
this.#filePath = filePath || parent?.filePath || kDefaultFilePath;
this.#parent = parent;
}
get signal(): AbortSignal {
if (this.#abortController === undefined) {
this.#abortController = new AbortController();
}
return this.#abortController.signal;
}
get name(): string {
return this.#name!;
}
get fullName(): string {
let fullName = this.#name;
let parent = this.#parent;
while (parent && parent.name) {
fullName = `${parent.name} > ${fullName}`;
parent = parent.#parent;
}
return fullName!;
}
get filePath(): string {
return this.#filePath!;
}
diagnostic(message: string) {
console.log(message);
}
plan(count: number, options: { wait?: boolean } = kEmptyObject) {
throwNotImplemented("plan()", 5090, "Use `bun:test` in the interim.");
}
get assert() {
return assert;
}
get mock() {
throwNotImplemented("mock", 5090, "Use `bun:test` in the interim.");
return undefined;
}
runOnly(value?: boolean) {
throwNotImplemented("runOnly()", 5090, "Use `bun:test` in the interim.");
}
skip(message?: string) {
throwNotImplemented("skip()", 5090, "Use `bun:test` in the interim.");
}
todo(message?: string) {
throwNotImplemented("todo()", 5090, "Use `bun:test` in the interim.");
}
before(arg0: unknown, arg1: unknown) {
const { fn, options } = createHook(arg0, arg1);
const { beforeAll } = bunTest(this);
beforeAll(fn);
}
after(arg0: unknown, arg1: unknown) {
const { fn, options } = createHook(arg0, arg1);
const { afterAll } = bunTest(this);
afterAll(fn);
}
beforeEach(arg0: unknown, arg1: unknown) {
const { fn, options } = createHook(arg0, arg1);
const { beforeEach } = bunTest(this);
beforeEach(fn);
}
afterEach(arg0: unknown, arg1: unknown) {
const { fn, options } = createHook(arg0, arg1);
const { afterEach } = bunTest(this);
afterEach(fn);
}
waitFor(condition: unknown, options: { timeout?: number } = kEmptyObject) {
throwNotImplemented("waitFor()", 5090, "Use `bun:test` in the interim.");
}
test(arg0: unknown, arg1: unknown, arg2: unknown) {
const { name, fn, options } = createTest(arg0, arg1, arg2);
if (this.#insideTest) {
throwNotImplemented("test() inside another test()", 5090, "Use `bun:test` in the interim.");
}
const { test } = bunTest(this);
if (options.only) {
test.only(name, fn);
} else if (options.todo) {
test.todo(name, fn);
} else if (options.skip) {
test.skip(name, fn);
} else {
test(name, fn);
}
}
describe(arg0: unknown, arg1: unknown, arg2: unknown) {
const { name, fn, options } = createDescribe(arg0, arg1, arg2);
if (this.#insideTest) {
throwNotImplemented("describe() inside another test()", 5090, "Use `bun:test` in the interim.");
}
const { describe } = bunTest(this);
describe(name, fn);
}
}
function bunTest(ctx: SuiteContext | TestContext) {
return jest(ctx.filePath);
}
let ctx = new TestContext(false, undefined, kDefaultFilePath, undefined);
function describe(arg0: unknown, arg1: unknown, arg2: unknown) {
const { name, fn, options } = createDescribe(arg0, arg1, arg2);
const { describe } = bunTest(ctx);
describe(name, fn);
}
describe.skip = function (arg0: unknown, arg1: unknown, arg2: unknown) {
const { name, fn, options } = createDescribe(arg0, arg1, arg2);
const { describe } = bunTest(ctx);
describe.skip(name, fn);
};
describe.todo = function (arg0: unknown, arg1: unknown, arg2: unknown) {
const { name, fn, options } = createDescribe(arg0, arg1, arg2);
const { describe } = bunTest(ctx);
describe.todo(name, fn);
};
describe.only = function (arg0: unknown, arg1: unknown, arg2: unknown) {
const { name, fn, options } = createDescribe(arg0, arg1, arg2);
const { describe } = bunTest(ctx);
describe.only(name, fn);
};
function test(arg0: unknown, arg1: unknown, arg2: unknown) {
const { name, fn, options } = createTest(arg0, arg1, arg2);
const { test } = bunTest(ctx);
test(name, fn, options);
}
test.skip = function (arg0: unknown, arg1: unknown, arg2: unknown) {
const { name, fn, options } = createTest(arg0, arg1, arg2);
const { test } = bunTest(ctx);
test.skip(name, fn, options);
};
test.todo = function (arg0: unknown, arg1: unknown, arg2: unknown) {
const { name, fn, options } = createTest(arg0, arg1, arg2);
const { test } = bunTest(ctx);
test.todo(name, fn, options);
};
test.only = function (arg0: unknown, arg1: unknown, arg2: unknown) {
const { name, fn, options } = createTest(arg0, arg1, arg2);
const { test } = bunTest(ctx);
test.only(name, fn, options);
};
function before(arg0: unknown, arg1: unknown) {
const { fn, options } = createHook(arg0, arg1);
const { beforeAll } = bunTest(ctx);
beforeAll(fn);
}
function after(arg0: unknown, arg1: unknown) {
const { fn, options } = createHook(arg0, arg1);
const { afterAll } = bunTest(ctx);
afterAll(fn);
}
function beforeEach(arg0: unknown, arg1: unknown) {
const { fn, options } = createHook(arg0, arg1);
const { beforeEach } = bunTest(ctx);
beforeEach(fn);
}
function afterEach(arg0: unknown, arg1: unknown) {
const { fn, options } = createHook(arg0, arg1);
const { afterEach } = bunTest(ctx);
afterEach(fn);
}
function isBuiltinModule(filePath: string) {
return filePath.startsWith("node:") || filePath.startsWith("bun:") || filePath.startsWith("[native code]");
}
function callerSourceOrigin(): string {
const error = new Error();
const originalPrepareStackTrace = Error.prepareStackTrace;
let origin: string | undefined;
Error.prepareStackTrace = (_, stack) => {
origin = stack
.find(s => {
const filePath = s.getFileName();
if (filePath && !isBuiltinModule(filePath)) {
return filePath;
}
return undefined;
})
?.getFileName();
};
error.stack;
Error.prepareStackTrace = originalPrepareStackTrace;
if (!origin) {
throw new Error("Failed to get caller source origin");
}
return origin;
}
function parseTestOptions(arg0: unknown, arg1: unknown, arg2: unknown) {
let name: string;
let options: TestOptions;
let fn: TestFn;
if (typeof arg0 === "function") {
name = arg0.name || kDefaultName;
fn = arg0 as TestFn;
if (typeof arg1 === "object") {
options = arg1 as TestOptions;
} else {
options = kDefaultOptions;
}
} else if (typeof arg0 === "string") {
name = arg0;
if (typeof arg1 === "object") {
options = arg1 as TestOptions;
if (typeof arg2 === "function") {
fn = arg2 as TestFn;
} else {
fn = kDefaultFunction;
}
} else if (typeof arg1 === "function") {
fn = arg1 as TestFn;
options = kDefaultOptions;
} else {
fn = kDefaultFunction;
options = kDefaultOptions;
}
} else {
name = kDefaultName;
fn = kDefaultFunction;
options = kDefaultOptions;
}
return { name, options, fn };
}
function createTest(arg0: unknown, arg1: unknown, arg2: unknown) {
const { name, options, fn } = parseTestOptions(arg0, arg1, arg2);
const originalContext = ctx;
const context = new TestContext(true, name, ctx.filePath, originalContext);
const runTest = (done: (error?: unknown) => void) => {
ctx = context;
const endTest = (error?: unknown) => {
try {
done(error);
} finally {
ctx = originalContext;
}
};
let result: unknown;
try {
result = fn(context);
} catch (error) {
endTest(error);
return;
}
if (result instanceof Promise) {
(result as Promise<unknown>).then(() => endTest()).catch(error => endTest(error));
} else {
endTest();
}
};
return { name, options, fn: runTest };
}
function createDescribe(arg0: unknown, arg1: unknown, arg2: unknown) {
const { name, fn, options } = parseTestOptions(arg0, arg1, arg2);
const originalContext = ctx;
const context = new TestContext(false, name, ctx.filePath, originalContext);
const runDescribe = () => {
ctx = context;
const endDescribe = () => {
ctx = originalContext;
};
try {
return fn(context);
} finally {
endDescribe();
}
};
return { name, options, fn: runDescribe };
}
function parseHookOptions(arg0: unknown, arg1: unknown) {
let fn: HookFn | undefined;
let options: HookOptions;
if (typeof arg0 === "function") {
fn = arg0 as HookFn;
} else {
fn = kDefaultFunction;
}
if (typeof arg1 === "object") {
options = arg1 as HookOptions;
} else {
options = kDefaultOptions;
}
return { fn, options };
}
function createHook(arg0: unknown, arg1: unknown) {
const { fn, options } = parseHookOptions(arg0, arg1);
const runHook = (done: (error?: unknown) => void) => {
let result: unknown;
try {
result = fn();
} catch (error) {
done(error);
return;
}
if (result instanceof Promise) {
(result as Promise<unknown>).then(() => done()).catch(error => done(error));
} else {
done();
}
};
return { options, fn: runHook };
}
type TestFn = (ctx: TestContext) => unknown | Promise<unknown>;
type HookFn = () => unknown | Promise<unknown>;
type TestOptions = {
concurrency?: number | boolean | null;
only?: boolean;
signal?: AbortSignal;
skip?: boolean | string;
todo?: boolean | string;
timeout?: number;
plan?: number;
};
type HookOptions = {
signal?: AbortSignal;
timeout?: number;
};
function setDefaultSnapshotSerializer(serializers: unknown[]) {
throwNotImplemented("setDefaultSnapshotSerializer()", 5090, "Use `bun:test` in the interim.");
}
function setResolveSnapshotPath(fn: unknown) {
throwNotImplemented("setResolveSnapshotPath()", 5090, "Use `bun:test` in the interim.");
}
test.describe = describe;
test.suite = describe;
test.test = test;
test.it = test;
test.before = before;
test.after = after;
test.beforeEach = beforeEach;
test.afterEach = afterEach;
test.assert = assert;
test.snapshot = {
setDefaultSnapshotSerializer,
setResolveSnapshotPath,
};
test.run = run;
test.mock = mock;
export default test;

View File

@@ -221,6 +221,7 @@ pub const ExternalModules = struct {
"stream",
"string_decoder",
"sys",
"test",
"timers",
"tls",
"trace_events",

View File

@@ -27,7 +27,7 @@
"bun-plugin-yaml": "0.0.1",
"comlink": "4.4.1",
"commander": "12.1.0",
"detect-libc": "^2.0.3",
"detect-libc": "2.0.3",
"devalue": "5.1.1",
"duckdb": "1.1.3",
"es-module-lexer": "1.3.0",

View File

@@ -1,8 +1,6 @@
/**
* @note this file patches `node:test` via the require cache.
*/
import { AnyFunction } from "bun";
import os from "node:os";
import { hideFromStackTrace } from "harness";
import assertNode from "node:assert";
@@ -130,11 +128,11 @@ export function createTest(path: string) {
}, exact);
}
function mustCallAtLeast(fn: AnyFunction, minimum: number) {
function mustCallAtLeast(fn: unknown, minimum: number) {
return _mustCallInner(fn, minimum, "minimum");
}
function _mustCallInner(fn: AnyFunction, criteria = 1, field: string) {
function _mustCallInner(fn: unknown, criteria = 1, field: string) {
// @ts-ignore
if (process._exiting) throw new Error("Cannot use common.mustCall*() in process exit handler");
if (typeof fn === "number") {
@@ -266,129 +264,3 @@ export function createTest(path: string) {
declare namespace Bun {
function jest(path: string): typeof import("bun:test");
}
const normalized = os.platform() === "win32" ? Bun.main.replaceAll("\\", "/") : Bun.main;
if (normalized.includes("node/test/parallel")) {
function createMockNodeTestModule() {
interface TestError extends Error {
testStack: string[];
}
type Context = {
filename: string;
testStack: string[];
failures: Error[];
successes: number;
addFailure(err: unknown): TestError;
recordSuccess(): void;
};
const contexts: Record</* requiring file */ string, Context> = {};
// @ts-ignore
let activeSuite: Context = undefined;
function createContext(key: string): Context {
return {
filename: key, // duplicate for ease-of-use
// entered each time describe, it, etc is called
testStack: [],
failures: [],
successes: 0,
addFailure(err: unknown) {
const error: TestError = (err instanceof Error ? err : new Error(err as any)) as any;
error.testStack = this.testStack;
const testMessage = `Test failed: ${this.testStack.join(" > ")}`;
error.message = testMessage + "\n" + error.message;
this.failures.push(error);
console.error(error);
return error;
},
recordSuccess() {
const fullname = this.testStack.join(" > ");
console.log("✅ Test passed:", fullname);
this.successes++;
},
};
}
function getContext() {
const key: string = Bun.main; // module.parent?.filename ?? require.main?.filename ?? __filename;
return (activeSuite = contexts[key] ??= createContext(key));
}
async function test(
label: string | Function,
optionsOrFn: Record<string, any> | Function,
fn?: Function | undefined,
) {
let options = optionsOrFn;
if (arguments.length === 2) {
assertNode.equal(typeof optionsOrFn, "function", "Second argument to test() must be a function.");
fn = optionsOrFn as Function;
options = {};
}
if (typeof fn !== "function" && typeof label === "function") {
fn = label;
label = fn.name;
options = {};
}
const ctx = getContext();
const { skip } = options;
if (skip) return;
try {
ctx.testStack.push(label as string);
await fn();
ctx.recordSuccess();
} catch (err) {
const error = ctx.addFailure(err);
throw error;
} finally {
ctx.testStack.pop();
}
}
function describe(labelOrFn: string | Function, maybeFnOrOptions?: Function, maybeFn?: Function) {
const [label, fn] =
typeof labelOrFn == "function" ? [labelOrFn.name, labelOrFn] : [labelOrFn, maybeFn ?? maybeFnOrOptions];
if (typeof fn !== "function") throw new TypeError("Second argument to describe() must be a function.");
getContext().testStack.push(label);
try {
fn();
} catch (e) {
getContext().addFailure(e);
throw e;
} finally {
getContext().testStack.pop();
}
const failures = getContext().failures.length;
const successes = getContext().successes;
console.error(`describe("${label}") finished with ${successes} passed and ${failures} failed tests.`);
if (failures > 0) {
throw new Error(`${failures} tests failed.`);
}
}
return {
test,
it: test,
describe,
suite: describe,
};
}
require.cache["node:test"] ??= {
exports: createMockNodeTestModule(),
loaded: true,
isPreloading: false,
id: "node:test",
parent: require.main,
filename: "node:test",
children: [],
path: "node:test",
paths: [],
require,
};
}

View File

@@ -5,7 +5,7 @@ import path from "path";
test("builtinModules exists", () => {
expect(Array.isArray(builtinModules)).toBe(true);
expect(builtinModules).toHaveLength(76);
expect(builtinModules).toHaveLength(77);
});
test("isBuiltin() works", () => {
@@ -17,6 +17,8 @@ test("isBuiltin() works", () => {
expect(isBuiltin("events")).toBe(true);
expect(isBuiltin("node:events")).toBe(true);
expect(isBuiltin("node:bacon")).toBe(false);
expect(isBuiltin("node:test")).toBe(true);
expect(isBuiltin("test")).toBe(false); // "test" does not alias to "node:test"
});
test("module.globalPaths exists", () => {

View File

@@ -90,20 +90,28 @@ function parseTestFlags(filename = process.argv[1]) {
fs.closeSync(fd);
const source = buffer.toString('utf8', 0, bytesRead);
const flags = [];
const flagStart = source.search(/\/\/ Flags:\s+--/) + 10;
const isNodeTest = source.includes('node:test');
if (isNodeTest) {
flags.push('test');
}
if (flagStart === 9) {
return [];
return flags;
}
let flagEnd = source.indexOf('\n', flagStart);
// Normalize different EOL.
if (source[flagEnd - 1] === '\r') {
flagEnd--;
}
return source
.substring(flagStart, flagEnd)
.split(/\s+/)
.filter(Boolean);
.filter(Boolean)
.concat(flags);
}
// Check for flags. Skip this for workers (both, the `cluster` module and
@@ -130,6 +138,10 @@ if (process.argv.length === 2 &&
process.env.SKIP_FLAG_CHECK = "1";
break;
}
if (flag === "test") {
process.env.SKIP_FLAG_CHECK = "1";
break;
}
console.log(
'NOTE: The test started as a child_process using these flags:',
inspect(flags),

View File

@@ -1,177 +0,0 @@
'use strict';
const { isWindows } = require('../../common');
const { test } = require('node:test');
const assert = require('node:assert');
const url = require('node:url');
test('invalid arguments', () => {
for (const arg of [null, undefined, 1, {}, true]) {
assert.throws(() => url.fileURLToPath(arg), {
code: 'ERR_INVALID_ARG_TYPE'
});
}
});
test('input must be a file URL', () => {
assert.throws(() => url.fileURLToPath('https://a/b/c'), {
code: 'ERR_INVALID_URL_SCHEME'
});
});
test('fileURLToPath with host', () => {
const withHost = new URL('file://host/a');
if (isWindows) {
assert.strictEqual(url.fileURLToPath(withHost), '\\\\host\\a');
} else {
assert.throws(() => url.fileURLToPath(withHost), {
code: 'ERR_INVALID_FILE_URL_HOST'
});
}
});
test('fileURLToPath with invalid path', () => {
if (isWindows) {
assert.throws(() => url.fileURLToPath('file:///C:/a%2F/'), {
code: 'ERR_INVALID_FILE_URL_PATH'
});
assert.throws(() => url.fileURLToPath('file:///C:/a%5C/'), {
code: 'ERR_INVALID_FILE_URL_PATH'
});
assert.throws(() => url.fileURLToPath('file:///?:/'), {
code: 'ERR_INVALID_FILE_URL_PATH'
});
} else {
assert.throws(() => url.fileURLToPath('file:///a%2F/'), {
code: 'ERR_INVALID_FILE_URL_PATH'
});
}
});
const windowsTestCases = [
// Lowercase ascii alpha
{ path: 'C:\\foo', fileURL: 'file:///C:/foo' },
// Uppercase ascii alpha
{ path: 'C:\\FOO', fileURL: 'file:///C:/FOO' },
// dir
{ path: 'C:\\dir\\foo', fileURL: 'file:///C:/dir/foo' },
// trailing separator
{ path: 'C:\\dir\\', fileURL: 'file:///C:/dir/' },
// dot
{ path: 'C:\\foo.mjs', fileURL: 'file:///C:/foo.mjs' },
// space
{ path: 'C:\\foo bar', fileURL: 'file:///C:/foo%20bar' },
// question mark
{ path: 'C:\\foo?bar', fileURL: 'file:///C:/foo%3Fbar' },
// number sign
{ path: 'C:\\foo#bar', fileURL: 'file:///C:/foo%23bar' },
// ampersand
{ path: 'C:\\foo&bar', fileURL: 'file:///C:/foo&bar' },
// equals
{ path: 'C:\\foo=bar', fileURL: 'file:///C:/foo=bar' },
// colon
{ path: 'C:\\foo:bar', fileURL: 'file:///C:/foo:bar' },
// semicolon
{ path: 'C:\\foo;bar', fileURL: 'file:///C:/foo;bar' },
// percent
{ path: 'C:\\foo%bar', fileURL: 'file:///C:/foo%25bar' },
// backslash
{ path: 'C:\\foo\\bar', fileURL: 'file:///C:/foo/bar' },
// backspace
{ path: 'C:\\foo\bbar', fileURL: 'file:///C:/foo%08bar' },
// tab
{ path: 'C:\\foo\tbar', fileURL: 'file:///C:/foo%09bar' },
// newline
{ path: 'C:\\foo\nbar', fileURL: 'file:///C:/foo%0Abar' },
// carriage return
{ path: 'C:\\foo\rbar', fileURL: 'file:///C:/foo%0Dbar' },
// latin1
{ path: 'C:\\fóóbàr', fileURL: 'file:///C:/f%C3%B3%C3%B3b%C3%A0r' },
// Euro sign (BMP code point)
{ path: 'C:\\€', fileURL: 'file:///C:/%E2%82%AC' },
// Rocket emoji (non-BMP code point)
{ path: 'C:\\🚀', fileURL: 'file:///C:/%F0%9F%9A%80' },
// UNC path (see https://docs.microsoft.com/en-us/archive/blogs/ie/file-uris-in-windows)
{ path: '\\\\nas\\My Docs\\File.doc', fileURL: 'file://nas/My%20Docs/File.doc' },
];
const posixTestCases = [
// Lowercase ascii alpha
{ path: '/foo', fileURL: 'file:///foo' },
// Uppercase ascii alpha
{ path: '/FOO', fileURL: 'file:///FOO' },
// dir
{ path: '/dir/foo', fileURL: 'file:///dir/foo' },
// trailing separator
{ path: '/dir/', fileURL: 'file:///dir/' },
// dot
{ path: '/foo.mjs', fileURL: 'file:///foo.mjs' },
// space
{ path: '/foo bar', fileURL: 'file:///foo%20bar' },
// question mark
{ path: '/foo?bar', fileURL: 'file:///foo%3Fbar' },
// number sign
{ path: '/foo#bar', fileURL: 'file:///foo%23bar' },
// ampersand
{ path: '/foo&bar', fileURL: 'file:///foo&bar' },
// equals
{ path: '/foo=bar', fileURL: 'file:///foo=bar' },
// colon
{ path: '/foo:bar', fileURL: 'file:///foo:bar' },
// semicolon
{ path: '/foo;bar', fileURL: 'file:///foo;bar' },
// percent
{ path: '/foo%bar', fileURL: 'file:///foo%25bar' },
// backslash
{ path: '/foo\\bar', fileURL: 'file:///foo%5Cbar' },
// backspace
{ path: '/foo\bbar', fileURL: 'file:///foo%08bar' },
// tab
{ path: '/foo\tbar', fileURL: 'file:///foo%09bar' },
// newline
{ path: '/foo\nbar', fileURL: 'file:///foo%0Abar' },
// carriage return
{ path: '/foo\rbar', fileURL: 'file:///foo%0Dbar' },
// latin1
{ path: '/fóóbàr', fileURL: 'file:///f%C3%B3%C3%B3b%C3%A0r' },
// Euro sign (BMP code point)
{ path: '/€', fileURL: 'file:///%E2%82%AC' },
// Rocket emoji (non-BMP code point)
{ path: '/🚀', fileURL: 'file:///%F0%9F%9A%80' },
];
test('fileURLToPath with windows path', { skip: !isWindows }, () => {
for (const { path, fileURL } of windowsTestCases) {
const fromString = url.fileURLToPath(fileURL, { windows: true });
assert.strictEqual(fromString, path);
const fromURL = url.fileURLToPath(new URL(fileURL), { windows: true });
assert.strictEqual(fromURL, path);
}
});
test('fileURLToPath with posix path', { skip: isWindows }, () => {
for (const { path, fileURL } of posixTestCases) {
const fromString = url.fileURLToPath(fileURL, { windows: false });
assert.strictEqual(fromString, path);
const fromURL = url.fileURLToPath(new URL(fileURL), { windows: false });
assert.strictEqual(fromURL, path);
}
});
const defaultTestCases = isWindows ? windowsTestCases : posixTestCases;
test('options is null', () => {
const whenNullActual = url.fileURLToPath(new URL(defaultTestCases[0].fileURL), null);
assert.strictEqual(whenNullActual, defaultTestCases[0].path);
});
test('defaultTestCases', () => {
for (const { path, fileURL } of defaultTestCases) {
const fromString = url.fileURLToPath(fileURL);
assert.strictEqual(fromString, path);
const fromURL = url.fileURLToPath(new URL(fileURL));
assert.strictEqual(fromURL, path);
}
});

View File

@@ -1,5 +1,6 @@
'use strict';
require('../common');
const { test } = require('node:test');
// This test ensures Math functions don't fail with an "illegal instruction"
// error on ARM devices (primarily on the Raspberry Pi 1)
@@ -7,9 +8,11 @@ require('../common');
// and https://code.google.com/p/v8/issues/detail?id=4019
// Iterate over all Math functions
Object.getOwnPropertyNames(Math).forEach((functionName) => {
if (!/[A-Z]/.test(functionName)) {
// The function names don't have capital letters.
Math[functionName](-0.5);
}
test('Iterate over all Math functions', () => {
Object.getOwnPropertyNames(Math).forEach((functionName) => {
if (!/[A-Z]/.test(functionName)) {
// The function names don't have capital letters.
Math[functionName](-0.5);
}
});
});

View File

@@ -1,5 +1,5 @@
'use strict';
require('../../common');
require('../common');
const assert = require('assert');
const { describe, it } = require('node:test');

View File

@@ -1,5 +1,5 @@
'use strict';
const { hasCrypto } = require('../../common');
const { hasCrypto } = require('../common');
const { test } = require('node:test');
const assert = require('assert');

View File

@@ -1,5 +1,5 @@
'use strict';
require('../../common');
require('../common');
const assert = require('assert');
const { test } = require('node:test');

View File

@@ -1,6 +1,6 @@
'use strict';
const { spawnPromisified } = require('../../common');
const { spawnPromisified } = require('../common');
const assert = require('node:assert');
const { describe, it } = require('node:test');

View File

@@ -0,0 +1,70 @@
// Flags: --no-warnings
'use strict';
const { expectWarning } = require('../common');
const assert = require('assert');
const { test } = require('node:test');
expectWarning(
'DeprecationWarning',
'assert.fail() with more than one argument is deprecated. ' +
'Please use assert.strictEqual() instead or only pass a message.',
'DEP0094'
);
test('Two args only, operator defaults to "!="', () => {
assert.throws(() => {
assert.fail('first', 'second');
}, {
code: 'ERR_ASSERTION',
name: 'AssertionError',
message: '\'first\' != \'second\'',
operator: '!=',
actual: 'first',
expected: 'second',
generatedMessage: true
});
});
test('Three args', () => {
assert.throws(() => {
assert.fail('ignored', 'ignored', 'another custom message');
}, {
code: 'ERR_ASSERTION',
name: 'AssertionError',
message: 'another custom message',
operator: 'fail',
actual: 'ignored',
expected: 'ignored',
generatedMessage: false
});
});
test('Three args with custom Error', () => {
assert.throws(() => {
assert.fail(typeof 1, 'object', new TypeError('another custom message'));
}, {
name: 'TypeError',
message: 'another custom message'
});
});
test('No third arg (but a fourth arg)', () => {
assert.throws(() => {
assert.fail('first', 'second', undefined, 'operator');
}, {
code: 'ERR_ASSERTION',
name: 'AssertionError',
message: '\'first\' operator \'second\'',
operator: 'operator',
actual: 'first',
expected: 'second'
});
});
test('The stackFrameFunction should exclude the foo frame', () => {
assert.throws(
function foo() { assert.fail('first', 'second', 'message', '!==', foo); },
(err) => !/^\s*at\sfoo\b/m.test(err.stack)
);
});

View File

@@ -1,6 +1,6 @@
'use strict';
require('../../common');
require('../common');
const assert = require('assert');
const { test } = require('node:test');

View File

@@ -1,6 +1,6 @@
'use strict';
require('../../common');
require('../common');
const assert = require('assert');
const { test } = require('node:test');

View File

@@ -23,9 +23,9 @@
const assert = require('node:assert');
require('../../../harness');
require('../../harness');
const {invalidArgTypeHelper} = require('../../common');
const {invalidArgTypeHelper} = require('../common');
const {inspect} = require('util');
const {test} = require('node:test');
const vm = require('vm');

View File

@@ -4,39 +4,26 @@
require('../common');
const { Buffer } = require('node:buffer');
const { strictEqual } = require('node:assert');
const { describe, it } = require('node:test');
{
{
describe('Using resizable ArrayBuffer with Buffer...', () => {
it('works as expected', () => {
const ab = new ArrayBuffer(10, { maxByteLength: 20 });
const buffer = Buffer.from(ab, 1);
strictEqual(ab.byteLength, 10);
strictEqual(buffer.buffer.byteLength, 10);
strictEqual(buffer.byteLength, 9);
ab.resize(15);
strictEqual(ab.byteLength, 15);
strictEqual(buffer.buffer.byteLength, 15);
strictEqual(buffer.byteLength, 14);
ab.resize(5);
strictEqual(ab.byteLength, 5);
strictEqual(buffer.buffer.byteLength, 5);
strictEqual(buffer.byteLength, 4);
}
}
});
{
{
it('works with the deprecated constructor also', () => {
const ab = new ArrayBuffer(10, { maxByteLength: 20 });
const buffer = new Buffer(ab, 1);
strictEqual(ab.byteLength, 10);
strictEqual(buffer.buffer.byteLength, 10);
strictEqual(buffer.byteLength, 9);
ab.resize(15);
strictEqual(ab.byteLength, 15);
strictEqual(buffer.buffer.byteLength, 15);
strictEqual(buffer.byteLength, 14);
ab.resize(5);
strictEqual(ab.byteLength, 5);
strictEqual(buffer.buffer.byteLength, 5);
strictEqual(buffer.byteLength, 4);
}
}
});
});

View File

@@ -0,0 +1,28 @@
'use strict';
// Test 'uncork' for WritableStream.
// Refs: https://github.com/nodejs/node/issues/50979
const common = require('../common');
const fs = require('fs');
const assert = require('assert');
const test = require('node:test');
const tmpdir = require('../common/tmpdir');
const filepath = tmpdir.resolve('write_stream.txt');
tmpdir.refresh();
const data = 'data';
test('writable stream uncork', () => {
const fileWriteStream = fs.createWriteStream(filepath);
fileWriteStream.on('finish', common.mustCall(() => {
const writtenData = fs.readFileSync(filepath, 'utf8');
assert.strictEqual(writtenData, data);
}));
fileWriteStream.cork();
fileWriteStream.write(data, common.mustCall());
fileWriteStream.uncork();
fileWriteStream.end();
});

View File

@@ -4,7 +4,7 @@ require('../common');
const fs = require('node:fs');
const path = require('node:path');
const assert = require('node:assert');
const { describe, it } = require('bun:test');
const { describe, it } = require('node:test');
const tmpdir = require('../common/tmpdir');
tmpdir.refresh();

View File

@@ -0,0 +1,8 @@
'use strict';
require('../common');
const { strictEqual } = require('node:assert');
const test = require('node:test');
strictEqual(test.test, test);
strictEqual(test.it, test);
strictEqual(test.describe, test.suite);

View File

@@ -0,0 +1,21 @@
'use strict';
require('../common');
const assert = require('node:assert');
const test = require('node:test');
test('expected methods are on t.assert', (t) => {
const uncopiedKeys = [
'AssertionError',
'CallTracker',
'strict',
];
const assertKeys = Object.keys(assert).filter((key) => !uncopiedKeys.includes(key));
const expectedKeys = ['snapshot', 'fileSnapshot'].concat(assertKeys).sort();
assert.deepStrictEqual(Object.keys(t.assert).sort(), expectedKeys);
});
test('t.assert.ok correctly parses the stacktrace', (t) => {
// FIXME: AssertionError message is incorrect
// t.assert.throws(() => t.assert.ok(1 === 2), /t\.assert\.ok\(1 === 2\)/);
t.assert.throws(() => t.assert.ok(1 === 2));
});

View File

@@ -0,0 +1,26 @@
'use strict';
const common = require('../common');
const { before, after, test } = require('node:test');
const { createServer } = require('node:http');
let server;
before(common.mustCall(() => {
server = createServer();
return new Promise(common.mustCall((resolve, reject) => {
server.listen(0, common.mustCall((err) => {
if (err) {
reject(err);
} else {
resolve();
}
}));
}));
}));
after(common.mustCall(() => {
server.close(common.mustCall());
}));
test();

View File

@@ -0,0 +1,12 @@
'use strict';
const common = require('../common');
const { test } = require('node:test');
// Regression test for https://github.com/nodejs/node/issues/51997
test('after hook should be called with no subtests', (t) => {
const timer = setTimeout(common.mustNotCall(), 2 ** 30);
t.after(common.mustCall(() => {
clearTimeout(timer);
}));
});

View File

@@ -0,0 +1,26 @@
'use strict';
require('../common');
// Return type of shorthands should be consistent
// with the return type of test
const assert = require('assert');
const { test, describe, it } = require('node:test');
const testOnly = test('only test', { only: true });
const testTodo = test('todo test', { todo: true });
const testSkip = test('skip test', { skip: true });
const testOnlyShorthand = test.only('only test shorthand');
const testTodoShorthand = test.todo('todo test shorthand');
const testSkipShorthand = test.skip('skip test shorthand');
describe('\'node:test\' and its shorthands should return the same', () => {
it('should return undefined', () => {
assert.strictEqual(testOnly, undefined);
assert.strictEqual(testTodo, undefined);
assert.strictEqual(testSkip, undefined);
assert.strictEqual(testOnlyShorthand, undefined);
assert.strictEqual(testTodoShorthand, undefined);
assert.strictEqual(testSkipShorthand, undefined);
});
});

View File

@@ -1,7 +1,7 @@
// Flags: --expose-internals
'use strict';
const common = require('../../common');
const common = require('../common');
const {
ok,
strictEqual,

View File

@@ -1,6 +1,6 @@
'use strict';
require('../../common');
require('../common');
const assert = require('node:assert');
const url = require('node:url');

View File

@@ -0,0 +1,29 @@
'use strict';
require('../common');
const { test } = require('node:test');
const assert = require('node:assert');
// https://github.com/nodejs/node/issues/57272
test('should throw error when writing after close', async (t) => {
const writable = new WritableStream({
write(chunk) {
console.log(chunk);
},
});
const writer = writable.getWriter();
await writer.write('Hello');
await writer.close();
await assert.rejects(
async () => {
await writer.write('World');
},
{
name: 'TypeError',
}
);
});

View File

@@ -0,0 +1,146 @@
const test = require("node:test");
const assert = require("node:assert");
test("test() is a function", () => {
assert(typeof test === "function", "test() is a function");
});
test("describe() is a function", () => {
assert(typeof test.describe === "function", "describe() is a function");
});
test.describe("TestContext", () => {
test("<exists>", t => {
t.assert.ok(typeof t === "object", "test() returns an object");
});
test("name", t => {
t.assert.equal(t.name, "name"); // matches the name of the test
});
test("filePath", t => {
t.assert.equal(t.filePath, __filename);
});
test("signal", t => {
t.assert.ok(t.signal instanceof AbortSignal);
});
test("assert", t => {
t.assert.ok(typeof t.assert === "object", "test() argument has an assert property");
const actual = Object.keys(t.assert).sort();
const expected = Object.keys({ ...assert })
.filter(key => !["CallTracker", "AssertionError", "strict"].includes(key))
.concat(["fileSnapshot", "snapshot"])
.sort();
t.assert.deepEqual(actual, expected, "test() argument is the same as the node:assert module");
});
test("diagnostic()", t => {
t.assert.ok(typeof t.diagnostic === "function", "diagnostic() is a function");
});
test("before()", t => {
t.assert.ok(typeof t.before === "function", "before() is a function");
});
test("after()", t => {
t.assert.ok(typeof t.after === "function", "after() is a function");
});
test("beforeEach()", t => {
t.assert.ok(typeof t.beforeEach === "function", "beforeEach() is a function");
});
test("afterEach()", t => {
t.assert.ok(typeof t.afterEach === "function", "afterEach() is a function");
});
test("test()", t => {
t.assert.ok(typeof t.test === "function", "test() method is a function");
});
});
test("before() is a function", t => {
t.assert.ok(typeof test.before === "function", "before() is a function");
});
test("after() is a function", t => {
t.assert.ok(typeof test.after === "function", "after() is a function");
});
test("beforeEach() is a function", t => {
t.assert.ok(typeof test.beforeEach === "function", "beforeEach() is a function");
});
test("afterEach() is a function", t => {
t.assert.ok(typeof test.afterEach === "function", "afterEach() is a function");
});
test.describe("test", () => {
test("test()", t => {
t.assert.ok(typeof test === "function", "test() is a function");
});
test("it()", t => {
t.assert.ok(typeof test.it === "function", "test.it() is a function");
});
test("skip()", t => {
t.assert.ok(typeof test.skip === "function", "test.skip() is a function");
});
test("todo()", t => {
t.assert.ok(typeof test.todo === "function", "test.todo() is a function");
});
test("only()", t => {
t.assert.ok(typeof test.only === "function", "test.only() is a function");
});
test("describe()", t => {
t.assert.ok(typeof test.describe === "function", "test.describe() is a function");
});
test("suite()", t => {
t.assert.ok(typeof test.suite === "function", "test.suite() is a function");
});
});
test.describe("describe", () => {
test("<exists>", t => {
t.assert.ok(typeof test.describe === "function", "describe() is a function");
});
test("skip()", t => {
t.assert.ok(typeof test.describe.skip === "function", "describe.skip() is a function");
});
test("todo()", t => {
t.assert.ok(typeof test.describe.todo === "function", "describe.todo() is a function");
});
test("only()", t => {
t.assert.ok(typeof test.describe.only === "function", "describe.only() is a function");
});
});
test.describe("describe 1", t => {
test("name is correct", t => {
t.assert.equal(t.name, "name is correct");
});
test("fullName is correct", t => {
t.assert.equal(t.fullName, "describe 1 > fullName is correct");
});
test.describe("describe 2", () => {
test("name is correct", t => {
t.assert.equal(t.name, "name is correct");
});
test("fullName is correct", t => {
t.assert.equal(t.fullName, "describe 1 > describe 2 > fullName is correct");
});
});
});

View File

@@ -0,0 +1,136 @@
const { describe, test, before, after, beforeEach, afterEach } = require("node:test");
const { readFileSync } = require("node:fs");
const { join } = require("node:path");
const assert = require("node:assert");
const expectedFile = readFileSync(join(__dirname, "02-hooks.json"), "utf-8");
const { node, bun } = JSON.parse(expectedFile);
const order = [];
before(() => {
order.push("before global");
});
before(async () => {
await new Promise(resolve => setTimeout(resolve, 1));
order.push("before global async");
});
after(() => {
order.push("after global");
});
after(async () => {
await new Promise(resolve => setTimeout(resolve, 1));
order.push("after global async");
});
beforeEach(() => {
order.push("beforeEach global");
});
beforeEach(async () => {
await new Promise(resolve => setTimeout(resolve, 1));
order.push("beforeEach global async");
});
afterEach(() => {
order.push("afterEach global");
});
afterEach(async () => {
await new Promise(resolve => setTimeout(resolve, 1));
order.push("afterEach global async");
});
describe("execution order", () => {
before(() => {
order.push("before");
});
before(async () => {
await new Promise(resolve => setTimeout(resolve, 1));
order.push("before");
});
beforeEach(() => {
order.push("beforeEach");
});
beforeEach(async () => {
await new Promise(resolve => setTimeout(resolve, 1));
order.push("beforeEach");
});
afterEach(() => {
order.push("afterEach");
});
afterEach(async () => {
await new Promise(resolve => setTimeout(resolve, 1));
order.push("afterEach");
});
after(() => {
order.push("after");
});
after(async () => {
await new Promise(resolve => setTimeout(resolve, 1));
order.push("after");
});
test("test 1", ({ fullName }) => {
order.push(`test: ${fullName}`);
});
describe("describe 1", () => {
before(() => {
order.push("before > describe 1");
});
beforeEach(() => {
order.push("beforeEach > describe 1");
});
afterEach(() => {
order.push("afterEach > describe 1");
});
after(() => {
order.push("after > describe 1");
});
test("test 2", ({ fullName }) => {
order.push(`test: ${fullName}`);
});
describe("describe 2", () => {
before(() => {
order.push("before > describe 2");
});
beforeEach(() => {
order.push("beforeEach > describe 2");
});
afterEach(() => {
order.push("afterEach > describe 2");
});
after(() => {
order.push("after > describe 2");
});
test("test 3", ({ fullName }) => {
order.push(`test: ${fullName}`);
});
});
});
});
after(() => {
// FIXME: Due to subtle differences between how Node.js and Bun (using `bun test`) run tests,
// this is a snapshot test. You must look at the snapshot to verify the output makes sense.
assert.deepEqual(order, "Bun" in globalThis ? bun : node);
});

View File

@@ -0,0 +1,96 @@
{
"node": [
"before global",
"before global async",
"before",
"before",
"beforeEach global",
"beforeEach global async",
"beforeEach",
"beforeEach",
"test: execution order > test 1",
"afterEach",
"afterEach",
"afterEach global",
"afterEach global async",
"before > describe 1",
"beforeEach global",
"beforeEach global async",
"beforeEach",
"beforeEach",
"beforeEach > describe 1",
"test: execution order > describe 1 > test 2",
"afterEach > describe 1",
"afterEach",
"afterEach",
"afterEach global",
"afterEach global async",
"before > describe 2",
"beforeEach global",
"beforeEach global async",
"beforeEach",
"beforeEach",
"beforeEach > describe 1",
"beforeEach > describe 2",
"test: execution order > describe 1 > describe 2 > test 3",
"afterEach > describe 2",
"afterEach > describe 1",
"afterEach",
"afterEach",
"afterEach global",
"afterEach global async",
"after > describe 2",
"after > describe 1",
"after",
"after",
"after global",
"after global async"
],
"bun": [
"before global",
"before global async",
"before",
"before",
"before > describe 1",
"before > describe 2",
"beforeEach global",
"beforeEach global async",
"beforeEach",
"beforeEach",
"beforeEach > describe 1",
"beforeEach > describe 2",
"test: execution order > describe 1 > describe 2 > test 3",
"afterEach > describe 2",
"afterEach > describe 1",
"afterEach",
"afterEach",
"afterEach global",
"afterEach global async",
"after > describe 2",
"beforeEach global",
"beforeEach global async",
"beforeEach",
"beforeEach",
"beforeEach > describe 1",
"test: execution order > describe 1 > test 2",
"afterEach > describe 1",
"afterEach",
"afterEach",
"afterEach global",
"afterEach global async",
"after > describe 1",
"beforeEach global",
"beforeEach global async",
"beforeEach",
"beforeEach",
"test: execution order > test 1",
"afterEach",
"afterEach",
"afterEach global",
"afterEach global async",
"after",
"after",
"after global",
"after global async"
]
}

View File

@@ -0,0 +1,55 @@
const test = require("node:test");
test(); // test without name or callback
test("test with name and callback", t => {
t.assert.ok(true);
});
test("test with name, options, and callback", { timeout: 5000 }, t => {
t.assert.ok(true);
});
test(t => {
t.assert.equal(t.name, "<anonymous>");
});
test(function testWithFunctionName(t) {
t.assert.equal(t.name, "testWithFunctionName");
});
test({ timeout: 5000 }, t => {
t.assert.equal(t.name, "<anonymous>");
});
test.describe("describe with name and callback", () => {
test("nested test", t => {
t.assert.ok(true);
});
});
test.describe("describe with name, options, and callback", { timeout: 5000 }, () => {
test("nested test", t => {
t.assert.ok(true);
});
});
test.skip("skipped test", t => {
t.assert.fail("This test should be skipped");
});
test.skip("skipped test with options", { timeout: 5000 }, t => {
t.assert.fail("This test should be skipped");
});
test.todo("todo test");
test.todo("todo test with options", { timeout: 5000 });
test.describe.skip("skipped describe", () => {
test("nested test", t => {
t.assert.fail("This test should be skipped");
});
});
test.describe.todo("todo describe");

View File

@@ -0,0 +1,54 @@
const { describe, test, after } = require("node:test");
const assert = require("node:assert");
let callCount = 0;
function mustCall(fn) {
try {
fn();
} finally {
callCount++;
}
}
test(
"test with an async function",
mustCall(async () => {
const result = await Promise.resolve(42);
assert.equal(result, 42);
}),
);
test(
"test with an async function that delays",
mustCall(async () => {
const start = Date.now();
await new Promise(resolve => setTimeout(resolve, 100));
const end = Date.now();
assert.ok(end - start > 10, "should wait at least 10ms");
}),
);
describe("nested tests", () => {
test(
"nested test with an async function",
mustCall(async () => {
const result = await Promise.resolve(42);
assert.equal(result, 42);
}),
);
test(
"nested test with an async function that delays",
mustCall(async () => {
const start = Date.now();
await new Promise(resolve => setTimeout(resolve, 100));
const end = Date.now();
assert.ok(end - start > 10, "should wait at least 10ms");
}),
);
});
after(() => {
assert.equal(callCount, 4);
});

View File

@@ -0,0 +1,57 @@
import { describe, test, expect } from "bun:test";
import { spawn } from "bun";
import { join } from "node:path";
import { bunEnv, bunExe } from "harness";
describe("node:test", () => {
test("should run basic tests", async () => {
const { exitCode, stderr } = await runTest("01-harness.js");
expect({ exitCode, stderr }).toMatchObject({
exitCode: 0,
stderr: expect.stringContaining("0 fail"),
});
});
test("should run hooks in the right order", async () => {
const { exitCode, stderr } = await runTest("02-hooks.js");
expect({ exitCode, stderr }).toMatchObject({
exitCode: 0,
stderr: expect.stringContaining("0 fail"),
});
});
test("should run tests with different variations", async () => {
const { exitCode, stderr } = await runTest("03-test-variations.js");
expect({ exitCode, stderr }).toMatchObject({
exitCode: 0,
stderr: expect.stringContaining("0 fail"),
});
});
test("should run async tests", async () => {
const { exitCode, stderr } = await runTest("04-async-tests.js");
expect({ exitCode, stderr }).toMatchObject({
exitCode: 0,
stderr: expect.stringContaining("0 fail"),
});
});
});
async function runTest(filename: string) {
const testPath = join(import.meta.dirname, "fixtures", filename);
const {
exited,
stdout: stdoutStream,
stderr: stderrStream,
} = spawn({
cmd: [bunExe(), "test", testPath],
env: bunEnv,
stderr: "pipe",
});
const [exitCode, stdout, stderr] = await Promise.all([
exited,
new Response(stdoutStream).text(),
new Response(stderrStream).text(),
]);
return { exitCode, stdout, stderr };
}