الاختبار البصري (Visual Testing): كيف أنقذنا واجهاتنا من كارثة الأخطاء الصامتة؟

بتذكرها زي كأنها مبارح. ليلة خميس، الساعة كانت داخلة على ١١ بالليل، وأنا يا دوب مخلّص فنجان الميرمية وبستعد لراحة نهاية الأسبوع. فجأة، التلفون برنّ… رقم مدير المنتج. قلبي نقزني، لأنه مكالمة بهيك وقت عمرها ما كانت خبر خير.

فتحت الخط، وصوته كان متوتر: “أبو عمر، الحق! الموقع خربان!”. دخلت بسرعة، فتحت الموقع على جهازي، كل إشي تمام التمام. الأزرار بتشتغل، النماذج بتبعت بيانات، وكل اختبارات الـ End-to-End اللي عملناها قبل ما نطلق التحديث الجديد كانت ناجحة 100%. قلتله بهدوء: “يا زلمة، شو بتحكي؟ هيو شغال عندي زي الحلاوة”.

بعتلي سكرين شوت… وهون كانت الصدمة. في متصفح Safari على أجهزة الماك، كان الـ Header تبع الموقع نازل لتحت ومغطي على نص المحتوى. كارثة بصرية بكل معنى الكلمة، بس كارثة “صامتة” بالنسبة لاختباراتنا. اختباراتنا كانت عمياء، بتفحص الوظيفة (Functionality) بس ما بتشوف الشكل (Appearance). هذيك الليلة، أدركت إننا بحاجة لعيون جديدة… عيون آلية لا تنام، وهيك بلشت رحلتنا مع الـ Visual Testing.

ما هو الاختبار البصري (Visual Testing)؟ وليش هو مهم؟

ببساطة شديدة، الاختبار البصري هو عملية أتمتة للتحقق من أن واجهة المستخدم (UI) تظهر للمستخدم النهائي بالشكل الصحيح. الفكرة قائمة على المقارنة.

تخيل إنك بتاخد “صورة مرجعية” (Baseline Image) لواجهتك وهي في أفضل حالاتها. مع كل تغيير جديد في الكود، النظام بياخد صورة جديدة وبيعمل مقارنة بصرية (Pixel by Pixel) مع الصورة المرجعية. لو في أي اختلاف، حتى لو بيكسل واحد، النظام بينبهك.

تشبيه بسيط: الاختبارات التقليدية (Unit, E2E) بتشبه لما تتأكد إن كل مكونات السيارة شغالة (المحرك، العجلات، الفرامل). أما الاختبار البصري، فهو اللي بتأكد إن “دهان” السيارة ما تقشّر، وإن الأبواب راكبة صح على بعضها، وإن شكلها النهائي هو الشكل اللي صممناه بالضبط.

اختباراتنا التقليدية كانت بتجاوب على سؤال “هل الزر يعمل؟”، لكنها كانت عاجزة عن الإجابة على سؤال “هل الزر يظهر في مكانه الصحيح وبشكله الصحيح؟”.

اختباراتنا كانت عمياء… كيف كنا بنشتغل قبل؟

قبل ما نتبنى الاختبار البصري، كانت منظومة الجودة عنا بتعتمد على الهرم التقليدي للاختبارات:

  • Unit Tests: للتحقق من أصغر وحدات الكود.
  • Integration Tests: للتحقق من تفاعل الوحدات مع بعضها.
  • End-to-End (E2E) Tests: باستخدام أدوات مثل Cypress أو Playwright لمحاكاة رحلة المستخدم كاملة.

كنا فخورين بهالمنظومة، وكانت بتصيد 90% من الأخطاء الوظيفية. لكن الـ 10% الباقية كانت الأخطاء المرئية الخبيثة اللي بتتسلل بدون ما حدا يحس عليها، مثل:

  • CSS Regressions: تغيير بسيط في ملف CSS مشترك ممكن يضرب الدنيا في صفحة تانية خالص ما إلها علاقة.
  • مشاكل الـ Responsive: الواجهة تبدو رائعة على شاشة 1080p، لكنها متداخلة ومكسورة على شاشة الموبايل.
  • تداخل العناصر: زر يظهر فوق نص، أو صورة تغطي على قائمة.
  • أخطاء في الخطوط والأيقونات: الخط الافتراضي يظهر بدل الخط المخصص لأن ملف الخط فشل في التحميل.

