use bun_core::ZBox;
use mozjs::glue::NewCompileOptions;
use mozjs::jsapi::*;
use mozjs::jsval::UndefinedValue;
use mozjs::rooted;
use mozjs::rust::wrappers2 as w2;
use crate::require::cache_builtin;
const TEST_SOURCE: &str = r#"
(function() {
var _g = globalThis;
// ── lazy infrastructure resolution ──
// Resolved at CALL time, never at module-eval time: node_test::install runs
// before bun_test::install_bun_test, so an install-time check would freeze
// a dead stub into the builtin cache (see module docs).
function _btm() {
var btm = _g.__bun_test_module;
if (!btm) {
throw new Error("node:test: bun:test runner infrastructure (globalThis.__bun_test_module) is not installed in this context — refusing to fake test registration.");
}
return btm;
}
function _fn(host, path, name) {
var f = host && host[name];
if (typeof f !== 'function') {
throw new Error("node:test: bun:test does not implement " + path + "." + name + " — refusing to fake it.");
}
return f;
}
// ── runner gate ──
// `bao test` is the only mode that executes registered suites
// (runtime.rs run_test_file → run_bun_tests_report). Everywhere else,
// registration would silently no-op and fake a pass. Fail closed.
function _runnerGate() {
var argv = (_g.process && _g.process.argv) || [];
if (argv[1] !== 'test') {
throw new Error("node:test requires `bao test <file>`: this process is not the bao test runner (process.argv[1] !== 'test'), so registered tests would never execute and would silently fake a pass. Re-run with: bao test <file>");
}
}
// ── test function ──
// Node.js node:test uses test() as the primary entry; Bun's bun:test uses
// test() too. Bridge directly, arguments pass through unchanged.
function withGate(host, path, name) {
var f = function() {
_runnerGate();
return _fn(host(), path, name).apply(host(), arguments);
};
f._target = name;
return f;
}
var testFn = withGate(_btm, 'bun:test', 'test');
// Sub-methods delegate to the bun:test variants of the same name.
['skip', 'todo', 'only', 'failing', 'if', 'skipIf', 'onlyIf', 'each'].forEach(function(variant) {
testFn[variant] = withGate(function() { return _fn(_btm(), 'bun:test', 'test'); },
'bun:test.test', variant);
});
// ── describe ──
var describeFn = withGate(_btm, 'bun:test', 'describe');
['skip', 'todo', 'only', 'each', 'if', 'skipIf'].forEach(function(variant) {
describeFn[variant] = withGate(function() { return _fn(_btm(), 'bun:test', 'describe'); },
'bun:test.describe', variant);
});
// ── it ──
var itFn = withGate(_btm, 'bun:test', 'it');
['skip', 'todo', 'only', 'each', 'failing', 'if', 'skipIf', 'onlyIf'].forEach(function(variant) {
itFn[variant] = withGate(function() { return _fn(_btm(), 'bun:test', 'it'); },
'bun:test.it', variant);
});
// ── hooks ──
// Node.js node:test uses before/after (aliases for beforeAll/afterAll).
var beforeFn = withGate(_btm, 'bun:test', 'beforeAll');
var afterFn = withGate(_btm, 'bun:test', 'afterAll');
var beforeEachFn = withGate(_btm, 'bun:test', 'beforeEach');
var afterEachFn = withGate(_btm, 'bun:test', 'afterEach');
// ── mock ──
// Node.js node:test exposes test.mock with fn/spyOn/restore/clear/reset.
// fn/spyOn delegate to bun:test jest (real call-tracking mocks); restore
// actually restores every spy created through this module.
var _spies = [];
var mockObj = {
fn: function(impl) {
var jest = _fn(_btm(), 'bun:test', 'jest');
return _fn(jest, 'bun:test.jest', 'fn')(impl);
},
spyOn: function(obj, method) {
if (!obj || typeof obj[method] !== 'function') {
throw new Error('mock.spyOn requires an object with a function property');
}
var jest = _fn(_btm(), 'bun:test', 'jest');
var mock = _fn(jest, 'bun:test.jest', 'spyOn')(obj, method);
_spies.push({ mock: mock, obj: obj, method: method });
return mock;
},
restore: function() {
for (var i = 0; i < _spies.length; i++) {
var s = _spies[i];
if (typeof s.mock.mockRestore === 'function') s.mock.mockRestore();
else s.obj[s.method] = s.mock._original;
}
_spies.length = 0;
},
clear: function() {
for (var i = 0; i < _spies.length; i++) {
if (typeof _spies[i].mock.mockClear === 'function') _spies[i].mock.mockClear();
}
},
reset: function() {
for (var i = 0; i < _spies.length; i++) {
if (typeof _spies[i].mock.mockReset === 'function') _spies[i].mock.mockReset();
}
},
getter: function(obj, prop, impl) {
var orig = Object.getOwnPropertyDescriptor(obj, prop);
Object.defineProperty(obj, prop, { get: impl, configurable: true });
return { mockRestore: function() { if (orig) Object.defineProperty(obj, prop, orig); } };
},
setter: function(obj, prop, impl) {
var orig = Object.getOwnPropertyDescriptor(obj, prop);
Object.defineProperty(obj, prop, { set: impl, configurable: true });
return { mockRestore: function() { if (orig) Object.defineProperty(obj, prop, orig); } };
}
};
// ── assert ──
// Node.js node:test assert subset. Delegates to the Node.js assert module
// (real implementation); the local fallback is itself a real asserting
// implementation, not a pass-through stub.
var _nodeAssert = null;
function _assert() {
if (_nodeAssert === null) {
_nodeAssert = (typeof _g.require === 'function')
? (function() { try { return _g.require('assert'); } catch (e) { return undefined; } })()
: undefined;
}
return _nodeAssert;
}
function _delegateAssert(name, fallback) {
return function() {
var na = _assert();
if (na && typeof na[name] === 'function') return na[name].apply(na, arguments);
return fallback.apply(null, arguments);
};
}
var assertObj = {
ok: _delegateAssert('ok', function(val, msg) { if (!val) throw new Error(msg || 'assertion failed'); }),
equal: _delegateAssert('equal', function(a, b, msg) { if (a != b) throw new Error(msg || 'expected ' + a + ' to equal ' + b); }),
notEqual: _delegateAssert('notEqual', function(a, b, msg) { if (a == b) throw new Error(msg || 'expected values not to be equal'); }),
deepEqual: _delegateAssert('deepEqual', function(a, b, msg) { if (JSON.stringify(a) !== JSON.stringify(b)) throw new Error(msg || 'deep equal failed'); }),
notDeepEqual: _delegateAssert('notDeepEqual', function(a, b, msg) { if (JSON.stringify(a) === JSON.stringify(b)) throw new Error(msg || 'expected values not to deep equal'); }),
strictEqual: _delegateAssert('strictEqual', function(a, b, msg) { if (a !== b) throw new Error(msg || 'strict equal failed'); }),
notStrictEqual: _delegateAssert('notStrictEqual', function(a, b, msg) { if (a === b) throw new Error(msg || 'expected values not to be strictly equal'); }),
throws: _delegateAssert('throws', function(fn, expected, msg) {
var threw = false;
try { fn(); } catch(e) { threw = true; if (expected && typeof expected === 'function' && !(e instanceof expected)) throw new Error(msg || 'wrong error type'); }
if (!threw) throw new Error(msg || 'expected function to throw');
}),
doesNotThrow: _delegateAssert('doesNotThrow', function(fn, expected, msg) {
try { fn(); } catch(e) { throw new Error(msg || 'expected function not to throw'); }
}),
rejects: _delegateAssert('rejects', function(asyncFn, expected, msg) {
return Promise.resolve().then(function() {
var p = typeof asyncFn === 'function' ? asyncFn() : asyncFn;
return p.then(function() { throw new Error(msg || 'expected promise to reject'); }, function(e) {
if (expected && typeof expected === 'function' && !(e instanceof expected)) throw new Error(msg || 'wrong rejection type');
});
});
}),
ifError: _delegateAssert('ifError', function(err) { if (err) throw err; }),
match: function(actual, regex, msg) {
if (!regex.test(actual)) throw new Error(msg || 'expected ' + actual + ' to match ' + regex);
},
doesNotMatch: function(actual, regex, msg) {
if (regex.test(actual)) throw new Error(msg || 'expected ' + actual + ' not to match ' + regex);
}
};
// ── run() ──
// Delegates to the real bun:test runner. Explicit API — not gated, the
// caller asked for execution, and a missing runner fails explicitly.
function runFn() {
if (typeof _g.__run_bun_tests !== 'function') {
throw new Error("node:test run(): bun:test runner (globalThis.__run_bun_tests) is not installed — refusing to return a fake empty report.");
}
return _g.__run_bun_tests();
}
return {
test: testFn,
describe: describeFn,
it: itFn,
before: beforeFn,
after: afterFn,
beforeAll: beforeFn,
afterAll: afterFn,
beforeEach: beforeEachFn,
afterEach: afterEachFn,
mock: mockObj,
assert: assertObj,
run: runFn
};
})();
"#;
pub fn install(cx: &mut mozjs::context::JSContext) {
rooted!(&in(cx) let mod_obj = unsafe { w2::JS_NewPlainObject(cx) });
if mod_obj.get().is_null() {
return;
}
unsafe {
let cx_raw = cx.raw_cx();
let c_filename = ZBox::from_bytes("node:test".as_bytes());
let opts = NewCompileOptions(cx_raw, c_filename.as_ptr(), 1);
if opts.is_null() {
return;
}
let mut src = mozjs::rust::transform_str_to_source_text(TEST_SOURCE);
let mut rval = UndefinedValue();
let rval_handle = MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut rval,
};
let ok = mozjs_sys::jsapi::JS::Evaluate2(cx_raw, opts, &mut src, rval_handle);
libc::free(opts as *mut _);
if !ok || !rval.is_object() {
return;
}
let exports_obj = rval.to_object();
rooted!(&in(cx) let exports_rooted = exports_obj);
for name in &[
"test",
"describe",
"it",
"before",
"after",
"beforeAll",
"afterAll",
"beforeEach",
"afterEach",
"mock",
"assert",
"run",
] {
let cname = ZBox::from_bytes(name.as_bytes());
let mut val = UndefinedValue();
JS_GetProperty(
cx_raw,
exports_rooted.handle().into(),
cname.as_ptr(),
MutableHandle::<Value> {
_phantom_0: ::std::marker::PhantomData,
ptr: &mut val,
},
);
if !val.is_undefined() {
rooted!(&in(cx) let val_root = val);
JS_DefineProperty(
cx_raw,
mod_obj.handle().into(),
cname.as_ptr(),
val_root.handle().into(),
JSPROP_ENUMERATE as u32,
);
}
}
cache_builtin(cx, "test", mod_obj.get());
}
}