Skip to main content

Visual Regression Testing

How to use Playwright's toHaveScreenshot() with SAP UI5 apps for visual regression testing. Covers theme variations, locale-dependent rendering, masking dynamic content, and CI integration.

Basic Visual Regression​

Playwright's built-in toHaveScreenshot() captures and compares page or element screenshots across test runs.

import { test, expect } from 'playwright-praman';

test.describe('Visual Regression - Purchase Order List', () => {
test('list report matches baseline', async ({ ui5Navigation, page }) => {
await ui5Navigation.navigateToIntent('#PurchaseOrder-manage');

await expect(page).toHaveScreenshot('po-list-report.png', {
maxDiffPixelRatio: 0.01,
});
});
});

Element-Level Screenshots​

Capture just a specific control instead of the full page:

test('smart table matches baseline', async ({ ui5, ui5Navigation }) => {
await ui5Navigation.navigateToIntent('#PurchaseOrder-manage');

const table = await ui5.control({
controlType: 'sap.ui.comp.smarttable.SmartTable',
});
const locator = await table.getLocator();

await expect(locator).toHaveScreenshot('po-smart-table.png', {
maxDiffPixelRatio: 0.01,
});
});

Masking Dynamic Content​

SAP apps contain dynamic data (timestamps, document numbers, user names) that changes between test runs. Mask these areas to prevent false positives.

CSS-Based Masking​

test('object page with masked dynamic content', async ({ ui5Navigation, page }) => {
await ui5Navigation.navigateToIntent('#PurchaseOrder-display?PurchaseOrder=4500000001');

await expect(page).toHaveScreenshot('po-object-page.png', {
mask: [
page.locator('[id*="createdAt"]'), // Creation timestamp
page.locator('[id*="changedAt"]'), // Last changed timestamp
page.locator('[id*="createdBy"]'), // User name
page.locator('.sapMMessageStrip'), // Dynamic messages
],
maxDiffPixelRatio: 0.01,
});
});

Masking with Praman Selectors​

test('form with masked values', async ({ ui5, ui5Navigation, page }) => {
await ui5Navigation.navigateToIntent('#PurchaseOrder-display?PurchaseOrder=4500000001');

// Get locators for dynamic fields
const dateField = await ui5.control({
controlType: 'sap.m.DatePicker',
id: /creationDate/,
});
const userField = await ui5.control({
controlType: 'sap.m.Text',
id: /createdByText/,
});

await expect(page).toHaveScreenshot('po-form.png', {
mask: [await dateField.getLocator(), await userField.getLocator()],
});
});

Theme Variations​

SAP UI5 supports multiple themes. Test visual consistency across them.

Multi-Theme Project Config​

// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

const themes = ['sap_horizon', 'sap_horizon_dark', 'sap_horizon_hcb', 'sap_horizon_hcw'];

export default defineConfig({
projects: themes.map((theme) => ({
name: `visual-${theme}`,
testDir: './tests/visual',
use: {
...devices['Desktop Chrome'],
// Pass theme as a custom parameter
launchOptions: {
args: [`--theme=${theme}`],
},
},
metadata: { theme },
})),
});

Applying Themes in Tests​

test.beforeEach(async ({ page }) => {
// Apply the UI5 theme via URL parameter
const theme = test.info().project.metadata.theme ?? 'sap_horizon';
const baseUrl = process.env.SAP_BASE_URL ?? '';
await page.goto(`${baseUrl}?sap-ui-theme=${theme}`);
});

test('list report in configured theme', async ({ ui5Navigation, page }) => {
const theme = test.info().project.metadata.theme ?? 'sap_horizon';
await ui5Navigation.navigateToIntent('#PurchaseOrder-manage');

await expect(page).toHaveScreenshot(`po-list-${theme}.png`, {
maxDiffPixelRatio: 0.01,
});
});

High Contrast Theme Testing​

High contrast themes (sap_horizon_hcb, sap_horizon_hcw) are required for accessibility compliance:

test('high contrast black theme renders correctly', async ({ ui5Navigation, page }) => {
// Apply high contrast theme
await page.goto(`${process.env.SAP_BASE_URL}?sap-ui-theme=sap_horizon_hcb`);
await ui5Navigation.navigateToIntent('#PurchaseOrder-manage');

await expect(page).toHaveScreenshot('po-list-hcb.png', {
maxDiffPixelRatio: 0.02, // Slightly higher tolerance for HC themes
});
});

