Back to Blog

Visual Testing vs E2E Testing: What's the Difference?

Visual Testing vs E2E Testing: What's the Difference?

E2E testing verifies behavior: "When I click Submit, does the form save?"

Visual testing verifies appearance: "After the CSS refactor, does the form still look right?"

They're complementary. Here's how to use both.

What E2E Catches

Simulates user interactions, verifies outcomes. Answers: does it work?

test('submit form', async ({ page }) => {
  await page.goto('/contact');
  await page.fill('#name', 'Jane');
  await page.click('button[type="submit"]');
  await expect(page.locator('.success')).toBeVisible();
});

Passes if the form submits. Doesn't check alignment, padding, or mobile layout.

Two-column comparison: the blue E2E column verifies behavior (form saves, API 200, navigation) but is blind to a 20px CSS shift, while the teal Visual column catches padding, z-index, and dark-mode regressions yet is blind to broken logic
E2E and visual testing answer different questions, and each is blind to exactly what the other catches.

What Visual Testing Catches

Screenshots compared against baselines. Answers: does it look right?

  • CSS shifts form fields 20px left
  • Typography makes labels unreadable
  • z-index hides the submit button
  • Responsive breakpoint collapses incorrectly
  • Dark mode makes text invisible

Overlap

ScenarioE2E?Visual?
Button doesn't submitYesNo
Button moved off-screenNoYes
Error message wrong colorNoYes
API returns 500YesNo
Modal has broken layoutNoYes

When Visual Testing Matters Most

CSS Refactoring: Migrating SCSS to Tailwind. E2E passes, 15 pages have broken layouts. Visual tests catch every one.

Design System Updates: New spacing scale. Functional behavior unchanged, visual impact spans everything.

Dependency Updates: React update causes 2px shift on 40 pages. E2E passes. Visual tests flag it.

Using Both Together

Run both in parallel in CI:

jobs:
  e2e-tests:
    steps:
      - run: npx playwright test

  visual-tests:
    steps:
      - uses: las-team/lastest/action@main
        with:
          server-url: ${{ secrets.LASTEST_SERVER_URL }}
          runner-token: ${{ secrets.LASTEST_RUNNER_TOKEN }}
Build results

E2E tests fail fast on broken behavior. Visual tests flag appearance changes for review. Both block the PR.

Flow from a pull request fanning out into two parallel CI jobs, E2E Playwright and Lastest visual diffing, then converging on a merge gate that only opens when both are green
Run E2E and visual checks in parallel as two CI gates, so a PR can only merge when behavior and appearance both pass.

E2E: "The app is broken" (it doesn't work).
Visual: "The app looks broken" (it works but looks wrong).

Users notice both. Ship neither.