Skip to content

Jest steps and attachments

The steps of a test show under its result in Probara, nested as they ran, each with its status, duration and error. Files attached by the test show on the result, or on the step they were attached in. Jest has no steps or files of its own (no screenshots, no traces): what a result carries is what the test recorded with the probara.* helpers, plus its console output when you turn that on.

probara.step(title, body, options) runs body, records it as a step, and returns what the body returns, so a step can compute a value the test uses (cart below):

tests/checkout.test.js
const { probara } = require('@probara/jest-reporter');
test('checks out', async () => {
const cart = await probara.step('Open the cart', async () => ({ items: 2 }));
await probara.step('Pay', async () => {
probara.step('Enter the card', () => {
expect(cart.items).toBe(2);
});
await probara.step('Confirm', async () => {
expect(await Promise.resolve('declined')).toBe('paid');
});
});
});
[
{
"status": "failed",
"steps": [
{ "action": "Open the cart", "status": "passed" },
{
"action": "Pay",
"status": "failed",
"steps": [
{ "action": "Enter the card", "status": "passed" },
{ "action": "Confirm", "status": "failed" }
]
}
]
}
]
The bodyThe stepWhat probara.step() does
ReturnspassedReturns its value
Throwsfailed, with the error’s message and stack; its parents failThrows the same error: the test fails as usual
Returns a promise (async)Ends when the promise settles: passed, or failedReturns a promise that settles the same way
Returns another thenableEnds when the body returns (see below)Returns it untouched
None (probara.step('Seeded'))passed, with no durationReturns undefined
  • Nesting follows the calls: a step started inside another step’s body is its child, across await too. Two steps of the same parent run with Promise.all are both its children.
  • Duration is measured with Node’s own clock, so Jest’s fake timers (jest.useFakeTimers()) never change it.
  • A step that never ended (a promise the test did not await, still pending when the test ends) is failed, with the error The step had not finished when the test ended. Await every probara.step() that has an async body.
  • Hooks: a step in beforeEach or afterEach is a step of the running test. In beforeAll, afterAll or a describe body no single test runs: the body runs, and the step is not recorded.

A result’s steps record what ran. A case’s steps are its specification: an action, the expected result and the test data. The outermost steps of an attempt, in the order they started, are the steps of the case the report creates; expected and data go with them:

tests/refund.test.js
const { probara } = require('@probara/jest-reporter');
test('refunds an order', () => {
probara.step('Open the order', () => {}, { expected: 'The order is shown', data: 'order=1042' });
probara.step(
'Refund it',
() => {
probara.step('Confirm the dialog', () => {});
},
{ expected: 'The order is refunded' },
);
});
[
{
"steps": [
{ "action": "Open the order", "expected": "The order is shown", "data": "order=1042" },
{
"action": "Refund it",
"expected": "The order is refunded",
"steps": [{ "action": "Confirm the dialog" }]
}
],
"case": {
"steps": [
{ "action": "Open the order", "expected": "The order is shown", "data": "order=1042" },
{ "action": "Refund it", "expected": "The order is refunded" }
]
}
}
]
  • Nested steps (Confirm the dialog) are steps of the result only: how a test works, not what it specifies.
  • Like every case field, case steps apply only when the report creates the case (created cases only). An existing case keeps its own steps.
  • expected and data are strings: a step given anything else is recorded with its title only, and one warning says so.

A body that returns a promise ends its step when the promise settles. probara.step() returns a new promise that settles the same way, so await probara.step(...) behaves as awaiting the body. A rejection the test never handles is still an unhandled rejection, which fails the test, as it would without the step.

A body can also return a thenable that is not a promise: a supertest request, a query builder. Calling its then() would start it (or start it twice), so probara.step() returns it untouched, and the step ends as soon as the body returns, before the request is sent. To time the request inside the step, await it in an async body:

tests/api.test.js
const { probara } = require('@probara/jest-reporter');
// A stand-in for a supertest request: it runs when awaited.
const request = () => ({ then: (resolve) => resolve({ status: 200 }) });
test('answers the health check', async () => {
const response = await probara.step('Call /health', async () => await request());
expect(response.status).toBe(200);
});
[{ "status": "passed", "steps": [{ "action": "Call /health", "status": "passed" }] }]

Give it a file on disk or a body. It works from anywhere a test runs: the test body, a beforeEach or afterEach hook, a helper or a page object the test calls.

tests/receipt.test.js
const { writeFileSync } = require('node:fs');
const { tmpdir } = require('node:os');
const { join } = require('node:path');
const { probara } = require('@probara/jest-reporter');
test('prints the receipt', () => {
const path = join(tmpdir(), 'receipt.txt');
writeFileSync(path, 'Total: 25.00');
probara.attach({ name: 'receipt', path });
probara.attach({
name: 'order',
body: JSON.stringify({ id: 1042, total: 25 }),
contentType: 'application/json',
});
probara.attach({ name: 'log', body: 'paid in 1.2 s' });
});

