Integrating Visual Tests in CI with Artifact Upload — automated screenshot comparison for catching visual regressions.
Integrating Visual Tests in CI with Artifact Upload enables catching unintended visual changes automatically. Instead of manually reviewing UI after every change, screenshot comparison detects pixel-level differences and reports them in CI. This prevents visual regressions from shipping to users undetected.
1npm install --save-dev @playwright/test
2npx playwright install --with-deps chromium
3
4# Generate initial baseline screenshots:
5npx playwright test tests/visual/ --update-snapshots
6
7# Run visual comparison tests:
8npx playwright test tests/visual/
9
10# On failure, screenshots are in test-results/
11# The diff image shows exactly what changed1// playwright.config.ts
2export default defineConfig({
3 expect: {
4 toHaveScreenshot: {
5 maxDiffPixelRatio: 0.02, // 2% pixel difference allowed
6 threshold: 0.2, // per-pixel color tolerance
7 animations: 'disabled', // freeze CSS animations
8 },
9 },
10 snapshotDir: './tests/visual/__screenshots__',
11});1await expect(page).toHaveScreenshot('page.png', {
2 // Mask elements that change between test runs
3 mask: [
4 page.locator('[data-testid="timestamp"]'),
5 page.locator('[data-testid="user-avatar"]'),
6 page.locator('.advertisement'),
7 ],
8 animations: 'disabled',
9});
10
11// Freeze dynamic content before screenshot:
12await page.evaluate(() => {
13 document.querySelectorAll('[data-testid="live-count"]')
14 .forEach(el => el.textContent = '1,234');
15 const style = document.createElement('style');
16 style.textContent = '*, *::before, *::after { transition-duration: 0s !important; }';
17 document.head.appendChild(style);
18});1- name: Run visual tests
2 run: npx playwright test tests/visual/
3 continue-on-error: true
4
5- name: Upload visual test artifacts
6 uses: actions/upload-artifact@v4
7 if: always()
8 with:
9 name: visual-results
10 path: |
11 playwright-report/
12 test-results/
13 retention-days: 30Workflow for baseline updates:
npx playwright test --update-snapshots locally1test('dark mode visual consistency', async ({ page }) => {
2 // Light mode baseline
3 await page.goto('/courses');
4 await expect(page).toHaveScreenshot('courses-light.png');
5
6 // Dark mode — emulate prefers-color-scheme: dark
7 await page.emulateMedia({ colorScheme: 'dark' });
8 await page.reload();
9 await expect(page).toHaveScreenshot('courses-dark.png');
10
11 // Verify dark mode is actually applied
12 const bodyBg = await page.evaluate(() =>
13 getComputedStyle(document.body).backgroundColor
14 );
15 // Dark backgrounds have low luminance
16 const [r, g, b] = bodyBg.match(/\d+/g)?.map(Number) || [];
17 const luminance = 0.299 * r + 0.587 * g + 0.114 * b;
18 expect(luminance).toBeLessThan(128); // dark background
19});1const BREAKPOINTS = [
2 { name: 'mobile', width: 375, height: 812 },
3 { name: 'tablet', width: 768, height: 1024 },
4 { name: 'desktop', width: 1280, height: 800 },
5];
6
7for (const { name, width, height } of BREAKPOINTS) {
8 test(`homepage at ${name}`, async ({ page }) => {
9 await page.setViewportSize({ width, height });
10 await page.goto('/');
11 await expect(page).toHaveScreenshot(`homepage-${name}.png`, {
12 fullPage: true,
13 animations: 'disabled',
14 });
15 });
16}
17
18// Also check for horizontal overflow:
19test('no horizontal scroll on mobile', async ({ page }) => {
20 await page.setViewportSize({ width: 375, height: 812 });
21 await page.goto('/');
22 const hasScroll = await page.evaluate(() =>
23 document.documentElement.scrollWidth > document.documentElement.clientWidth
24 );
25 expect(hasScroll).toBe(false);
26});Production visual regression tests with Playwright covering pages, components, dark mode, and responsive breakpoints.
1# .github/workflows/visual-tests.yml
2name: Visual Regression Tests
3
4on:
5 pull_request:
6
7jobs:
8 visual:
9 runs-on: ubuntu-latest
10 steps:
11 - uses: actions/checkout@v4
12 with: { fetch-depth: 0 } # needed to compare with main branch baselines
13
14 - uses: actions/setup-node@v4
15 with: { node-version: '20', cache: 'npm' }
16
17 - run: npm ci
18 - run: npx playwright install --with-deps chromium
19
20 - name: Start app
21 run: npm run build && npm start &
22 - run: npx wait-on http://localhost:3000
23
24 - name: Run visual tests
25 run: npx playwright test tests/visual/ --reporter=html
26 continue-on-error: true # upload artifacts even on failure
27
28 - name: Upload screenshots and report
29 uses: actions/upload-artifact@v4
30 if: always()
31 with:
32 name: visual-test-results
33 path: |
34 playwright-report/
35 test-results/
36 tests/visual/__screenshots__/
37 retention-days: 30
38
39 - name: Comment PR with diff link
40 if: failure()
41 uses: actions/github-script@v7
42 with:
43 script: |
44 const runUrl = context.serverUrl + '/' + context.repo.owner + '/' + context.repo.repo + '/actions/runs/' + context.runId;
45 await github.rest.issues.createComment({
46 ...context.repo,
47 issue_number: context.issue.number,
48 body: 'Visual regression failures detected. [View screenshots](' + runUrl + ')',
49 });This visual test implementation covers integrating visual tests in ci with artifact upload with Playwright's built-in screenshot comparison. Baseline screenshots are stored in __screenshots__ and compared on every test run. The 2% maxDiffPixelRatio allows for minor rendering differences across environments. CI uploads failure artifacts for review, and developers update baselines by running npx playwright test --update-snapshots when changes are intentional.
Continue with the next visual regression topic to build comprehensive screenshot coverage across your entire UI.
Your progress is saved in this browser, on this device only.