Stabilizing visual tests with API mocking in Playwright
1. Visual tests next to functional tests
Stabilizing visual tests with API mocking in Playwright
1. Visual tests next to functional tests
Problem
Functional tests verify behavior, but they often miss visual regressions. Examples of issues functional tests won’t detect:
- Broken layouts after CSS change
- Missing icons or images
- Incorrect fonts or styling
- Overlapping components
A test may pass because the button is clickable, even if the UI looks broken. Example:
await page.click('#checkout');
await expect(page.locator('#confirmation')).toBeVisible();
The test passes even if the page layout is visually broken.
Solution
Add visual snapshot tests next to your functional tests using Playwright. Playwright can capture a screenshot and compare it with a baseline snapshot.
import { test, expect } from '@playwright/test';
test('checkout page looks correct', async ({ page }) => {
await page.goto('/checkout');
// Functional validation
await expect(page.locator('h1')).toHaveText('Checkout');
// Visual validation
await expect(page).toHaveScreenshot('checkout-page.png');
});
Playwright stores a baseline screenshot and compares future runs against it. If the UI changes, the test fails and shows a diff image comparing the baseline with the actual result.

Benefits and Examples
Benefits:
- Detect UI regressions automatically
- Catch CSS and layout issues
- Validate complete page rendering
- Reduce manual visual QA
Think of it as:
Functional tests check what the UI does Visual tests check what the UI looks like
2. Stabilizing visual tests with network mocking
Problem
Visual tests can become unstable if they rely on dynamic data from APIs. Examples:
- Random usernames
- Changing timestamps
- Rotating product lists
- Real-time prices
This causes screenshot differences even when the UI is correct.Example:
await page.goto('/dashboard');
await expect(page).toHaveScreenshot();
If the dashboard contains dynamic data, the screenshot will change every run.
Solution
Use Playwright network interception to mock the API responses. This ensures the UI always renders predictable data.
test('dashboard visual test with stable data', async ({ page }) => {
await page.route('**/api/dashboard', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
user: "Test User",
notifications: 3,
lastLogin: "2024-01-01"
})
});
});
await page.goto('/dashboard');
await expect(page).toHaveScreenshot('dashboard.png');
});
Now the UI renders consistent test data, ensuring stable visual snapshots.

Benefits and Examples
Benefits:
- Eliminates flaky visual tests
- Ensures deterministic UI rendering
- Allows testing edge cases
- Removes dependency on backend availability
Example use cases:
- Show an empty state
- Display specific error messages
- Force edge-case UI states
- Freeze timestamps and values
Example mocked data scenario:
{
"orders": [
{
"id": 1,
"status": "Delivered",
"price": 99.99
}
{
"id": 2,
"status": "Returned",
"price": 49.99
]
}
Your UI will always render the same orders, producing stable screenshots.
3. Hybrid mocking (combining real and mocked data)
Problem
Full mocking is stable, but sometimes you still want real backend responses.
Reasons:
- Validate API contracts
- Use realistic data structures
- Avoid duplicating backend logic in tests
However, real responses still contain unstable fields like timestamps or IDs.
Solution
Use hybrid mocking:
- Fetch the real API response
- Modify unstable fields
- Return the modified response to the browser
test('hybrid mocked visual test', async ({ page, request }) => {
await page.route('**/api/profile', async route => {
const response = await request.fetch(route.request());
const data = await response.json();
// Override unstable fields
data.lastLogin = "2024-01-01";
data.notifications = 5;
await route.fulfill({
response,
body: JSON.stringify(data)
});
});
await page.goto('/profile');
await expect(page).toHaveScreenshot('profile.png');
});

Benefits and Examples
Benefits:
- Keep realistic API responses
- Remove unstable fields
- Maintain stable visual snapshots
- Reduce maintenance of large mock datasets
Example: Real API response:
{
"name": "Alice",
"lastLogin": "2026-03-06T12:43:22Z",
"notifications": 7
}
Hybrid mocked response:
{
"name": "Alice",
"lastLogin": "2024-01-01",
"notifications": 5
}
The UI remains realistic while visual tests stay stable.
Recommended framework structure
Keep visual and functional tests separated. This ensures:
- clear responsibility
- faster debugging
- independent pipelines
tests
├── functional
│ ├── login.spec.ts
│ └── checkout.spec.ts
│
├── visual
│ ├── login.visual.spec.ts
│ └── checkout.visual.spec.ts
│
└── mocks
└── apiMocks.ts
Key Takeaway
Reliable visual testing in Playwright comes from three layers:
- Functional tests → validate behavior
- Visual snapshots → detect UI regressions
- Network mocking → guarantee stable rendering
Together they provide high-confidence UI testing with minimal flakiness.
Checkout my other article about snapshot data management:
Useful links: Visual Testing in Playwright API mocking in Playwright
메타데이터
- post_id
- 1bb2aac0aba4
- slug
- stabilizing-visual-tests-with-api-mocking-in-playwright-1bb2aac0aba4
- url
- https://medium.com/@d.desutter91/stabilizing-visual-tests-with-api-mocking-in-playwright-1bb2aac0aba4
- canonical_url
- https://medium.com/@d.desutter91/stabilizing-visual-tests-with-api-mocking-in-playwright-1bb2aac0aba4
- author_url
- https://medium.com/@d.desutter91
- status
- ok
- fetched_at
- 2026-07-24 01:22:17