Vitest Testing skill for Codex
Hoisted mocks, named projects, real-browser tests. Codex configures Vitest the current way: projects per environment, provider factories for browser mode, coverage that still counts untested files, and mocks that respect hoisting. It knows the Vitest 4 and 5 breaking changes, runs vitest run instead of an endless watcher, and migrates Jest suites without losing mock behavior.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- vitest-testing Installs to ~/.agents/skills/vitest-testing/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install vitest-testing The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project vitest-testing Installs to .agents/skills/vitest-testing/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/vitest-testing.zipunzip -o vitest-testing.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) vitest-testing PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $vitest-testing, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Sets up and writes Vitest 4/5 suites - vitest.config with projects, node vs jsdom vs browser mode via @vitest/browser-playwright, vi.mock hoisting and vi.hoisted, spies and fake timers, v8 coverage thresholds, sharding and Jest migration. Use when a Vite, Nuxt, SvelteKit or TS library repo runs Vitest, when vi.mock does not apply, when moving from Jest or from the old workspace file, or when component tests need a real browser.
Use it when
- The repo is Vite-based or already uses Vitest
- Migrating from Jest or from a vitest.workspace file
- Component tests need a real browser instead of jsdom
- Coverage numbers changed after a Vitest upgrade
Use something else when
- The suite is staying on Jest: use jest-testing
- You need full user journeys across pages: use playwright-e2e
How to check the result
vitest run passes for every project, shuffled order still passes, and the coverage report lists untested files under coverage.include.
The mistake it prevents
Referencing a top-level variable inside a vi.mock factory, which is hoisted above that variable and throws; move it into vi.hoisted.
Done well: one vitest.config.ts with named projects for each environment, mocks that are
hoisted and typed, component tests in a real browser where DOM fidelity matters, and a
coverage gate that measures the files you meant to measure.
Inspect first
- Read
AGENTS.md, thenpackage.jsonscripts and the Vitest version (npx vitest --version). Vitest 4 moved toprojects, top-level pool options and provider factories; Vitest 5 requires Node 22.12+ and Vite 6.4+. - Locate config:
vitest.config.*, or atestblock insidevite.config.*. A separatevitest.workspace.*file means a pre-v4 setup that must move intoprojects. - Note the environment per area: pure logic (
node), DOM simulation (jsdomorhappy-dom), orbrowser.enabled. - Check
tsconfig.jsontypesforvitest/globalsif tests usedescribewithout imports.
Baseline config with projects
// vitest.config.ts
import { defineConfig } from "vitest/config";
import { playwright } from "@vitest/browser-playwright";
export default defineConfig({
test: {
restoreMocks: true,
coverage: {
provider: "v8",
include: ["src/**/*.{ts,tsx}"], // v4+: required to report untested files
exclude: ["src/**/*.stories.tsx", "src/**/*.d.ts"],
thresholds: { lines: 80, branches: 75 },
},
projects: [
{
extends: true,
test: { name: "unit", include: ["src/**/*.test.ts"], environment: "node" },
},
{
extends: true,
test: {
name: "browser",
include: ["src/**/*.browser.test.tsx"],
browser: {
enabled: true,
provider: playwright(),
headless: true,
instances: [{ browser: "chromium" }],
},
},
},
],
},
});
extends: truemakes a project inherit root options (default in Vitest 5).- Run one project:
npx vitest run --project unit. - Browser mode downloads browsers with
npx playwright install chromium; that needs network access, which may require approval in the Codex sandbox.
Environment choice
| Test subject | Environment |
|---|---|
| Pure functions, server code, CLI | node (default, fastest) |
| Components with simple DOM, no layout or real events | jsdom (most compatible) or happy-dom (faster, less complete) |
Layout, focus, pointer events, CSS, canvas, real fetch streaming, Web APIs jsdom lacks | browser mode |
Mocking
vi.mock is hoisted to the top of the file. Values it needs must come from vi.hoisted.
In Vitest 5, vi.mock, vi.unmock and vi.hoisted must be top-level statements.
import { describe, it, expect, vi } from "vitest";
import { registerUser } from "./register";
import { sendEmail } from "./mailer";
const { sendMock } = vi.hoisted(() => ({ sendMock: vi.fn() }));
vi.mock(import("./mailer"), () => ({ sendEmail: sendMock }));
it("sends one welcome email", async () => {
sendMock.mockResolvedValueOnce({ id: "msg_1" });
await registerUser({ email: "[email protected]" });
expect(sendEmail).toHaveBeenCalledOnce();
});
Partial mock with types preserved:
vi.mock(import("./config"), async (importOriginal) => {
const actual = await importOriginal();
return { ...actual, flags: { ...actual.flags, newCheckout: true } };
});
- Passing
import("./mod")instead of a string gives the factory typedimportOriginal. vi.mocked(fn)narrows a mocked import for TypeScript.vi.spyOn(obj, "method")for methods on objects you hold;restoreMocks: trueundoes spies before each test. Since v4,vi.restoreAllMocks()restores only spies, not automocks.vi.stubEnv("API_URL", "http://test")andvi.stubGlobal("fetch", fakeFetch); enableunstubEnvs/unstubGlobalsin config so they reset automatically.- Mocks invoked with
newconstruct instances since v4; give class mocks afunctionorclassimplementation, not an arrow function.
Fake timers
beforeEach(() => vi.useFakeTimers());
afterEach(() => vi.useRealTimers());
it("polls every 5 seconds until ready", async () => {
const done = waitUntilReady(client);
await vi.advanceTimersByTimeAsync(15_000);
await expect(done).resolves.toBe("ready");
});
- Use the
Asyncvariants when code awaits between timers. vi.setSystemTime(new Date("2026-03-01T00:00:00Z"))for date logic.- For conditions that settle on their own,
await vi.waitFor(() => expect(x).toBe(1))orawait expect.poll(() => store.count).toBe(3)beats arbitrary sleeps.
Browser mode tests
// Counter.browser.test.tsx
import { expect, test } from "vitest";
import { render } from "vitest-browser-react";
import { Counter } from "./Counter";
test("increments on click", async () => {
const screen = await render(<Counter initial={1} />);
await screen.getByRole("button", { name: "Increment" }).click();
await expect.element(screen.getByText("Count: 2")).toBeVisible();
});
- Import
pageanduserEventfromvitest/browser(older code used@vitest/browser/context). expect.element(...)retries until the assertion holds; plainexpectdoes not.- Vitest 5 locators match text exactly by default and
toHaveTextContentneeds an exact match; usetoMatchTextContentfor partial or regex matches. renderfromvitest-browser-reactcan be awaited safely across versions; forvitest-browser-vueandvitest-browser-svelteit must be awaited in Vitest 5.
Running
npx vitest # watch mode, local only
npx vitest run # single run, use in CI and in Codex
npx vitest run src/cart -t "applies coupon"
npx vitest related src/cart.ts --run
npx vitest run --changed origin/main
npx vitest run --coverage
npx vitest run --shard=1/4 --reporter=blob # then: npx vitest --merge-reports
Always use vitest run (or --run) in non-interactive contexts; bare vitest starts a
watcher that never exits.
Migrating from Jest
- Replace
jest.fn/spyOn/mockwithvi.*;jest.requireActualbecomesimportOriginal - Move values referenced by mock factories into
vi.hoisted -
jest.useFakeTimers()->vi.useFakeTimers(); checkAsynctimer variants - Set
globals: truetemporarily if many files lack imports; add imports over time - Map
moduleNameMapperaliases toresolve.alias(orvite-tsconfig-paths) - Convert
testEnvironmentper file to projects or// @vitest-environment jsdom - Re-record snapshots only after diffing them; serializers differ slightly
Upgrading to Vitest 4 or 5
workspace->projects; deletevitest.workspace.*.poolOptionsremoved:maxThreads/maxForks->maxWorkers; single worker ismaxWorkers: 1, isolate: false.coverage.allremoved: setcoverage.includeor untested files vanish from the report and the percentage jumps.basicreporter removed; use['default', { summary: false }].- Vitest 5:
clearMocksdefaults to true;-tmatches names joined with>; unawaitedresolves/rejectsfail the test;expect.pollrejects on timeout.
Gotchas
vi.mockpath must match the specifier the module under test resolves to, after aliases.environment: "jsdom"at root slows every pure test; scope it with projects.- Coverage that suddenly rises after an upgrade usually means untested files dropped out.
- Mocking
fetchwithvi.stubGlobalper test is fine for one call; for whole API surfaces use MSW ($api-mocking-msw).
Verification
npx vitest runfor the touched project, then all projects.- Re-run changed files with
--sequence.shuffleto expose order dependence. npx vitest run --coverageand confirmincludecovers untested files.- Break a mocked behavior on purpose and confirm the test fails.
What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Vitest Testing"
short_description: "Hoisted mocks, named projects, real-browser tests"
default_prompt: "$vitest-testing Set up Vitest with separate unit and browser projects for this React app" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.