Ir al contenido

Pasos y adjuntos en Vitest

Los pasos de una prueba se muestran bajo su resultado en Probara, anidados tal como se ejecutaron, cada uno con su estado, su duración y su error. Vitest no tiene pasos propios: escríbelos con probara.step(), que ejecuta su cuerpo y lo registra. Un resultado lleva también los archivos que su intento adjuntó con probara.attach() y con el propio context.annotate() de Vitest, las capturas de pantalla que Vitest toma de un fallo en modo navegador y, con captureOutput, la salida de consola del intento.

tests/checkout.test.ts
import { probara } from '@probara/vitest-reporter';
import { expect, test } from 'vitest';
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" }
]
}
]
}
]

probara.step(title, body, options) ejecuta body y devuelve lo que este devuelve, así que un paso puede calcular un valor que use la prueba (cart, arriba):

El cuerpoEl pasoQué hace probara.step()
Devuelve un valorpassedDevuelve su valor
Lanza un errorfailed, con el mensaje y la traza del errorLanza el mismo error: la prueba falla como siempre
Devuelve una promesa (async)Termina cuando la promesa se resuelve: passed, o failedDevuelve una promesa que se resuelve igual
Devuelve otro thenableTermina cuando vuelve el cuerpo (ver abajo)Lo devuelve intacto
Ninguno (probara.step('Seeded'))passed, sin duraciónDevuelve undefined
  • El anidamiento sigue a las llamadas: un paso que empieza dentro del cuerpo de otro es su hijo, también a través de await. Dos pasos del mismo padre ejecutados con Promise.all son ambos sus hijos. Un error que sale del cuerpo de un paso hace fallar el paso, y también cada paso padre del que sale.
  • Un paso que nunca terminó (una promesa que la prueba no esperó, todavía pendiente al acabar la prueba) queda failed, con el error The step had not finished when the test ended. Espera con await cada probara.step() que tenga un cuerpo async.
  • Hooks: un paso en beforeEach o afterEach es un paso de la prueba en curso. En beforeAll, afterAll o el cuerpo de un describe no corre una sola prueba: el cuerpo se ejecuta, y el paso no se registra (dónde llamar a los helpers).
  • Reintentos: cada intento registra sus propios pasos; un reintento empieza sin ninguno (reintentos y repeticiones).
  • Modo navegador: un navegador no tiene un contexto asíncrono que seguir, así que un paso solo queda anidado bajo otro cuando empieza en la parte síncrona del cuerpo del otro. Un paso que empieza después de un await en un cuerpo no es hijo de ese paso.

Los pasos de un resultado registran lo que se ejecutó. Los pasos de un caso son su especificación: una acción, el resultado esperado y los datos de prueba. Los pasos más externos de un intento, en el orden en que empezaron, son los pasos del caso que crea el informe; expected y data van con ellos:

tests/refund.test.ts
import { probara } from '@probara/vitest-reporter';
import { test } from 'vitest';
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" }
]
}
}
]
  • Los pasos anidados (Confirm the dialog) son solo pasos del resultado: cómo funciona una prueba, no lo que especifica.
  • Como todos los campos del caso, los pasos del caso solo se aplican cuando el informe crea el caso (solo casos creados). Un caso existente conserva sus propios pasos.
  • expected y data son cadenas: un paso que recibe otra cosa se registra solo con su título, y una advertencia lo indica.

Un cuerpo que devuelve una promesa termina su paso cuando la promesa se resuelve. probara.step() devuelve una promesa nueva que se resuelve igual, así que await probara.step(...) equivale a esperar el cuerpo. Un rechazo que la prueba nunca gestiona sigue siendo un rechazo no gestionado, que Vitest reporta, igual que sin el paso.

Un cuerpo también puede devolver un thenable que no es una promesa: una petición de supertest, un constructor de consultas. Llamar a su then() lo iniciaría (o lo iniciaría dos veces), así que probara.step() lo devuelve intacto, y el paso termina en cuanto vuelve el cuerpo, antes de que se envíe la petición. Para medir la petición dentro del paso, espérala en un cuerpo async:

