PW Test Step Reporter
You structure a test so that when it fails in CI, a human understands
what happened from the HTML report alone — never from re-running it
locally first. Every logical phase becomes a named test.step; evidence
gets attached, not logged; and annotations keep tests traceable to the
issues they cover — including the bug-ID references pw-test-health-reporter
looks for.
When to use
- Writing a non-trivial multi-step flow that will be hard to debug from a
bare stack trace.
- Someone asks to group a test into steps, attach evidence, use soft
assertions, or add issue/known-issue annotations.
- A test fails in CI and nobody can tell which phase broke without
re-running it.
When not to use
- Generating the flow itself from a scenario →
pw-test-generator.
- Diagnosing why a step actually fails (root cause) →
pw-flaky-debugger / pw-trace-analyzer.
- Configuring CI-level reporters/artifacts at the pipeline level →
pw-ci-configurator.
Workflow
- Wrap each logical phase in
test.step, named like a test plan —
imperative and business-readable ("Add Pro plan to cart"), never
mechanical ("click button #3"). Steps appear as a collapsible, timed
tree in the HTML report, turning a failure into "which step failed"
instead of "which line number."
- Attach evidence, don't log it.
console.log never appears in the
HTML report; testInfo.attach puts a screenshot, the relevant JSON
payload, or a computed diff into the report at the step where it
matters.
- Use
expect.soft only for grouped, independent checks, and always
end with a hard gate (expect(test.info().errors).toHaveLength(0), or
a hard expect) so the test still fails — soft assertions with no hard
gate silently pass a test that actually found problems.
- Annotate for traceability. Push
{ type: 'issue', description: '<url or ID>' } to link a test to a
tracked bug, and use a real, checkable identifier for any
test.skip/fixme reason — a vague "known issue" comment with no ID is
exactly what pw-test-health-reporter flags as untracked.
- Box shared helper steps (
{ box: true }) so a failure inside a
login/setup helper surfaces at the calling test, not buried in the
helper's internals.
- Configure reporters to carry this evidence —
trace: 'retain-on-failure', screenshot: 'only-on-failure', and an
html reporter so every attachment and step actually renders somewhere
a human reads it.
Output shape
test('checkout flow', async ({ page }, testInfo) => {
await test.step('Sign in', async () => {
await page.getByLabel('Email').fill('buyer@example.com');
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page.getByText('Welcome back')).toBeVisible();
});
await test.step('Verify all profile fields at once', async () => {
await expect.soft(page.getByLabel('Display name')).toHaveValue('Ada Lovelace');
await expect.soft(page.getByLabel('Email')).toHaveValue('ada@example.com');
});
expect(test.info().errors).toHaveLength(0); // hard gate — soft failures still fail the test
await test.step('Capture dashboard state', async () => {
await testInfo.attach('dashboard.png', {
body: await page.screenshot({ fullPage: true }),
contentType: 'image/png',
});
});
});
test('payment retries on gateway 503', async ({ page }) => {
test.info().annotations.push({ type: 'issue', description: 'PROJ-4821' }); // real ID, not a vague comment
// ...
});
Guardrails
- Never leave a test as a flat, unstepped wall of actions when it's
non-trivial — a failure should point at a phase, not just a line number.
- Never use
console.log as "evidence" — it doesn't appear in the HTML
report; use testInfo.attach.
- Never use
expect.soft without a following hard gate — a test with only
soft assertions can go green while real checks failed.
- Never skip a test with a bare
test.skip() and no reason string, and
never use a vague "known issue" comment where a real, checkable bug ID
belongs.
- Never attach a full-page screenshot on every step of a passing test —
attach on failure or at genuine checkpoints; bloated reports get
ignored.
- Name steps after intent, not mechanics ("Submit signup form", not
"click button").
1---2name: pw-test-step-reporter3description: Structures a Playwright test so it explains itself in the HTML report — test.step for logical phases, testInfo.attach for evidence (screenshots, JSON, diffs), expect.soft with a hard gate for grouped checks, and annotations for issue/bug-ID traceability. Use when an SDET says "make this test's report readable", "group this into steps", "attach a screenshot to the report", "how do I do soft assertions", or "why can't I tell which step failed in CI". Produces a report a human can debug from without re-running the test — a draft the engineer reviews and runs.4license: MIT5---67# PW Test Step Reporter89You structure a test so that **when it fails in CI, a human understands10what happened from the HTML report alone** — never from re-running it11locally first. Every logical phase becomes a named `test.step`; evidence12gets attached, not logged; and annotations keep tests traceable to the13issues they cover — including the bug-ID references `pw-test-health-reporter`14looks for.1516## When to use17- Writing a non-trivial multi-step flow that will be hard to debug from a18 bare stack trace.19- Someone asks to group a test into steps, attach evidence, use soft20 assertions, or add issue/known-issue annotations.21- A test fails in CI and nobody can tell which phase broke without22 re-running it.2324## When *not* to use25- Generating the flow itself from a scenario → `pw-test-generator`.26- Diagnosing *why* a step actually fails (root cause) →27 `pw-flaky-debugger` / `pw-trace-analyzer`.28- Configuring CI-level reporters/artifacts at the pipeline level →29 `pw-ci-configurator`.3031## Workflow321. **Wrap each logical phase in `test.step`**, named like a test plan —33 imperative and business-readable ("Add Pro plan to cart"), never34 mechanical ("click button #3"). Steps appear as a collapsible, timed35 tree in the HTML report, turning a failure into "which step failed"36 instead of "which line number."372. **Attach evidence, don't log it.** `console.log` never appears in the38 HTML report; `testInfo.attach` puts a screenshot, the relevant JSON39 payload, or a computed diff into the report at the step where it40 matters.413. **Use `expect.soft` only for grouped, independent checks**, and always42 end with a hard gate (`expect(test.info().errors).toHaveLength(0)`, or43 a hard `expect`) so the test still fails — soft assertions with no hard44 gate silently pass a test that actually found problems.454. **Annotate for traceability.** Push46 `{ type: 'issue', description: '<url or ID>' }` to link a test to a47 tracked bug, and use a real, checkable identifier for any48 `test.skip`/`fixme` reason — a vague "known issue" comment with no ID is49 exactly what `pw-test-health-reporter` flags as untracked.505. **Box shared helper steps** (`{ box: true }`) so a failure inside a51 login/setup helper surfaces at the calling test, not buried in the52 helper's internals.536. **Configure reporters to carry this evidence** —54 `trace: 'retain-on-failure'`, `screenshot: 'only-on-failure'`, and an55 `html` reporter so every attachment and step actually renders somewhere56 a human reads it.5758## Output shape59```typescript60test('checkout flow', async ({ page }, testInfo) => {61 await test.step('Sign in', async () => {62 await page.getByLabel('Email').fill('buyer@example.com');63 await page.getByRole('button', { name: 'Sign in' }).click();64 await expect(page.getByText('Welcome back')).toBeVisible();65 });6667 await test.step('Verify all profile fields at once', async () => {68 await expect.soft(page.getByLabel('Display name')).toHaveValue('Ada Lovelace');69 await expect.soft(page.getByLabel('Email')).toHaveValue('ada@example.com');70 });71 expect(test.info().errors).toHaveLength(0); // hard gate — soft failures still fail the test7273 await test.step('Capture dashboard state', async () => {74 await testInfo.attach('dashboard.png', {75 body: await page.screenshot({ fullPage: true }),76 contentType: 'image/png',77 });78 });79});8081test('payment retries on gateway 503', async ({ page }) => {82 test.info().annotations.push({ type: 'issue', description: 'PROJ-4821' }); // real ID, not a vague comment83 // ...84});85```8687## Guardrails88- Never leave a test as a flat, unstepped wall of actions when it's89 non-trivial — a failure should point at a phase, not just a line number.90- Never use `console.log` as "evidence" — it doesn't appear in the HTML91 report; use `testInfo.attach`.92- Never use `expect.soft` without a following hard gate — a test with only93 soft assertions can go green while real checks failed.94- Never skip a test with a bare `test.skip()` and no reason string, and95 never use a vague "known issue" comment where a real, checkable bug ID96 belongs.97- Never attach a full-page screenshot on every step of a passing test —98 attach on failure or at genuine checkpoints; bloated reports get99 ignored.100- Name steps after intent, not mechanics ("Submit signup form", not101 "click button").