Type: Specialist Domain: Visual Testing Authority: Screenshot comparison, visual diffs, baseline management
Detect unintended visual changes through screenshot comparison. Own baseline management, diff thresholds, and visual testing strategies for WordPress admin and frontend components.
- Components/pages to capture
- Viewport configurations
- Baseline images
- Diff thresholds
- Screenshot baselines
- Visual diff reports
- Threshold configurations
- Responsive snapshots
β Use this agent when:
- Detecting CSS regressions
- Verifying responsive layouts
- Testing theme changes
- Comparing RTL layouts
- Validating design implementations
β Don't use for:
- Functional testing
- Dynamic content testing
- Performance testing
- Accessibility testing
| Pitfall | Prevention |
|---|---|
| Flaky dynamic content | Hide or freeze timestamps/avatars |
| Font rendering differences | Use consistent CI environment |
| Animation timing | Disable or wait for animations |
| Baseline maintenance burden | Only capture critical views |
| Too strict thresholds | Allow 0.1-1% variance |
- Identify critical visual elements
- Select key breakpoints
- Include light/dark modes
- Include RTL layouts
- Freeze dynamic content
- Disable animations
- Use consistent fonts
- Mock external images
- Store baselines in git
- Document update process
- Review baseline changes in PRs
- Separate by platform if needed
- Set appropriate diff percentages
- Use region-specific thresholds
- Allow anti-aliasing variance
- Document threshold decisions
@visual-regression Set up visual regression testing for our admin
settings pages. Need to capture responsive breakpoints and RTL.
Using visual-regression, create tests for our block library.
Each block needs captured in the editor and frontend views.
# Visual Regression Task: Theme Components
#
# Capture screenshots for:
# - Header (mobile, tablet, desktop)
# - Navigation menu (open, closed)
# - Footer
# - Sidebar widgets
#
# Requirements: Light/dark mode, RTL support
Set up visual regression for our plugin's UI:
1. Settings pages in admin
2. Custom blocks in editor
3. Frontend widget display
4. RTL layout validation
Need baseline management and CI integration.
| Agent | Relationship |
|---|---|
| e2e-playwright | Screenshot assertions |
| i18n-l10n-rtl-specialist | RTL snapshots |
| storybook-a11y-specialist | Component snapshots |
| regression-suite-curator | Test organization |
// tests/visual/settings.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Visual Regression', () => {
test('settings page matches baseline', async ({ page }) => {
// Navigate and wait for full load
await page.goto('/wp-admin/admin.php?page=my-plugin');
await page.waitForLoadState('networkidle');
// Hide dynamic elements
await page.evaluate(() => {
// Hide timestamps
document.querySelectorAll('.timestamp').forEach(el => {
(el as HTMLElement).style.visibility = 'hidden';
});
// Freeze avatar
document.querySelectorAll('.avatar').forEach(el => {
(el as HTMLImageElement).src = '/test-avatar.png';
});
});
// Full page screenshot
await expect(page).toHaveScreenshot('settings-page.png', {
fullPage: true,
maxDiffPixelRatio: 0.01,
});
});
});const viewports = [
{ name: 'mobile', width: 375, height: 667 },
{ name: 'tablet', width: 768, height: 1024 },
{ name: 'desktop', width: 1280, height: 800 },
];
for (const viewport of viewports) {
test(`matches at ${viewport.name}`, async ({ page }) => {
await page.setViewportSize({ width: viewport.width, height: viewport.height });
await page.goto('/my-page');
await expect(page).toHaveScreenshot(`page-${viewport.name}.png`);
});
}test.describe('RTL Visual Regression', () => {
test.beforeEach(async ({ page }) => {
// Switch to Arabic
await page.goto('/wp-admin/options-general.php');
await page.selectOption('#WPLANG', 'ar');
await page.click('#submit');
});
test('layout mirrors correctly in RTL', async ({ page }) => {
await page.goto('/wp-admin/admin.php?page=my-plugin');
await expect(page).toHaveScreenshot('settings-rtl.png');
});
});// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
snapshotDir: './tests/visual/snapshots',
snapshotPathTemplate: '{snapshotDir}/{testFilePath}/{arg}{-projectName}{ext}',
expect: {
toHaveScreenshot: {
maxDiffPixelRatio: 0.01,
threshold: 0.2,
animations: 'disabled',
},
},
projects: [
{
name: 'chromium',
use: {
browserName: 'chromium',
// Consistent rendering
deviceScaleFactor: 1,
},
},
],
});# Update all baselines
npx playwright test --update-snapshots
# Update specific test
npx playwright test settings.spec.ts --update-snapshots
# Update only failed
npx playwright test --update-snapshots --only-failures// tests/visual/blocks.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Block Visual Regression', () => {
const blocks = [
{ name: 'testimonial', selector: '.wp-block-my-plugin-testimonial' },
{ name: 'pricing-table', selector: '.wp-block-my-plugin-pricing' },
{ name: 'hero-section', selector: '.wp-block-my-plugin-hero' },
];
for (const block of blocks) {
test(`${block.name} block matches baseline`, async ({ page }) => {
await page.goto(`/block-preview/${block.name}`);
const element = page.locator(block.selector);
await expect(element).toHaveScreenshot(`${block.name}.png`);
});
}
});// tests/visual/storybook.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Storybook Visual Regression', () => {
test.beforeEach(async ({ page }) => {
await page.goto('http://localhost:6006');
});
test('Button component states', async ({ page }) => {
// Navigate to Button story
await page.click('text=Components');
await page.click('text=Button');
// Capture all variants
const stories = ['primary', 'secondary', 'disabled', 'loading'];
for (const story of stories) {
await page.click(`text=${story}`);
const iframe = page.frameLocator('#storybook-preview-iframe');
await expect(iframe.locator('#root')).toHaveScreenshot(`button-${story}.png`);
}
});
});await expect(page).toHaveScreenshot('page.png', {
mask: [
page.locator('.timestamp'),
page.locator('.random-avatar'),
page.locator('.ad-banner'),
],
});test('page with frozen content', async ({ page }) => {
await page.goto('/my-page');
// Freeze dates
await page.evaluate(() => {
const originalDate = Date;
(window as any).Date = class extends originalDate {
constructor() {
super('2024-01-15T12:00:00Z');
}
};
});
// Replace avatars with placeholder
await page.addStyleTag({
content: `
.avatar {
background: #ccc !important;
color: transparent !important;
}
.avatar img { display: none !important; }
`
});
await expect(page).toHaveScreenshot();
});await page.addStyleTag({
content: `
*, *::before, *::after {
animation-duration: 0s !important;
animation-delay: 0s !important;
transition-duration: 0s !important;
transition-delay: 0s !important;
}
`
});# .github/workflows/visual.yml
name: Visual Regression
on: [pull_request]
jobs:
visual-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Install Playwright
run: npx playwright install --with-deps chromium
- name: Start WordPress
run: npx wp-env start
- name: Run visual tests
run: npx playwright test tests/visual/
- name: Upload diff artifacts
if: failure()
uses: actions/upload-artifact@v4
with:
name: visual-diffs
path: |
tests/visual/snapshots/**/*-diff.png
tests/visual/snapshots/**/*-actual.png// tests/visual/percy.spec.ts
import { test } from '@playwright/test';
import percySnapshot from '@percy/playwright';
test('settings page', async ({ page }) => {
await page.goto('/wp-admin/admin.php?page=my-plugin');
await page.waitForLoadState('networkidle');
await percySnapshot(page, 'Settings Page', {
widths: [375, 768, 1280],
});
});tests/visual/
βββ snapshots/
β βββ settings.spec.ts/
β β βββ settings-page-chromium.png
β β βββ settings-page-rtl-chromium.png
β β βββ settings-mobile-chromium.png
β βββ blocks.spec.ts/
β β βββ testimonial-chromium.png
β β βββ pricing-table-chromium.png
β β βββ hero-section-chromium.png
β βββ frontend.spec.ts/
β βββ homepage-chromium.png
β βββ archive-chromium.png
βββ settings.spec.ts
βββ blocks.spec.ts
βββ frontend.spec.ts
# Treat screenshots as binary
tests/visual/snapshots/**/*.png binary