interface WatchSession

A live watch session: the run's browser, server and file watchers, kept open.

Returned rather than "the process stays alive and you figure it out", so a caller other than the CLI — the JS API, a test of the watcher itself — can drive reruns and then shut the whole thing down deterministically.

The verbs are here rather than in the CLI's keyboard bindings because there is more than one caller now: qa and session.runAll() must mean the same thing, and they only do if there is one implementation. Binding a keystroke to it is keyboard-events.ts's whole remaining job.

// Defined, not invoked: a real session owns a browser and a bound port.
async function restartOnce(session: WatchSession) {
  await session.run(); // same rerun the file watcher performs
  await session.close();
}

Properties

The resolved config this session runs with; its state carries the live counters.

The session's browser, page and HTTP server.

fileWatchers: Record<string, FSWatcher>

The live per-path fs.watch handles, keyed by watched path — the same object the watcher mutates, not a copy. A PARTIAL view: the parent-directory watchers, rescan intervals and symlink pollers killFileWatchers also owns are not in here, so it answers "what is being watched", not "every handle the watcher holds".

url: string

Where the QUnit view is being served, e.g. http://localhost:1234.

readonly
running: boolean

Whether a run is executing or queued — true from the moment one is asked for.

Methods

run(files?: string[]): Promise<void>

Re-runs now, optionally scoped to files; resolves when that run finishes.

runAll(): Promise<void>

Runs the whole suite, dropping any line-target selectors this session was scoped to.

runFailed(): Promise<void>

Re-runs the files that last failed, or repeats the last run when nothing has failed yet.

abort(): void

Tells the browser to drop the rest of the current run's queue.

Fire-and-forget by design: it is a message to a page that may not be running anything, and the run it interrupts settles through its own normal path. Awaiting the interrupted run is what the caller already holds a promise for.

settled(): Promise<void>

Resolves once nothing is in flight — a no-op at the back of the rerun queue.

What abort() is awaited through: queueing a real rerun to wait for quiet would start the very thing the caller just asked to stop.

close(): Promise<Abandoned>

Stops the watchers and closes the browser and server. Idempotent.

Resolves with whatever the cleanup grace gave up on, so a caller that intends to exit can wait for it rather than discover it as a process that will not end.

teardown(disposeEsbuild?: boolean): Promise<Abandoned>

close minus the two teardowns a restart must not do, because it is building a replacement in the same process rather than ending.

The pre-launched Chrome is PROCESS-global: reaping it would take it from every other session here and leave the restart paying for a cold chromium.launch(). And esbuild's incremental context is kept because disposing it re-reads NOTHING — a restart reuses the same Config, so the plugin objects in the new context would be the very ones in the old. All it achieves is respawning esbuild's service child. contextKey still swaps the context out by itself when the build inputs actually change.

That holds only while the config is REUSED. restart(patch) builds a new one, so the old context belongs to a config nothing will use again — pass disposeEsbuild there, or it is orphaned with its ref still on esbuild's service child and the process can never exit.

Usage

import { type WatchSession } from "lib/commands/test.ts";