أتمتة Visual Diff: امنع كسر الواجهة قبل النشر
لو عندك صفحة checkout أو pricing بتتغير كتير، الأوتوميشن ده هيخليك تكتشف كسر الواجهة قبل ما المستخدم يشوفه، بدل ما تعتمد على عين المراجع في آخر دقيقة.
مستوى القارئ: متوسط
المشكلة باختصار
الاختبارات العادية بتقولك إن الزر موجود، وإن الطلب رجع 200. لكنها غالبًا مش هتقولك إن الزر نزل تحت الشاشة، أو إن banner غطى السعر، أو إن خط عربي اتكسر بعد تحديث CSS.
اللي بيحصل فعلاً في فرق صغيرة: Pull Request يعدي، reviewer يبص بسرعة، وبعد النشر تظهر مشكلة بصرية في mobile. لو عندك 3 صفحات مهمة وبتراجع كل صفحة على desktop وmobile يدويًا، ده ممكن ياخد 30 إلى 40 دقيقة في كل release.
أفضل طريقة هنا مش إنك تلغي المراجعة البشرية. الأفضل إنك تخلي Playwright يلقط screenshots ثابتة، يقارنها بمرجع محفوظ، ويوقف الـ CI لو الفرق أكبر من حد معين.
مثال واقعي قبل الشرح العلمي
افترض إن عندك SaaS بسيط فيه 3 صفحات حساسة: landing، pricing، checkout. حصل تعديل في component مشترك اسمه Button. الاختبار الوظيفي نجح لأن الزر ما زال قابلًا للضغط، لكن padding الجديد خلّى زر الدفع على شاشة iPhone SE يطلع تحت fold.
بدل ما تفتح الصفحات يدويًا كل مرة، تعمل test يزور الصفحات الثلاث على viewport موبايل وdesktop. أول تشغيل يولّد الصور المرجعية. بعد كده كل PR يقارن الصور الجديدة بالمرجع. لو الفرق أعلى من 100 pixel مثلًا، الـ CI يفشل ويعرض diff للمراجعة.
الإعداد العملي بـ Playwright
ركز: أول تشغيل مش هدفه ينجح. أول تشغيل هدفه يولّد baseline. بعد ما تراجع الصور وتتأكد إنها صحيحة، تضيفها للـ repo.
npm i -D @playwright/test
npx playwright install chromium
mkdir -p tests
اكتب اختبار بصري صغير للصفحات المهمة فقط. لا تبدأ بكل الموقع، لأن الضوضاء هتخليك تطفي الاختبار بعد أسبوع.
// tests/visual.spec.ts
import { test, expect } from '@playwright/test';
const pages = [
{ name: 'landing', path: '/' },
{ name: 'pricing', path: '/pricing' },
{ name: 'checkout', path: '/checkout' },
];
for (const pageInfo of pages) {
test(`${pageInfo.name} visual snapshot`, async ({ page }) => {
await page.goto(`http://localhost:3000${pageInfo.path}`);
await page.locator('body').waitFor({ state: 'visible' });
await expect(page).toHaveScreenshot(`${pageInfo.name}.png`, {
fullPage: true,
maxDiffPixels: 100,
});
});
}
شغّل الأمر ده محليًا أول مرة لتوليد snapshots:
npx playwright test --update-snapshots
بعدها راجع الصور الناتجة. لو الشكل صحيح، اعمل commit لمجلد snapshots. Playwright نفسه يوضح إن الصور المرجعية تتحفظ بجانب ملف الاختبار، وإن تحديثها يتم صراحةً باستخدام --update-snapshots.
تشغيله في GitHub Actions
الافتراض إن التطبيق يشتغل بـ npm run build ثم npm run start على port 3000. لو عندك Next.js أو Vite، عدّل الأوامر فقط.
name: visual-diff
on:
pull_request:
paths:
- 'src/**'
- 'app/**'
- 'components/**'
- 'public/**'
- 'package-lock.json'
schedule:
- cron: '20 5 * * 1'
jobs:
screenshots:
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://localhost:3000
- run: npx playwright test tests/visual.spec.ts --project=chromium
- uses: actions/upload-artifact@v4
if: failure()
with:
name: visual-diff-report
path: test-results/
وجود pull_request يخلي الفحص يشتغل قبل الدمج. ووجود schedule مرة أسبوعيًا مفيد لو عندك dependencies أو fonts خارجية ممكن تغيّر rendering. GitHub Actions يدعم تشغيل workflow في وقت محدد عن طريق schedule وcron.
الأرقام والـ trade-off
في سيناريو 3 صفحات × 2 viewports، المراجعة اليدوية قد تاخد 35 دقيقة مع فتح الصفحات وتسجيل الملاحظات. نفس الفحص في CI غالبًا ياخد 4 إلى 7 دقائق حسب حجم build وسرعة runner. المكسب: وقت أقل، ونسيان أقل. الخسارة: هتدفع تكلفة CI إضافية وهتحتاج صيانة للـ snapshots عند التغييرات المقصودة.
الـ trade-off هنا مهم. لو خليت maxDiffPixels قليل جدًا مثل 5، هتتعب من false positives بسبب fonts وanti-aliasing. لو خليته عالي جدًا مثل 2000، ممكن تعدي مشكلة حقيقية. ابدأ بـ 100 للصفحات الصغيرة، و500 للصفحات الطويلة، ثم اضبطه بعد أول أسبوع.
Playwright ينبه إن rendering ممكن يختلف حسب نظام التشغيل، وإصدار المتصفح، والهاردوير، وheadless mode. لذلك الأفضل تولّد baseline وتفحصه في نفس بيئة CI كل مرة، بدل ما تولّده على Windows وتفحصه على Linux.
متى لا تستخدم هذه الطريقة
لا تستخدم Visual Diff كاختبار وحيد. هو ممتاز لاكتشاف كسر الشكل، لكنه لا يثبت إن الدفع تم، أو إن API رجعت البيانات الصحيحة. لو الصفحة فيها محتوى متغير جدًا مثل أسعار لحظية أو إعلانات أو timestamps، لازم تخفي الأجزاء المتغيرة بـ CSS أثناء screenshot.
كمان لا تبدأ به على 80 صفحة. ابدأ بالصفحات اللي خسارتها واضحة: checkout، pricing، login، dashboard الرئيسي. بعد ما يقل عدد الإنذارات الكاذبة، وسّع التغطية بالتدريج.
مصادر اعتمد عليها المقال
- Playwright Visual Comparisons: توثيق
toHaveScreenshot، تحديث snapshots، وخياراتmaxDiffPixels. - pixelmatch على npm: مكتبة مقارنة pixels التي يستخدمها Playwright في المقارنة البصرية.
- GitHub Actions schedule: تشغيل workflows بجدولة cron.
الخطوة التالية
اختار 3 صفحات فقط من مشروعك، اكتب لها toHaveScreenshot، وشغّل --update-snapshots مرة واحدة. لو أول أسبوع طلع أكثر من 3 إنذارات كاذبة، اضبط maxDiffPixels أو أخفِ العناصر المتغيرة قبل ما تزود صفحات جديدة.