Locale-Dependent Rendering​

SAP apps render differently based on locale settings: date formats, number formats, right-to-left (RTL) text direction, and translated labels.

Multi-Locale Testing​

const locales = [
{ code: 'en', name: 'English', rtl: false },
{ code: 'de', name: 'German', rtl: false },
{ code: 'ar', name: 'Arabic', rtl: true },
{ code: 'ja', name: 'Japanese', rtl: false },
];

for (const locale of locales) {
test(`list report renders correctly in ${locale.name}`, async ({ ui5Navigation, page }) => {
await page.goto(`${process.env.SAP_BASE_URL}?sap-language=${locale.code}`);
await ui5Navigation.navigateToIntent('#PurchaseOrder-manage');

await expect(page).toHaveScreenshot(`po-list-${locale.code}.png`, {
maxDiffPixelRatio: 0.02,
});
});
}

RTL Layout Validation​

test('RTL layout renders correctly for Arabic', async ({ ui5Navigation, page }) => {
await page.goto(`${process.env.SAP_BASE_URL}?sap-language=ar`);
await ui5Navigation.navigateToIntent('#PurchaseOrder-manage');

// Verify RTL direction is applied
const dir = await page.getAttribute('html', 'dir');
expect(dir).toBe('rtl');

await expect(page).toHaveScreenshot('po-list-rtl.png', {
maxDiffPixelRatio: 0.02,
});
});

Updating Baselines​

When intentional UI changes are made, update the screenshot baselines:

# Update all visual snapshots
npx playwright test tests/visual/ --update-snapshots

# Update snapshots for a specific theme
npx playwright test tests/visual/ --project=visual-sap_horizon --update-snapshots

# Update a specific test's snapshot
npx playwright test tests/visual/ -g "list report" --update-snapshots

CI Integration​

Storing Snapshots in Git​

Screenshot baselines should be committed to version control:

tests/
visual/
po-list-report.spec.ts
po-list-report.spec.ts-snapshots/
po-list-report-chromium-linux.png
po-list-report-chromium-darwin.png
po-list-report-chromium-win32.png

Platform-Specific Snapshots​

Font rendering differs across operating systems. Playwright automatically generates platform-specific snapshot names:

// Playwright appends -{browserName}-{platform}.png automatically
await expect(page).toHaveScreenshot('my-page.png');
// Generates: my-page-chromium-linux.png, my-page-chromium-darwin.png, etc.

GitHub Actions Workflow​

# .github/workflows/visual-regression.yml
name: Visual Regression
on:
pull_request:

jobs:
visual:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npx playwright install chromium

- run: npx playwright test tests/visual/ --project=visual-sap_horizon
env:
SAP_BASE_URL: ${{ secrets.SAP_QA_URL }}
SAP_USERNAME: ${{ secrets.SAP_QA_USER }}
SAP_PASSWORD: ${{ secrets.SAP_QA_PASS }}

- uses: actions/upload-artifact@v4
if: failure()
with:
name: visual-regression-report
path: |
playwright-report/
test-results/

Configuration Options​

// Global defaults in playwright.config.ts
export default defineConfig({
expect: {
toHaveScreenshot: {
maxDiffPixelRatio: 0.01, // 1% pixel difference allowed
threshold: 0.2, // Per-pixel color threshold (0-1)
animations: 'disabled', // Disable CSS animations for stability
},
},
});
OptionDefaultRecommended for SAP
maxDiffPixelRatio00.01 (1%) — handles anti-aliasing differences
threshold0.20.2 — default works well for UI5
animations'allow''disabled' — prevents animation-caused diffs
scale'css''css' — consistent across DPI settings

Best Practices​

PracticeWhy
Mask dynamic content (dates, IDs, usernames)Prevents false positives from changing data
Use element screenshots for componentsFull-page screenshots are brittle and slow to review
Set animations: 'disabled'CSS animations cause non-deterministic diffs
Test high contrast themesRequired for accessibility compliance (WCAG)
Keep tolerance low (1%)Catches real regressions while tolerating rendering noise
Store baselines in GitTrack visual changes alongside code changes
Update baselines intentionallyRun --update-snapshots only after verifying changes