هاي الأخطاء كانت بتكلفنا وقت وجهد في “الإصلاحات الطارئة” (Hotfixes) بعد كل إطلاق، والأهم، كانت بتأثر على ثقة المستخدم بمنتجنا.

الإنقاذ: رحلتنا مع أدوات الاختبار البصري

بعد ليلة الكارثة إياها، كان القرار واضح: لازم نضيف طبقة جديدة من الحماية. طبقة بتشوف زي ما المستخدم بشوف.

اختيار الأداة المناسبة: مش كل الطرق بتودي لروما!

بدأنا رحلة البحث عن الأداة المناسبة. السوق مليان خيارات، من الخدمات المدفوعة الضخمة زي Applitools و Percy، للحلول مفتوحة المصدر المدمجة في أدوات بنستخدمها أصلاً.

نصيحة من أبو عمر: ما تنجرّ ورا الأداة اللامعة الأغلى ثمناً. ابدأ بالبسيط والمتاح. احنا كنا بنستخدم Playwright لاختبارات الـ E2E، واكتشفنا إنه بيوفر إمكانيات اختبار بصري مدمجة ممتازة كنقطة بداية.

قررنا نبدأ مع Playwright لسببين:

  1. مجاني ومدمج: ما في تكلفة إضافية ولا حاجة لتعلم أداة جديدة من الصفر.
  2. سهولة التطبيق: إضافة الاختبار البصري كانت مجرد سطر كود إضافي على اختباراتنا الحالية.

خطوات عملية للتطبيق باستخدام Playwright

هاي هي الخطوات العملية اللي اتبعناها لدمج الاختبار البصري في مشروعنا. لو بتستخدم Playwright، بتقدر تطبقها اليوم.

1. كتابة أول اختبار بصري

الأمر أبسط مما تتخيل. في ملف الاختبار تبعك، كل اللي عليك تضيفه هو سطر واحد بعد ما تتأكد إن الصفحة حملت بالكامل.


import { test, expect } from '@playwright/test';

test('Homepage should look correct', async ({ page }) => {
  // 1. انتقل إلى الصفحة المطلوبة
  await page.goto('https://my-awesome-app.com');

  // 2. انتظر تحميل عنصر معين لضمان اكتمال الصفحة
  await page.waitForSelector('.main-content');

  // 3. هذا هو السطر السحري!
  // التقط صورة وقارنها بالصورة المرجعية
  await expect(page).toHaveScreenshot();
});

2. إنشاء الصور المرجعية (Baselines)

لما تشغل هذا الاختبار لأول مرة، Playwright رح يفشل ويحكيلك: “ما لقيت صورة مرجعية (Baseline) أقارن فيها، بس أنا عملتلك وحدة جديدة”.

بتشغل الأمر التالي لإنشاء كل الصور المرجعية لأول مرة:


npx playwright test --update-snapshots

هذا الأمر رح ينشئ مجلد جديد (عادة `tests/example.spec.ts-snapshots`) ويحط فيه الصور المرجعية بصيغة PNG. لازم تضيف هاي الصور للـ Git repository تبعك، لأنها جزء أساسي من الكود.

3. دورة حياة الاختبار البصري

من هاي اللحظة، دورة العمل بتصير كالتالي:

  1. المطور يعمل تغيير: مثلاً، غيّر لون زر.
  2. يشغل الاختبارات محلياً: الاختبار البصري رح يفشل، ورح ينتج Playwright ثلاث ملفات: الصورة المرجعية (expected)، الصورة الجديدة (actual)، وصورة بتوضح الاختلافات (diff).
  3. المراجعة: المطور بيفتح صورة الفروقات (diff).
    • إذا كان التغيير مقصوداً (لون الزر الجديد صحيح): يقوم بتحديث الصورة المرجعية باستخدام أمر `–update-snapshots` ويعمل commit للصورة الجديدة.
    • إذا كان التغيير غير مقصود (التغيير أثر على عنصر آخر): فهذا يعتبر “Bug”! يقوم بإصلاح المشكلة ويعيد تشغيل الاختبار حتى ينجح.

