DEV Community

Cover image for Using Puppeteer to Test WordPress Pages for Visual Regressions
Martijn Assie
Martijn Assie

Posted on

Using Puppeteer to Test WordPress Pages for Visual Regressions

Developer nightmare: "We updated the theme... the homepage layout is COMPLETELY broken!!"

The problem:

  • Theme/plugin update
  • CSS change breaks layout
  • Discovered AFTER deployment
  • Users already seeing broken site!!

My solution:

  • Puppeteer automated screenshots
  • Jest image comparison
  • CI/CD visual regression tests
  • Catches layout breaks BEFORE production!!

Here's how to automate visual regression testing for WordPress:

The Visual Regression Problem

Manual QA approach:

  1. Update theme/plugin on staging
  2. Manually check 20+ pages
  3. Click through responsive views
  4. Miss subtle layout shifts
  5. Deploy to production
  6. User reports broken mobile menu!!

Automated approach:

  1. Update theme/plugin on staging
  2. Run automated screenshot tests
  3. Puppeteer compares 50+ pages automatically
  4. Test fails on ANY visual difference
  5. Fix BEFORE production!!

What is Visual Regression Testing?

Visual regression = comparing screenshots to detect unintended layout changes

How it works:

  1. Baseline screenshot (reference image)
  2. Current screenshot (after changes)
  3. Pixel-by-pixel comparison
  4. Fail if differences detected

Example scenario:

  • Baseline: Homepage hero section 600px height
  • Current: After CSS update, hero section 400px height
  • Comparison: Detects 200px difference
  • Test fails, prevents broken deployment!!

Setting Up Puppeteer + Jest

Project Structure

wordpress-visual-tests/
├── package.json
├── jest.config.js
├── tests/
│   ├── homepage.test.js
│   ├── single-post.test.js
│   ├── archive.test.js
│   └── woocommerce.test.js
├── __image_snapshots__/
│   ├── homepage-test-js-homepage-desktop-1-snap.png
│   └── homepage-test-js-homepage-mobile-1-snap.png
└── __diff_output__/
    └── homepage-test-js-homepage-desktop-1-diff.png
Enter fullscreen mode Exit fullscreen mode

Step 1: Install Dependencies

npm init -y

npm install --save-dev \
  puppeteer \
  jest \
  jest-puppeteer \
  jest-image-snapshot
Enter fullscreen mode Exit fullscreen mode

puppeteer = headless Chrome automation

jest = testing framework

jest-puppeteer = Jest + Puppeteer integration

jest-image-snapshot = screenshot comparison

Step 2: Configure Jest

jest.config.js:

module.exports = {
  preset: 'jest-puppeteer',
  testTimeout: 30000,
  setupFilesAfterEnv: ['./jest.setup.js'],
  testMatch: ['**/tests/**/*.test.js']
};
Enter fullscreen mode Exit fullscreen mode

jest.setup.js:

const { toMatchImageSnapshot } = require('jest-image-snapshot');

expect.extend({ toMatchImageSnapshot });
Enter fullscreen mode Exit fullscreen mode

Step 3: Configure Puppeteer

jest-puppeteer.config.js:

module.exports = {
  launch: {
    headless: true,
    args: [
      '--no-sandbox',
      '--disable-setuid-sandbox',
      '--disable-dev-shm-usage'
    ]
  },
  browserContext: 'default'
};
Enter fullscreen mode Exit fullscreen mode

Writing Visual Regression Tests

Test 1: Homepage Desktop/Mobile

tests/homepage.test.js:

