Skip to content

How it works ​

Terms ​

TermMeaning
RecipeOptional prepare, required start, optional ready / share / hashInputs
DriverFactory that returns a Recipe (staticDir, nuxt, host, …)
Running{ kind: 'url', url, stop } and/or { kind: 'dir', dir }
HarnessHandleurl?, dir?, $fetch, files.*
IdentityHash of recipe id + inputs → cache key under .untestutils

Lifecycle ​

  1. Register — defineRecipes({ … }, import.meta.url)
  2. Prepare (optional) — build into an artifact dir; skipped when cache is warm
  3. Start — local server, dir, or remote URL
  4. Ready — HTTP (or custom) gate
  5. Teardown — stop processes; clear registry

Local servers bind to 127.0.0.1.

run: 'dev' is not the fast path ​

Framework drivers default to a built mode (nuxt → server, Vite → preview, …) so prepare can be shared. run: 'dev' skips prepare, sets share: 'never', and exists only for niche HMR assertions. See Drivers.

Shared prepare (speed) ​

Prepare outputs live under .untestutils/builds, keyed by identity. Parallel workers take a file lock so they do not corrupt the same build; a waiter keeps waiting while the holder's heartbeat is fresh (long builds are fine) and only fails — naming the holder pid — once the heartbeat stops. Running URLs are registered for Playwright/Vitest (UNTESTUTILS_HOST_<ID>).

Per-run state — registry and locks/ — lives under the artifacts root's run/ directory (default .untestutils/run/), apart from builds/. Each test run (vitest/playwright plugin) gets its own registry in run/runs/<runId>/ (targets.json + owner.json, the run's main pid). So two vitest runs in one checkout never stop each other's servers. Each run starts its own servers from the shared builds/ cache, and a cold build waits on the other run's build lock. Of other runs' servers, global setup stops only those whose run's owner process is gone (a run dir with no owner.json counts after 60 s). It also stops everything in the shared no-run-id registry (run/targets.json, used by code that calls core without a plugin) and the pre-0.8 <root>/targets.json; neither has an owner check. A nested runner (a vitest run spawned by a test) gets its own run id. Teardown stops its own run's servers and removes the run dir. Registry entries record the server's OS start time when available (Linux /proc, macOS ps); teardown uses it to skip a pid the OS recycled after a crash. On Windows (or without ps) it falls back to a plain pid check. A server start that hits EADDRINUSE is retried on a fresh port.

Global teardown prunes builds/: per recipe id it keeps the 2 most recently used identities plus everything the run used, and never removes a build whose lock is held (gc option / UNTESTUTILS_GC).

EnvEffect
UNTESTUTILS_SHARE=0Disable sharing
UNTESTUTILS_DEBUG=1Verbose logs
UNTESTUTILS_ARTIFACTS_DIRCustom artifacts root
UNTESTUTILS_SESSIONPlaywright session namespace (see Playwright)
UNTESTUTILS_PREWARM_CONCURRENCYParallel prewarm limit
UNTESTUTILS_GC0 disables build GC, a number sets keep-per-recipe

CI: cache the artifacts root's builds/ dir (default .untestutils/builds; <root>/sessions/<session>/builds with a session; not run/) when inputs are stable; use prewarm; always tear down.

Imports ​

ImportUse in
untestutilsdefineRecipes, drivers
untestutils/vitest/pluginVitest config only
untestutils/vitestSpecs
untestutils/playwrightPlaywright config + specs
untestutils/nuxtnuxt({ run })
untestutils/utilsCookies, SEO, poll, …

Next ​

Released under the MIT License.