تحديات ونصائح من المطبخ

الطريق ما كان مفروش بالورود، وواجهتنا شوية تحديات تعلمنا منها دروس غالية.

التعامل مع المحتوى الديناميكي والتواريخ

أول مشكلة واجهتنا كانت مع الصفحات اللي فيها محتوى متغير، مثل تاريخ اليوم، أو اسم المستخدم. الاختبار كان بفشل كل مرة لأن التاريخ بيتغير!

الحل: خاصية الإخفاء (Masking). Playwright بيسمحلك تحدد عناصر معينة عشان “يخفيها” قبل ما ياخد الصورة. بيغطيها بلون ثابت، وبالتالي ما بتأثر على المقارنة.


test('User profile should look correct', async ({ page }) => {
  await page.goto('/profile/abu-omar');

  // "أخفِ" العنصر اللي بيعرض تاريخ آخر تسجيل دخول
  await expect(page).toHaveScreenshot({ 
    mask: [page.locator('.last-login-date')] 
  });
});

الحساسية المفرطة (Flaky Tests)

أحياناً، كانت الاختبارات تفشل بسبب اختلافات طفيفة جداً (1-2 بيكسل) ناتجة عن طريقة عرض الخطوط (Anti-aliasing) المختلفة بين بيئة الـ CI وبيئة المطور المحلية.

الحل: استخدام هامش للخطأ (Threshold). معظم الأدوات بتسمحلك تحدد نسبة مئوية من الاختلاف المسموح به قبل ما الاختبار يعتبر فاشل. استخدمها بحذر شديد!


// اسمح بنسبة اختلاف تصل إلى 5%
await expect(page).toHaveScreenshot({ maxDiffPixelRatio: 0.05 });

نصيحة شخصية: لا تلجأ للـ threshold إلا كملاذ أخير. الحل الأفضل هو التأكد من أن بيئة الاختبارات في الـ CI مطابقة تماماً لبيئة المطور (نفس نظام التشغيل، نفس المتصفح، نفس إصدار Docker image).

فوضى مراجعة التغييرات

لما يكون عندك 20 مطور بيشتغلوا على المشروع، وكل Pull Request فيه 10-15 تغيير بصري، عملية المراجعة ممكن تصير كابوس.

الحل:

  • ابدأ صغيراً: لا تحاول تغطية كل الموقع دفعة واحدة. ابدأ بالصفحات والمكونات الحرجة (صفحة الدفع، الواجهة الرئيسية، الـ Design System).
  • استخدم أدوات مساعدة: الخدمات المدفوعة مثل Percy و Applitools تتألق في هذه النقطة، حيث توفر واجهات رائعة لمراجعة التغييرات بشكل جماعي والموافقة عليها.
  • اجعلها جزءاً من ثقافة الفريق: مراجعة التغييرات البصرية يجب أن تكون بنفس أهمية مراجعة الكود.

الخلاصة: عيون لا تنام تراقب واجهاتك 👁️

تبني الاختبار البصري كان نقلة نوعية في جودة منتجاتنا. انتقلنا من فريق “يتفاعل” مع الأخطاء بعد وقوعها، إلى فريق “يمنع” الأخطاء قبل وصولها للمستخدم. صحيح، الأمر تطلب جهد في البداية لضبط الإعدادات وتدريب الفريق، لكن العائد كان هائلاً.

العائد لم يكن فقط في تقليل الأخطاء، بل في زيادة ثقة المطورين. صار المطور يعمل Refactoring لملف CSS ضخم وهو مرتاح البال، لأنه بيعرف إن في شبكة أمان بصرية رح تصيد أي مشكلة غير متوقعة.

نصيحتي الأخيرة لك: ابدأ اليوم، ولو باختبار واحد لصفحة واحدة. لا تنتظر الكارثة لتتحرك. وجود عين آلية تراقب تطبيقك 24/7 هو استثمار في راحة بالك، وجودة منتجك، وسعادة مستخدميك. وصدقني، راح تدعيلي! 😉

