The qunitx JS API: run your QUnit tests in a real browser from Node.js or Deno, and get the results back as a value.

import { test } from './test.ts';

// Defined, not invoked: launches a browser and runs the project's tests.
async function check() {
  const result = await test('test/');
  return result.ok ? 0 : result.failures.length;
}

Two verbs, mirroring the CLI: test runs a suite (qunitx test/), run executes ONE file as a plain script (qunitx run seed.ts). run was the suite verb until the script verb needed the name — if you are upgrading, rename those calls to test.

Four things worth knowing before reading further:

  • Nothing is printed unless you ask. No reporter, no output — the returned RunResult is the whole answer. reporter: 'tap' gets the CLI's text as well.
  • Failing tests are not an error. test() resolves with ok: false; it rejects only when the run could not happen at all (a bad option, an unreadable input, no package.json).
  • Everything is lazy. These return a Task — a Promise superset — so nothing starts until you await it, and .result() hands back a Result union instead of throwing.
  • It is the same engine as the CLI. test(options) and qunitx <flags> assemble the same config and take the same code path; there is no second implementation to drift.

Functions

f
Daemon.start(): Task<boolean, never>

Ensures a daemon is running for this project, spawning one if there isn't.

f
Daemon.status(): Task<DaemonStatus, never>

Probes this project's daemon. An actual round-trip over the socket, not a look at the sentinel file, so a crashed daemon that left its files behind reports { running: false }.

f
Daemon.stop(): Task<boolean, never>

Stops this project's daemon. Resolves true if one was running, false if there was nothing to stop — either way, no daemon is running afterwards.

f
Daemon.test(options?: DaemonRunOptions): Task<RunResult, Client.DaemonRunFailure>

Runs the suite inside this project's daemon and resolves with the same RunResult a local run would produce — reusing the daemon's browser and warm bundle instead of paying for a fresh one.

f
openSession(
input?: SessionOptions | string | string[],
extra?: SessionOptions
): Task<TestSession | WatchSession, RunFailure>
4 overloads

With watch: true, the DURABLE shape: one page behind a stable WatchSession.url, kept open with the rerun verbs and file watching, exactly as watch gives you.

f
openSession(
input?: SessionOptions | string | string[],
extra?: SessionOptions
): Task<TestSession | WatchSession, RunFailure>
4 overloads

With watch: true, the DURABLE shape: one page behind a stable WatchSession.url, kept open with the rerun verbs and file watching, exactly as watch gives you.

f
openSession(
input?: SessionOptions | string | string[],
extra?: SessionOptions
): Task<TestSession | WatchSession, RunFailure>
4 overloads

With watch: true, the DURABLE shape: one page behind a stable WatchSession.url, kept open with the rerun verbs and file watching, exactly as watch gives you.

f
openSession(
input?: SessionOptions | string | string[],
extra?: SessionOptions
): Task<TestSession | WatchSession, RunFailure>
4 overloads

With watch: true, the DURABLE shape: one page behind a stable WatchSession.url, kept open with the rerun verbs and file watching, exactly as watch gives you.

f
openSession(
input?: SessionOptions | string | string[],
extra?: SessionOptions
): Task<TestSession | WatchSession, RunFailure>
4 overloads

With watch: true, the DURABLE shape: one page behind a stable WatchSession.url, kept open with the rerun verbs and file watching, exactly as watch gives you.

f
run(
file: string,
options?: ScriptOptions
): Task<ScriptResult, ScriptFailure>

Runs ONE file as a plain script in a real browser — the API twin of qunitx run <file>, and the sibling of test, which runs a suite.

f
run(options?: InitOptions): Task<InitResult, ProjectRootNotFoundFailure>

Bootstraps a qunitx project: writes the test HTML template, updates package.json, and writes tsconfig.json when there isn't one.

f
run(options?: GenerateOptions): Task<GenerateResult, ProjectRootNotFoundFailure>

Writes a new test file from the boilerplate template, deriving the QUnit module name from the path. Never overwrites an existing file.

f
streamConsole(
stdout: { write(text: string): unknown; },
stderr?: { write(text: string): unknown; }
): Console

Adapts anything with a write(string) — a node:fs stream, a socket, an array-backed fake — into a Console. stderr defaults to stdout, so one stream collects the whole run.

f
test(
input?: UserRunOptions | string | string[],
extra?: UserRunOptions
): Task<RunResult, RunFailure>
2 overloads

Runs the suite once in a real browser and resolves with everything it produced.

f
test(
input?: UserRunOptions | string | string[],
extra?: UserRunOptions
): Task<RunResult, RunFailure>
2 overloads

Runs the suite once in a real browser and resolves with everything it produced.

