QA Engineer Skills 2026QA-2026Visual Testing and Edge Cases

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

  1. First run: Playwright captures a screenshot and saves it as the baseline in a __snapshots__ directory
  2. Subsequent runs: Playwright captures a new screenshot and compares it pixel-by-pixel
  3. 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 mask and maxDiffPixelRatio to 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