Visual Comparisons (toHaveScreenshot — so khớp ảnh chụp màn hình)
automationplaywright
Visual comparison (snapshot testing) là cơ chế chụp màn hình page/element rồi so pixel với ảnh baseline đã lưu: toHaveScreenshot tạo baseline lần đầu, các lần sau diff từng pixel — vượt ngưỡng cho phép (threshold/maxDiffPixels) thì fail kèm ảnh diff. Giá trị lớn nhất: bắt regression về giao diện (vỡ layout, đổi màu, font rớt) mà assertion text/DOM không thấy — đổi lại là test rất nhạy với môi trường render.
Khái niệm & Bản chất
- Snapshot/baseline testing: lần chạy đầu
toHaveScreenshotchụp và LƯU ảnh vào thư mục snapshot (không fail); các lần sau chụp mới rồi so pixel-by-pixel với baseline — sai khác vượt ngưỡng → fail kèm diff image (ảnh gốc, ảnh mới, mask vùng khác). - Pixel-diff, không phải AI: Playwright so màu từng pixel qua thuật toán (pixelmatch).
thresholdlà độ lệch màu cho phép trên MỘT pixel (0–1),maxDiffPixels/maxDiffPixelRatiolà tổng số/tỉ lệ pixel khác cho phép. - Baseline theo project + OS: tên file tự động kèm hậu tố
<project>-<platform>(vdchromium-darwin,firefox-linux) — vì font render, sub-pixel anti-aliasing khác nhau giữa macOS/Linux/Windows và giữa browser. - Vai trò: phủ lỗ hổng của functional test — DOM vẫn 'đúng' (text đúng, element visible) nhưng CSS vỡ, nút bị che, font biến mất. Visual test là lớp bắt cuối cho regression giao diện.
Cách hoạt động & API chính
- API:
await expect(page).toHaveScreenshot('homepage.png')(theo viewport, thêmfullPage: trueđể chụp cả trang cuộn) hoặcawait expect(locator).toHaveScreenshot()(chỉ element). Không truyền tên → tự gen theo tên test. - Option chống rung:
animations: 'disabled'(tắt CSS animation trước khi chụp),caret: 'hide'(ẩn con trỏ input),scale: 'css'(chụp theo CSS pixel thay vì device pixel — giảm khác biệt giữa máy retina/thường). - Ngưỡng chịu lỗi:
threshold: 0.2(độ nhạy màu từng pixel),maxDiffPixels: 100hoặcmaxDiffPixelRatio: 0.01(cho phép 1% pixel lệch — dung sai anti-aliasing). - Che vùng động:
mask: [page.getByTestId('timestamp'), page.locator('.ad-banner')]— phủ màu đặc lên vùng thay đổi mỗi lần chạy (đồng hồ, quảng cáo, ảnh random). - Config global:
playwright.config.ts→expect: { toHaveScreenshot: { maxDiffPixelRatio: 0.01, animations: 'disabled' } }— vàsnapshotPathTemplatetuỳ biến nơi lưu baseline. - Update baseline:
npx playwright test --update-snapshots(hoặc--update-snapshots changedchỉ cập nhật cái sai khác) — chạy sau khi CỐ Ý đổi giao diện.
Ví dụ & Cú pháp thực tế
- ```ts import { test, expect } from '@playwright/test'; test('trang chủ khớp baseline', async ({ page }) => { await page.goto('/'); await page.waitForLoadState('networkidle'); await expect(page).toHaveScreenshot('homepage.png', { fullPage: true, animations: 'disabled', // Cho phép lệch nhẹ do anti-aliasing font giữa các lần render maxDiffPixelRatio: 0.01, }); });
- ```ts test('component snapshot + mask vùng động', async ({ page }) => { await page.goto('/dashboard'); await expect(page.getByTestId('stat-card')).toHaveScreenshot({ // Mask đồng hồ realtime & avatar random — vùng này luôn đổi mask: [ page.getByTestId('current-time'), page.locator('.user-avatar'), ], threshold: 0.2, // độ nhạy màu mỗi pixel }); });
- ```ts // playwright.config.ts — chuẩn hoá cho cả suite import { defineConfig } from '@playwright/test'; export default defineConfig({ expect: { toHaveScreenshot: { animations: 'disabled', caret: 'hide', scale: 'css', maxDiffPixelRatio: 0.02, }, timeout: 10_000, // toHaveScreenshot cũng retry chờ ảnh ổn định }, snapshotPathTemplate: '{testFilePath}-snapshots/{testName}-{projectName}-{platform}{ext}', });
- ```bash # Cập nhật baseline sau khi cố ý đổi UI — review diff trước khi commit npx playwright test --update-snapshots # Chỉ update những snapshot bị sai khác (giữ nguyên cái đang pass) npx playwright test --update-snapshots changed # Gen baseline ĐÚNG môi trường CI bằng Docker npx playwright test --update-snapshots --project=chromium
Best practices & Khi nào dùng
- Gen & chạy baseline trong CÙNG một môi trường: tốt nhất là Docker image giống CI (
mcr.microsoft.com/playwright:v1.x). Baseline gen trên macOS rồi CI Linux = fail hàng loạt vì khác font engine. - Mask mọi thứ động: timestamp, tên user đăng nhập, ảnh quảng cáo, captcha, số liệu realtime — hoặc mock dữ liệu cố định (route interception) để trang render deterministic.
- Snapshot ở mức component thay vì full page khi có thể — vùng so sánh nhỏ hơn → ít flaky, diff report dễ đọc, lỗi khoanh đúng component hỏng.
- Dung sai vừa đủ: bắt đầu với
maxDiffPixelRatio: 0.01–0.02+animations: 'disabled'+scale: 'css'; siếtthreshold: 0chỉ cho component tĩnh hoàn toàn. - Review diff như review code: snapshot file là artifact phải commit vào git; khi
--update-snapshots, mở ảnh diff (đính kèm trong HTML report) xác nhận đúng thay đổi mong muốn trước khi commit baseline mới. - Đặt vào suite riêng: visual test chậm và nhạy môi trường — tách project/tag riêng, không nhét lẫn functional suite để tránh noise.
Gotchas, So sánh & Lưu ý
- Flaky kinh điển: font rendering, GPU rasterization, lazy-load ảnh, animation giữa chừng — giảm bằng
networkidle/waitFortrước khi chụp,animations: 'disabled', ảnh mock cố định. toHaveScreenshotcũng auto-retry: nó chụp lại nhiều lần chờ ảnh 'ổn định' khớp baseline trong timeout — nhưng nếu trang mãi không ổn định (banner xoay vòng) thì phải mask/freeze trước.- Baseline nằm cạnh file test, tách theo platform: snapshot tự sinh vào
<file>-snapshots/với hậu tố<project>-<os>(chromium-linux.pngvschromium-darwin.pnglà 2 baseline riêng) — phải commit vào git, CI fix một OS, đừng commit baseline gen trên máy dev lẫn lộn. threshold≠maxDiffPixels:thresholdlà 'một pixel lệch bao nhiêu thì TÍNH là khác' (độ nhạy màu);maxDiffPixelslà 'tổng cộng bao nhiêu pixel khác thì cho qua'. Nới threshold để chống răng cưa, nới maxDiff cho vùng động nhỏ.- So với Percy/Chromatic/Applitools: tool SaaS chụp trên infra chuẩn hoá + UI review đẹp, nhưng tốn tiền;
toHaveScreenshotmiễn phí, local-first, đủ dùng nếu kỷ luật Docker + mask tốt. - Không thay thế functional test: visual pass chưa chắc logic đúng (ảnh đẹp nhưng nút click không ăn) — nó là lớp bổ sung, không phải thay thế.
