Async Witness: The Testing Framework That Treats Vite Like a Black Box Recorder
It does not just check whether your app rendered. It records how Vite behaved, whether HMR was accepted, and whether a passing test was quietly contaminated by hidden failures.
- Async Witness turns Vite behavior into evidence, so a run can pass and still be contested.
- Its core idea is not broader browser automation, but tighter accountability for pipeline events that ordinary tests miss.
- The project treats browser, driver, client, and pipeline as separate witnesses whose testimony must agree.
- That receipts model is especially useful when hidden failures matter more than a green DOM assertion.
Pass, but contested
Most test suites are binary. They pass or fail. Async Witness adds a third state that is more honest: contested. A test can satisfy its assertions and still be marked suspect if the pipeline showed a console error, an unexpected reload, or an HMR path that did not behave the way the box expected.
That is the project’s real wedge. It is not trying to be yet another wrapper around browser assertions. It is trying to prove that the thing you think happened inside Vite actually happened.
Why Vite needed receipts
Vite already knows a lot more than most test runners do. It sees dependency scans, dev server restarts, HMR payloads, and build-time behavior that never reaches the DOM. Ordinary E2E tools usually start too late. They only see the page after the pipeline has already made its decisions.
The result is a different definition of correctness. Not just, did the page render. Also, did Vite accept the update, did the client stay quiet, and did the driver observe anything that should disqualify the run?
The witness model
Async Witness divides the world into four witnesses: Pipeline, Client, Driver, and Box. That sounds abstract until you realize what it buys you. Each witness sees a different slice of reality, and no single slice is trusted to tell the whole story.
| Witness | What it sees | What it misses | Failure style | Blind spot |
|---|---|---|---|---|
| Pipeline | Vite events like HMR, scans, and restarts | Browser-only behavior | Infrastructure evidence | UI impact after the event |
| Client | Console errors and DOM-visible custom events | Server-side build state | In-browser symptoms | Whether the browser event came from a clean pipeline |
| Driver | CDP-level browser behavior and navigations | App intent and assertions | Transport evidence | Semantics of the update |
| Box | Its own expectations and assertions | Everything outside its scope | Direct verdict | Hidden contamination |
The important part is not the taxonomy itself. It is the decision rule. Async Witness can say the box passed while still reporting that the surrounding testimony was dirty.
How evidence becomes a receipt
The evidence layer is the part that makes the whole idea work. Async Witness injects a client-side script that listens for HMR activity through import.meta.hot, then turns those events into DOM-visible custom events. That means the system does not have to guess whether a hot update happened. It can wait for it.
That difference matters. A lot of flaky tests are really just sleepy tests. They pause for a fixed interval and hope the update arrives in time. Async Witness replaces sleep with observation. The evidence store waits for the actual signal, then records it as a receipt.
A runner that stays close to the metal
The runner is unusually opinionated. It does not lean on Playwright or Puppeteer. Instead, it speaks CDP directly and discovers system browsers on its own. That keeps the stack lean, but more importantly, it keeps the project focused on the browser as a transport layer, not a testing product with its own abstractions piled on top.
| Approach | What it optimizes for | What it avoids | Main strength | Main trade-off |
|---|---|---|---|---|
| Async Witness | Vite-specific receipts and pipeline evidence | Heavy browser framework dependencies | Tight control over browser and server lifecycles | More implementation responsibility |
| Playwright or Puppeteer | General browser automation | Custom transport and browser discovery | Broad ecosystem familiarity | Less intimate knowledge of Vite behavior |
| Conventional E2E tools | User-visible page outcomes | Pipeline-level evidence | Good at end-user flows | Blind to silent build or HMR failures |
There is also a practical payoff in the teardown logic. The runner waits for dependency scans to settle before shutting down, which avoids the kind of cleanup noise that makes Vite tests look flaky when the real issue is timing.
What the Box abstraction buys you
The box is the unit of truth. It is not a generic test case sitting above the system. It is a test that lives inside the pipeline, carries its own context, and evaluates against both expected outcomes and surrounding evidence.
const updateBox = box('message updates without reload', async ({ browser, expect }) => {
const page = await browser.newPage()
await page.goto('http://localhost:5173')
await expect.edit('src/message.ts', {
client: { hmr: 'accepted' }
})
await expect(page.locator('[data-message]')).toContainText('updated')
})
The code reads like a normal test, but the model underneath is different. The box can ask for a behavioral receipt, not just an assertion. That is why Async Witness can separate a clean pass from a pass that was contaminated by a bad pipeline event.
Where this beats ordinary E2E
Async Witness does not replace end-to-end testing. It narrows in on a failure class that ordinary tools often miss: the app looks fine, but the pipeline was not. That could mean HMR fell back to a full reload, a console error appeared in the client, or a dependency scan went sideways and the browser never told you.
| Tool | Sees the rendered page | Sees Vite pipeline events | Can mark pass but contested | Best use |
|---|---|---|---|---|
| Unit tests | No | No | No | Isolated logic |
| Traditional E2E | Yes | Usually no | No | User journeys |
| Async Witness | Yes | Yes | Yes | Pipeline-aware verification |
That makes it less like a replacement and more like a missing sensor. It gives you a direct read on the part of the stack that usually stays invisible until something is already broken.
Why AI agents care
The receipts model is especially interesting for automated systems. A screenshot says only that the page looked right. A receipt says what happened. That distinction matters if an AI agent is trying to verify its own changes, because the useful question is not just whether the UI matches, but whether the update path behaved cleanly enough to trust.
That is where Async Witness feels ahead of a lot of testing talk. It is not only about confidence for humans. It is about machine-readable proof that can be acted on by other systems.