WebSocket relay for TWD — lets AI agents and external tools trigger and observe in-browser test runs.
Your app runs tests in the browser with twd-js. twd-relay adds a relay server and a browser client so that a client (script, CI, or AI agent) can send a “run” command over WebSocket; the relay forwards it to the browser, and test events are streamed back. No Vite or specific framework required: the relay can run standalone and work with any app that loads the browser client.
-
Relay server — WebSocket server that accepts one browser connection and many client connections. Clients send commands (
run,status); the relay forwards them to the browser. The browser runs tests and streams events back; the relay broadcasts those to all clients. A lock prevents concurrent runs. -
Browser client (
twd-relay/browser) — Runs in your app. Connects to the relay, listens for commands, usestwd-js/runnerto execute tests, and streams results back. Logs connection state in the console (e.g.[twd-relay] Connected to relay). -
Vite plugin (
twd-relay/vite) — Optional. Attaches the relay to your Vite dev server so the WebSocket is on the same origin. Also available: a standalone CLI that runs the relay on its own HTTP server (default port 9876).
- Browser connects → sends
{ type: 'hello', role: 'browser' } - Client connects → sends
{ type: 'hello', role: 'client' } - Client sends
{ type: 'run', scope: 'all' }(optionally withtestNamesto filter) → relay forwards to browser - Browser runs tests and streams events → relay broadcasts to clients
- Browser sends
{ type: 'heartbeat' }every 3s during a run (relay consumes these, never forwarded to clients) run:completeclears the run lock (and the send-run script exits)
During an active test run the browser sends a heartbeat every 3 seconds. The relay tracks the last heartbeat time and checks every 10 seconds. If no heartbeat arrives for 120 seconds during an active run, the relay considers the run dead (browser tab frozen by the OS), resets the run lock, and broadcasts to all clients:
{ "type": "run:abandoned", "reason": "heartbeat_timeout" }The CLI prints a clear message — Run abandoned — browser tab appears frozen. Refresh the browser tab and retry. — and exits with code 1. This is especially useful for AI agent workflows, where the agent gets an actionable signal instead of a silent 180s timeout followed by a cryptic RUN_IN_PROGRESS error.
npm install twd-relayPeer dependency: twd-js (>=1.4.0). Your app must use twd-js for tests; the browser client imports twd-js/runner at runtime.
Works with any framework. Run the relay on one port and your app on another.
1. Start the relay (from this repo, or use the CLI in your project):
npm run relay
# or: npx twd-relay
# Listens on ws://localhost:9876/__twd/ws (use --port to change)2. In your app, connect the browser client and call connect():
import { createBrowserClient } from 'twd-relay/browser';
const client = createBrowserClient({
url: 'ws://localhost:9876/__twd/ws',
});
client.connect();Once connected, the browser client sets a colored favicon and prefixes document.title so you can spot the active TWD tab at a glance:
| Favicon | Title prefix | State |
|---|---|---|
| Blue | [TWD] |
Connected, idle |
| Orange | [TWD ...] |
Tests running |
| Green | [TWD ✓] |
Last run passed |
| Red | [TWD ✗] |
Last run had failures |
On disconnect or eviction (another tab taking over), the original favicon and title are restored.
Chrome aggressively throttles timers in backgrounded tabs, which can stretch a 1-second test run to 30+ seconds. To avoid AI/CI hangs, the browser client monitors per-test wall-clock time. If any single test runs longer than 10 seconds (configurable), the browser emits run:aborted, the CLI prints a clear error with recovery guidance, and the run ends with exit code 1.
Override the threshold with --max-test-duration <ms> on twd-relay run, or pass maxTestDurationMs to createBrowserClient. Set it to 0 to disable detection entirely:
twd-relay run --max-test-duration 20000 # raise to 20s for heavy multistep tests
twd-relay run --max-test-duration 0 # disable detectionThe default of 10 s is chosen to sit above the Testing Library default findBy* timeout (3 s). A legitimately failing test with one or two missed selectors still completes under the threshold, while throttled runs — where tests typically cluster in the 10–30 s range — trip the abort reliably.
Recovery when an abort fires: foreground the TWD tab (identified by the [TWD …] title prefix set by the favicon indicator) and retry. For unattended runs (CI, agents), prefer twd-cli: it drives a headless browser where the tab is always focused and throttling doesn't apply.
3. Open your app in a browser — the page connects to the relay as “browser”.
4. Trigger a run — something must connect as a client and send run:
- From this repo:
npm run send-run(ornode scripts/send-run.js [--port 9876]). The script exits when it receivesrun:complete. - From another project (if
wsis available, e.g. via twd-relay): use the one-liner below.
Run from a directory where ws is installed (e.g. project with twd-relay):
node -e 'const Ws=require("ws");const w=new Ws("ws://localhost:9876/__twd/ws");let s=false;w.on("open",()=>w.send(JSON.stringify({type:"hello",role:"client"})));w.on("message",d=>{const m=JSON.parse(d);console.log(m.type,m);if(m.type==="connected"&&m.browser&&!s){s=true;w.send(JSON.stringify({type:"run",scope:"all"}));}if(m.type==="run:complete"){w.close();}});w.on("close",()=>process.exit(0));'Change the URL if your relay uses another port or path.
If you use Vite, you can attach the relay to the dev server so the WebSocket is on the same host/port:
// vite.config.ts
import { twdRemote } from 'twd-relay/vite';
export default defineConfig({
plugins: [react(), twdRemote()],
});Then in your app you can omit the URL; the client defaults to ws(s)://<current host>/__twd/ws.
| Script | Description |
|---|---|
npm run build |
Build relay, browser, vite entry points + CLI |
npm run relay |
Build and start the standalone relay (port 9876) |
npm run send-run |
Connect as client and send run; exits on run:complete |
npm run dev |
Start relay only (assumes already built) |
npm run test |
Run tests (watch) |
npm run test:ci |
Run tests with coverage |
| Export | Use |
|---|---|
twd-relay (main) |
Relay server: createTwdRelay(httpServer, options) |
twd-relay/browser |
Browser client: createBrowserClient(options) |
twd-relay/vite |
Vite plugin: twdRemote(options) |
CLI: twd-relay (or npx twd-relay) — two subcommands:
twd-relay serve(default) — start the standalone relaytwd-relay run— connect to a relay and trigger a test run
Connect to an existing relay, trigger tests, stream output, and exit with 0 (all pass) or 1 (failures).
# Run all tests (connects to Vite dev server on port 5173 by default)
twd-relay run
# Run on a different port
twd-relay run --port 9876
# Run specific tests by name (substring match, case-insensitive)
twd-relay run --test "should show error"
# Multiple filters — runs tests matching any of them
twd-relay run --test "login" --test "signup"| Flag | Description | Default |
|---|---|---|
--port <port> |
Relay port | 5173 |
--host <host> |
Relay host | localhost |
--path <path> |
WebSocket path | /__twd/ws |
--timeout <ms> |
Timeout | 180000 |
--test <name> |
Filter tests by name substring (repeatable) | — |
When --test is used and no tests match, the CLI prints the available test names so you can correct the filter.
MIT · BRIKEV