أتمتة 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,
});
});
}