f
test(
input?: UserRunOptions | string | string[],
extra?: UserRunOptions
): Task<RunResult, RunFailure>
2 overloads

Runs the suite once in a real browser and resolves with everything it produced.

f
validate(userRunOptions: UserRunOptions): void

Rejects options the run cannot honour, before anything is launched.

f
watch(
input?: UserRunOptions | string | string[],
extra?: UserRunOptions
): Task<WatchSession, RunFailure>
2 overloads

Starts a watch session: builds once, runs once, then re-runs on every save until closed.

f
watch(
input?: UserRunOptions | string | string[],
extra?: UserRunOptions
): Task<WatchSession, RunFailure>
2 overloads

Starts a watch session: builds once, runs once, then re-runs on every save until closed.

f
watch(
input?: UserRunOptions | string | string[],
extra?: UserRunOptions
): Task<WatchSession, RunFailure>
2 overloads

Starts a watch session: builds once, runs once, then re-runs on every save until closed.

Interfaces

I
BrowserLog

A console.* call or an uncaught error from the page under test.

  • args: unknown[]

    The call's arguments, resolved to JSON values where the page could serialize them.

  • text: string

    The rendered single-line text. Always present.

  • type: string

    The page console type (log/warning/error/info/debug), or pageerror.

I
Console

Where a run's text goes: console, made injectable.

I
CoverageSummary

The run's line coverage: one entry per source file, plus the totals across all of them.

I
Daemon.DaemonRunOptions

The options a daemon run accepts: everything from UserRunOptions that survives a socket.

I
DaemonRunOptions

The options a daemon run accepts: everything from UserRunOptions that survives a socket.

I
FileCoverageSummary

Per-file line coverage, when coverage was requested.

I
FoundTest

One test found by the static scan, named exactly as QUnit would name it.

I
GenerateOptions

Where to scaffold a test file. target is a project-relative path; a missing .js/.ts extension becomes .js, and missing directories are created.

  • cwd: string

    Directory to find the project root from. Defaults to process.cwd().

  • target: string

    Project-relative path of the test file to write.

I
GenerateResult

What generate did: the file it wrote, or the one it refused to overwrite.

I
InitOptions

Where to bootstrap, and which HTML fixtures to write. Both default the way the CLI does: the working directory, and whatever .html arguments were passed.

  • cwd: string

    Directory to find the project root from. Defaults to process.cwd().

  • htmlPaths: string[]

    HTML fixtures to create, relative to the project root. Defaults to ['test/tests.html'].

I
InitResult

What init did, so the caller can report it. Returned rather than printed: the CLI turns these into its messages, and a programmatic caller gets the same facts as data.

I
Notice

One diagnostic from qunitx itself: which files a narrowing flag scoped the run to, a filter that matched nothing, a build error, a timeout.

I
Reporter

The reporter contract — the public extension point for observing a run. Reporters render it to text (the built-in tap/spec/dot/github), write an artifact (junit), or simply collect, which is how the JS API turns a run into a value.

I
ReporterContext

What a reporter is given on every hook: where to write, what the run has counted so far, and the few resolved paths a message needs.

I
ResolvedRun

What the run actually resolved to, after package.json, the defaults and the options were merged — the answers a caller cannot otherwise recover from what it passed in.

I
RunEndInfo

Final run info; the counts themselves live on config.state.results.counter.

I
RunGroup

One concurrent group of a run: the files bundled into a single page, and where that page's artifacts were written.

  • files: string[]

    Absolute paths of the test files this group bundled and ran together.

  • index: number

    Position in the run's group list, and the group-<index> suffix on output.

  • output: string

    Absolute path of this group's build output directory.

I
RunResult

Everything a finished run produced.

I
RunStartInfo

Run-scope counts. fileCount === null means "counts unknown at this point" (watch mode, where the header is emitted per browser connection rather than per file batch).

I
ScriptOptions

Everything run accepts beyond the file itself — the subset of a script run a caller can set.

I
ScriptResult

What one script run produced.

  • browserLogs: BrowserLog[]

    Everything the script printed — its console calls and any uncaught error — in emit order, whatever console option was passed. The same shape a test run reports, capped the same way.

  • browserLogsDropped: number

    How many lines were dropped to stay under the cap. 0 when nothing was.

  • durationMs: number

    Wall-clock ms from the first bundle to the script's top level settling.

  • exitCode: number

    globalThis.exitCode if the script set one, 1 if it threw, else 0.

  • file: string

    Absolute path of the file that ran.

  • ok: boolean

    True when the script finished with exit code 0.

  • tests: RunResult | null

    The suite the file declared, or null when it was a plain script.

  • value: unknown

    The script's export default, or undefined when it has none.

  • valueProblem: string | null

    Why ScriptResult.value is undefined even though the script exported something, or null when there is nothing to explain — including when it exported nothing at all.

