# Vitest test metadata and links

> The probara.* helpers of @probara/vitest-reporter — title, suite, tags, fields, parameters, comments, ignoring an attempt, links and issues — where they work, and what differs from the Jest reporter.

`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:

```ts
// 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.

| Helper                                | What it does                                                                                                                  | Repeated calls     |
| ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | ------------------ |
| `id(id \| ids)`                       | Links existing cases ([linking](https://docs.probara.net/en/developers/ci-integrations/vitest/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](#links-and-issues))                                                                   | Add up             |
| `attach({ name, path \| body, ... })` | Attaches a file; await it ([attachments](https://docs.probara.net/en/developers/ci-integrations/vitest/steps-and-attachments/#probaraattach))         | Each one attaches  |
| `step(title, body?, options?)`        | A step of the result, and of the new case ([steps](https://docs.probara.net/en/developers/ci-integrations/vitest/steps-and-attachments/#probarastep)) | 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](#differences-from-the-jest-api) lists what Vitest changes.

## 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](https://docs.probara.net/en/developers/ci-integrations/vitest/linking/#automation-keys)), so a new title still
matches the same case.

```ts
// 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',
    });
});
```

```json
[
  {
    "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

`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

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

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`:

```ts
// vitest.config.mts
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    tags: [{ name: 'smoke' }],
    reporters: ['default', '@probara/vitest-reporter'],
  },
});
```

```ts
// tests/search.test.ts
import { probara } from '@probara/vitest-reporter';
import { test } from 'vitest';

test('finds a product', { tags: ['smoke'] }, () => {
  probara.tags('search');
});
```

```json
[
  {
    "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

`comment()` writes a line first in the notes of the result, before the error and its stack:

```ts
// 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');
});
```

```json
[
  {
    "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

`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.

```ts
// 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 });
});
```

```json
[
  {
    "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](https://docs.probara.net/en/developers/ci-integrations/vitest/results-and-retries/#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 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.

```ts
// tests/experimental.test.ts
import { probara } from '@probara/vitest-reporter';
import { test } from 'vitest';

test('tries the new checkout', () => {
  probara.ignore();
});

test('pays', () => {});
```

```text
$ 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
```

## 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()`

`probara.link(url, name?)` takes an absolute `http` or `https` URL and an optional name, shown
instead of the URL:

```ts
// 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');
});
```

```json
[
  {
    "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`

`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:

```ts
reporters: [
  'default',
  ['@probara/vitest-reporter', { issueUrlTemplate: 'https://jira.example.com/browse/%s' }],
],
```

```ts
// 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');
});
```

```json
[
  {
    "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.

```ts
// tests/checkout.test.ts
import { probara } from '@probara/vitest-reporter';
import { test } from 'vitest';

test('rounds the total', () => {
  probara.issue('PAY-7');
});
```

```text
$ 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
```

### 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](https://docs.probara.net/en/guides/integrations/jira-traceability/).

## 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:

```ts
// 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', '');
});
```

```text
$ 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
```

## Differences from the Jest API

`probara.*` has the [Jest reporter](https://docs.probara.net/en/developers/ci-integrations/jest/metadata/)'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`](https://docs.probara.net/en/developers/ci-integrations/vitest/sharding/#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

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](https://docs.probara.net/en/developers/ci-integrations/vitest/steps-and-attachments/) ·
[Linking](https://docs.probara.net/en/developers/ci-integrations/vitest/linking/) ·
[Migrating from Qase](https://docs.probara.net/en/developers/ci-integrations/vitest/migrating/#from-qase) · Source:
[metadata.md](https://github.com/cynch-me/probara-sdk/blob/main/packages/vitest-reporter/docs/metadata.md),
[links.md](https://github.com/cynch-me/probara-sdk/blob/main/packages/vitest-reporter/docs/links.md)