The result gets three files:

receipt.txt text/plain
order.json application/json
log.txt text/plain
FieldWith pathWith body
nameRequired: the name in Probara; the file’s extension is added when it has noneRequired: the name in Probara; the content type’s extension is added when it has none
pathThe file, absolute or relative to the directory jest runs in—
body—A string, a Buffer or a Uint8Array, sent as it is
contentTypeOptional: guessed from the file nameOptional: guessed from name’s extension, else text/plain for a string, application/octet-stream for bytes
  • Copied when called. The file (or the body) is copied at once, so a test can delete or overwrite it right after: what Probara gets is what it was at the call. probara.attach() returns a promise that is already resolved; awaiting it is harmless.
  • A string is sent as it is, never decoded: pass a Buffer for binary content.
  • A file that cannot be attached (it does not exist, say) is a [probara] warning in the reporter’s log; the test goes on.
  • Each attempt has its own files: a retried test uploads the files of every attempt, each to its own result.

A file attached while a probara.step() runs (from the step’s body or anything it calls) goes to that step, and Probara shows it under the step:

tests/upload.test.js
const { probara } = require('@probara/jest-reporter');
test('uploads an avatar', async () => {
await probara.step('Choose the file', async () => {
probara.attach({ name: 'avatar.png', body: Buffer.from([137, 80, 78, 71]) });
});
});
avatar.png image/png

With captureOutput: true (PROBARA_CAPTURE_OUTPUT=true) and the reporter’s setup file, each attempt’s console output is attached as stdout.log (console.log, console.info, console.debug) and stderr.log (console.warn, console.error), when there is any, its beforeEach hooks and the afterEach hooks of its describe blocks included:

jest.config.js
module.exports = {
reporters: ['default', ['@probara/jest-reporter', { captureOutput: true }]],
setupFilesAfterEnv: ['@probara/jest-reporter/setup'],
};
tests/logs.test.js
beforeEach(() => {
console.info('Seeding the cart');
});
test('prints what it does', () => {
console.log('Opening the cart');
console.error('Slow response: 2.1 s');
});
stdout.log text/plain
stderr.log text/plain

Jest still prints the output as usual. The setup file wraps the console of each test file, in every worker; without it, the tests run and are reported without their output, with one warning:

[probara] captureOutput needs the setup file: add setupFilesAfterEnv: ['@probara/jest-reporter/setup'] to the Jest config (first seen in tests/checkout.test.js; repeats are logged at debug)
Not capturedWhy
console.dir, table, group, count, time, timeEnd, trace, assertOnly the five methods above are wrapped
process.stdout.write, process.stderr.writeThey bypass the console
Output of test.concurrent testsTheir bodies run outside the beforeEach/afterEach of each test
Output of a top-level afterEach of the test fileThe setup file’s own afterEach, which ends the capture, runs first
A console method the test mocked (jest.spyOn(console, 'log').mockImplementation(...), console.log = jest.fn())The mock receives the calls; a spy that calls through is captured
Output in beforeAll, afterAll, a describe body or the module scopeNo single test runs there

Each stream is cut at 32 MiB, the largest file Probara takes, with a last line that says it was cut. With Jest projects, give every project that needs it the setup file (Jest projects).

LimitWhat happens
32 MiB per fileA larger file is skipped with a warning
20 files per resultThe files after the first 20 are skipped, with one warning
Images over 10 MiB or 8192 px wide or tallSkipped with a warning (Probara converts images to WebP)
Executables and scriptsSkipped: Probara refuses their content types
A missing, unreadable or empty fileSkipped with a warning
A result that recorded nothingIts files are not uploaded

Warnings name the file, and the log ends with how many files were attached, skipped and failed. uploadAttachments: false (PROBARA_UPLOAD_ATTACHMENTS=false) uploads none. Every result with files costs two more requests against the organization’s rate limit.

Files upload after their report is recorded, while the run is still open, then the run is closed. Until then, the reporter keeps its copies in a folder of the system’s temporary directory, removed when jest ends; with a results file, the copies of what was not sent are kept next to it.

  • A step is missing. Only probara.step() makes steps: expect calls and helper functions do not. A step called outside a test (beforeAll, a describe body) runs its body and records nothing, with a warning.
  • Steps are flattened, or fail with The step had not finished. A step started without await ends after the test: await every probara.step() whose body is async.
  • The case has no steps. Only a case the report creates takes steps. An existing case keeps its own.
  • A file is missing. The log names every file it skipped and why. A file attached inside a step goes to that step, not to the result (troubleshooting).

Related: Metadata and links · Configuration · Troubleshooting · Source: steps.md, attachments.md