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.

8-10 min read View on GitHub More from async

A wide editorial illustration of a filing cabinet built from receipts and evidence slips. One drawer is labeled Pass, another Contested, and a third spills out HMR events, console warnings, and server restart notes. A browser window and a Vite server tower sit in the foreground, linked by a thread of paper trails. It explains that Async Witness records pipeline behavior as proof, not just page state.
Async Witness treats Vite like a black box recorder, preserving the events that conventional tests usually ignore.
Key Takeaways

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.

Async Witness turns scattered pipeline signals into a single verdict, then explains why that verdict is clean or contested.

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.

WitnessWhat it seesWhat it missesFailure styleBlind spot
PipelineVite events like HMR, scans, and restartsBrowser-only behaviorInfrastructure evidenceUI impact after the event
ClientConsole errors and DOM-visible custom eventsServer-side build stateIn-browser symptomsWhether the browser event came from a clean pipeline
DriverCDP-level browser behavior and navigationsApp intent and assertionsTransport evidenceSemantics of the update
BoxIts own expectations and assertionsEverything outside its scopeDirect verdictHidden 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.

A close-up cross-section of a Vite pipeline. On the left, a dev server emits a hot update. In the middle, a hidden evidence chamber stamps the event and routes it into a store. On the right, a browser receives the update, but one branch silently reroutes into a full reload. It explains that Async Witness captures evidence in motion instead of reconstructing it afterward.
Evidence is collected as the pipeline runs, which lets Async Witness detect a clean HMR path versus a silent fallback.

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.

ApproachWhat it optimizes forWhat it avoidsMain strengthMain trade-off
Async WitnessVite-specific receipts and pipeline evidenceHeavy browser framework dependenciesTight control over browser and server lifecyclesMore implementation responsibility
Playwright or PuppeteerGeneral browser automationCustom transport and browser discoveryBroad ecosystem familiarityLess intimate knowledge of Vite behavior
Conventional E2E toolsUser-visible page outcomesPipeline-level evidenceGood at end-user flowsBlind 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.

ToolSees the rendered pageSees Vite pipeline eventsCan mark pass but contestedBest use
Unit testsNoNoNoIsolated logic
Traditional E2EYesUsually noNoUser journeys
Async WitnessYesYesYesPipeline-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.