أبو عمر

سجل دخولك لعمل نقاش تفاعلي

كافة المحادثات خاصة ولا يتم عرضها على الموقع نهائياً

آراء من النقاشات

لا توجد آراء منشورة بعد. كن أول من يشارك رأيه!

آخر المدونات

نصائح برمجية

متغيراتنا كانت مجرد نصوص ساذجة: كيف أنقذتنا ‘كائنات القيمة’ (Value Objects) من جحيم الأخطاء الصامتة؟

هل تعاني من أخطاء صامتة ومُرهقة في برامجك؟ في هذه المقالة، أشارككم تجربتي مع 'التعلق الساذج بالمتغيرات البدائية' وكيف أنقذتنا 'كائنات القيمة' (Value Objects) من...

19 أبريل، 2026 قراءة المزيد
​معمارية البرمجيات

خدماتنا كانت متشابكة كخيوط العنكبوت: كيف أنقذتنا ‘المعمارية الموجهة بالأحداث’ (EDA) من جحيم الانهيار المتسلسل؟

أروي لكم حكايتي كـ "أبو عمر"، مطور برمجيات فلسطيني، وكيف انتقلنا من نظام هش على وشك الانهيار بسبب الاقتران الشديد بين خدماته، إلى نظام مرن...

19 أبريل، 2026 قراءة المزيد
ذكاء اصطناعي

قرارات ذكائنا الاصطناعي كانت صناديق سوداء: كيف أنقذنا ‘الذكاء الاصطناعي القابل للتفسير’ (XAI) من جحيم انعدام الثقة؟

أشارككم قصة من تجربتي كمطور ذكاء اصطناعي، حين واجهت نموذج "صندوق أسود" عجزت عن تفسير قراراته، وكيف كان الذكاء الاصطناعي القابل للتفسير (XAI) هو طوق...

19 أبريل، 2026 قراءة المزيد
خوارزميات

حساباتنا كانت تعيد اختراع العجلة: كيف أنقذتنا ‘البرمجة الديناميكية’ من جحيم التعقيد الأسي؟

أشارككم قصة من قلب المعركة البرمجية، حين كاد تطبيقنا أن ينهار بسبب خوارزمية بسيطة، وكيف كانت "البرمجة الديناميكية" هي طوق النجاة. سنتعلم معاً كيف نتجنب...

19 أبريل، 2026 قراءة المزيد
تسويق رقمي

إنفاقنا الإعلاني كان يذهب سدى: كيف أنقذتنا ‘معلمات UTM’ من جحيم عدم معرفة مصدر عملائنا؟

أشارككم قصتي مع إهدار ميزانيات التسويق وكيف أن أداة بسيطة ومجانية تُدعى "معلمات UTM" كانت البوصلة التي أنقذتنا. سأشرح لكم بالتفصيل وبأمثلة عملية كيف تستخدمونها...

19 أبريل، 2026 قراءة المزيد
تجربة المستخدم والابداع البصري

مكوناتنا كانت جزرًا معزولة: كيف أنقذنا ‘نظام التصميم’ (Design System) من جحيم الفوضى

في هذه المقالة، أشارككم قصة حقيقية من قلب المعركة البرمجية، كيف انتقلنا من فوضى المكونات المتناثرة إلى نظام متسق وفعال. سأستعرض معكم رحلتنا خطوة بخطوة...

19 أبريل، 2026 قراءة المزيد
برمجة وقواعد بيانات

استعلاماتنا كانت تزحف كالسلحفاة: كيف أنقذتنا ‘فهرسة قواعد البيانات’ من جحيم الاستجابة البطيئة؟

أشارككم قصة حقيقية من الميدان، يوم كادت الاستعلامات البطيئة أن تقضي على مشروعنا. سأشرح لكم بلغة بسيطة كيف أنقذتنا فهرسة قواعد البيانات (Database Indexing)، وكيف...

19 أبريل، 2026 قراءة المزيد
البودكاست