أتمتة Visual Regression: امنع كسر الواجهة قبل النشر
مستوى القارئ: متوسط
هتكسب من المقال ده gate عملي في GitHub Actions يمسك كسر الواجهة قبل ما يوصل للمستخدم، بدل ما تكتشفه من رسالة عميل أو screenshot في Slack.
المشكلة باختصار
اختبارات الـ unit ممتازة في منطق الكود، لكنها ضعيفة جدًا في سؤال بسيط: هل زر الدفع اتحرك تحت الـ fold؟ هل الـ navbar غطّى العنوان؟ هل RTL اتكسر بعد تعديل CSS؟
اللي بيحصل فعلاً إن تغيير صغير في spacing أو font loading ممكن يعدّي من كل الاختبارات. في موقع عنده 8 صفحات تسويقية وصفحتين checkout، فرق بصري واحد في صفحة الدفع قد يخفض التحويل 2% خلال يوم. لو عندك 50,000 زيارة يوميًا، ده رقم يستاهل gate واضح.
مثال بسيط قبل التعريف الدقيق
ركز في المثال ده: عندك صفحة pricing. أول مرة تشغّل الاختبار، Playwright يحفظ صورة مرجعية اسمها pricing.png. بعد أي تعديل، يفتح نفس الصفحة، يأخذ screenshot جديدة، ويقارنها بالمرجعية. لو الفرق أكبر من 0.5%، الـ CI يفشل ويرفع صور actual وdiff كـ artifact.
بالتفاصيل، Visual Regression Testing هو مقارنة بصرية آلية بين حالة واجهة معتمدة وحالة جديدة. Playwright يدعم toHaveScreenshot()، وبيستخدم مقارنة screenshots مع إعدادات مثل maxDiffPixelRatio وmaxDiffPixels. الفكرة مش إنك تمنع أي تغيير بصري. الفكرة إن أي تغيير بصري مهم يبقى مقصود ومراجع.
الإعداد العملي
الافتراض إن عندك تطبيق ويب Node.js، وتقدر تشغله محليًا بأمر مثل npm run dev. أفضل طريقة تبدأ بثلاث صفحات فقط: الصفحة الرئيسية، صفحة pricing، وصفحة checkout. لا تبدأ بـ 60 صفحة؛ هتغرق في noise.
npm init playwright@latest
npm install -D @playwright/test
npx playwright install --with-deps chromiumبعدها اعمل ملف اختبار بسيط:
// tests/visual.spec.ts
import { test, expect } from '@playwright/test';
const pages = [
['home', '/'],
['pricing', '/pricing'],
['checkout', '/checkout'],
] as const;
for (const [name, path] of pages) {
test(`${name} visual check`, async ({ page }) => {
await page.goto(path);
await page.addStyleTag({ content: `
[data-dynamic], .chat-widget { visibility: hidden !important; }
`});
await expect(page).toHaveScreenshot(`${name}.png`, {
maxDiffPixelRatio: 0.005,
animations: 'disabled',
fullPage: true,
});
});
}أول تشغيل غالبًا هيفشل لأنه لا توجد baseline. شغّل التحديث مرة واحدة بعد مراجعة شكل الصفحات:
npx playwright test --update-snapshots
npx playwright testالرقم 0.005 يعني 0.5% فرق مسموح. في صفحة 1365x768، ده حوالي 5,200 بكسل. لو صفحة checkout اتحرك فيها زر أساسي 40px، غالبًا هتعدّي هذا الحد وتفشل.
شغّله يوميًا في GitHub Actions
اختبار الـ PR مهم، لكن التشغيل الليلي يمسك مشاكل تظهر من تحديث dependencies أو تغيّر rendering داخل البيئة. GitHub Actions يدعم schedule بصيغة cron، والتشغيل المجدول بيشتغل على آخر commit في الفرع الافتراضي. هنا workflow عملي:
name: visual-regression
on:
pull_request:
schedule:
- cron: '15 2 * * 1-5'
workflow_dispatch:
jobs:
visual:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npx playwright install --with-deps chromium
- run: npm run build
- run: npm run start &
- run: npx wait-on http://127.0.0.1:3000
- run: npx playwright test tests/visual.spec.ts
- name: Upload visual report
if: failure()
uses: actions/upload-artifact@v4
with:
name: visual-regression-report
path: |
test-results/**
playwright-report/**
retention-days: 5الـ trade-off هنا واضح. هتدفع 2 إلى 5 دقائق زيادة في CI لكل تشغيل، مقابل إنك تمسك كسر واجهة قبل النشر. لو عندك CI محدود جدًا، شغّل الاختبارات البصرية على PRs اللي تلمس src أو styles فقط.
أهم 4 قواعد لتقليل الإنذارات الكاذبة
- ثبّت البيئة. لا تقارن baseline من macOS مع actual من Ubuntu. Playwright نفسه ينبه إن rendering يختلف حسب النظام والخطوط والهاردوير.
- اخفِ العناصر المتغيرة. الساعة، اسم المستخدم، widget الدعم، وإعلانات A/B لازم تختفي أثناء screenshot.
- ابدأ بصفحات قليلة. 3 صفحات حرجة أفضل من 40 صفحة noisy. زوّد النطاق بعد أول أسبوع.
- راجع diff قبل تحديث baseline. تحديث الصور المرجعية بدون review يحوّل الاختبار إلى ختم مطاط.
قياس منطقي لأول شهر: أقل من 10 دقائق صيانة أسبوعيًا، وfalse positives أقل من 2 لكل 20 PR. لو الأرقام أعلى، المشكلة غالبًا في dynamic content أو اختلاف البيئة.
متى لا تستخدم هذه الطريقة
لا تستخدمها كبديل لاختبارات accessibility أو flows الوظيفية. لو الزر موجود شكليًا لكنه لا يعمل، screenshot مش هينقذك. لا تستخدمها أيضًا على صفحات شديدة التغيّر مثل feed إخباري مباشر، إلا لو هتخفي المحتوى الديناميكي. ولو المنتج لسه في مرحلة redesign يومي، خليها على صفحات checkout أو auth فقط.
المصادر
- Playwright Visual Comparisons
- Playwright Screenshots API
- GitHub Actions workflow syntax
- GitHub Actions artifacts
- pixelmatch package
الخطوة التالية
اختار 3 صفحات لا تقبل الكسر، شغّل toHaveScreenshot() عليها، وارفع diff artifacts عند الفشل. بعد أسبوع، زوّد صفحة واحدة فقط لو الـ noise تحت السيطرة.