Skip to content

Vitest ​

Plugin vs barrel ​

WARNING

Config: untestutils/vitest/plugin. Specs: untestutils/vitest. Do not load the barrel in config.

Plugin options ​

ts
untestutils({
  recipes,              // from defineRecipes(..., import.meta.url)
  prewarm: ['site'],
  browsers: ['chromium'],
  artifactsRoot: undefined,
})
OptionNotes
recipesMap from your recipes module
prewarmIds to prepare/start in global setup
browsersEngines for page / goto (default ['chromium'])
artifactsRootSets UNTESTUTILS_ARTIFACTS_DIR

Override engine per run: UNTESTUTILS_BROWSER=firefox.

useHarness ​

ts
const app = await useHarness('site')
await app.$fetch('/')
app.url
app.dir
await app.files.read('index.html')

Prefer registered ids so Vitest and Playwright share the prepare cache.

Fixtures ​

FixtureRole
harnessRecipe id via test.override({ harness })
browserNamechromium | firefox | webkit
baseURLCurrent harness URL
pagePlaywright Page
gotopage.goto (+ Nuxt waitUntil: 'hydration' | 'route')
requestAPIRequestContext
ts
import { describe, test, expect } from 'untestutils/vitest'

describe('ui', () => {
  test.override({ harness: 'site' })

  test('home', async ({ page, goto }) => {
    await goto('/', { waitUntil: 'hydration' })
    await expect(page.getByRole('heading')).toBeVisible()
  })
})

Ensure useHarness or prewarm started the target so baseURL resolves.

Unit environments (multi-framework) ​

environment: 'untestutils' is a router (vitest-environment-untestutils) that loads @untestutils/<framework>/environment.

ts
// Vite example — same pattern for next / astro / sveltekit / remix / solidstart
import { defineVitestProject } from '@untestutils/vite/config'

export default defineVitestProject({
  test: {
    environmentOptions: {
      untestutils: {
        framework: 'vite', // set automatically by defineVitestProject
        appIsolation: 'worker', // opt-in; soft reset between tests
        domEnvironment: 'happy-dom',
      },
    },
  },
})
OptionNotes
untestutils.frameworknuxt | vite | next | astro | sveltekit | remix | solidstart
Legacy environmentOptions.nuxtStill resolves Nuxt (no framework required)
Shared DOM@untestutils/vitest/unit-dom
Lifecycle@untestutils/vitest/unit-lifecycle (registerSetupEntry, host reset)

Contract helpers (where the adapter exports them): setupApp / resetSharedApp / restartSharedApp / mount helper. Nuxt also keeps resetSharedNuxtApp / restartSharedNuxtApp and aliases resetSharedApp / restartSharedApp.

Honest boundaries (client unit ≠ full SSR): Next/Remix — no RSC/loader SSR; SvelteKit — no load/actions; SolidStart — no Vinxi SSR; Astro — island/container only. Status: Support matrix.

E2e plugin and unit env still do not share one Vitest project.

Nuxt unit (separate project) ​

E2e plugin and Nuxt unit env do not share one Vitest config:

  • E2e: untestutils/vitest/plugin
  • Unit: environment: 'untestutils' + untestutils/config / untestutils/runtime (or @untestutils/nuxt/config)

See API overview (config, runtime, module).

appIsolation: 'worker' (opt-in) ​

Today this surface is the Nuxt unit adapter (environment: 'untestutils'). Framework-agnostic hooks live in @untestutils/vitest/unit-lifecycle (registerSetupEntry, disposeBestEffort, host/timers reset, applyWorkerIsolationDefaults, registerSharedReset, getOrCreateWorkerState) so future unit environments can reuse the same worker/file lifecycle without Nuxt imports.

By default each unit file pays for a full setupNuxt() boot. For large component suites, reuse one Nuxt app per Vitest worker:

ts
export default defineVitestProject({
  test: {
    environment: 'untestutils',
    pool: 'threads',
    environmentOptions: {
      nuxt: {
        appIsolation: 'worker', // auto-sets isolate: false + maxConcurrency: 1 when unset
        // resetBetweenTests: true  // default in worker mode
      },
    },
  },
})
Default fileworker
setupNuxtonce per test file (remounts even with isolate: false)once per Vitest worker
isolateVitest default (true)false (auto)
maxConcurrencyVitest default1 (auto)
State between filesfresh window / remountshared app — soft-reset between tests
Soft resetn/aresetSharedNuxtApp via onTestFinished (scheduled from beforeEach; falls back to afterEach)

resetSharedNuxtApp (auto when resetBetweenTests is on) clears route/state/data/errors, VTU mounts (including plain mount() via enableAutoUnmount), registerEndpoint handlers, cookies (tracked document.cookie name+path — covers path-scoped cookies set in tests; jsdom jar when present), localStorage/sessionStorage, fake timers, Vitest env/global stubs, and per-test overrideNuxtImport values. Opt out per layer: resetSharedNuxtApp({ host: false, timers: false, stubs: false }).

Custom cleanups: registerSharedNuxtReset(key, fn) — keyed, replace-in-place on setupFile re-eval (prefer over unkeyed (fn)).

Not reset: startup plugin state, HttpOnly / other-path cookies the jar cannot see in some envs, DOM nodes that existed at baseline capture, background work flushPromises does not settle.

After Vite HMR / full reload in watch, call restartSharedNuxtApp() (or rely on enableSharedNuxtHotRestart, registered automatically in worker mode) to invalidate the memoized boot.

Per-test import overrides ​

mockNuxtImport after the shared boot is unreliable. Ship worker-level wrappers:

ts
// setup file (once per worker)
import { mockNuxtImport } from 'untestutils/runtime'
import { overridableNuxtImport } from 'untestutils/runtime'
mockNuxtImport('useFoo', overridableNuxtImport('useFoo', () => 'default'))

// in a test
import { overrideNuxtImport, overrideNuxtRoute } from 'untestutils/runtime'
const restore = overrideNuxtImport('useFoo', () => 'mocked')
// … assertions …
restore()

Also: getOrCreateWorkerState(name, create) for registries the app captured at startup. Soft reset clears override values; wrappers stay.

Do not call enableAutoUnmount yourself in Nuxt worker mode — the Nuxt runtime installs it (VTU throws on a second enable).

Alignment with nuxt/test-utils ​

ConcernPR #1821#1750untestutils
Memoize setupNuxtalways (window promise)opt-in appIsolation: 'worker'opt-in appIsolation: 'worker'
Soft reset APIdocs caveat onlyroute/state/host checklistshipped (resetSharedNuxtApp + host/timers/stubs)
Browser entrysetupWindow + promise + skip node entryn/asame shape + worker-aware registerNuxtSetupEntry

Keep the Vitest project homogeneous (environment: 'untestutils' only). Mixing @vitest-environment node in the same project tears the DOM env down between files and kills reuse.

Measured on this repo (pnpm test:unit-bench, 12 files, maxWorkers: 1, mean-of-3): file-scoped ~9.6 s vs worker-scoped ~2.8 s (~3.5×). See playground/unit-bench/results.json.

Next ​

Released under the MIT License.