I
SearchReport

What the static scan found, before anything is printed.

I
TestAssertion

One QUnit assertion inside a testEnd payload.

I
TestDetails

The QUnit testEnd payload as it arrives over the WebSocket. Passing tests carry the trimmed { status, fullName, runtime }; failing tests additionally carry assertions.

I
TestResult

One finished test.

I
TestSession

One run, watched as it happens.

  • abort(): Promise<void>

    Cuts the run short. The browser drops the rest of its queue and the run ends where it is, with aborted: true on the result — which TestSession.result still resolves with, so an aborted run is answered rather than left hanging.

  • close(): Promise<void>

    Closes the session, ending iteration. Idempotent; implied by await using.

  • droppedEvents: number

    How many events the feed dropped because a consumer fell behind. 0 in every ordinary run.

  • events(): Stream<RunEvent>

    The same events as iterating the session, as a Stream — so the combinators are there without a Stream.from wrapper.

  • result(): Promise<RunResult>

    The finished run. Starts it if nothing has yet, and resolves with the same RunResult the final runEnd event carries — never undefined, which is the whole reason this is a session rather than a bare event stream.

I
UnlistableCounts

Why some declarations could not be listed, split by cause.

I
UserRunOptions

Everything a run can be told to do. Every field is optional: run() with no arguments runs the project exactly as a bare qunitx would, minus the printing.

I
WatchSession

A running watch session.

Namespaces

N
Daemon

Daemon control: start, stop, status, and a test that reuses the daemon's warm browser and returns the same RunResult a local run does.

Type Aliases

T
AnyFailure = Any

A declared failure: something the runner decided it could not do, carrying a code to branch on and a message to show.

T
Daemon.DaemonRunFailure = Failure.Of<DaemonUnreachable | DaemonDisconnected | DaemonSilent>

Every way a daemon-routed run can fail to produce an exit code.

T
DaemonRunFailure = Failure.Of<DaemonUnreachable | DaemonDisconnected | DaemonSilent>

Every way a daemon-routed run can fail to produce an exit code.

T
ReporterName = (REPORTERS)[number]

A valid --reporter value.

T
ReporterOption = ReporterName | Reporter | false

--reporter by name, a reporter of your own, or false for none.

T
Result<T, E = Any> = T | E

The success value or a declared failure — a bare union, discriminated by the Failure brand.

T
RunCounts = Counter

Outcome totals for a run. total is the sum of the four buckets; assertionsFailed counts individual assertions rather than tests, so one test can contribute several.

T
RunFailure = ConfigFailure | InvalidOptionFailure

Every way a run can fail to happen: an option the runner will not accept, an unreadable input, a directory with no package.json above it, an esbuild plugin that will not load.

T
ScriptFailure =
Failure.Of<NotAScriptFile | RunCommand.ScriptBuildFailed>
| ScriptEntryFailure

Every way run can reject. A script that merely exits non-zero is NOT one of them.

T
SessionOptions = UserRunOptions & { watch?: boolean; }

What openSession accepts: every run option, plus the one that chooses the shape.

Variables

v
processConsole: Console

The CLI's: the real process streams. .write is looked up per call, so the daemon's stdout interception still reaches it.

v
REPORTERS: "tap" | "spec" | "dot" | "github"[]

Every stdout reporter --reporter accepts, in help/error-message order. Exactly one is active per run — artifact outputs (--junit, --coverage) are separate additive flags. This module is a leaf (type-only imports), so Args.parse can validate against it without pulling the reporter implementations into the CLI's startup path.

v
silentConsole: Console

Discards everything. The JS API's default, so a programmatic run prints nothing unless it was asked to.

v
T
Stream

The exported value: builders (Stream.from, Stream.unfold, Stream.lines) are the only entry points — there is no public constructor and no call form.

v
T
Stream

The exported value: builders (Stream.from, Stream.unfold, Stream.lines) are the only entry points — there is no public constructor and no call form.

v
T
Task: TaskConstructor

Call-or-construct, like Boolean/Date: Task(recipe) and new Task(recipe) build the same lazy Task. ES classes reject the call form, so the export is a Proxy whose apply forwards to construction — statics, instanceof, and the prototype all pass through.

v
T
Task: TaskConstructor

Call-or-construct, like Boolean/Date: Task(recipe) and new Task(recipe) build the same lazy Task. ES classes reject the call form, so the export is a Proxy whose apply forwards to construction — statics, instanceof, and the prototype all pass through.