describe('Homepage Visual Tests', () => {
  let browser;
  let page;

  beforeAll(async () => {
    browser = await puppeteer.launch({
      headless: true
    });
    page = await browser.newPage();
  });

  afterAll(async () => {
    await browser.close();
  });

  test('Homepage Desktop', async () => {
    // Set desktop viewport
    await page.setViewport({
      width: 1920,
      height: 1080
    });

    // Navigate to homepage
    await page.goto('https://staging.yoursite.com', {
      waitUntil: 'networkidle0'
    });

    // Wait for hero section to load
    await page.waitForSelector('.hero-section');

    // Take screenshot
    const screenshot = await page.screenshot({
      fullPage: true
    });

    // Compare with baseline
    expect(screenshot).toMatchImageSnapshot({
      failureThreshold: 0.01, // 1% tolerance
      failureThresholdType: 'percent'
    });
  });

  test('Homepage Mobile', async () => {
    // Set mobile viewport (iPhone 12)
    await page.setViewport({
      width: 390,
      height: 844
    });

    await page.goto('https://staging.yoursite.com', {
      waitUntil: 'networkidle0'
    });

    await page.waitForSelector('.hero-section');

    const screenshot = await page.screenshot({
      fullPage: true
    });

    expect(screenshot).toMatchImageSnapshot({
      failureThreshold: 0.01,
      failureThresholdType: 'percent'
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

Test 2: Single Post Layout

tests/single-post.test.js:

describe('Single Post Visual Tests', () => {
  let page;

  beforeAll(async () => {
    page = await browser.newPage();
  });

  test('Single Post Desktop', async () => {
    await page.setViewport({ width: 1920, height: 1080 });

    await page.goto('https://staging.yoursite.com/sample-post/', {
      waitUntil: 'networkidle0'
    });

    // Wait for post content
    await page.waitForSelector('.entry-content');

    // Screenshot only content area (not sidebar)
    const element = await page.$('.post-content-wrapper');
    const screenshot = await element.screenshot();

    expect(screenshot).toMatchImageSnapshot({
      customSnapshotIdentifier: 'single-post-content-desktop',
      failureThreshold: 0.01,
      failureThresholdType: 'percent'
    });
  });

  test('Single Post Comments Section', async () => {
    await page.setViewport({ width: 1920, height: 1080 });

    await page.goto('https://staging.yoursite.com/sample-post/#comments', {
      waitUntil: 'networkidle0'
    });

    // Scroll to comments
    await page.evaluate(() => {
      document.querySelector('#comments').scrollIntoView();
    });

    // Screenshot comments section only
    const element = await page.$('#comments');
    const screenshot = await element.screenshot();

    expect(screenshot).toMatchImageSnapshot({
      customSnapshotIdentifier: 'single-post-comments',
      failureThreshold: 0.02,
      failureThresholdType: 'percent'
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

Test 3: WooCommerce Product Page

tests/woocommerce.test.js:

describe('WooCommerce Visual Tests', () => {
  let page;

  beforeAll(async () => {
    page = await browser.newPage();
  });

  test('Product Page Desktop', async () => {
    await page.setViewport({ width: 1920, height: 1080 });

    await page.goto('https://staging.yoursite.com/product/sample-product/', {
      waitUntil: 'networkidle0'
    });

    // Wait for product gallery
    await page.waitForSelector('.woocommerce-product-gallery');

    const screenshot = await page.screenshot({
      fullPage: true
    });

    expect(screenshot).toMatchImageSnapshot({
      customSnapshotIdentifier: 'product-page-desktop',
      failureThreshold: 0.01,
      failureThresholdType: 'percent'
    });
  });

  test('Product Page Add to Cart Button', async () => {
    await page.setViewport({ width: 1920, height: 1080 });

    await page.goto('https://staging.yoursite.com/product/sample-product/', {
      waitUntil: 'networkidle0'
    });

    // Screenshot add to cart section only
    const element = await page.$('.product_meta');
    const screenshot = await element.screenshot();

    expect(screenshot).toMatchImageSnapshot({
      customSnapshotIdentifier: 'add-to-cart-section',
      failureThreshold: 0.01,
      failureThresholdType: 'percent'
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

If you're optimizing WooCommerce performance, check out my guide on Avada Theme WooCommerce Speed: From Slow to Fast.

Running Tests

First Run (Generate Baselines)

npm test

# Output:
# PASS  tests/homepage.test.js
#   ✓ Homepage Desktop (4523ms)
#   ✓ Homepage Mobile (3821ms)
#
# Snapshots:   2 written, 2 total
Enter fullscreen mode Exit fullscreen mode

Baselines saved in image_snapshots/ directory!!

Subsequent Runs (Compare)

npm test

# Output if NO changes:
# PASS  tests/homepage.test.js
#   ✓ Homepage Desktop (4312ms)
#   ✓ Homepage Mobile (3654ms)
#
# Snapshots:   2 passed, 2 total

# Output if changes detected:
# FAIL  tests/homepage.test.js
#   ✗ Homepage Desktop (4421ms)
#
# Expected image to match or be a close match to snapshot
# but was 2.34% different from snapshot
#
# See diff at: __diff_output__/homepage-test-js-homepage-desktop-1-diff.png
Enter fullscreen mode Exit fullscreen mode

Update Baselines (After Intentional Changes)

npm test -- --updateSnapshot

# or add to package.json:
# "test:update": "jest --updateSnapshot"

npm run test:update
Enter fullscreen mode Exit fullscreen mode

Advanced Testing Scenarios

Test Multiple Viewports

const viewports = [
  { name: 'Desktop', width: 1920, height: 1080 },
  { name: 'Laptop', width: 1366, height: 768 },
  { name: 'Tablet', width: 768, height: 1024 },
  { name: 'Mobile', width: 390, height: 844 }
];

describe('Responsive Homepage Tests', () => {
  let page;

  beforeAll(async () => {
    page = await browser.newPage();
  });

  viewports.forEach(viewport => {
    test(`Homepage ${viewport.name}`, async () => {
      await page.setViewport({
        width: viewport.width,
        height: viewport.height
      });

      await page.goto('https://staging.yoursite.com', {
        waitUntil: 'networkidle0'
      });

      const screenshot = await page.screenshot({
        fullPage: true
      });

      expect(screenshot).toMatchImageSnapshot({
        customSnapshotIdentifier: `homepage-${viewport.name.toLowerCase()}`,
        failureThreshold: 0.01,
        failureThresholdType: 'percent'
      });
    });
  });
});
Enter fullscreen mode Exit fullscreen mode

Test Dark Mode vs Light Mode

test('Homepage Light Mode', async () => {
  await page.goto('https://staging.yoursite.com', {
    waitUntil: 'networkidle0'
  });

  // Ensure light mode active
  await page.evaluate(() => {
    document.body.classList.remove('dark-mode');
  });

  const screenshot = await page.screenshot({ fullPage: true });

  expect(screenshot).toMatchImageSnapshot({
    customSnapshotIdentifier: 'homepage-light-mode'
  });
});

test('Homepage Dark Mode', async () => {
  await page.goto('https://staging.yoursite.com', {
    waitUntil: 'networkidle0'
  });

  // Activate dark mode
  await page.evaluate(() => {
    document.body.classList.add('dark-mode');
  });

  const screenshot = await page.screenshot({ fullPage: true });

  expect(screenshot).toMatchImageSnapshot({
    customSnapshotIdentifier: 'homepage-dark-mode'
  });
});
Enter fullscreen mode Exit fullscreen mode

Test Logged-In User Views

test('User Dashboard Logged In', async () => {
  // Login first
  await page.goto('https://staging.yoursite.com/wp-login.php');

  await page.type('#user_login', 'testuser');
  await page.type('#user_pass', 'testpassword');
  await page.click('#wp-submit');

  await page.waitForNavigation({ waitUntil: 'networkidle0' });

  // Navigate to dashboard
  await page.goto('https://staging.yoursite.com/my-account/', {
    waitUntil: 'networkidle0'
  });

  const screenshot = await page.screenshot({ fullPage: true });

  expect(screenshot).toMatchImageSnapshot({
    customSnapshotIdentifier: 'user-dashboard-logged-in',
    failureThreshold: 0.02,
    failureThresholdType: 'percent'
  });
});
Enter fullscreen mode Exit fullscreen mode

CI/CD Integration

GitHub Actions Workflow

.github/workflows/visual-tests.yml:

name: Visual Regression Tests

on:
  pull_request:
    branches:
      - main
      - develop

jobs:
  visual-tests:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Setup Node
        uses: actions/setup-node@v3
        with:
          node-version: '18'

      - name: Install dependencies
        run: npm install

      - name: Run visual tests
        run: npm test

      - name: Upload diff images on failure
        if: failure()
        uses: actions/upload-artifact@v3
        with:
          name: visual-test-diffs
          path: __diff_output__/

      - name: Comment PR with results
        if: failure()
        uses: actions/github-script@v6
        with:
          script: |
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: '⚠️ Visual regression tests failed! Check artifacts for diff images.'
            })
Enter fullscreen mode Exit fullscreen mode

Now:

  • Developer creates PR
  • GitHub Actions runs visual tests automatically
  • Tests fail if layout changes detected
  • Blocks merge until visual regressions fixed!!

For more on WordPress theme performance testing, see my article on Why Your Avada Theme Site Fails Core Web Vitals.

Handling Dynamic Content

Problem: Timestamps Break Tests

// WRONG - timestamps cause false failures
test('Blog Archive', async () => {
  await page.goto('https://staging.yoursite.com/blog/');

  const screenshot = await page.screenshot({ fullPage: true });

  // This will ALWAYS fail because post dates change!!
  expect(screenshot).toMatchImageSnapshot();
});
Enter fullscreen mode Exit fullscreen mode

Solution: Hide Dynamic Elements

test('Blog Archive (Stable)', async () => {
  await page.goto('https://staging.yoursite.com/blog/', {
    waitUntil: 'networkidle0'
  });

  // Hide timestamps before screenshot
  await page.evaluate(() => {
    document.querySelectorAll('.entry-date').forEach(el => {
      el.style.visibility = 'hidden';
    });
  });

  const screenshot = await page.screenshot({ fullPage: true });

  expect(screenshot).toMatchImageSnapshot({
    failureThreshold: 0.01,
    failureThresholdType: 'percent'
  });
});
Enter fullscreen mode Exit fullscreen mode

Alternative: Screenshot Specific Elements

test('Blog Post Card Layout', async () => {
  await page.goto('https://staging.yoursite.com/blog/');

  // Screenshot only first post card (stable structure)
  const element = await page.$('.post-card:first-child .post-card-content');
  const screenshot = await element.screenshot();

  expect(screenshot).toMatchImageSnapshot({
    customSnapshotIdentifier: 'blog-post-card-layout'
  });
});
Enter fullscreen mode Exit fullscreen mode

Real-World Scenario: Theme Update

Client: "We need to update Divi theme from 5.0 to 5.2"

Before automated testing:

  1. Update Divi on staging
  2. Manually check 10 pages
  3. Deploy to production
  4. Mobile navigation completely broken!!
  5. Emergency rollback
  6. Client furious!!

With automated testing:

# Update Divi on staging
# Run visual tests

npm test

# Output:
# FAIL  tests/navigation.test.js
#   ✗ Mobile Navigation (3821ms)
#
# Expected image to match snapshot but was 15.3% different
# Mobile menu button position shifted 50px
#
# See: __diff_output__/navigation-test-js-mobile-nav-1-diff.png
Enter fullscreen mode Exit fullscreen mode

Visual diff shows:

  • Baseline: Mobile menu button top-right corner
  • Current: Mobile menu button off-screen
  • Caught BEFORE production!!

Fix CSS, re-run tests:

npm test

# PASS  tests/navigation.test.js
#   ✓ Mobile Navigation (3654ms)
#
# Snapshots:   1 passed, 1 total
Enter fullscreen mode Exit fullscreen mode

Deploy to production with confidence!!

For comprehensive Divi optimization, check out Divi Speed Optimization: Get 90+ PageSpeed Without Breaking Your Site.

Performance Optimization

Parallel Test Execution

package.json:

{
  "scripts": {
    "test": "jest --maxWorkers=4"
  }
}
Enter fullscreen mode Exit fullscreen mode

Runs 4 tests simultaneously = 4x faster!!

Skip Unnecessary Resources

beforeAll(async () => {
  page = await browser.newPage();

  // Block images for faster tests (layout-only testing)
  await page.setRequestInterception(true);

  page.on('request', request => {
    if (request.resourceType() === 'image') {
      request.abort();
    } else {
      request.continue();
    }
  });
});
Enter fullscreen mode Exit fullscreen mode

20-40% faster test execution!!

Bottom Line

Stop deploying broken layouts to production!!

Automated visual regression testing:

  • Puppeteer screenshots (50+ pages in minutes)
  • Jest image comparison (pixel-perfect)
  • CI/CD integration (blocks bad deploys)
  • Catches layout breaks BEFORE users see them!!

My agency results:

Before automation:

  • 3 broken deployments per month
  • 2-4 hours emergency fixes each
  • Client complaints = lost revenue

After automation:

  • 0 broken deployments in 6 months
  • Visual regressions caught in CI/CD
  • 100% deployment confidence!!

Setup time: 4 hours initial configuration

ROI: First caught regression paid for entire setup!!

For WordPress agencies: Visual regression testing is NON-NEGOTIABLE!!

Compare 1,000 screenshots automatically vs manually checking 20 pages = massive time savings + zero broken deployments!! 📸

This article contains affiliate links!

Top comments (1)

Collapse
 
martijn_assie_12a2d3b1833 profile image
Martijn Assie

Set this up 8 months ago after deploying broken mobile nav TWICE in one month (so embarrassing). Now we test 47 pages across 4 viewports automatically in GitHub Actions before every merge. Caught a Divi update last week that shifted our product grid by 100px - would've been a disaster in production. Pro tip: use failureThreshold around 0.01-0.02 to handle antialiasing differences but still catch real layout breaks...