Vitest test metadata and links
probara tells the reporter more about a test, from the test body, its beforeEach and
afterEach hooks, or onTestFinished. Import it from the reporter’s package:
import { probara } from '@probara/vitest-reporter';import { test } from 'vitest';
test('pays with a card', () => { probara.title('Pays with a saved card').suite(['Payments', 'Cards']).tags('smoke');});Nothing needs setting up: the reporter adds what the helpers need to every Vitest project, in the threads and forks pools and in browser mode.
| Helper | What it does | Repeated calls |
|---|---|---|
id(id | ids) | Links existing cases (linking) | Add up |
title(text) | Title of the case the report creates | The last one wins |
suite(title | titles) | Suite path of the case the report creates | The last one wins |
comment(text) | A comment, first in the notes of the result, before the error | The last one wins |
ignore() | Leaves this attempt out: it is not reported | — |
tags(...names) | Tags of the case the report creates | Add up, each once |
fields(record) | Fields of the case the report creates: system fields, custom fields, description | Merged by name |
parameters(record) | Parameters of the result, shown with it | Merged by name |
link(url, name?), issue(id) | Links of the result (links and issues) | Add up |
attach({ name, path | body, ... }) | Attaches a file; await it (attachments) | Each one attaches |
step(title, body?, options?) | A step of the result, and of the new case (steps) | Each one is a step |
The helpers that return nothing else return probara, so calls chain. Each applies to the running
attempt only: a retry starts empty. The names, arguments and rules are the Jest reporter’s;
differences from the Jest API lists what Vitest changes.
Created cases only
Section titled “Created cases only”title, suite, tags, fields and the case steps describe the case a report creates, the
first time a test is reported. An existing case never changes: Probara keeps what people wrote
there. The automation key never changes either
(keys), so a new title still
matches the same case.
import { probara } from '@probara/vitest-reporter';import { test } from 'vitest';
test('pays', () => { probara .title('Pays with a saved card') .suite(['Payments', 'Cards']) .tags('smoke', 'payments') .fields({ description: 'A returning customer pays with the card saved in their profile.', severity: 'critical', priority: 'high', 'Risk area': 'Checkout', });});[ { "automationKey": "tests/payments.test.ts > pays", "title": "Pays with a saved card", "suitePath": ["Payments", "Cards"], "case": { "description": "A returning customer pays with the card saved in their profile.", "tags": ["smoke", "payments"], "fields": { "severity": "critical", "priority": "high", "Risk area": "Checkout" } } }]Fields
Section titled “Fields”fields() takes names and values (numbers and booleans become strings):
| Name | What it sets |
|---|---|
description | The case description |
severity, priority, type, layer, behavior, status, is_flaky | The system fields, by the name of an option (high) |
preconditions, postconditions | The system text fields |
Any other name, such as 'Risk area' | The custom field of that name |
Probara resolves names and options when it creates the case. A field or an option it does not know is skipped and logged as a warning; the result is recorded all the same. A field with an empty value is skipped silently.
Title and suite
Section titled “Title and suite”The title of a created case is the test title (case ids removed) unless title() says otherwise.
Its suite is the test file (tests/payments.test.ts), or, with keyIncludesFile: false, the
outermost describe, unless suite() gives one: suite('Payments') or
suite(['Payments', 'Cards']) for nested suites, created as needed under suiteUlid.
Vitest tags
Section titled “Vitest tags”The tags of a test (Vitest 4.1 and later) join the tags of the case the report creates, after
those of probara.tags(), each once. Vitest refuses a tag its config does not declare, so declare
each one in test.tags:
// vitest.config.mtsimport { defineConfig } from 'vitest/config';
export default defineConfig({ test: { tags: [{ name: 'smoke' }], reporters: ['default', '@probara/vitest-reporter'], },});import { probara } from '@probara/vitest-reporter';import { test } from 'vitest';
test('finds a product', { tags: ['smoke'] }, () => { probara.tags('search');});[ { "automationKey": "tests/search.test.ts > finds a product", "case": { "tags": ["search", "smoke"] } }]Like the tags of probara.tags(), they go to a created case only: never to a result, never into
the automation key, so adding a tag to a test never moves it to another case.
Comment
Section titled “Comment”comment() writes a line first in the notes of the result, before the error and its stack:
import { probara } from '@probara/vitest-reporter';import { test } from 'vitest';
test('searches the catalog', () => { probara.comment('Ran against the staging catalog');});[ { "automationKey": "tests/search.test.ts > searches the catalog", "notes": "Ran against the staging catalog" }]When the attempt fails, its errors follow the comment in the same notes, without the terminal’s
colors: the comment never hides an error. Vitest’s own context.annotate() messages are not sent
as notes: comment() is the way to write one.
Parameters
Section titled “Parameters”parameters() records the values an attempt ran with, shown with its result, such as the user or
the data set of a data-driven test. They are never part of the key: the test is one case whatever
its parameters. When the case declares a parameter with the same name, the result is recorded on the run case with those values. A parameter the case does not declare stays on the result for display only. The Vitest project, attempt, and repeat are context: they are shown on the result and never become a parameter of a case a report creates.
import { probara } from '@probara/vitest-reporter';import { test } from 'vitest';
test.each(['admin', 'viewer'])('opens the dashboard as %s', (role) => { probara.parameters({ role, seats: 5 });});[ { "automationKey": "tests/users.test.ts > opens the dashboard as admin", "parameters": { "role": "admin", "seats": "5" } }, { "automationKey": "tests/users.test.ts > opens the dashboard as viewer", "parameters": { "role": "viewer", "seats": "5" } }]test.eachandtest.forrows are tests of their own, keyed by the title Vitest formats for them: keep a placeholder in the title (%s,$role), so each row is a case. Vitest does not keep a row’s values anywhere else, soparameters()is the way to send them.describe.each: call the helpers in each test or itsbeforeEach; a call in thedescribebody runs while no test does.- Two tests with the same full name share one key, so one case: each result still keeps what its own helpers said.
- The reporter’s own parameters:
attemptandrepeattell the executions of a retried or repeated test apart. They are context: shown on the result, and they never become a parameter of a case a report creates. (retries and repeats), andprojectnames the Vitest project of a result when the config has more than one. Aprojectof your own wins over Vitest’s;attemptandrepeatwin over yours.
Ignore an attempt
Section titled “Ignore an attempt”ignore() leaves the running attempt out: it still runs, and Vitest reports it, but Probara does
not get it. The count of the log says how many were left out.
import { probara } from '@probara/vitest-reporter';import { test } from 'vitest';
test('tries the new checkout', () => { probara.ignore();});
test('pays', () => {});$ npx vitest run[probara] Sending 1 result of 1 test (1 passed, 0 failed, 0 skipped, 0 blocked); 1 ignored with probara.ignore()[probara] Recorded 1 result (1 new case, 0 unmatched) in R-1 (closed): https://app.probara.net/projects/SHOP/runs/R-1Links and issues
Section titled “Links and issues”A result can carry links, shown with it in Probara: the build that ran it, the page it tested, the
issues of your tracker it is about. probara.link() adds a link; probara.issue() names an issue,
which issueUrlTemplate turns into a link.
probara.link()
Section titled “probara.link()”probara.link(url, name?) takes an absolute http or https URL and an optional name, shown
instead of the URL:
import { probara } from '@probara/vitest-reporter';import { test } from 'vitest';
test('searches the catalog', () => { probara .link('https://shop.example.com/search?q=boots', 'Search page') .link('https://ci.example.com/builds/1042');});[ { "automationKey": "tests/search.test.ts > searches the catalog", "links": [ { "url": "https://shop.example.com/search?q=boots", "name": "Search page" }, { "url": "https://ci.example.com/builds/1042" } ] }]probara.issue() and issueUrlTemplate
Section titled “probara.issue() and issueUrlTemplate”probara.issue(id) names an issue of your tracker by its id. With issueUrlTemplate
(PROBARA_ISSUE_URL_TEMPLATE), a URL with %s where the id goes, it becomes a link of the result,
named by the id:
reporters: [ 'default', ['@probara/vitest-reporter', { issueUrlTemplate: 'https://jira.example.com/browse/%s' }],],import { probara } from '@probara/vitest-reporter';import { test } from 'vitest';
test('rounds the total', () => { probara.issue('PAY-7').issue('PAY 12');});[ { "automationKey": "tests/checkout.test.ts > rounds the total", "links": [ { "url": "https://jira.example.com/browse/PAY-7", "name": "PAY-7" }, { "url": "https://jira.example.com/browse/PAY%2012", "name": "PAY 12" } ] }]- The id goes into the template URL-encoded (
PAY 12becomesPAY%2012), in place of the first%s. - The template is an
httporhttpsURL with%s; any other value turns reporting off with an error that namesissueUrlTemplate. - Without a template, the issues are left out, and one warning says so: the result is recorded without them.
import { probara } from '@probara/vitest-reporter';import { test } from 'vitest';
test('rounds the total', () => { probara.issue('PAY-7');});$ npx vitest run[probara] Dropped the issues of probara.issue(): no issueUrlTemplate turns their ids into links (first seen in the run; repeats are logged at debug)[probara] Sending 1 result of 1 test (1 passed, 0 failed, 0 skipped, 0 blocked)[probara] Recorded 1 result (1 new case, 0 unmatched) in R-1 (closed): https://app.probara.net/projects/SHOP/runs/R-1Link rules
Section titled “Link rules”| Rule | What happens |
|---|---|
| Links and issues of one attempt | Sent in call order, links and issues together |
A URL that is not an absolute http/https URL | Left out, with a warning |
| A URL over 2048 characters | Left out, with a warning |
| A name over 255 characters | Cut to 255 characters, with a warning |
| More than 20 links in a result | The first 20 are sent, with a warning |
| A retry | Starts without links: each attempt sends its own |
Links belong to the result, not to the case: an existing case never changes, and each result keeps the links its attempt added. To link a case itself to a Jira issue, see Traceability with Jira.
Where to call the helpers
Section titled “Where to call the helpers”| Where | What happens |
|---|---|
The test body, beforeEach, afterEach, onTestFinished | Applies to the running attempt |
| A helper function or module the test calls | The same: the helpers find the running test on their own |
A test.concurrent test | Applies to that test |
The module scope, a describe body, beforeAll, afterAll | Ignored, with one warning: no single test runs there |
A helper never throws into the test. A call while no test runs is one warning per helper and test file, which the test file prints in Vitest’s output; a wrong argument (a blank tag here) is one warning of the reporter’s log for the run. Either way what is wrong is ignored and the test runs on:
import { probara } from '@probara/vitest-reporter';import { beforeAll, test } from 'vitest';
beforeAll(() => { probara.tags('orders');});
test('lists the orders', () => { probara.tags('orders', '');});$ npx vitest run[probara] probara.tags() only works while a test runs (in a test, or a beforeEach or afterEach hook)[probara] probara.tags() ignores blank tags (first seen in the run; repeats are logged at debug)[probara] Sending 1 result of 1 test (1 passed, 0 failed, 0 skipped, 0 blocked)[probara] Recorded 1 result (1 new case, 0 unmatched) in R-1 (closed): https://app.probara.net/projects/SHOP/runs/R-1Differences from the Jest API
Section titled “Differences from the Jest API”probara.* has the Jest reporter’s names,
arguments, merge rules and messages. Vitest changes the following:
| In the Vitest reporter | Why |
|---|---|
Nothing goes in the config besides the reporter: no setupFilesAfterEnv, not even for captureOutput or runCasesOnly | Vitest lets a reporter add its setup file and runner to each project before the run |
Await probara.attach(). In Node the file is copied at the call, as in Jest; in browser mode the copy ends later, and one that ends after its test ended is not attached, with a warning that says Await probara.attach() before the test ends | In a browser, Vitest’s Node side reads the file, after the call returns |
A test.concurrent call goes to its own test through the reporter’s runner. Where that runner cannot run (browser mode, a custom runner Node cannot import, such as a .jsx file), it is dropped with one warning | Vitest’s own current test is one per worker, so it can name the wrong concurrent test: a call is dropped rather than recorded in another test |
A call outside a test warns once per helper and test file, in the test file’s output (stderr | tests/orders.test.ts), not in the reporter’s log | The call runs in a Vitest worker, which cannot write to the reporter’s log in the main process |
A wrong argument in a test warns once per run, (first seen in the run; ...) | The warning travels with the test’s result, and the reporter logs each one once |
| Vitest tags (4.1 and later) join the tags of a created case | Jest has no test tags |
A test.each or test.for row is keyed by Vitest’s formatted title, which Vitest 4 and 5 write differently for object values | The key is the name Vitest’s JUnit output gives the test, and that name comes from Vitest’s own formatting |
| Two tests with the same full name each keep their own helper calls (the Jest reporter leaves them out, with a warning) | Vitest keeps the calls in each test’s own metadata, so nothing has to tell two names apart |
A shard run with --reporter=blob alone on the command line records no helper call: name the reporter beside it (the reporter goes beside blob) | --reporter replaces the config’s reporters, so the reporter never starts and adds no setup file |
| With reporting off, the helpers still record, and nothing is sent | The reporter adds its setup file whatever the reporting setup, so the tests run the same with and without a token |
In browser mode the helpers work in the browser, and probara.attach({ path }) copies only a file Vite serves | A browser cannot read a file: Vitest’s Node side reads it, under Vite’s server.fs rules |
How metadata travels
Section titled “How metadata travels”Vitest runs the tests in workers and the reporter in its main process. Each probara.* call is
written into the running attempt’s entry of the test’s own metadata (task.meta.probara), which
Vitest sends to the reporter with the test’s result; the reporter reads it when the run ends.
probara.attach() copies its file at once into a folder under Vitest’s attachmentsDir. Vitest’s
blob keeps each test’s metadata, so a shard’s blob carries the calls to vitest --merge-reports.
Without the reporter in the run’s reporters, every helper does nothing (and probara.step() still
runs its body), so tests keep passing where the reporter is not registered.
Known limits:
task.meta.probarais the reporter’s. Test code can see it (context.task.meta.probara), but its shape is not an API, and writing to it changes what is sent: keep your own metadata under another key.- A custom
runnerNode cannot import (.jsx,.tsx, or.tson a Node without type stripping) is kept as it is, with one warning that names it: its attempts are still recorded, but the calls of itstest.concurrenttests are dropped, with a warning.
Related: Steps and attachments · Linking · Migrating from Qase · Source: metadata.md, links.md