DEV Community

Cover image for Why Your Playwright Test Reports Are Messy (And How test.step() Fixes It)
Pratik Patel
Pratik Patel

Posted on

Why Your Playwright Test Reports Are Messy (And How test.step() Fixes It)

You’ve written solid Playwright tests.
Your CI runs them every day.
But when a test fails at 3 AM, your on-call engineer opens the report and sees:

“Test failed: should complete checkout flow”
…and nothing else.

No clear reason.
No step-level context.
Just a long wall of logs, random screenshots, and 20 minutes of guesswork.

This isn’t a tooling problem.
It’s a test structure problem.


The Cost of Unstructured Playwright Tests

Most Playwright tests look like this:

js

test('should complete checkout flow', async ({ page }) => {
  await page.goto('/products');
  await page.click('[data-testid="add-to-cart"]');
  await page.click('[data-testid="checkout-button"]');
  await page.fill('#email', 'user@example.com');
  await page.fill('#card-number', '4242424242424242');
  await page.click('[data-testid="place-order"]');
  await expect(page.locator('.confirmation')).toBeVisible();
});
Enter fullscreen mode Exit fullscreen mode

When this test fails, the report shows one generic failure.
There’s no clue whether the failure happened while:

  • Adding to cart
  • Navigating to checkout
  • Typing payment details
  • Or clicking “Place order”

So your team ends up:

  • Downloading multiple screenshots
  • Scrubbing logs to find the exact failure point
  • Re-running tests with extra logging
  • Asking, “Which step actually broke?”

Unstructured tests → unstructured reports → wasted debugging time.


Three Approaches to Cleaner Playwright Test Reports

The Playwright community widely uses these patterns.
Here are the three best approaches, from simplest to most scalable.

1. Inline test.step() (Simple & Effective)

The easiest improvement is to wrap each logical action in a step.

js

test('should complete checkout flow', async ({ page }) => {
  await test.step('Navigate to products page', async () => {
    await page.goto('/products');
  });

  await test.step('Add product to cart', async () => {
    await page.click('[data-testid="add-to-cart"]');
  });

  await test.step('Proceed to checkout', async () => {
    await page.click('[data-testid="checkout-button"]');
  });

  await test.step('Fill payment details', async () => {
    await page.fill('#email', 'user@example.com');
    await page.fill('#card-number', '4242424242424242');
  });

  await test.step('Place order', async () => {
    await page.click('[data-testid="place-order"]');
    await expect(page.locator('.confirmation')).toBeVisible();
  });
});
Enter fullscreen mode Exit fullscreen mode

Result:

  • Clear step-by-step breakdown in the report
  • Each step gets its own screenshot, logs, and trace section
  • You instantly know which step failed

Trade-off: More verbose,but massively improved debugging.


2. Decorator Pattern (Best for Bigger Teams & POM)

If you use Page Object Models, decorators avoid repeating test.step() everywhere.

step.ts

ts

export function step(stepName?: string) {
  return function decorator(target: Function, context: ClassMethodDecoratorContext) {
    return async function (...args: any[]) {
      const name = stepName || `${this.constructor.name}.${String(context.name)}`;
      return await test.step(name, async () => {
        return target.apply(this, args);
      });
    };
  };
}
Enter fullscreen mode Exit fullscreen mode

checkout.action.ts

ts

export class CheckoutAction {
  constructor(private page: Page) {}

  @step('Add product to cart')
  async addToCart() {
    await this.page.click('[data-testid="add-to-cart"]');
  }

  @step('Fill payment details')
  async fillPaymentDetails(email: string, cardNumber: string) {
    await this.page.fill('#email', email);
    await this.page.fill('#card-number', cardNumber);
  }
}
Enter fullscreen mode Exit fullscreen mode

checkout.spec.ts

ts

test('should complete checkout flow', async ({ page }) => {
  const checkout = new CheckoutAction(page);

  await checkout.addToCart();
  await checkout.fillPaymentDetails(
    'user@example.com',
    '4242424242424242'
  );
});
Enter fullscreen mode Exit fullscreen mode

Result:

  • Automatic step names
  • Clean test files
  • Consistent reporting
  • Works beautifully across large test suites

Trade-off: Requires TypeScript decorators (ES2023) and a bit of setup.


3. Magic Steps (Comment-Based, Very Lightweight)

Using the playwright-magic-steps package, comments become steps.

js

test('should complete checkout flow', async ({ page }) => {
  // step: Navigate to products page
  await page.goto('/products');

  // step: Add product to cart
  await page.click('[data-testid="add-to-cart"]');

  // step: Fill payment details
  await page.fill('#email', 'user@example.com');
  await page.fill('#card-number', '4242424242424242');
});
Enter fullscreen mode Exit fullscreen mode

Result:

  • Cleanest syntax
  • Zero wrapping
  • Steps appear in the report automatically

Trade-off: Requires installing an additional package.


How Structured Steps Transform Your Reporting

Before structure:

“Test failed somewhere in checkout.”

After structure:

“❌ Failed at step: Fill payment details”

Before:

  • 10 screenshots
  • Flat logs

After:

  • Evidence grouped per step
  • Clear hierarchy
  • Faster root-cause analysis

Structured steps unlock powerful analytics:

  • Which step fails most often
  • Which step takes the longest
  • Which workflow became flaky after a recent commit
  • Step-level screenshots, videos, and trace events

This is the difference between debugging blindly and debugging intelligently.

Structured Tests + Intelligent Reporting = Zero Guesswork

test.step() gives your test structure.
But structure is only half the story.
You also need a reporting platform that actually uses it.

Modern test reporting should:

  • Visualize step hierarchies
  • Attach screenshots/logs per step
  • Show duration & performance trends
  • Highlight flaky steps
  • Link failures to commits

When structured tests meet a smart reporting layer, debugging goes from detective work to diagnostics.


Want to See What Playwright Reporting Should Look Like?

If your Playwright report still looks like a long, flat log dump,you’re not debugging ,you’re guessing.

See how TestDino turns structured tests into actionable insights,not noise

Here

Top comments (0)