tests/api.test.ts
import { probara } from '@probara/vitest-reporter';
import { expect, test } from 'vitest';
// A stand-in for a supertest request: it runs when awaited.
const request = () => ({
then: (resolve: (response: { status: number }) => void) => 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" }] }]

Pásale un archivo en disco o un cuerpo, y espéralo con await. Funciona desde cualquier lugar en el que corre una prueba: el cuerpo de la prueba, un hook beforeEach o afterEach, onTestFinished, un paso, una función auxiliar o un page object que llama la prueba.

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

El resultado recibe tres archivos:

receipt.txt text/plain
order.json application/json
log.txt text/plain
CampoCon pathCon body
nameObligatorio: el nombre en Probara; se añade la extensión del archivo si no tieneObligatorio: el nombre en Probara; se añade la extensión del tipo de contenido si no tiene
pathEl archivo, absoluto o relativo al directorio en el que se ejecuta vitest—
body—Una cadena, un Buffer o un Uint8Array, enviado tal cual
contentTypeOpcional: se deduce del nombre del archivoOpcional: se deduce de la extensión de name; si no, text/plain para una cadena y application/octet-stream para bytes
  • Se copia al llamarlo. El archivo (o el cuerpo) se copia en ese momento, así que una prueba puede borrarlo o sobrescribirlo justo después: Probara recibe lo que había en la llamada.
  • Espéralo con await. En Node la copia se hace antes de que vuelva la llamada; en modo navegador termina cuando se resuelve la promesa, y una copia que termina cuando su prueba ya acabó no se adjunta, con una advertencia que dice Await probara.attach() before the test ends (probara.attach() en un navegador).
  • Una cadena se envía tal cual, como texto UTF-8, nunca se decodifica: pasa un Buffer o un Uint8Array para contenido binario.
  • Un archivo que no se puede adjuntar (no existe, es una carpeta, pesa más de 32 MiB) es una advertencia, como probara.attach() did not attach "receipt": it does not exist; la prueba continúa, y su resultado se envía.
  • Cada intento tiene sus propios archivos: una prueba reintentada sube los archivos de cada intento, cada uno a su propio resultado.

Un archivo adjuntado mientras se ejecuta un probara.step(), desde el cuerpo del paso o desde cualquier cosa que este llame, va a ese paso, y Probara lo muestra bajo el paso:

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

Los archivos del context.annotate(message, attachment) de Vitest se envían con el intento que los creó, con el mensaje de la anotación como nombre. El mensaje en sí no se envía: probara.comment() escribe una nota.

tests/export.test.ts
import { test } from 'vitest';
test('exports the report', async ({ annotate }) => {
await annotate('summary', { body: 'Exported 3 rows', bodyEncoding: 'utf-8' });
});
summary.txt text/plain
VitestUn body de textoContenido binario
4.1.3 y posteriores, 5Pasa bodyEncoding: 'utf-8': sin él, Vitest marca el cuerpo como base64Un cuerpo Uint8Array con su contentType, o un path
3.2, de 4.0 a 4.1.2Tal cual: todavía no hay bodyEncodingUn path: Vitest 3 pierde algunos bytes de un cuerpo binario, y ese cuerpo se omite, con una advertencia

Vitest copia un path en su attachmentsDir, y el archivo en Probara recibe el mensaje de la anotación y la extensión de la ruta: annotate('screen', { path: 'shot.png' }) es screen.png. Vitest rechaza annotate() cuando el cuerpo de la prueba ya terminó: en afterEach y onTestFinished, usa probara.attach().

En modo navegador, Vitest toma una captura de pantalla cuando una prueba falla, y el reporter la adjunta a la ejecución fallida, como image/png: la primera captura al primer intento fallido, y así sucesivamente, o todas las capturas al último intento fallido cuando sus cantidades no coinciden. attachScreenshots: false (PROBARA_ATTACH_SCREENSHOTS=false) las deja fuera. Una prueba que se ejecuta en Node no tiene ninguna.

captureOutput: true (PROBARA_CAPTURE_OUTPUT=true) adjunta lo que cada intento escribe en la consola: console.log, console.info y console.debug como stdout.log, y console.warn y console.error como stderr.log, text/plain, cada uno solo cuando tiene texto. Está desactivado por defecto y no necesita nada más: ni archivo de setup ni cambios en las pruebas. Vitest sigue imprimiendo la salida como siempre.

reporters: ['default', ['@probara/vitest-reporter', { captureOutput: true }]],
tests/logs.test.ts
import { beforeEach, test } from 'vitest';
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

Aquí stdout.log contiene Seeding the cart y Opening the cart, y stderr.log contiene Slow response: 2.1 s: los hooks beforeEach y afterEach de una prueba escriben en su intento.

SalidaAdónde va
El cuerpo de la prueba, sus hooks beforeEach y afterEachEl intento que la escribió
Cada intento de una prueba reintentada o repetidaSu propio stdout.log y stderr.log
Una prueba test.concurrent, o una prueba en un describe.concurrentSe deja fuera, con una advertencia
Una prueba reintentada cuyos intentos no se registraronSe deja fuera, con una advertencia (ver cuándo se ejecutan los reintentos)
El ámbito del módulo, el cuerpo de un describe, beforeAll, afterAllNo se adjunta: Vitest no la asigna a ninguna prueba
  • Las pruebas concurrentes se dejan fuera. Vitest asigna cada escritura a la prueba en curso en su worker, que no siempre es la que la escribió cuando varias pruebas se ejecutan a la vez: antes que adjuntar una escritura a la prueba equivocada, el reporter deja fuera la salida de las pruebas concurrentes, y dice cuántas.
  • Los reintentos se separan por tiempo. Los archivos de un intento contienen lo que Vitest recibió desde su inicio hasta el inicio del intento siguiente. Vitest entrega al reporter las escrituras de una prueba por lotes, así que una escritura hecha justo cuando empieza un reintento puede acabar en el archivo del intento anterior.
reporters: ['default', ['@probara/vitest-reporter', { captureOutput: true }]],
tests/search.test.ts
import { test } from 'vitest';
test.concurrent('finds boots', async () => {
console.log('searching for boots');
});
test('finds hats', () => {
console.log('searching for hats');
});

Solo la prueba que se ejecuta sola recibe su salida, stdout.log, y el registro dice por qué la otra no tiene ninguna:

$ npx vitest run
[probara] The console output of 1 test is left out: Vitest attributes the writes of concurrent tests to the wrong test, and the attempts of a retried or repeated test without recorded attempts cannot be told apart (first seen in the run; repeats are logged at debug)
[probara] Sending 2 results of 2 tests (2 passed, 0 failed, 0 skipped, 0 blocked)
[probara] Recorded 2 results (2 new cases, 0 unmatched) in R-1 (closed): https://app.probara.net/projects/SHOP/runs/R-1
[probara] Attached 1 file to results (0 skipped, 0 failed)

silent y onConsoleLog de Vitest deciden lo que muestra la terminal, no lo que reciben los reporters: la salida que ocultan se adjunta igualmente. Para que la salida de una prueba no llegue a Probara, deja captureOutput desactivado.

silent: true,
reporters: ['default', ['@probara/vitest-reporter', { captureOutput: true }]],
tests/quiet.test.ts
import { test } from 'vitest';
test('works quietly', () => {
console.log('nothing shows in the terminal');
});
stdout.log text/plain
  • 32 MiB por prueba y flujo. A partir de ahí, el flujo conserva lo que cabe y termina con la línea [probara] The output of this test was cut here: an attachment holds at most 32 MiB. La salida de una ejecución completa no tiene límite.
  • Se guarda en memoria. El reporter conserva la salida de las pruebas de la ejecución hasta que esta termina, cuando envía los resultados: una ejecución cuyas pruebas escriben mucho necesita la memoria para ello.
  • Las ejecuciones con sharding la conservan. El blob de un shard contiene la salida de sus pruebas, y vitest --merge-reports adjunta los mismos archivos que una ejecución sin sharding (sharding).
LímiteQué pasa
32 MiB por archivo o cuerpoUno mayor se omite, con una advertencia
20 archivos por resultadoLos archivos a partir del vigésimo se omiten, con una advertencia
Imágenes de más de 10 MiB o de más de 8192 px de ancho o altoSe omiten con una advertencia (Probara convierte las imágenes a WebP)
Ejecutables y scriptsSe omiten: Probara rechaza sus tipos de contenido
Un archivo que falta, que no se puede leer o vacíoSe omite con una advertencia
Un resultado que no registró nadaSus archivos no se suben

Las advertencias nombran el archivo, y el registro termina con cuántos archivos se adjuntaron, se omitieron y fallaron. uploadAttachments: false (PROBARA_UPLOAD_ATTACHMENTS=false) no sube ninguno. Los archivos se suben después de que su informe queda registrado, mientras la ejecución sigue abierta, y después se cierra la ejecución.

El reporter guarda sus copias en <attachmentsDir>/probara/<id>/, dentro del attachmentsDir de Vitest (.vitest-attachments en Vitest 3 y 4, .vitest/attachments en Vitest 5, por defecto), hasta que se envían:

La ejecuciónSus copias
Todos los reporters son de los que trae Vitest (default, verbose, junit…) o esteSe eliminan cuando se envía la ejecución
Un shard (--shard), una ejecución con blob, o cualquier otro reporterSe conservan: un blob puede nombrarlas, y una fusión las lee
Resultados que no se pudieron enviar, con resultsFileSe copian junto al archivo de resultados, en <name>-attachments/
  • Registra el reporter por el nombre de su paquete, '@probara/vitest-reporter'. Indicado por una ruta o como objeto, cuenta como otro reporter, y sus copias se conservan.
  • Lleva el attachmentsDir con los blobs. El blob de un shard nombra los archivos que adjuntó por su ubicación en la máquina del shard: copia el attachmentsDir del shard junto con su blob al job que ejecuta vitest --merge-reports, en la misma ruta. Un archivo que la fusión no encuentra se omite con la advertencia Skipped an attachment: its file is missing or unreadable, y su resultado se envía igualmente (lleva los blobs y los adjuntos).
  • Las copias propias de Vitest de las rutas de las anotaciones se quedan en attachmentsDir: Vitest nunca las elimina, y el reporter tampoco. Elimina la carpeta cuando crezca.
  • Falta un paso. Solo probara.step() crea pasos: las llamadas a expect y las funciones auxiliares no. Un paso llamado fuera de una prueba (beforeAll, el cuerpo de un describe) ejecuta su cuerpo y no registra nada, con una advertencia.
  • Los pasos quedan aplanados, o fallan con The step had not finished. Un paso iniciado sin await termina después de la prueba: espera con await cada probara.step() cuyo cuerpo sea async.
  • El caso no tiene pasos. Solo un caso que crea el informe recibe pasos. Un caso existente conserva los suyos.
  • Falta un archivo. El registro nombra cada archivo que dejó fuera, y por qué (falta un adjunto). Un archivo adjuntado dentro de un paso va a ese paso, no al resultado.

Relacionado: Metadatos y enlaces · Configuración · Solución de problemas · Fuente: steps.md, attachments.md, output.md