Playwright test.step & Reporting — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited Playwright test.step & Reporting (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
This skill makes the agent produce tests that explain themselves in the report. Instead of a flat wall of actions, the agent groups logical phases with test.step, attaches screenshots/JSON/diffs via testInfo.attach, records annotations for traceability, and uses expect.soft to collect multiple failures in one run. When a test fails in CI, a human should understand what happened from the HTML report alone.
Use this skill when writing non-trivial flows, when a test is hard to debug from its output, or when the user mentions reports, steps, annotations, attachments, or issue traceability.
console.log is invisible in the report; testInfo.attach puts screenshots, JSON, and text into the report next to the step.expect.poll so the test still fails.annotation, and use test.info().annotations to surface skips/known-issues in the report.test.stepSteps nest and report their own duration. Return values from a step to chain them.
import { test, expect } from '@playwright/test';
test('checkout flow', async ({ page }) => {
await test.step('Sign in', async () => {
await page.goto('https://shop.example.com/login');
await page.getByLabel('Email').fill('[email protected]');
await page.getByLabel('Password').fill('Secret123!');
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page.getByText('Welcome back')).toBeVisible();
});
const orderId = await test.step('Place order for Pro plan', async () => {
await page.getByRole('link', { name: 'Pro plan' }).click();
await page.getByRole('button', { name: 'Buy now' }).click();
await page.getByRole('button', { name: 'Confirm purchase' }).click();
const id = await page.getByTestId('order-id').textContent();
return id!.trim();
});
await test.step(`Verify order ${orderId} in history`, async () => {
await page.goto('https://shop.example.com/orders');
await expect(page.getByText(orderId)).toBeVisible();
});
});testInfo.attachAttachments render inline in the HTML report. Attach a screenshot, the API response, or a computed diff at the moment it matters.
test('attaches evidence to the report', async ({ page }, testInfo) => {
await page.goto('https://example.com/dashboard');
await test.step('Capture dashboard state', async () => {
// Screenshot attachment (shown inline in the report).
await testInfo.attach('dashboard.png', {
body: await page.screenshot({ fullPage: true }),
contentType: 'image/png',
});
// JSON attachment — the raw API payload behind the screen.
const widgets = await page.evaluate(() => (window as any).__WIDGETS__ ?? []);
await testInfo.attach('widgets.json', {
body: JSON.stringify(widgets, null, 2),
contentType: 'application/json',
});
// Plain-text attachment for a human-readable note.
await testInfo.attach('environment.txt', {
body: `Project: ${testInfo.project.name}\nBase URL: ${page.url()}`,
contentType: 'text/plain',
});
});
});expect.soft records the failure and keeps going. End the test with expect(test.info().errors).toHaveLength(0) or a hard check so it still fails — and the report shows every problem at once.
test('validates a form with soft assertions', async ({ page }) => {
await page.goto('https://example.com/profile');
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('[email protected]');
await expect.soft(page.getByLabel('Timezone')).toHaveValue('UTC');
await expect.soft(page.getByRole('img', { name: 'Avatar' })).toBeVisible();
});
// Hard gate: fail the test if any soft assertion failed.
expect(test.info().errors).toHaveLength(0);
});Annotations attach metadata to a test; they show up in the report and JSON output. Use them to link issues and to document why something is skipped.
test('payment retries on gateway 503', async ({ page }) => {
test.info().annotations.push(
{ type: 'issue', description: 'https://github.com/acme/app/issues/4821' },
{ type: 'suite', description: 'payments-regression' },
);
await page.goto('https://example.com/pay');
await expect(page.getByRole('button', { name: 'Pay' })).toBeEnabled();
});
test('legacy export still works', async ({ page }) => {
test.skip(process.env.LEGACY !== 'on', 'Legacy export disabled in this env');
test.info().annotations.push({ type: 'known-issue', description: 'Slow > 5s, tracked in #5102' });
await page.goto('https://example.com/export');
await expect(page.getByText('Export ready')).toBeVisible({ timeout: 30_000 });
});A boxed step collapses its internals in the report; the error is reported at the step call site, not buried in shared helper code.
import { test, expect, type Page } from '@playwright/test';
async function loginAs(page: Page, email: string, password: string) {
await test.step(
`Log in as ${email}`,
async () => {
await page.goto('https://example.com/login');
await page.getByLabel('Email').fill(email);
await page.getByLabel('Password').fill(password);
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page.getByTestId('user-menu')).toBeVisible();
},
{ box: true }, // failure surfaces at this step, internals collapsed
);
}
test('admin can open settings', async ({ page }) => {
await loginAs(page, '[email protected]', 'Admin123!');
await page.getByRole('link', { name: 'Settings' }).click();
await expect(page.getByRole('heading', { name: 'Settings' })).toBeVisible();
});Wire up reporters in playwright.config.ts. The HTML reporter consumes the steps, attachments, and annotations above; JSON/JUnit feed CI dashboards.
import { defineConfig } from '@playwright/test';
export default defineConfig({
reporter: [
['html', { open: 'never', outputFolder: 'playwright-report' }],
['json', { outputFile: 'test-results/results.json' }],
['junit', { outputFile: 'test-results/junit.xml' }],
['list'],
],
use: {
// Auto-capture trace + screenshot on failure — they attach to the report.
trace: 'retain-on-failure',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
});testInfo.attach.expect(test.info().errors).toHaveLength(0) gate.testInfo.attach.test.step"~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.