Skip to content

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:

tests/payments.test.ts
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.

HelperWhat it doesRepeated calls
id(id | ids)Links existing cases (linking)Add up
title(text)Title of the case the report createsThe last one wins
suite(title | titles)Suite path of the case the report createsThe last one wins
comment(text)A comment, first in the notes of the result, before the errorThe last one wins
ignore()Leaves this attempt out: it is not reported—
tags(...names)Tags of the case the report createsAdd up, each once
fields(record)Fields of the case the report creates: system fields, custom fields, descriptionMerged by name
parameters(record)Parameters of the result, shown with itMerged 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.

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.

tests/payments.test.ts
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() takes names and values (numbers and booleans become strings):

NameWhat it sets
descriptionThe case description
severity, priority, type, layer, behavior, status, is_flakyThe system fields, by the name of an option (high)
preconditions, postconditionsThe 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.

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.

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.mts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
tags: [{ name: 'smoke' }],
reporters: ['default', '@probara/vitest-reporter'],
},
});
tests/search.test.ts
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() writes a line first in the notes of the result, before the error and its stack:

tests/search.test.ts
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() 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.

tests/users.test.ts
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.each and test.for rows 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, so parameters() is the way to send them.
  • describe.each: call the helpers in each test or its beforeEach; a call in the describe body 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: attempt and repeat tell 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), and project names the Vitest project of a result when the config has more than one. A project of your own wins over Vitest’s; attempt and repeat win over yours.

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.

tests/experimental.test.ts
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-1

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(url, name?) takes an absolute http or https URL and an optional name, shown instead of the URL:

tests/search.test.ts
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(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' }],
],
tests/checkout.test.ts
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 12 becomes PAY%2012), in place of the first %s.
  • The template is an http or https URL with %s; any other value turns reporting off with an error that names issueUrlTemplate.
  • Without a template, the issues are left out, and one warning says so: the result is recorded without them.
tests/checkout.test.ts
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-1
RuleWhat happens
Links and issues of one attemptSent in call order, links and issues together
A URL that is not an absolute http/https URLLeft out, with a warning
A URL over 2048 charactersLeft out, with a warning
A name over 255 charactersCut to 255 characters, with a warning
More than 20 links in a resultThe first 20 are sent, with a warning
A retryStarts 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.

WhereWhat happens
The test body, beforeEach, afterEach, onTestFinishedApplies to the running attempt
A helper function or module the test callsThe same: the helpers find the running test on their own
A test.concurrent testApplies to that test
The module scope, a describe body, beforeAll, afterAllIgnored, 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:

tests/orders.test.ts
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-1

probara.* has the Jest reporter’s names, arguments, merge rules and messages. Vitest changes the following:

In the Vitest reporterWhy
Nothing goes in the config besides the reporter: no setupFilesAfterEnv, not even for captureOutput or runCasesOnlyVitest 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 endsIn 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 warningVitest’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 logThe 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 caseJest 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 valuesThe 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 sentThe 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 servesA browser cannot read a file: Vitest’s Node side reads it, under Vite’s server.fs rules

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.probara is 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 runner Node cannot import (.jsx, .tsx, or .ts on 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 its test.concurrent tests are dropped, with a warning.

Related: Steps and attachments · Linking · Migrating from Qase · Source: metadata.md, links.md