A browser agent can open a page, click a control, and inspect the result. What slows it down is setting up the state behind that page. A signed-in account, a particular database record, a device permission, a model response, or a failure at the right moment can take longer to arrange than the screen takes to review.
Direct handles that setup. Your app reaches each slow or unpredictable dependency through a port, a small interface your app defines. In a Direct build, the port returns fixture data instead of calling the live system, and each starting state gets a name and a URL. Your browser tool still does the clicking and checking.
Open a named state from a browser check
This Playwright check opens the populated state of the Todo example, completes one todo, and confirms that the real interface updated. It needs no sign-in, seed request, or stored data from an earlier run.
Browser check using a named Direct scenario
await page.goto(
"/direct/?__direct_scenario=todos.populated",
);
await waitForQuiescence(page);
await page.getByRole("checkbox", {
name: "Write the public guide",
}).check();
await waitForQuiescence(page);
await expect(page.getByRole("checkbox", {
name: "Write the public guide",
})).toBeChecked();
waitForQuiescence is a helper in your own check that waits for Direct's readiness snapshot, described below. Direct doesn't ship it.
The example declares two more states the same way. ?__direct_scenario=todos.empty opens the empty list, and ?__direct_scenario=todos.write-failure opens the list with a save that fails, so a check can confirm the error message without breaking a real store.
Put a port under your feature code
The interface, reducers, parsing, navigation, and feature logic above the port run the same code in both builds. Only the implementation behind the port changes.
Where Direct sits
agent-browser or Playwright
│
real interface + feature state
│
your app's port
┌───┴────┐
live system Direct world
The Todo example defines one TodoPort. Its production entry passes a port that reads and writes browser local storage. Its Direct entry passes an in-memory port built from a fixture world. The production entry never imports Direct.
One port, two entries
// src/todo-port.ts
export interface TodoPort {
readonly readTodos: () =>
Promise<readonly TodoItem[]>;
readonly setCompleted: (id: string, completed: boolean) =>
Promise<readonly TodoItem[]>;
}
// src/main.tsx (production entry)
<TodoApp
port={createLocalStorageTodoPort(globalThis.localStorage)}
/>
// direct/workbench.tsx (Direct entry)
<TodoApp port={props.harness.port} />
The port speaks in the app's own terms: todos and completion. It contains no Direct types. Put the port as low as you can while keeping the behavior you want to review above it. If the Direct side has to copy the logic you're trying to test, the port is too high, and the fixture would imitate that logic instead of testing it.
Name each starting state
A Direct world is validated JSON that describes one starting state. A scenario gives a world a name and a route. Scenarios contain no browser actions; your browser check decides what to click and what to assert.
At runtime, a Direct build has three parts:
- A definition lists the named scenarios, their routes, and which systems each check claims to exercise, and it checks that those declarations agree.
- A session opens one scenario and holds its fixture state, a controllable clock, pending work, reset, and cleanup.
- A browser installation publishes the available and active scenarios on
window.__direct, exposes readiness and reset controls, and by default blocksfetchcalls to URLs your app hasn't allowed.
An agent can read window.__direct to list valid scenario IDs and routes and to confirm that the page opened the scenario and route it asked for, without reading your source files.
The fetch block is on by default so that a missing fixture shows up as a failed request instead of a quiet call to a live service. Your app can allow specific URLs. The block covers only fetch calls made in the page where Direct is installed. Other traffic, such as XMLHttpRequest, WebSockets, EventSource, beacons, navigation, asset loads, and requests from workers or other frames, is not intercepted.
Wait until the app is ready
A fixed delay says, “wait 500 milliseconds and hope.” Direct exposes a readiness snapshot instead: no tracked operation is active, and every pending counter your app names is zero. Your browser check polls until the active scenario, the fixture state's revision number, and those counters stay the same for a settle interval you choose, then checks the interface.
A settled snapshot shows only that the work Direct knows about has stopped changing. It doesn't show that the screen is correct, so your check still has to reject relevant console, runtime, and unhandled-request errors, then make its own assertions or visual checks.
Follow one check from URL to result
Each check carries one of three coverage labels that record which systems it exercised. A fixture check stops at the fixture ports. A mixed check pairs fixture data with a named live check. A direct check needs the real system. The labels keep a fast development check from being reported as a test of a system it never touched.
In the Todo example, each step of the check above is something you can inspect:
| Step | What you can see |
|---|---|
| Open | ?__direct_scenario=todos.populated selects the validated world. |
| Confirm | window.__direct reports scenario todos.populated, route /, and the todos.completion coverage entry. |
| Wait | The readiness snapshot reports zero todoOperations, zero on every declared error counter, and no change across the settle interval. |
| Act | The browser driver checks the “Write the public guide” box in the real Todo interface. |
| Check | A second stable snapshot and the checked box show that the change went through the app's port. |
| Limit | The run is labeled fixture. Local-storage parsing, quota behavior, and persistence need a separate direct check against real browser storage. |
You can read the same results through the TypeScript package, or from the page itself with any browser driver that can run a script there.
When to use Direct
agent-browser gives coding agents a compact command-line interface for opening pages, reading accessibility snapshots, and interacting with elements. Playwright and other browser drivers do the same job with different APIs. Direct doesn't drive the browser; it hands the browser tool a known page state to start from.
Other tools already cover many fast-setup cases (checked 2026-09-28):
| Tool | What it replaces | App changes | Best when |
|---|---|---|---|
| Playwright route mocking, saved auth state, and clock | Network responses, sign-in, and the browser clock | None | Those already reach the state quickly |
| Mock Service Worker | HTTP and GraphQL responses, in the browser or Node | A start call in development, plus a worker file in the browser | Network responses are the only setup to replace |
| Storybook | A component's props and context | One story per state | The subject is one component, not the whole app |
| Direct | Product-owned ports such as sign-in, records, device permissions, native modules, and model calls | A small port for each replaced system | Setup sits below the network or in native code, and the agent needs a named app state with a readiness signal |
Direct asks more of you up front: a port and a separate Direct entry. Storybook and MSW are older, more widely used, and support more frameworks; Direct's core is plain TypeScript with React and React Native bindings.
- Use browser automation alone when the state you need is already quick to reach, or when the live backend and browser assembly are part of the check.
- Pair Direct with agent-browser or Playwright when setup and reset dominate the loop: repeated sign-in, slow seed requests, empty or error states that are hard to create, native modules that aren't available, paid model calls, or device permissions that automation can't reset cleanly.
- Use unit or component tests when the subject is isolated logic or rendering that doesn't need the full app.
- Keep live integration and end-to-end tests when the backend, native host, browser assembly, filesystem, operating system, or device is the subject. Direct never exercises the systems behind the ports it replaces.
Add Direct to a project
Direct is a development dependency. Install the $direct Agent Skill so your coding agent can add a port, scenarios, and a check that production builds exclude Direct. The Direct README has the current release commands for the skill, Bun, and npm, plus archive verification and the Todo example.