The Playwright Playbook — Part 5: Visual Regression Testing
"Your assertions say the button exists. Visual regression says it's not buried under the header anymore."
In Part 1, we built the foundation. Part 2 owned the network. Part 3 handled multiple users. Part 4 replaced Postman with a full API testing layer.
Every single assertion we've written so far has one thing in common.
They all test behaviour — what the app does. Not appearance — what the user actually sees.
And that's where bugs hide.
The button is there. toBeVisible() passes. But a CSS regression pushed it behind the navigation bar. The user can't click it. Your test doesn't know.
The task list renders. The count is correct. But a font-size change made the text overflow and truncate. Half the title is hidden. Your test doesn't know.
The dashboard loads. All assertions pass. But the mobile layout is completely broken. Your test doesn't know — because you never checked a mobile viewport.
Visual regression testing catches all of this. And Playwright has it built in — no third-party tool required.
Let's add it to the framework. 🎯
🏗️ Where We Left Off
After Part 4, our full project structure is:
playwright-playbook/
├── tests/
│ ├── auth/
│ │ └── login.spec.ts ✅ Part 1
│ ├── tasks/
│ │ └── task-management.spec.ts ✅ Part 1
│ ├── network/ ✅ Part 2
│ │ ├── api-mocking.spec.ts
│ │ ├── error-simulation.spec.ts
│ │ └── network-assertions.spec.ts
│ ├── multi-user/ ✅ Part 3
│ │ ├── role-permissions.spec.ts
│ │ └── realtime-collaboration.spec.ts
│ ├── multi-tab/ ✅ Part 3
│ │ └── multi-tab-flows.spec.ts
│ └── api/ ✅ Part 4
│ ├── tasks-api.spec.ts
│ ├── auth-api.spec.ts
│ ├── graphql-api.spec.ts
│ └── api-ui-chain.spec.ts
├── pages/
│ ├── LoginPage.ts ✅ Part 1
│ ├── TaskPage.ts ✅ Part 1
│ └── DashboardPage.ts ✅ Part 3
├── api/ ✅ Part 4
│ ├── TaskApiClient.ts
│ └── AuthApiClient.ts
├── fixtures/
│ ├── auth.fixture.ts ✅ Part 1
│ ├── tasks.json ✅ Part 2
│ ├── empty-tasks.json ✅ Part 2
│ ├── tasks-har.har ✅ Part 2
│ ├── multi-user.fixture.ts ✅ Part 3
│ └── api.fixture.ts ✅ Part 4
├── scripts/
│ └── record-har.ts ✅ Part 2
├── utils/
│ └── schema-validator.ts ✅ Part 4
├── .auth/
│ ├── admin.json
│ └── user.json
├── global-setup.ts ✅ Part 1
├── playwright.config.ts ✅ Part 1 (updated Parts 3 & 4)
└── .env
By the end of Part 5, we add:
playwright-playbook/
├── tests/
│ └── visual/ ← NEW
│ ├── dashboard-visual.spec.ts
│ ├── task-visual.spec.ts
│ └── responsive-visual.spec.ts
├── utils/
│ └── visual-helpers.ts ← NEW
├── snapshots/ ← NEW (auto-generated, committed to git)
│ └── [baseline PNG files live here]
Every file gets fully built below. 👇
🧠 How Playwright Visual Regression Works — The Mental Model
Before we write code, understand what's happening under the hood.
First run (no baseline exists):
test runs → screenshot taken → saved as baseline PNG → test PASSES
Subsequent runs:
test runs → screenshot taken → compared pixel-by-pixel to baseline
├── Match within threshold → ✅ PASS
└── Diff exceeds threshold → ❌ FAIL + diff image generated
When UI intentionally changes:
run with --update-snapshots flag → baseline replaced → commit new baseline
The baseline PNGs live in your repo alongside your tests. Every developer, every CI run, compares against the same baseline. That's what makes it reliable.
Three things that make or break VRT in practice:
- Masking — hide dynamic content (timestamps, avatars, ads) that changes every run
- Threshold — how many pixels can differ before it's a failure
- Consistent environment — fonts, OS rendering, and viewport must be identical across runs
We'll handle all three. 🎯
⚙️ Updating playwright.config.ts for Visual Testing
Visual regression needs its own project config — consistent viewport, single browser (Chromium for VRT), and snapshot settings.
// playwright.config.ts — full updated version
import { defineConfig, devices } from '@playwright/test';
import * as dotenv from 'dotenv';
dotenv.config();
export default defineConfig({
testDir: './tests',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 1 : 0,
workers: process.env.CI ? 4 : undefined,
globalSetup: './global-setup.ts',
reporter: [
['html', { open: 'never' }],
['list'],
],
// Snapshot configuration — applies to all toHaveScreenshot calls
expect: {
toHaveScreenshot: {
// Allow up to 0.2% of pixels to differ — handles anti-aliasing differences
maxDiffPixelRatio: 0.002,
// How long to wait for the screenshot to stabilize
timeout: 10000,
// Animations must be disabled for consistent screenshots
animations: 'disabled',
},
},
use: {
baseURL: process.env.BASE_URL || 'http://localhost:3000',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
trace: 'on-first-retry',
extraHTTPHeaders: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
},
projects: [
{
name: 'admin',
use: {
...devices['Desktop Chrome'],
storageState: '.auth/admin.json',
},
testMatch: ['**/auth/**', '**/tasks/**', '**/network/**'],
},
{
name: 'user',
use: {
...devices['Desktop Chrome'],
storageState: '.auth/user.json',
},
testMatch: ['**/tasks/**'],
},
{
name: 'multi-context',
use: { ...devices['Desktop Chrome'] },
testMatch: ['**/multi-user/**', '**/multi-tab/**'],
},
{
name: 'api',
use: {},
testMatch: ['**/api/**'],
},
{
// Visual regression — Chromium only, fixed viewport, admin auth
// Single browser keeps baselines consistent across machines
name: 'visual',
use: {
...devices['Desktop Chrome'],
storageState: '.auth/admin.json',
// Fixed viewport — VRT breaks if viewport varies
viewport: { width: 1280, height: 720 },
// Disable animations — they cause pixel differences mid-screenshot
launchOptions: {
args: ['--disable-gpu', '--force-device-scale-factor=1'],
},
},
testMatch: ['**/visual/**'],
// Store snapshots next to the test files
snapshotDir: './snapshots',
},
{
// Responsive VRT — separate project, different viewports per test
name: 'visual-responsive',
use: {
...devices['Desktop Chrome'],
storageState: '.auth/admin.json',
},
testMatch: ['**/visual/responsive**'],
snapshotDir: './snapshots/responsive',
},
],
});
🛠️ Building the Visual Helpers Utility
Masking and screenshot config comes up in every VRT test. Instead of repeating it, we centralise it.
// utils/visual-helpers.ts
import { Page, Locator } from '@playwright/test';
/**
* Elements that change every run — timestamps, avatars, live counters.
* Pass these to toHaveScreenshot({ mask: [...] }) to hide them from comparison.
*/
export function getDynamicMasks(page: Page): Locator[] {
return [
page.getByTestId('timestamp'), // "Created 2 mins ago"
page.getByTestId('user-avatar'), // Profile photos
page.getByTestId('notification-count'), // Live badge counts
page.getByTestId('last-updated'), // "Last updated: 14:32:01"
page.getByTestId('session-timer'), // Session countdown
];
}
/**
* Standard screenshot options used across all full-page VRT tests.
* Disable animations, set threshold, mask dynamic elements.
*/
export function fullPageScreenshotOptions(page: Page) {
return {
fullPage: true,
mask: getDynamicMasks(page),
maxDiffPixelRatio: 0.002,
animations: 'disabled' as const,
};
}
/**
* Component-level screenshot options — for isolated widget/card snapshots.
* Tighter threshold since we're testing a smaller, more controlled area.
*/
export function componentScreenshotOptions(page: Page) {
return {
mask: getDynamicMasks(page),
maxDiffPixelRatio: 0.001,
animations: 'disabled' as const,
};
}
/**
* Wait for all images on the page to fully load before screenshotting.
* Prevents half-loaded images causing false failures.
*/
export async function waitForImagesLoaded(page: Page): Promise<void> {
await page.waitForFunction(() => {
const images = Array.from(document.images);
return images.every(img => img.complete && img.naturalHeight !== 0);
});
}
/**
* Wait for all network activity to settle before screenshotting.
* Prevents loading spinners appearing in snapshots.
*/
export async function waitForPageStable(page: Page): Promise<void> {
await page.waitForLoadState('networkidle');
await waitForImagesLoaded(page);
// Small buffer for CSS transitions to complete
await page.waitForTimeout(300);
}
📸 Dashboard Visual Tests — Full Page & Component Level
// tests/visual/dashboard-visual.spec.ts
import { test, expect } from '@playwright/test';
import { DashboardPage } from '../../pages/DashboardPage';
import { TaskApiClient } from '../../api/TaskApiClient';
import { AuthApiClient } from '../../api/AuthApiClient';
import {
fullPageScreenshotOptions,
componentScreenshotOptions,
waitForPageStable,
} from '../../utils/visual-helpers';
test.describe('Dashboard — Full Page Visual', () => {
test('dashboard full page snapshot — default state', async ({ page, request }) => {
// Seed known data via API so the screenshot is always consistent
const authApi = new AuthApiClient(request);
const token = await authApi.getAdminToken();
const taskApi = new TaskApiClient(request, token);
// Create exactly 3 known tasks for a stable snapshot
const seeded = await Promise.all([
taskApi.createTask({ title: 'Write unit tests', status: 'pending' }),
taskApi.createTask({ title: 'Review pull request', status: 'completed' }),
taskApi.createTask({ title: 'Fix flaky test in CI', status: 'pending' }),
]);
const dashboard = new DashboardPage(page);
await dashboard.goto();
await waitForPageStable(page);
// Full page snapshot — first run creates baseline, subsequent runs compare
await expect(page).toHaveScreenshot(
'dashboard-default.png',
fullPageScreenshotOptions(page)
);
// Cleanup
await Promise.all(seeded.map(({ task }) => taskApi.deleteTask(task.id)));
});
test('dashboard snapshot — empty state (no tasks)', async ({ page, request }) => {
// Ensure there are no tasks for a clean empty state screenshot
const authApi = new AuthApiClient(request);
const token = await authApi.getAdminToken();
const taskApi = new TaskApiClient(request, token);
const allTasks = await taskApi.getAllTasks();
await Promise.all(allTasks.map(t => taskApi.deleteTask(t.id)));
const dashboard = new DashboardPage(page);
await dashboard.goto();
await waitForPageStable(page);
await expect(page).toHaveScreenshot(
'dashboard-empty.png',
fullPageScreenshotOptions(page)
);
});
});
test.describe('Dashboard — Component Level Visual', () => {
test('task list component snapshot', async ({ page }) => {
const dashboard = new DashboardPage(page);
await dashboard.goto();
await waitForPageStable(page);
// Screenshot just the task list — not the whole page
await expect(dashboard.taskList).toHaveScreenshot(
'task-list-component.png',
componentScreenshotOptions(page)
);
});
test('notification panel snapshot — when open', async ({ page }) => {
const dashboard = new DashboardPage(page);
await dashboard.goto();
await dashboard.openNotifications();
// Wait for panel animation to settle
await page.waitForTimeout(300);
await expect(dashboard.notificationPanel).toHaveScreenshot(
'notification-panel-open.png',
componentScreenshotOptions(page)
);
});
test('admin panel component snapshot', async ({ page }) => {
const dashboard = new DashboardPage(page);
await dashboard.goto();
await waitForPageStable(page);
await expect(dashboard.adminPanel).toHaveScreenshot(
'admin-panel-component.png',
componentScreenshotOptions(page)
);
});
});
test.describe('Dashboard — State Change Visual', () => {
test('task status changes are reflected visually', async ({ page, request }) => {
const authApi = new AuthApiClient(request);
const token = await authApi.getAdminToken();
const taskApi = new TaskApiClient(request, token);
const { task } = await taskApi.createTask({
title: 'Task for visual status test',
status: 'pending',
});
const dashboard = new DashboardPage(page);
await dashboard.goto();
await waitForPageStable(page);
// Screenshot in pending state
const taskLocator = dashboard.getTaskLocator('Task for visual status test');
await expect(taskLocator).toHaveScreenshot(
'task-item-pending.png',
componentScreenshotOptions(page)
);
// Mark as complete via API
await taskApi.updateTask(task.id, { status: 'completed' });
await page.reload();
await waitForPageStable(page);
// Screenshot in completed state — should show visual difference (strikethrough, green badge etc.)
await expect(taskLocator).toHaveScreenshot(
'task-item-completed.png',
componentScreenshotOptions(page)
);
// Cleanup
await taskApi.deleteTask(task.id);
});
});
🎨 Task Page Visual Tests — States & Error Handling
// tests/visual/task-visual.spec.ts
import { test, expect } from '@playwright/test';
import { TaskPage } from '../../pages/TaskPage';
import { TaskApiClient } from '../../api/TaskApiClient';
import { AuthApiClient } from '../../api/AuthApiClient';
import {
fullPageScreenshotOptions,
componentScreenshotOptions,
waitForPageStable,
} from '../../utils/visual-helpers';
test.describe('Task Page — Visual States', () => {
test('task list page — with data', async ({ page, request }) => {
// Seed consistent data via API
const authApi = new AuthApiClient(request);
const token = await authApi.getAdminToken();
const taskApi = new TaskApiClient(request, token);
const seeded = await Promise.all([
taskApi.createTask({ title: 'Design new feature', status: 'pending' }),
taskApi.createTask({ title: 'Write tests for login', status: 'in_progress' }),
taskApi.createTask({ title: 'Deploy to staging', status: 'completed' }),
]);
const taskPage = new TaskPage(page);
await taskPage.goto();
await waitForPageStable(page);
await expect(page).toHaveScreenshot(
'task-page-with-data.png',
fullPageScreenshotOptions(page)
);
await Promise.all(seeded.map(({ task }) => taskApi.deleteTask(task.id)));
});
test('task page — empty state visual', async ({ page }) => {
// Use network mock to force empty state cleanly
await page.route('**/api/tasks', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: '[]',
});
});
const taskPage = new TaskPage(page);
await taskPage.goto();
await waitForPageStable(page);
await expect(page).toHaveScreenshot(
'task-page-empty.png',
fullPageScreenshotOptions(page)
);
});
test('task page — error state visual (API 500)', async ({ page }) => {
// Use network mock to force error state
await page.route('**/api/tasks', async route => {
await route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({ error: 'Internal Server Error' }),
});
});
const taskPage = new TaskPage(page);
await taskPage.goto();
await waitForPageStable(page);
await expect(page).toHaveScreenshot(
'task-page-error.png',
fullPageScreenshotOptions(page)
);
});
test('task creation modal — open state', async ({ page }) => {
const taskPage = new TaskPage(page);
await taskPage.goto();
await waitForPageStable(page);
// Open the new task modal
await taskPage.newTaskButton.click();
await page.waitForTimeout(300); // wait for open animation
// Screenshot the modal — checks layout, spacing, form fields
await expect(page.getByTestId('task-modal')).toHaveScreenshot(
'task-creation-modal.png',
componentScreenshotOptions(page)
);
});
test('task item — hover state visual', async ({ page, request }) => {
const authApi = new AuthApiClient(request);
const token = await authApi.getAdminToken();
const taskApi = new TaskApiClient(request, token);
const { task } = await taskApi.createTask({ title: 'Hover state test task' });
const taskPage = new TaskPage(page);
await taskPage.goto();
await waitForPageStable(page);
// Hover to reveal action buttons — screenshot the hover state
const taskItem = taskPage.getTaskLocator('Hover state test task');
await taskItem.hover();
await expect(taskItem).toHaveScreenshot(
'task-item-hover.png',
componentScreenshotOptions(page)
);
await taskApi.deleteTask(task.id);
});
});
📱 Responsive Visual Tests — Mobile, Tablet, Desktop
This is the VRT layer most teams skip entirely. A layout that looks perfect on desktop can be completely broken on mobile. toBeVisible() will never tell you that.
// tests/visual/responsive-visual.spec.ts
import { test, expect, devices } from '@playwright/test';
import { DashboardPage } from '../../pages/DashboardPage';
import { TaskPage } from '../../pages/TaskPage';
import {
fullPageScreenshotOptions,
waitForPageStable,
} from '../../utils/visual-helpers';
// Define the viewports we care about
const viewports = [
{ name: 'mobile', width: 375, height: 812 }, // iPhone 14
{ name: 'tablet', width: 768, height: 1024 }, // iPad
{ name: 'desktop', width: 1280, height: 720 }, // Standard desktop
{ name: 'wide', width: 1920, height: 1080 }, // Large monitor
];
test.describe('Dashboard — Responsive Visual', () => {
for (const viewport of viewports) {
test(`dashboard layout — ${viewport.name} (${viewport.width}x${viewport.height})`, async ({
page,
}) => {
await page.setViewportSize({
width: viewport.width,
height: viewport.height,
});
const dashboard = new DashboardPage(page);
await dashboard.goto();
await waitForPageStable(page);
await expect(page).toHaveScreenshot(
`dashboard-${viewport.name}.png`,
fullPageScreenshotOptions(page)
);
});
}
});
test.describe('Task Page — Responsive Visual', () => {
for (const viewport of viewports) {
test(`task list layout — ${viewport.name} (${viewport.width}x${viewport.height})`, async ({
page,
}) => {
// Seed stable data via mock so screenshots are consistent
await page.route('**/api/tasks', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([
{ id: 1, title: 'Design new feature', status: 'pending', assignee: 'admin' },
{ id: 2, title: 'Write tests for login', status: 'in_progress', assignee: 'user' },
{ id: 3, title: 'Deploy to staging', status: 'completed', assignee: 'admin' },
]),
});
});
await page.setViewportSize({
width: viewport.width,
height: viewport.height,
});
const taskPage = new TaskPage(page);
await taskPage.goto();
await waitForPageStable(page);
await expect(page).toHaveScreenshot(
`task-page-${viewport.name}.png`,
fullPageScreenshotOptions(page)
);
});
}
});
test.describe('Login Page — Responsive Visual', () => {
// Login page is special — clear storageState so we see the actual login form
test.use({ storageState: { cookies: [], origins: [] } });
for (const viewport of viewports) {
test(`login page layout — ${viewport.name}`, async ({ page }) => {
await page.setViewportSize({
width: viewport.width,
height: viewport.height,
});
await page.goto('/login');
await waitForPageStable(page);
await expect(page).toHaveScreenshot(
`login-page-${viewport.name}.png`,
fullPageScreenshotOptions(page)
);
});
}
});
🔄 The Update Workflow — When UI Changes Are Intentional
When a developer intentionally changes the UI — a redesign, a new component, a colour update — your VRT tests will fail. That's correct behaviour.
The workflow:
# Step 1 — Run VRT to see what changed
npx playwright test --project=visual
# Step 2 — Open the HTML report to review the diffs visually
npx playwright show-report
# Step 3 — If the change is intentional, update the baselines
npx playwright test --project=visual --update-snapshots
# Step 4 — Commit the new baseline PNGs alongside your code change
git add snapshots/
git commit -m "chore: update visual baselines for new dashboard redesign"
This is the critical discipline: baseline updates must be a conscious decision, reviewed before committing. Never update baselines automatically in CI without human review. That defeats the entire purpose. ⚠️
🤔 When to Use Component vs Full Page Screenshots
This comes up constantly. Here's the decision framework:
Full page screenshot — use when:
├── Testing overall page layout and composition
├── Testing how components relate to each other spatially
├── Testing page-level responsive behaviour
└── First-time coverage of a new page
Component screenshot — use when:
├── Testing a specific widget in isolation (modal, card, dropdown)
├── Testing state changes within a component (hover, active, error)
├── The page has a lot of dynamic content you can't fully mask
└── You want faster, more targeted failure messages
In practice — start with full page, move to component when you find your full page tests are too noisy to maintain. 🎯
🚫 Common VRT Mistakes — And How We Avoid Them
❌ Screenshotting before the page is stable
// 🔴 Bad — loading spinner might still be visible
await page.goto('/dashboard');
await expect(page).toHaveScreenshot('dashboard.png');
// ✅ Good — use waitForPageStable from our utils
await page.goto('/dashboard');
await waitForPageStable(page);
await expect(page).toHaveScreenshot('dashboard.png', fullPageScreenshotOptions(page));
❌ Not masking dynamic content
// 🔴 Bad — timestamp changes every run → test always fails
await expect(page).toHaveScreenshot('dashboard.png');
// ✅ Good — masks hide the dynamic elements
await expect(page).toHaveScreenshot('dashboard.png', {
mask: getDynamicMasks(page),
});
❌ Running VRT across different OS/browsers
Screenshots render differently on macOS vs Linux vs Windows. A baseline taken on your MacBook will fail on a Linux CI runner — every single time.
// ✅ Good — lock VRT to one browser and use Docker in CI
// playwright.config.ts visual project already uses Desktop Chrome only
// In CI, run Playwright inside the official Docker image:
// mcr.microsoft.com/playwright:v1.44.0-jammy
❌ Setting threshold too high
// 🔴 Bad — 10% pixel difference allowed = real bugs slip through
maxDiffPixelRatio: 0.1
// ✅ Good — tight threshold, mask dynamic content instead of widening tolerance
maxDiffPixelRatio: 0.002
📁 Final Project Structure After Part 5
Every file listed below has been fully built across Parts 1 through 5:
playwright-playbook/
├── tests/
│ ├── auth/
│ │ └── login.spec.ts ✅ Part 1
│ ├── tasks/
│ │ └── task-management.spec.ts ✅ Part 1
│ ├── network/ ✅ Part 2
│ │ ├── api-mocking.spec.ts
│ │ ├── error-simulation.spec.ts
│ │ └── network-assertions.spec.ts
│ ├── multi-user/ ✅ Part 3
│ │ ├── role-permissions.spec.ts
│ │ └── realtime-collaboration.spec.ts
│ ├── multi-tab/ ✅ Part 3
│ │ └── multi-tab-flows.spec.ts
│ ├── api/ ✅ Part 4
│ │ ├── tasks-api.spec.ts
│ │ ├── auth-api.spec.ts
│ │ ├── graphql-api.spec.ts
│ │ └── api-ui-chain.spec.ts
│ └── visual/ ✅ Part 5
│ ├── dashboard-visual.spec.ts
│ ├── task-visual.spec.ts
│ └── responsive-visual.spec.ts
├── pages/
│ ├── LoginPage.ts ✅ Part 1
│ ├── TaskPage.ts ✅ Part 1
│ └── DashboardPage.ts ✅ Part 3
├── api/ ✅ Part 4
│ ├── TaskApiClient.ts
│ └── AuthApiClient.ts
├── fixtures/
│ ├── auth.fixture.ts ✅ Part 1
│ ├── tasks.json ✅ Part 2
│ ├── empty-tasks.json ✅ Part 2
│ ├── tasks-har.har ✅ Part 2
│ ├── multi-user.fixture.ts ✅ Part 3
│ └── api.fixture.ts ✅ Part 4
├── scripts/
│ └── record-har.ts ✅ Part 2
├── utils/
│ ├── schema-validator.ts ✅ Part 4
│ └── visual-helpers.ts ✅ Part 5
├── snapshots/ ✅ Part 5 (committed to git)
│ ├── dashboard-default.png
│ ├── dashboard-empty.png
│ ├── task-list-component.png
│ ├── task-page-with-data.png
│ ├── task-page-empty.png
│ ├── task-page-error.png
│ └── responsive/
│ ├── dashboard-mobile.png
│ ├── dashboard-tablet.png
│ ├── dashboard-desktop.png
│ ├── dashboard-wide.png
│ └── [more responsive baselines...]
├── .auth/ ← git-ignored
│ ├── admin.json
│ └── user.json
├── global-setup.ts ✅ Part 1
├── playwright.config.ts ✅ Part 1 (updated Parts 3, 4 & 5)
├── .env ← git-ignored
└── package.json
🗺️ What's Coming in This Series
Part 1 — Stop Writing Tests Like a Beginner ✅ Done
Part 2 — Network Interception: The Complete Guide ✅ Done
Part 3 — Multi-User, Multi-Tab & Context Testing ✅ Done
Part 4 — API Testing (The Underrated Superpower) ✅ Done
Part 5 — Visual Regression Testing ← You are here
Part 6 — Debugging Like a Pro: Trace Viewer & Inspector
Part 7 — The CI/CD Setup Nobody Shows You
Part 8 — Playwright Meets AI: Agents, MCP & Self-Healing Tests
In Part 6, we switch from writing tests to understanding what went wrong when they fail. Playwright's Trace Viewer, Inspector, VS Code integration, and page.pause() — the debugging toolkit that replaces console.log entirely.
🔖 Before You Go
Five parts in, and look at what you've built:
- A POM-based UI layer with proper auth management
- Full network interception and API mocking
- Multi-user and multi-tab test coverage
- A typed API testing layer with schema validation
- Visual regression across full pages, components, and four viewports
Every assertion type. Every test layer. One framework. One tool. One pipeline.
The bugs that slipped through before? They have nowhere to hide now. 🔥
Follow me so you don't miss Part 6 — where we go deep on debugging. When a test fails at 2am in CI, you need to know exactly what happened. Trace Viewer will show you every click, every network call, every DOM snapshot — in a timeline.
Drop a comment below 👇
- Are you currently doing any visual regression testing?
- What tool are you using — Percy, Chromatic, or something else?
- Did the responsive VRT loop pattern click for you?
Let's talk in the comments. 🙌
Faizal Shaikh | Senior Automation Engineer | Playwright & AI Testing
Connect with me on LinkedIn
Top comments (0)