Visual Testing and Edge Cases
Functional tests verify behavior; visual tests verify appearance. Playwright supports both, along with a set of capabilities for handling edge cases that commonly break automated tests: file uploads/downloads, multiple tabs, authentication dialogs, and accessibility checks.
Visual Comparison (Screenshot Testing)
Playwright can capture screenshots and compare them pixel-by-pixel against baselines. Visual tests catch CSS regressions, layout shifts, and rendering issues that functional tests miss.
test('homepage matches visual baseline', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveScreenshot('homepage.png');
});
test('product card renders correctly', async ({ page }) => {
await page.goto('/products');
const card = page.getByTestId('product-card').first();
await expect(card).toHaveScreenshot('product-card.png');
});
How It Works
- First run: Playwright captures a screenshot and saves it as the baseline in a
__snapshots__directory - Subsequent runs: Playwright captures a new screenshot and compares it pixel-by-pixel
- On mismatch: The test fails and generates a diff image showing exactly what changed
Dealing with Dynamic Content
// Mask dynamic elements (timestamps, avatars, ads)
await expect(page).toHaveScreenshot('dashboard.png', {
mask: [page.getByTestId('timestamp'), page.getByTestId('avatar')],
});
// Allow a small pixel difference threshold
await expect(page).toHaveScreenshot('chart.png', {
maxDiffPixelRatio: 0.01, // Allow 1% of pixels to differ
});
// Wait for animations to settle
await expect(page).toHaveScreenshot('animated-widget.png', {
animations: 'disabled',
});
Accessibility Testing
Playwright integrates with axe-core for automated accessibility checks:
import AxeBuilder from '@axe-core/playwright';
test('homepage has no accessibility violations', async ({ page }) => {
await page.goto('/');
const results = await new AxeBuilder({ page }).analyze();
expect(results.violations).toEqual([]);
});
// Check specific rules or sections
test('login form is accessible', async ({ page }) => {
await page.goto('/login');
const results = await new AxeBuilder({ page })
.include('#login-form')
.withRules(['color-contrast', 'label', 'aria-required-attr'])
.analyze();
expect(results.violations).toEqual([]);
});
File Upload and Download
File Upload
test('upload a profile photo', async ({ page }) => {
await page.goto('/profile');
// Standard file input
await page.getByLabel('Profile photo').setInputFiles('test-data/photo.jpg');
// Multiple files
await page.getByLabel('Attachments').setInputFiles([
'test-data/doc1.pdf',
'test-data/doc2.pdf',
]);
// Clear file selection
await page.getByLabel('Profile photo').setInputFiles([]);
});
// Drag-and-drop file upload (non-input)
test('drag and drop file upload', async ({ page }) => {
await page.goto('/upload');
const fileChooserPromise = page.waitForEvent('filechooser');
await page.getByText('Drop files here').click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles('test-data/document.pdf');
});
File Download
test('download a report', async ({ page }) => {
const downloadPromise = page.waitForEvent('download');
await page.getByRole('button', { name: 'Export CSV' }).click();
const download = await downloadPromise;
expect(download.suggestedFilename()).toBe('report.csv');
await download.saveAs('test-results/report.csv');
});
Multi-Tab and Pop-up Handling
test('external link opens in new tab', async ({ page, context }) => {
await page.goto('/');
// Wait for the new page (tab) to open
const newPagePromise = context.waitForEvent('page');
await page.getByRole('link', { name: 'Documentation' }).click();
const newPage = await newPagePromise;
await newPage.waitForLoadState();
await expect(newPage).toHaveURL(/.*docs/);
await expect(newPage).toHaveTitle(/Documentation/);
});
Dialog Handling
test('confirm dialog before delete', async ({ page }) => {
// Set up dialog handler before triggering it
page.on('dialog', async (dialog) => {
expect(dialog.message()).toContain('Are you sure?');
await dialog.accept();
});
await page.getByRole('button', { name: 'Delete Account' }).click();
await expect(page.getByText('Account deleted')).toBeVisible();
});
Geolocation and Permissions
test('shows nearby stores for London location', async ({ context }) => {
await context.grantPermissions(['geolocation']);
await context.setGeolocation({ latitude: 51.5074, longitude: -0.1278 });
const page = await context.newPage();
await page.goto('/stores');
await expect(page.getByText('London')).toBeVisible();
});
Key Takeaways
- Visual testing catches CSS regressions and layout issues that functional tests miss
- Use
maskandmaxDiffPixelRatioto handle dynamic content in screenshots - Integrate axe-core for automated accessibility audits
- Playwright handles file uploads, downloads, multi-tab workflows, and dialogs natively
- Test geolocation, permissions, and device emulation through context configuration
- Edge case handling is built into the API — no third-party plugins needed