DEV Community

TestDino
TestDino

Posted on

Mistake 12/14: You're not catching visual regressions

A simple CSS change broke the checkout button on mobile. Every functional test passed.

The scary part? Nobody knew. The button was technically in the DOM. It had the correct text. But to an actual user, it was completely invisible, overlapped by a rogue div on mobile screens only. The kind of bug that gets caught in production when a customer tries to check out and can't.

Functional tests are great for verifying behavior. They are not designed to check appearance. They can't tell you if your app actually looks right.

BEFORE : Functional test passes. Button is covered on mobile.

// โŒ BEFORE โ€” Functional test passes, but the button is covered on mobile.
await expect(page.getByRole('button', { name: 'Checkout' })).toBeVisible();
// Passes because the button is in the DOM, even if users can't actually click it.
Enter fullscreen mode Exit fullscreen mode

AFTER : Visual regression catches layout issues across devices.

// โœ… AFTER โ€” Visual regression catches layout issues across devices.
test('cart page looks correct', async ({ page }) => {
  await page.goto('/cart');

  await expect(page).toHaveScreenshot('cart.png', {
    mask: [
      page.getByTestId('timestamp'),   // changes every run
      page.getByTestId('order-id'),    // unique per session
    ],
    maxDiffPixelRatio: 0.01,
  });
});

// ๐Ÿงช Test across viewports in playwright.config.ts
{ name: 'desktop', use: { viewport: { width: 1280, height: 720 } } },
{ name: 'mobile',  use: { viewport: { width: 375, height: 667 } } },
Enter fullscreen mode Exit fullscreen mode

Playwright's built-in visual comparisons (toHaveScreenshot) catch these layout breaks instantly. The quick pro-tip that saves every team time: always mask dynamic elements like timestamps or order IDs to avoid flaky false positives.

The distinction most teams miss is this: functional tests verify that something works. Visual tests verify that users can actually see and interact with it. Both questions matter. Most suites only answer one of them.

When you add visual coverage at the viewport level, you stop shipping layout bugs that your test suite is structurally incapable of catching.

Have visual tests ever caught a bug your functional tests completely missed? Drop it in the comments.

Top comments (1)

Collapse
 
automate-archit profile image
Archit Mittal

The invisible checkout button scenario is terrifying because it's a class of bug that's almost impossible to catch with traditional testing. I've started incorporating screenshot-based diff testing into CI pipelines for exactly this reason โ€” tools like Playwright's screenshot comparison API make it surprisingly easy to set up. The key insight is that visual regression testing should run on every PR, not just before releases. The cost of a missed visual bug in production (lost conversions, broken UX) almost always outweighs the 30 seconds of CI time per build. Great addition to this series.