Vitest
This guide covers Vitest 3.0 or newer. You need Node.js 22.12 or newer to run test-inventory and an existing JavaScript or TypeScript test suite. The scanner reads your source without executing your tests or starting a browser. These are minimum supported versions; CI comparisons list tested versions.
Choose another framework · Use an AI client
Install
npm install --save-dev test-inventoryScan your suite
Run from the folder containing your project's package.json:
npx test-inventory --framework vitest --pretty --output inventory.jsonTo select files explicitly, use quoted globs. This example matches the order-service fixture used below; adjust the globs to your suite's layout:
npx test-inventory "tests/**/*.test.ts" --framework vitest --output inventory.jsonThe command replaces inventory.json once the new output is complete. It does not change your test files.
Check the result
Print the counts and diagnostics from the saved file:
node -e "const i = require('./inventory.json'); console.log(i.summary); console.log(i.diagnostics)"For matching tests, summary.fileCount and summary.testCount should be greater than zero. Review diagnostics even when the command exits with 0: errors can mean tests are missing. If the result is empty or unexpected, use troubleshooting.
The inventory describes source declarations, not execution results. Conditional skips are not proof that a run skipped the test. Loops and tables are one record by default; supported cases can be expanded with --expand-cases. Expected-to-fail tests execute and check for failure; they are not disabled tests.
Tests created by unreadable helpers can be missing without a diagnostic. A dependency hash does not prove complete coverage of runtime dependencies. Read the shared limits before using scans to make CI decisions.
Find skipped tests
npx test-inventory --framework vitest --state skip --fields fullName,state,condition,reason --prettyThis uses the same file selection as the first scan. If you supplied globs, --root or --framework-version, repeat those options here.
This selects source declarations marked skip; it is not a report of a test run. See query recipes for other inventory queries.
Example
order-service is an HTTP service that sends orders to a broker, tested with Vitest. Its repository tests need a database and skip themselves without one:
// tests/repository/orders-repository.test.ts
describe.skipIf(!process.env.DATABASE_URL)('orders repository', { tags: ['db'] }, () => {
// ...
test('stores an order with its broker id', async () => {npx test-inventory "tests/**/*.test.ts" --framework vitest --output inventory.jsonEach test in the describe gets the describe's condition and tags. stateSource and stateLine say where the state comes from:
{
"fullName": "orders repository > stores an order with its broker id",
"state": "skip",
"isConditional": true,
"condition": "!process.env.DATABASE_URL",
"stateSource": "suite",
"stateLine": 5,
"tags": ["db"]
}The project's Vitest config defines the db tag with only a description, so the tag changes nothing else about these tests. A tag the config doesn't define would fail the file, and the scan would report it as undefined-tag.
A skip from the test context is read as well, with its condition and note:
// tests/risk/daily-loss-limit.test.ts
test('resets the loss at the start of the next session', ({ skip }) => {
skip(process.env.TZ !== 'America/New_York', 'session boundaries are checked in New York time');{
"fullName": "daily loss limit > resets the loss at the start of the next session",
"state": "skip",
"condition": "process.env.TZ !== 'America/New_York'",
"reason": "session boundaries are checked in New York time",
"stateSource": "test",
"stateLine": 14,
"isParallel": true
}A known bug marked with test.fails is expectedToFail, and the comment above it is kept:
// tests/broker/fill-events.test.ts
// BUG: TRD-588 fractional partial fills are rounded down to whole shares
test.fails('keeps the fractional shares of a partial fill', () => {{
"fullName": "filledShares > keeps the fractional shares of a partial fill",
"state": "expectedToFail",
"comments": ["BUG: TRD-588 fractional partial fills are rounded down to whole shares"]
}Other Vitest examples: React, Vue, browser mode, in-source and type tests, monorepos.
Configuration and limits
The scan finds vitest.config.*, or vite.config.* when absent, in the test file's folder and its ancestors. A config elsewhere that reaches the file is not found from it. It reads file and project selection, test.globals, run settings, modes, allowOnly and defined tags. A Vitest 3 project list in vitest.workspace.* or test.workspace is not read, so the config is taken to run the file.
A tag whose definition cannot be worked out produces a config-may-change-state warning on files using tags: the tag may skip a test or change its settings. vi.setConfig() and annotate() in hooks are not read.
resolve.alias and test.alias are followed for imports and dependency hashes. mergeConfig puts overriding list aliases first, converting object aliases to lists when either side is a list; two objects keep overriding values. Config paths built with string + or a template over __dirname or import.meta.dirname are read.
Tables and case titles
Vitest's titles follow the rules of the project's Vitest version: %s, %d, %i, %f, %j, %o, %O, %c, %#, %$ and $name, with values cut as the config of the project that runs the file cuts them: taskTitleValueFormatTruncate, or before Vitest 5 chaiConfig.truncateThreshold, and 40 characters, Vitest's default, when it sets neither. A table stays one record, with a cases-not-expanded warning, when the scan can't work out a value in it or its title, or the title would show an array or object.
Loops over var are not expanded because the variable holds its last value by the time Vitest runs describes. Include source files when scanning in-source tests, and *.test-d.ts files for type tests. See the in-source and type-test example.
Mocks, snapshots and dependency hashes
Automatic mocks in __mocks__ are not followed. A module named by vi.mock() enters the hash even when a mock replaces it. Nondefault snapshot paths from resolveSnapshotPath, snapshotEnvironment or browser screenshot settings give snapshot tests a dependency-not-followed warning at that setting. Unreadable projects, such as a Vitest 3 workspace, mark tests with hasUnfollowedDependencies; the config's warning identifies the cause.
Vitest keeps one .snap file for a spec, so changing one snapshot changes the hash of every test in the spec using toMatchSnapshot(). A custom matcher calling Snapshots.toMatchSnapshot.call(this, received) includes the snapshot file for every test that reaches it.
Exact file selection
For Vitest: the files under test.dir, test.root or root that include matches and exclude doesn't, with each version's defaults, includeSource files that hold in-source tests, typecheck.include when typecheck.enabled is on, and benchmark.include, whose files are marked isBenchmark. A project written in test.projects inherits the root config as each version decides, or the config its extends names. It runs from the config's root unless it sets its own, which Vitest 5 resolves from the config's root and older versions from the config's folder. A config file or folder it names is a project of its own, a folder without a config gets Vitest's defaults, and in Vitest 5 a config that lists projects runs only those.
Supported syntax
Vitest 3.0 and newer.
Where test comes from
| Form | Example |
|---|---|
| Named import, also renamed | import { test, it, describe, suite } from 'vitest' |
| Namespace import | import * as vt from 'vitest' then vt.test(...) |
| Globals | describe, it, test and suite used without an import or declaration. A warning says when no config turns globals on. |
| In-source tests | const { it, describe } = import.meta.vitest |
| Describe callback | describe('fees', (test) => { test(...) }): Vitest passes its test to the callback |
| Derived | test.extend({...}), test.extend('name', value), chained; test.override() and test.scoped() are known |
| Local fixtures file | import { test } from './context', or a path alias, # import or workspace package, followed through re-exports, extend chains and functions that build it, across files |
| Returned by a function | onNode(22)('reads the order book', ...), also kept in a variable, where onNode is declared in the file or a local file and returns a test or describe function, as in it.skip |
| Destructured members | const { describe, skip, beforeEach } = test: each works as test.describe or test.skip does. Some members read the function they are called on, so a call through one destructured fails the file, reported as invalid-chain: each, for, skipIf and runIf of test and each of describe in every version, extend, scoped and override from Vitest 4.1, and for of describe from Vitest 5 |
| Kept in a variable | const onCi = process.env.CI ? test.skip : test: only what both branches share applies, also to a choice written in place, as (process.env.CI ? test.skip : test)(...) |
Calls
| Call | Read as |
|---|---|
test(title, fn), test(title, options, fn), test(title, fn, timeout) | A test. Without a function, a todo. |
test(title, fn, options) | Accepted before Vitest 4; a removed-api error from Vitest 4. |
.concurrent, .only, .skip, .todo, .fails, before a called .each or .for | .only wins over .skip, which wins over .todo. |
.each(table), .for(table), also as tagged templates | One parameterized record, with caseCount when the scan can work out the table; one record per case with --expand-cases. |
.skipIf(condition), .runIf(condition) | A conditional skip. runIf(x) is stored as the condition !(x). |
describe with .concurrent, .shuffle, .skip, .only, .todo, .each, .for, .skipIf, .runIf | A describe. describe(title) without a function is a todo. |
describe(fn) without a title | A describe-not-called error: Vitest never calls the function. |
context.skip(), skip(note), skip(condition, note) in a test, beforeEach or aroundEach | A state for that test, or for the tests in the describe. |
Options skip, only, todo, fails, concurrent, sequential, shuffle, tags (4.1), meta | Read when written in the file; a test's own states win over its chain. |
// @module-tag name | A tag on every test in the file, from Vitest 4.1. |
.sequential | The default mode before Vitest 5; a removed-api error from Vitest 5. |
bench(...), with bench imported from vitest | A removed-api error from Vitest 5. Benchmarks are never tests. |
vi.mock, vi.unmock, vi.hoisted below the top level of the file | A removed-api error from Vitest 5, except in files with in-source tests. |
Any other chain, such as test.fixme | An invalid-chain error. |
The shared syntax rules also apply.
Next
- Shared limits: imports, generated tests, conditions and dependency coverage.
- Key concepts: counts, states, IDs and hashes.
- Output reference and diagnostics: interpret every field and warning.
- CLI, library and MCP: use the same inventory through each interface.
- Compare with Vitest: differences from runner collection and execution reports.