@rongjs/test is Rong's small, engine-neutral JavaScript test framework. It
works in QuickJS, JavaScriptCore, ArkJS, and other hosts that can evaluate
JavaScript and await promises. The package has no runtime dependencies and
does not require Node.js APIs, timers, a filesystem, or Rong modules.
The API for defining tests consists of describe and test. The complete
author-facing API also includes test.skip, beforeEach, afterEach, and
expect.
Install the package when tests are bundled as ECMAScript modules:
npm install --save-dev @rongjs/testImport only the APIs used by the test:
import { describe, expect, test } from "@rongjs/test";
describe("URL", () => {
test("parses the host", () => {
const url = new URL("https://example.com/path");
expect(url.host).toBe("example.com");
});
});describe groups cases and contributes to their full names. Its callback must
be synchronous because it only registers tests. Put asynchronous work in a
hook or test callback instead.
Tests run sequentially in registration order. Return a promise, or use an
async function, when a test performs asynchronous work:
test("loads a record", async () => {
const record = await loadRecord("42");
expect(record.id).toBe("42");
});There is no callback-style done API. A synchronous throw or rejected promise
fails the current case; later cases still run.
Suites may be nested. beforeEach hooks run from the outer suite inward, and
afterEach hooks run from the inner suite outward. Hooks are awaited and a
hook failure fails the affected case.
import {
afterEach,
beforeEach,
describe,
expect,
test,
} from "@rongjs/test";
describe("storage", () => {
let storage;
beforeEach(async () => {
storage = await openStorage();
});
afterEach(async () => {
await storage.close();
});
test("stores a value", async () => {
await storage.set("answer", 42);
expect(await storage.get("answer")).toBe(42);
});
test.skip("deletes expired values");
});Teardown hooks still run after setup or test-body failures. If teardown also fails, its error is retained as a cause of the primary failure.
Call expect(actual) and select a matcher:
| Matcher | Behavior |
|---|---|
toBe(expected) |
Compares with Object.is |
toEqual(expected) |
Recursively compares primitives, arrays, plain objects, and typed arrays |
toContain(expected) |
Checks a string substring or an array item |
toMatch(expected) |
Matches a string against a substring or regular expression |
toBeTruthy() / toBeFalsy() |
Checks JavaScript truthiness |
toBeDefined() / toBeUndefined() |
Checks whether the value is undefined |
toBeInstanceOf(Type) |
Applies instanceof |
toThrow(expected?) |
Checks a synchronous function for a thrown value, message, pattern, or error type |
Every matcher supports .not:
expect(result).not.toBeUndefined();
expect(() => parse("bad input")).toThrow(/invalid/i);Matcher failures throw AssertionError. The error includes matcher,
actual, and expected fields in addition to the normal error name, message,
and stack.
Loading a test file registers cases; it does not execute them. The embedding host must call the controller after source evaluation completes:
const report = await globalThis.__RONG_TEST__.run();run() resolves to a JSON-compatible report even when cases fail:
interface TestReport {
total: number;
passed: number;
failed: number;
skipped: number;
duration_ms: number;
cases: Array<{
name: string;
full_name: string;
status: "passed" | "failed" | "skipped";
duration_ms: number;
error?: {
name: string;
message: string;
stack: string;
causes?: unknown[];
};
}>;
}The host decides how report.failed affects its process exit code or protocol
status. A registry can run only once, so each independent run should use a
fresh JavaScript context.
ESM test bundles import from @rongjs/test; the package installs the framework
when the module is evaluated. A host without an ESM loader can instead resolve
@rongjs/test/runtime and evaluate that dependency-free file as a classic
script before evaluating test source. Classic-script loading installs
describe, test, beforeEach, afterEach, and expect as globals. It does
not install it.
The execution order is always:
- Optionally set host extensions.
- Load the test runtime.
- Evaluate test source to register cases.
- Await
globalThis.__RONG_TEST__.run(). - Consume the returned report and tear down the context.
An embedder may provide arguments, artifact handling, and live case events before loading the runtime:
globalThis.__RONG_TEST_HOST__ = {
args: { locale: "en" },
attach: async (name, artifact) => {
// Store an artifact in the host.
},
report: async (event) => {
// Receive case_started and case_finished events.
},
};The runtime exposes args and attach as test.args and test.attach.
Their meaning is defined by the host, not by Rong.
The host remains responsible for discovering files, transforming TypeScript, loading modules, enforcing deadlines, interrupting execution, capturing console output, and selecting a process or protocol status. The framework does not provide parallel cases, per-case timeouts, snapshots, mocks, spies, fake timers, coverage, or browser emulation.
Rong's JavaScript suites under tests/unit use the same globals and runtime.
The Rust UnitJSRunner creates the context, loads the shared runtime, evaluates
the selected suite, and runs its registered cases. Use the Cargo and engine
commands in the repository testing guide to execute
those suites.