From qa-unit-tests-js
Configures and runs Vitest - Vite-native unit framework with Jest-compatible API (`expect`, `vi.fn`, `vi.mock`, `vi.spyOn`); reads `vite.config.*` so existing Vite plugins work; supports in-source testing via `if (import.meta.vitest)`, browser-mode UI for headed tests, type-checking via `vitest --typecheck`, native ESM, and coverage via v8 (default) or istanbul providers. Use when the user works with Vite-based projects (Vue, Svelte, Solid, modern React with Vite) or wants Jest-compatible API with faster Vite-native runs.
How this skill is triggered — by the user, by Claude, or both
Slash command
/qa-unit-tests-js:vitest-testsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Per [vitest.dev/guide][vt-guide]:
Per vitest.dev/guide:
Vitest is the Vite-native test framework. The model:
"Vitest reads your
vite.config.*by default, so your existing Vite plugins and configuration work out-of-the-box."
This is the differentiator vs Jest - Jest needs separate
babel-jest/ts-jest transform setup; Vitest reuses Vite's
already-configured pipeline. Same code transforms in dev + test.
API is intentionally Jest-compatible (expect, describe, it /
test, vi.fn, vi.mock) - migration from Jest is mostly mechanical.
if (import.meta.vitest)) is desired.Per vt-guide:
npm install -D vitest
If the project already has Vite + a vite.config.* file, no
additional config needed.
Per vt-guide:
// sum.js
export function sum(a, b) { return a + b; }
// sum.test.js
import { expect, test } from 'vitest'
import { sum } from './sum.js'
test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3)
})
Wire package.json:
{
"scripts": {
"test": "vitest run",
"test:watch": "vitest",
"coverage": "vitest run --coverage"
}
}
vitest (no subcommand) defaults to watch mode; vitest run is the
single-pass run.
Vitest reads vite.config.ts by default. For Vitest-specific options:
// vite.config.ts
import { defineConfig } from 'vitest/config' // note: vitest/config wrapper
export default defineConfig({
test: {
environment: 'jsdom', // 'jsdom' (browser-like) | 'node' | 'happy-dom' | 'edge-runtime'
globals: false, // import {test, expect} explicitly (recommended) vs global injection
include: ['**/*.test.{js,ts}', '**/*.spec.{js,ts}'],
exclude: ['node_modules', 'dist'],
setupFiles: ['./vitest.setup.ts'],
coverage: {
provider: 'v8', // 'v8' (default; native) | 'istanbul'
reporter: ['text', 'json', 'html', 'lcov'],
thresholds: {
lines: 80,
functions: 80,
branches: 80,
statements: 80,
},
include: ['src/**'],
exclude: ['**/*.test.ts', '**/types.ts'],
},
},
})
provider: 'v8' is Vitest's default; istanbul is more accurate
for branch coverage but slower.
import { vi, expect, test } from 'vitest';
// vi.fn() — standalone mock
const myMock = vi.fn();
myMock.mockReturnValue(42);
// vi.mock() — module mock (hoisted to top of file)
vi.mock('./api-client', () => ({
fetchUser: vi.fn().mockResolvedValue({ id: 1 }),
}));
// vi.spyOn() — wrap existing method
const spy = vi.spyOn(myObject, 'someMethod');
Timer mocks:
vi.useFakeTimers();
setTimeout(callback, 1000);
vi.advanceTimersByTime(1000);
expect(callback).toHaveBeenCalled();
vi.useRealTimers();
Migration from Jest: replace jest. with vi. (mostly mechanical).
Vitest's distinguishing feature - tests live in the implementation file:
// sum.ts
export function sum(a: number, b: number): number {
return a + b;
}
if (import.meta.vitest) {
const { test, expect } = import.meta.vitest;
test('adds', () => {
expect(sum(1, 2)).toBe(3);
});
}
Enable in config:
test: {
includeSource: ['src/**/*.{js,ts}'],
}
In production builds, the if (import.meta.vitest) block is
tree-shaken away. Useful for tiny utility files where separate test
files feel like overkill - but mainstream test suites should use
separate files for greppability.
For tests that need real browser APIs (vs jsdom approximations):
npm install -D @vitest/browser playwright
// vite.config.ts
test: {
browser: {
enabled: true,
name: 'chromium', // 'chromium' | 'firefox' | 'webkit'
provider: 'playwright',
headless: true,
},
}
Tests run in a real browser instance; tradeoff is speed vs fidelity. For DOM-only assertions, jsdom is faster; for CSS layout / Web API correctness, browser mode catches more.
vitest run --typecheck
Runs tsc --noEmit against test files alongside the test run.
Without --typecheck, TypeScript type errors in tests don't fail
the run.
vitest run --coverage
Output formats configured in coverage.reporter (Step 3).
- run: npm ci
- run: npx vitest run --coverage --reporter=verbose --reporter=junit --outputFile=junit.xml
- uses: codecov/codecov-action@v4
with: { files: ./coverage/lcov.info }
vitest run (not vitest) is required in CI - without run,
Vitest enters watch mode and hangs the runner.
| Anti-pattern | Why it fails | Fix |
|---|---|---|
vitest (no subcommand) in CI | Enters watch mode; CI hangs | vitest run (Step 9) |
globals: true in config | Jest-style global injection; harder to type | Explicit import { test, expect } from 'vitest' (Step 3) |
| In-source tests for non-trivial logic | Hard to grep, mixed with prod code | Separate *.test.ts files for non-trivial (Step 5) |
Skip --typecheck in CI | Type errors in tests bypass | --typecheck flag (Step 7) |
Use provider: 'istanbul' by default | Slower than v8 with no benefit for line coverage | Default v8 (Step 3) |
jest-tests,
mocha-tests,
ava-tests,
jasmine-tests - sister toolstest-code-conventions - cross-plugin: test code hygienenpx claudepluginhub testland/qa --plugin qa-unit-tests-jsProvides CDSS development patterns for drug interaction checking, dose validation, clinical scoring (NEWS2, qSOFA), and alert classification integrated into EMR workflows.