Vitest test reporting
Vitest’s reporters are good at telling you about this run — and that’s where they stop.
Qualflare has a
native Vitest reporter
that turns those runs into hosted, historical reporting: AI clusters
failures by root cause, flaky scores come from run history rather than a single retry, and every
--shard lands in one launch — with steps, attachments
and labels Vitest has no way to record.
Vitest’s built-in reporters, explained
Vitest ships more reporting out of the box than most runners — default,
verbose, dot,
json, junit,
html, tap,
github-actions and
blob, and you can pass several at once:
npx vitest run # default reporter: a live summary, then it's gone
npx vitest run --reporter=junit # JUnit XML, for CI plugins that read it
npx vitest run --reporter=json # Jest-compatible JSON
npx vitest run --coverage # a different question entirely json. Jest-compatible JSON — the format most tools that “support Jest” will already read. Carries status and duration per test, and not much else.junit. JUnit XML, for CI plugins that display it natively. The oldest common denominator, and correspondingly the least detailed.html(with @vitest/ui). A browsable local report — genuinely nice for inspecting one run on your own machine. Like any local file, it doesn’t survive to the next one.blob. The interesting one. It writes a machine-readable blob per run so that--merge-reportscan combine shards afterwards — Vitest solving sharded CI natively, which most runners don’t.github-actions. Emits workflow annotations so failures appear inline on the PR diff. Scoped to one provider and one run, by design.--coverage. Worth separating out: coverage is how much of your code ran, which is a different question from what passed and what failed.
Where the built-in reporters stop
Credit where it’s due — blob plus
--merge-reports really does merge a sharded run, and
that’s a feature many runners lack entirely:
# Vitest can merge its own shards — worth knowing before you reach for a tool
npx vitest run --shard=1/3 --reporter=blob # each shard writes to .vitest/blob/
npx vitest run --shard=2/3 --reporter=blob
npx vitest run --shard=3/3 --reporter=blob
npx vitest run --merge-reports # one report, after the fact
But a merged report is still one run. It is written to disk, read once, and replaced by the
next one. Nothing accumulates, so “has checkout > expired card
been getting flakier this month”, “which failures share a root cause”, and “is this suite slower than it
was in March” are not questions any built-in reporter can answer — not because they’re poorly built, but
because answering them requires storing results over time and analyzing them.
Send Vitest results to Qualflare
Add the reporter alongside default, so you keep the
console output you already read:
// vitest.config.ts
import { defineConfig } from 'vitest/config';
import { qualflareReporter } from '@qualflare/vitest';
export default defineConfig({
test: {
reporters: [['default'], qualflareReporter({ environment: 'staging' })],
},
}); Then run and upload. The reporter makes no network calls and holds no credential — it writes a directory, and the CLI uploads it:
npm install --save-dev @qualflare/vitest
npx vitest run # writes ./qualflare-results, zero network calls
qf my-project collect ./qualflare-results # uploads it In CI that’s one added step (GitHub Actions shown — GitLab CI, Bitbucket Pipelines, and Jenkins work the same way). Authenticate the CLI once with your Qualflare access token, stored as a CI secret — see the CLI docs.
# .github/workflows/tests.yml
- name: Run tests
run: npx vitest run
- name: Upload results to Qualflare
if: always() # upload even when tests fail — that's the point
run: qf my-project collect ./qualflare-results
Don’t want to add a reporter? Vitest’s built-in JSON works:
npx vitest run --reporter=json, then
qf my-project collect results.json --format vitest. You
get status, duration and name; the native reporter is what adds retry history, steps, attachments and
metadata.
What you get on top of Vitest
- AI failure clustering. When one broken mock or a downed dependency takes out 40 tests, Qualflare groups them by root cause — so you fix one thing instead of reading 40 stack traces.
- Flaky detection from history. Vitest can tell you a test flaked in this run. Qualflare scores it across every run, which catches the tests that are intermittent but happened not to flake today.
- Sharding with no merge step. Every shard writes into one directory and
qf collectreads it once — no--merge-reportspass, and shard attribution is automatic. - Steps, attachments and labels. Vitest has no
test.step()and no notion of a label or a linked issue. The reporter adds all of it. - Assertion diffs preserved. The expected/received diff Vitest prints in your terminal reaches the report, rather than being flattened into a message string.
- History, trends & defects. Pass rate, slowest tests, and flakiness over time across branches — plus a defect you can open straight from a failing run.
Built-in reporters vs Qualflare
| Vitest built-ins | Qualflare | |
|---|---|---|
| Merges sharded CI jobs | Yesblob + --merge-reports | Yesno merge step |
| Flags a test that flaked in this run | Yes | Yes |
| History across CI runs | — | Yes |
| Flaky scoring over time | — | Yes |
| AI failure clustering (root cause) | — | Yes |
| Steps, attachments & labels | — | Yes |
| Local, zero-setup, offline | Yes | — |
Complementary: keep the default reporter for local runs,
add Qualflare for hosted, historical CI observability.
Get AI analysis on your Vitest runs
Start free — add the reporter, run qf collect, and get your first AI analysis in minutes.
Qualflare works the same with Jest, Playwright, Cypress, pytest, Go, JUnit and 20+ more frameworks. Testing a mobile app too? See our Android (Espresso), iOS (XCTest), and Maestro guides. Weighing tools? See how it compares to other test management platforms, or browse all framework reporting guides. Every reporter is open source.
Retries, sharding, and the missing test.step()
Three Vitest behaviors shape what your reports look like. First,
retries produce Vitest’s own flaky signal. Set
retry and a test that fails then passes is marked flaky
in that run, and that per-attempt history reaches the report:
// vitest.config.ts — Vitest marks a test that passed on a retry as flaky
export default defineConfig({
test: { retry: 2 },
});
It’s a real signal, but a narrow one — it only ever fires for tests that happened to flake while you were
watching. History-based scoring is what catches the rest, which is why retries are worth treating as a
supplement to it rather than a substitute. One caveat worth knowing: Vitest exposes no per-attempt array,
so attempt history is reconstructed from its accumulated error list, and a test using
expect.soft() reports none.
Second, sharding needs no merge step. Point every shard at the
same output directory and collect once; each process writes a uniquely named file, so shards never
overwrite each other. Vitest hands reporters its own --shard
value, so cases are attributed to the worker that ran them with nothing to configure — including the
1-based to 0-based index conversion, which is handled for you:
# With the reporter, shards need no merge step — they write to one directory
npx vitest run --shard="$SHARD_INDEX/$SHARD_TOTAL"
# once, after every shard finishes
qf my-project collect ./qualflare-results
Third, Vitest has no test.step().
Playwright users reach for one and find it missing; there is no built-in way to say “this test did four
things and the third one is what broke”. qualflare.step()
is the only way to get that structure into a report, and steps nest:
import { expect, test } from 'vitest';
import { qualflare } from '@qualflare/vitest';
test('a user can check out', async () => {
qualflare.label('epic', 'Billing');
qualflare.link('https://tracker.example/QF-42', { type: 'issue', name: 'QF-42' });
qualflare.tag('smoke');
await qualflare.step('add an item to the cart', () => {
qualflare.parameter('sku', 'BOOK-1');
expect(cart.items).toHaveLength(1);
});
});
Metadata travels on task.meta, the channel Vitest already
uses to serialise data from the test worker back to the reporter — so these are ordinary synchronous calls
with nothing to await and no promise that can reject into your run. Full reference in the
metadata API docs.
Frequently asked questions
How do I send Vitest results to Qualflare?
Install @qualflare/vitest and add qualflareReporter() to the reporters array in vitest.config.ts. Running npx vitest run then writes a ./qualflare-results directory — the reporter makes no network calls — and qf my-project collect ./qualflare-results uploads it. If you would rather not add a reporter, Vitest’s built-in JSON output works too: npx vitest run --reporter=json, then qf my-project collect results.json --format vitest.
Which Vitest versions are supported?
Vitest 3.0 and newer. The reporter is built on the Reported Tasks API (onTestCaseResult, onTestRunEnd, TestCase), which does not exist before 3.0, so Vitest 2 and earlier are not supported. Versions 3.0, 3.2 and 4.x are exercised in CI against a real vitest run. The peer range is deliberately open-ended so a new Vitest release never hard-blocks npm install.
Does Vitest already merge sharded runs? Why use Qualflare?
Yes — Vitest’s blob reporter plus --merge-reports genuinely merges shards, and it is a good feature. The difference is what happens next: a merged report is still one run, written to disk and replaced by the next one. Qualflare keeps every run, so it can score flakiness from history, cluster failures by root cause across runs, and show whether a suite is getting slower. With the reporter you also skip the merge step, since every shard writes into one directory that qf collect reads.
Does Qualflare detect flaky Vitest tests?
Yes, and from two sources. Vitest marks a test flaky when it fails and then passes within the same run under the retry option, and that per-attempt history is captured. More importantly, Qualflare scores each test from its pass/fail record across every run — which catches the tests that are intermittent but happened not to flake today, and which in-run retries tend to mask rather than expose.
Does Vitest have test steps?
No. Unlike Playwright, Vitest has no test.step() of its own, so qualflare.step() is the only way to get step structure into a report. Steps nest, and timing is real elapsed time around the awaited body. Metadata travels on task.meta — the channel Vitest already uses to serialise data from the test worker back to the reporter — so the calls are ordinary synchronous functions with nothing to await and no promise that can reject into your run.
Does it work in GitHub Actions and GitLab CI?
Yes. Add a step after your test run that calls qf collect on the output directory, guarded with if: always() so failing runs still upload. The CLI auto-attaches the Git branch and commit, so each CI run becomes a tracked launch. Vitest hands reporters its own --shard value, so cases are attributed to the shard that ran them without any configuration. The same flow works in GitLab CI, Bitbucket Pipelines, and Jenkins.
Setup reflects @qualflare/vitest and the Qualflare CLI (docs.qualflare.com) as of September 2026. Flags were checked against Vitest 3.2.7 and the current Vitest docs. Written by İbrahim Süren, Qualflare.