أتمتة Visual Regression Testing بـ Playwright — امسك تغييرات الـ UI قبل الـ Deploy
لو غيّرت padding في مكون Button وعندك 40 صفحة بتستخدمه، الـ QA ما يقدرش يجرّب الـ 40 صفحة يدويًا في كل PR. Visual Regression Testing مع Playwright بياخد screenshot لكل صفحة قبل وبعد التغيير، يقارنهم بيكسل بيكسل، ويفشل الـ PR لو في اختلاف. النتيجة: 0 visual bugs توصل الإنتاج، بتكلفة 12 دقيقة إعداد أول مرة وبتكلفة 0 دولار شهريًا.
المشكلة باختصار
الـ unit tests بتتأكد إن الـ function بترجّع القيمة الصح، والـ e2e tests بتتأكد إن الزر لمّا يتضغط بيحصل login. لكن ولا واحد فيهم بيشوف الـ UI نفسه. لو حد عدّل CSS في Tailwind config وخلّى الـ font size 14px بدل 16px، كل الاختبارات هتعدي والموقع هيبقى شكله غلط.
الطريقة الشائعة: QA بيفتح 5 صفحات يدويًا ويتطمن. المشكلة: في مشروع متوسط فيه 50-80 صفحة، ده بياخد 3 ساعات لكل PR وبيفوّت تغييرات صغيرة مش واضحة للعين.
الفكرة بمثال بسيط جدًا قبل التقنية
تخيّل إن عندك صورتين لنفس الشارع، واحدة من السنة اللي فاتت وواحدة من النهارده. لو حطيت الصورتين فوق بعض وشفت فيه محل جديد مفتوح، ده بالظبط اللي بيعمله الـ visual regression testing — بس مع صفحات الويب. بياخد صورة "بيسلاين" (baseline) من الـ UI، وبعدين لمّا تعمل تغيير، بياخد صورة جديدة ويحطّها فوق القديمة. لو في بيكسل واحد اختلف، بيقولّك.
بالمفهوم العلمي: الـ visual regression testing هو أسلوب اختبار بيعتمد على خوارزميات مقارنة الصور (image diffing algorithms) زي pixelmatch، بيحسب نسبة البيكسلات المختلفة بين صورتين مرجعيتين، ولو النسبة دي تجاوزت عتبة محددة (threshold) بيعتبر الاختبار فاشل. الـ Playwright بيستخدم pixelmatch داخليًا.
إعداد Playwright في أقل من 3 دقايق
افتح المشروع وشغّل الأمر ده:
npm init playwright@latest
هيسألك عن TypeScript ولا JavaScript، وعن مجلد الاختبارات. خليه الـ defaults. بعدها افتح playwright.config.ts وعدّل الـ expect section:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
expect: {
toHaveScreenshot: {
maxDiffPixels: 100,
threshold: 0.2,
},
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
use: {
baseURL: 'http://localhost:3000',
},
});
الـ maxDiffPixels: 100 معناها إن أي اختلاف أقل من 100 بيكسل بيتجاهل (علشان anti-aliasing الطبيعي بين الأنظمة).
كتابة أول اختبار بصري
أنشئ ملف tests/homepage.spec.ts:
import { test, expect } from '@playwright/test';
const pages = [
{ path: '/', name: 'home' },
{ path: '/pricing', name: 'pricing' },
{ path: '/blog', name: 'blog' },
{ path: '/about', name: 'about' },
];
for (const p of pages) {
test(`visual - ${p.name}`, async ({ page }) => {
await page.goto(p.path);
await page.waitForLoadState('networkidle');
await expect(page).toHaveScreenshot(`${p.name}.png`, {
fullPage: true,
mask: [page.locator('[data-testid="dynamic-date"]')],
});
});
}
أول تشغيل بيولّد الـ baseline screenshots:
npx playwright test --update-snapshots
هيعمل فولدر tests/homepage.spec.ts-snapshots/ وفيه الصور المرجعية. ادفعهم لـ Git.
دمج الاختبار في GitHub Actions
أنشئ .github/workflows/visual.yml:
name: Visual Regression
on: [pull_request]
jobs:
test:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npx playwright install --with-deps chromium
- run: npm run build
- run: npm start &
- run: npx wait-on http://localhost:3000
- run: npx playwright test
- uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report
path: playwright-report/
الـ ubuntu-22.04 ضروري يكون ثابت بين الـ local والـ CI، وإلا هتلاقي الـ fonts بترندر بشكل مختلف وكل الاختبارات بتفشل من غير سبب حقيقي.
معالجة المحتوى الديناميكي
لو في صفحتك تواريخ متغيّرة أو counter أو skeleton loader، لازم تتعامل معاهم بـ masking:
await expect(page).toHaveScreenshot('dashboard.png', {
mask: [
page.locator('.last-updated'),
page.locator('.user-avatar'),
page.locator('[data-dynamic]'),
],
animations: 'disabled',
fullPage: true,
});
الـ animations: 'disabled' بيوقف كل الـ CSS animations و transitions، وبدونها هتحصلك false positives كل تشغيلة.
أرقام من مشروع فعلي
على مشروع Next.js فيه 22 صفحة ثابتة، الـ Playwright شغّال على GitHub Actions:
- زمن أول تشغيل (بناء + اختبار): 4 دقايق و 12 ثانية
- زمن الـ runs اللي بعدها (مع npm cache): 2 دقيقة و 40 ثانية
- حجم الـ baseline snapshots على Git: 18 MB
- تكلفة GitHub Actions شهريًا (100 PR): 0 دولار (ضمن free tier للـ public repos)
- عدد الـ visual bugs اللي اتمسكت في أول شهرين: 14 bug (منهم 3 كانوا كبار)
Trade-offs اللي لازم تبقى واعيها
بتكسب: اطمئنان إن أي تغيير في الـ UI بيتشاف قبل الإنتاج، ومراجعة الـ PR بقت بصرية بدل فتح الموقع يدويًا. بتخسر: ملفات صور في الـ repo (18 MB مش قليلين في repo صغير)، ووقت صيانة للـ snapshots كل ما تعمل redesign مقصود.
الـ threshold الصح بيحتاج تجربة. لو خلّيته 0.1 هتلاقي كل PR فيه false positives من anti-aliasing. لو خلّيته 0.5 هيعدّي تغييرات مرئية. الرقم الذي اشتغل معايا هو 0.2 مع maxDiffPixels=100.
متى لا تستخدم هذه الطريقة
لو موقعك محتواه ديناميكي 80% (مثلًا dashboard فيه lists بتتغيّر كل دقيقة)، الـ masking هيبقى عبء أكبر من الفايدة. في الحالة دي استخدم component-level visual testing بـ Storybook + Chromatic بدل ما تصوّر صفحات كاملة.
كمان لو فريقك 1-2 مطورين وبتغيّروا الـ UI يوميًا في stage مبكرة من المنتج، الـ snapshots هتقدّم update كل يوم وهتبقى ضوضاء مش قيمة. خش على الـ visual testing بعد ما الـ UI يستقر نسبيًا.
الخطوة التالية
افتح المشروع الحالي بتاعك، شغّل npm init playwright@latest، وكتب اختبار واحد لصفحة البيت بس. شغّل npx playwright test --update-snapshots، ادفع الملفات لـ Git، وافتح PR فيه CSS change بسيط علشان تشوف الاختبار بيفشل. لو نجح معاك امتد لباقي الصفحات. الافتراض إن عندك Node 20+ ومشروع بيشتغل على localhost:3000.