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 withok: false; it rejects only when the run could not happen at all (a bad option, an unreadable input, nopackage.json). - Everything is lazy. These return a Task — a Promise superset — so nothing starts
until you await it, and
.result()hands back aResultunion instead of throwing. - It is the same engine as the CLI.
test(options)andqunitx <flags>assemble the same config and take the same code path; there is no second implementation to drift.
Ensures a daemon is running for this project, spawning one if there isn't.
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 }.
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.
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.
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.
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.
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.
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.
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.
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.
Bootstraps a qunitx project: writes the test HTML template, updates package.json, and writes tsconfig.json when there isn't one.
Writes a new test file from the boilerplate template, deriving the QUnit module name from the path. Never overwrites an existing file.
Lists the tests a selection would run, without running them.
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.
Runs the suite once in a real browser and resolves with everything it produced.
Runs the suite once in a real browser and resolves with everything it produced.
Runs the suite once in a real browser and resolves with everything it produced.
Rejects options the run cannot honour, before anything is launched.
Starts a watch session: builds once, runs once, then re-runs on every save until closed.
Starts a watch session: builds once, runs once, then re-runs on every save until closed.
Starts a watch session: builds once, runs once, then re-runs on every save until closed.
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), orpageerror.
Where a run's text goes: console, made injectable.
-
error(text: string): void
Writes to the error stream. Diagnostics that must survive a swallowed stdout go here too.
-
log(text: string): void
Writes to the primary stream — the TAP/spec/dot document itself.
The run's line coverage: one entry per source file, plus the totals across all of them.
-
coverableLines: number
Coverable lines across every file.
-
coveredLines: number
Covered lines across every file.
-
files: FileCoverageSummary[]
One entry per non-test source file the bundle mapped back to.
-
percent: number
Overall percentage covered.
The options a daemon run accepts: everything from UserRunOptions that survives a socket.
-
reporter: ReporterName | false
One built-in reporter by name, or
false. An instance cannot cross a socket. -
reporters: ReadonlyArray<ReporterName>
Several built-in reporters by name. Mutually exclusive with
reporter, as it is locally.
The options a daemon run accepts: everything from UserRunOptions that survives a socket.
-
reporter: ReporterName | false
One built-in reporter by name, or
false. An instance cannot cross a socket. -
reporters: ReadonlyArray<ReporterName>
Several built-in reporters by name. Mutually exclusive with
reporter, as it is locally.
Per-file line coverage, when coverage was requested.
-
coverableLines: number
Lines the source map attributes to executable positions in the bundle.
-
coveredLines: number
Lines executed at least once.
-
path: string
Path relative to the project root, with forward slashes.
-
percent: number
coveredLines / coverableLines, as a percentage rounded to two decimals.
One test found by the static scan, named exactly as QUnit would name it.
-
file: string
Absolute path of the file it was declared in — used to apply that file's line targets.
-
fullName: string
"Module > Sub: test name"— the string a filter matches against. -
line: number
1-based line of the declaration.
-
modules: string[]
The QUnit module path it is declared under; empty for a top-level test.
-
name: string
The test's own name.
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.
What generate did: the file it wrote, or the one it refused to overwrite.
-
created: boolean
falsewhen the file already existed and nothing was written. -
path: string
Absolute path of the target file.
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'].
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.
-
skipped: string[]
Paths that already existed and were left alone.
-
written: string[]
Absolute paths of the files this call created.
One diagnostic from qunitx itself: which files a narrowing flag scoped the run to, a filter that matched nothing, a build error, a timeout.
-
level: "info" | "warning" | "error"
infois a decision,warninga surprise,errora diagnostic that also hits stderr. -
message: string
The text, already colored where the CLI colors it, with no
#prefix and no newline. -
raw: boolean
Write
messageverbatim rather than as a#-prefixed comment. For pre-formatted blocks — the coverage table, a stack trace — whose own layout is the point. -
stream: "output" | "error" | "both"
Which of the run's two streams the default rendering goes to;
outputby default.
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.
-
onBrowserLog(): voidcontext: ReporterContext,log: BrowserLog
Called for each
console.*call and uncaught error from the page under test. Only warnings and errors arrive unlessdebugis on — the same selection the CLI prints. -
onNotice(): voidcontext: ReporterContext,notice: Notice
Called for each of qunitx's own diagnostics — the
# …lines about what it decided to run, what it could not find, what timed out. The default rendering has already gone toconfig.state.console; implement this only to capture them as data. -
onRunEnd(): void | Promise<void>context: ReporterContext,info: RunEndInfo
Called once when the run finishes, with the final counts on
config.state.results.counter. -
onRunStart(): voidcontext: ReporterContext,info: RunStartInfo
Called once before any test output. In watch mode, once per rerun.
-
onTestEnd(): voidcontext: ReporterContext,details: TestDetails
Called once per test, after
counterhas already been updated for this test.
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.
-
console: Console
Where this reporter's text goes.
silentConsolewhen the run was asked to print nothing. -
counts: Counter
The run's live outcome totals — the same object the runner updates, not a copy.
-
daemon: boolean
Whether this run is executing inside the persistent daemon.
-
junit: boolean | string
--junit's value:truefor the default path, a string for an explicit one. -
output: string
Absolute path of the build output directory.
-
projectRoot: string
Absolute path of the directory holding
package.json, for rendering paths relative to it. -
sourceMapDecoder: SourceMapDecoder | null
Maps a bundle stack frame back to source, once the run has built one.
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.
-
browser: "chromium" | "firefox" | "webkit"
The engine the tests ran in.
-
coverageFormats: string[]
Coverage artifact formats beyond the terminal summary.
-
extensions: string[]
File extensions treated as test files.
-
filter: string
The active test-name filter, when one was set.
-
output: string
Absolute path of the build output directory — where the bundle and artifacts landed.
-
port: number
The port actually bound, which may differ from the one requested.
-
projectRoot: string
Absolute path of the directory holding
package.json.
Final run info; the counts themselves live on config.state.results.counter.
-
durationMs: number
Wall-clock duration of the run in milliseconds.
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 onoutput. -
output: string
Absolute path of this group's build output directory.
Everything a finished run produced.
-
browserLogs: BrowserLog[]
console.*calls and uncaught errors from the page — warnings and errors unlessdebug. -
browserLogsDropped: number
How many page-log entries were dropped to stay under the cap.
0in every ordinary run. -
counts: RunCounts
Outcome totals.
-
coverage: CoverageSummary | null
Line coverage, or
nullwhencoveragewas not requested. -
durationMs: number
Wall-clock duration of the test phase, in milliseconds.
-
exitCode: number
What the CLI would have exited with:
0whenok,1otherwise. -
failedFiles: string[]
Absolute paths of the test files with at least one failure, attributed via source maps.
-
failures: TestResult[]
The subset of
teststhat failed — the list you almost always want first. -
files: string[]
Absolute paths of the test files this run executed — the ones scoped to, in a filtered watch rerun, rather than everything being watched.
-
finishedAt: number
Epoch ms when it ended.
-
groups: RunGroup[]
How the files were split across concurrent groups — one entry per group, never empty.
groups.lengthis the concurrency the run actually used;1for watch and single-file runs. -
junitXml: string | null
The JUnit XML document, when
junitwas requested. Written to disk as well. -
notices: Notice[]
qunitx's own diagnostics for this run, in emission order.
-
ok: boolean
truewhen every test passed and nothing else went wrong. -
resolved: ResolvedRun
What the run resolved to — see ResolvedRun.
-
startedAt: number
Epoch ms when the test phase began.
-
status: "completed" | "aborted" | "failFast"
How the run finished: whether it got through everything it selected, and if not, what stopped it. Distinct from RunResult.ok, which is the verdict — a
completedrun can be entirely red, and anabortedone can have no failures at all. -
tests: TestResult[]
Every finished test, in the order the browser reported them.
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).
-
fileCount: number | null
Test files in this run, or
nullwhen not known at announce time. -
groupCount: number | null
Concurrent groups the files were split across, or
nullalongside a nullfileCount.
Everything run accepts beyond the file itself — the subset of a script run a caller can set.
-
browser: "chromium" | "firefox" | "webkit"
Browser engine. Defaults to
chromium. - console: Console
-
cwd: string
Directory the file, its relative imports and
node_moduleslookups resolve against. -
open: boolean
Run in a visible browser window instead of headless.
-
port: number
Port the local server binds. Defaults to 1234, stepping over a taken one.
-
timeout: number
Ms the script may run before it is declared hung. Unbounded by default, like
deno run.
What one script run produced.
-
browserLogs: BrowserLog[]
Everything the script printed — its
consolecalls and any uncaught error — in emit order, whateverconsoleoption 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.
0when nothing was. -
durationMs: number
Wall-clock ms from the first bundle to the script's top level settling.
-
exitCode: number
globalThis.exitCodeif 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
nullwhen it was a plain script. -
value: unknown
The script's
export default, orundefinedwhen it has none. -
valueProblem: string | null
Why ScriptResult.value is
undefinedeven though the script exported something, ornullwhen there is nothing to explain — including when it exported nothing at all.
What the static scan found, before anything is printed.
-
files: number
How many files were scanned.
-
filter: string
The expression matched against, or
undefinedwhen everything was listed. -
matches: FoundTest[]
The tests the current selection matches, in declaration order.
-
total: number
Every listable test found, matched or not.
-
unlistable: UnlistableCounts
What the scan could not name, split by cause.
-
warnings: string[]
Line-target resolution warnings, in input order.
One QUnit assertion inside a testEnd payload.
-
actual: unknown
The value the assertion actually saw.
-
expected: unknown
The value the assertion required.
-
message: string
The assertion's message, when one was given.
-
passed: boolean
truewhen the assertion held. -
stack: string
Raw stack captured at the assertion, with frames pointing at the bundle.
-
todo: boolean
truefor assertions inside atodotest, which are expected to fail.
The QUnit testEnd payload as it arrives over the WebSocket. Passing tests carry the
trimmed { status, fullName, runtime }; failing tests additionally carry assertions.
-
assertions: TestAssertion[]
Present on failing tests only (QUnit trims the payload otherwise).
-
fullName: string[]
Module path followed by the test name, e.g.
['Math', 'adds']. -
runtime: number
Test duration in milliseconds.
-
status: string
QUnit's outcome:
passed|failed|skipped|todo.
One finished test.
-
assertions: TestAssertion[]
The test's assertions. QUnit trims these for passing tests, so this is populated for failures and empty otherwise — a passing test's assertion count is not reported.
-
durationMs: number
How long the test took, in milliseconds.
-
file: string | null
Source file this test was declared in, relative to the project root, or
null. -
fullName: string
"Module > Sub: test name"— the stringfiltermatches against. -
modules: string[]
The QUnit module path it was declared under; empty for a top-level test.
-
name: string
The test's own name, without its modules.
-
status: "passed" | "failed" | "skipped" | "todo"
QUnit's outcome for this test.
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: trueon 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.
0in every ordinary run. -
events(): Stream<RunEvent>
The same events as iterating the session, as a Stream — so the combinators are there without a
Stream.fromwrapper. -
result(): Promise<RunResult>
The finished run. Starts it if nothing has yet, and resolves with the same RunResult the final
runEndevent carries — neverundefined, which is the whole reason this is a session rather than a bare event stream.
Why some declarations could not be listed, split by cause.
-
computedNames: number
Declarations whose name is computed at run time —
test(`case ${index}`). -
silent: number
Files that parsed but declared no test the scan could see, e.g. via a local alias.
-
total: number
The three below, added up.
-
unparseable: number
Files that could not be read or parsed at all.
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.
-
after: string | false
Path to a module run after the tests; it receives the run's counters.
-
before: string | false
Path to a module run before the tests; it receives the resolved config.
-
browser: "chromium" | "firefox" | "webkit"
Browser engine. Defaults to
chromium, the only one that can collect coverage. -
changedSince: string
Run only files whose transitive imports changed since this git ref (
'HEAD'for uncommitted). -
console: Console
Where the run's text goes. Defaults to the process streams when a reporter is NAMED, and to silentConsole otherwise — so a programmatic run prints nothing unless it was asked to.
-
coverage: boolean | { formats?: Array<"lcov" | "html">; }
Collect V8 line coverage (chromium only).
formatsadditionally writes lcov/html artifacts. -
cwd: string
Directory the project root and relative inputs resolve against. Defaults to
process.cwd(). -
debug: boolean
Forward every page console call and print the server URL.
-
extensions: string[]
File extensions treated as test files. Defaults to
['js', 'ts', 'jsx', 'tsx']. -
failFast: boolean
Stop the run at the first failing test.
-
filter: string
Run only tests whose
"Module: test name"matches. QUnit's own semantics: case-insensitive substring,/regex/,/regex/i, or a leading!to invert. -
html: string[]
HTML fixture files to wrap the bundle in, relative to the project root.
-
inputs: string[]
Files, directories, globs, or
file.ts#34line targets — the same grammar as the command line's positional arguments. Defaults topackage.json#qunitx.inputs. -
junit: boolean | string
Write a JUnit XML report.
truewrites<output>/junit.xml; a string is a path. -
onlyFailed: boolean
Run only the files that failed last time, from the persistent failure cache.
-
open: boolean | string
Open the output in a browser:
truefor the default, a string to name a binary. -
output: string
Directory for the compiled bundle and HTML output. Defaults to
'tmp'. -
plugins: EsbuildPlugin[]
esbuild plugins for the test bundle — live objects, not specifiers.
-
port: number
Port for the local test server. Defaults to 1234, incrementing on conflict.
-
reporter: ReporterOption
Print the run with ONE reporter: a built-in name (
'tap','spec','dot','github'), your own Reporter, orfalse. The same spelling as the CLI's--reporter. -
reporters: ReadonlyArray<ReporterOption>
Print the run with SEVERAL. Mutually exclusive with UserRunOptions.reporter: pass
reporterfor one,reportersfor many, and validate rejects both at once. -
signal: AbortSignal
Cancels the run when it fires.
-
timeout: number
Milliseconds a single test may take before the run is declared stalled. Defaults to 20000.
A running watch session.
-
abort(): Promise<void>
Cuts the current run short: the browser drops the rest of its queue and the run ends where it is, with
aborted: trueon its result. -
browser: Browser
Playwright's
Browserfor this session — unstable, playwright-core's type. -
close(): Promise<void>
Stops watching, closes the browser and server, and ends the iteration. Idempotent.
-
droppedEvents: number
How many events the feed dropped because a consumer fell behind.
0unless something stalls. -
esbuild: BuildContext | null
esbuild's incremental
BuildContext— unstable, esbuild's type. -
events(): Stream<RunEvent>
The fine-grained feed: every event of every run, flat and in order, until the session closes.
-
fileWatchers: Record<string, FSWatcher>
The live
fs.watchhandles keyed by watched path — unstable, and a PARTIAL view: the parent-directory watchers, rescan intervals and symlink pollers the session also owns are not in here. It answers "what is being watched", not "every handle the watcher holds". -
initial: RunResult
The initial run's result, available before anything has changed on disk.
-
latest: RunResult
The most recent run's result —
initialuntil something re-runs. -
page: Page
The page the suite runs in — unstable, playwright-core's type.
-
restart(patch?: SessionPatch): Promise<RunResult>
Tears the session's machinery down and boots it again — browser, page, server, esbuild context and watchers — then runs the suite once, resolving with that run's result.
- results(): Stream<RunResult>
-
run(files?: string[]): Promise<RunResult>
Re-runs now, optionally scoped to
files, and resolves with that run's result. -
runAll(): Promise<RunResult>
Runs the whole suite, dropping any line-target selectors this session was scoped to.
-
runFailed(): Promise<RunResult>
Re-runs the files that last failed, or repeats the last run when nothing has failed yet — saying so as an
infonotice on the result rather than silently doing something else. -
running: boolean
Whether a run is in flight right now.
-
url: string
Where the QUnit view is being served, e.g.
http://localhost:1234. -
webServer: HTTPServer
The HTTP + WebSocket server serving the bundle — unstable, this project's own
HTTPServerrather than a documented interface.
A declared failure: something the runner decided it could not do, carrying a code to branch
on and a message to show.
Every way a daemon-routed run can fail to produce an exit code.
| { running: true; pid: number; cwd: string; nodeVersion: string; startedAt: number; socketPath: string; }
A live daemon, as reported by status.
Every way a daemon-routed run can fail to produce an exit code.
| { running: true; pid: number; cwd: string; nodeVersion: string; startedAt: number; socketPath: string; }
A live daemon, as reported by status.
The one failure validate raises.
A valid --reporter value.
--reporter by name, a reporter of your own, or false for none.
The success value or a declared failure — a bare union, discriminated by the Failure
brand.
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.
| { kind: "test"; test: TestResult; }
| { kind: "notice"; notice: Notice; }
| { kind: "browserLog"; log: BrowserLog; }
| { kind: "runEnd"; result: RunResult; }
One thing that happened during a run, as it happened.
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.
| ScriptEntryFailure
Every way run can reject. A script that merely exits non-zero is NOT one of them.
What openSession accepts: every run option, plus the one that chooses the shape.
The options a WatchSession.restart may change.
The failure taxonomy, as this API's public surface.
-
format: () => stringerror: unknown,options?: { stacks?: boolean; }
Renders a failure as the one-line message the CLI would print.
-
hasCode: <Codes extends readonly string[]>() => value is AnyFailure & { code: Codes[number]; }value: unknown,...codes: Codes
Narrows to a specific set of codes, for handling some failures and rethrowing the rest.
-
is: (value: unknown) => value is AnyFailure
Narrows an unknown value to a declared failure. The guard to reach for after
.result().
An option was given a value the runner will not accept.
run was handed something that is not one script file.
The CLI's: the real process streams. .write is looked up per call, so the daemon's stdout
interception still reaches it.
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.
Discards everything. The JS API's default, so a programmatic run prints nothing unless it was asked to.
The exported value: builders (Stream.from, Stream.unfold, Stream.lines) are the only
entry points — there is no public constructor and no call form.
The exported value: builders (Stream.from, Stream.unfold, Stream.lines) are the only
entry points — there is no public constructor and no call form.
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.
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.
Usage
import * as mod from "lib/api/index.ts";