Skip to content

Early Adopter Offer:Get 40% off Core & Scale for your first year with code EARLYQFView pricing

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-reports can 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 collect reads it once — no --merge-reports pass, 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 jobsYesblob + --merge-reportsYesno merge step
Flags a test that flaked in this runYesYes
History across CI runsYes
Flaky scoring over timeYes
AI failure clustering (root cause)Yes
Steps, attachments & labelsYes
Local, zero-setup, offlineYes

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.

Get Started Free

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.