المقارنة البصرية
تُقدم الدالة toHaveScreenshot() القدرة على مقارنة المظهر الفعلي للشاشة مع صورة مرجعية (Baseline) محفوظة مُسبقاً. وبفضل ذلك، سيفشل الاختبار تلقائياً وينبهك فور رصد أي تغييرات بصرية بدلاً من المرور عليها بصمت.
await expect(app.home.heroCard).toHaveScreenshot('home-hero-card.png');في إطار عمل Playwright العادي، تعتمد الدالة expect(page).toHaveScreenshot() بشكل أساسي على كائن الـ Page المخصص للويب، وهو ما لا يتوفر في بيئات الهواتف. الدالة المُقدمة هنا هي بديلها الأصلي المخصص؛ حيث يقوم Astur بالتقاط صورة للجهاز، تغطية الأجزاء المسموح بتغيرها باللون الأرجواني، ثم مقارنتها بالصورة المرجعية المخصصة لكل جهاز. وهذه الآلية تعمل بنفس الكفاءة مع React Native و Flutter، على كلاً من Android و iOS.
الأهمية العملية للمقارنة البصرية
Section titled “الأهمية العملية للمقارنة البصرية”تُعد التوكيدات الوظيفية (Functional Assertions) ممتازة للتحقق من وجود العنصر وصحة النص المكتوب فيه. لكنها بكل تأكيد ستمر بنجاح حتى لو تغير لون خلفية الزر فأصبح النص مخفياً، أو لو فقدت البطاقة حشوتها (Padding)، أو لو اختفت الأيقونة تماماً — فدوال مثل toBeVisible() و toHaveText() لا تأبه بالتنسيق، بل بالمحتوى.
هنا يبرز دور المقارنة البصرية، والتي تُعتبر الملاذ الأمثل في الحالات التالية:
- المظهر هو المُنتَج بحد ذاته: كما هو الحال عند بناء أنظمة التصميم (Design Systems)، أو الواجهات المتعددة السمات (Themes)، أو الرسوم البيانية الدقيقة.
- إعادة الهيكلة الآمنة (Refactoring): عند تبديل محرك الواجهة أو تحديث مكتبة خارجية، فإن التغيرات البصرية الطفيفة هي أول ما قد يتأثر بدون أن تنكسر الأكواد.
- اختبارات الانحدار (Regression Tests): متى ما اكتشفت خللاً بصرياً وقمّت بإصلاحه، تُصبح الصورة المرجعية الضامن الأرخص والأكثر كفاءة لعدم تكرار المشكلة.
نصيحة: تُعتبر المقارنة البصرية أداة سيئة عند استخدامها للتأكد من السلوك المنطقي للتطبيق. استخدم التوكيدات الوظيفية متى ما أمكن؛ فهي أسرع، وأكثر قدرة على تحمل تغييرات الخطوط أو الألوان، وتُعبر عن نيتك بصراحة.
كيف تبدو الفروقات عند الفشل؟
Section titled “كيف تبدو الفروقات عند الفشل؟”عندما يخفق الاختبار لاختلاف الصورة، سيقوم الـ Playwright HTML Report بإدراج الصور الثلاث: “المتوقعة”، “الفعلية”، و“صورة الفروق (Diff)”، ليوفر لك لوحة مقارنة سهلة:

في تبويب Diff، يُبهت النظام الأجزاء المتطابقة ويُبرز الأجزاء المختلفة — كما في الصورة أعلاه حيث يُظلل الزر لتغير لونه فقط. أما تبويب Side by side فيُعرض الصورتين جنباً إلى جنب؛ وهو مفيد جداً لاتخاذ القرار إن كان التغيير مطلوباً أو خللاً:

ولتسهيل قراءة النتائج من الطرفية (CI Logs) بدون الحاجة لفتح التقرير، تقوم الرسالة بتحديد حجم الخلل بالأرقام:
Screenshot home-hero-card.png does not match its baseline: 49888 pixels differ(6.34% of the image).Baseline: specs/visual-comparison.test.ts-snapshots/android-native-1080x2424/home-hero-card.pngRe-run with --update-snapshots once you have confirmed the change is intended.التشغيل الأول والتقاط الصورة المرجعية
Section titled “التشغيل الأول والتقاط الصورة المرجعية”عندما تستدعي دالة المقارنة لأول مرة، ولعدم وجود صورة مرجعية مسبقة، سيقوم Astur بالتقاط الصورة و سيفشل الاختبار عمداً:
No baseline yet, so this run wrote one: specs/visual-comparison.test.ts-snapshots/android-native-1080x2424/home-hero-card.pngCheck the image looks right, commit it, and re-run.هذا الإخفاق مقصود! فالاختبار الذي يلتقط الصورة المرجعية لأول مرة بدون إخفاق يعني أنه “لم يختبر شيئاً بعد”. وفي بيئة الـ CI، لو تم تمريره بدون إخفاق سيتحول لاختبار “أخضر” وهمي. افحص الصورة المحفوظة، أضفها لمستودعك (Commit)، وفي التشغيل التالي سيبدأ الـ Astur بالمقارنة معها.
الموافقة على التغييرات الجديدة
Section titled “الموافقة على التغييرات الجديدة”لتحديث الصورة المرجعية (في حال كان التغيير البصري مقصوداً)، أعد التشغيل مُضيفاً المُعامل --update-snapshots. يعتمد Playwright الأوضاع التالية:
| الوضع (Flag) | النتيجة |
|---|---|
| (بدون) | يُقارن فقط. يلتقط الصورة الجديدة إذا لم تكن موجودة ويفشل. |
-u / --update-snapshots |
(الأكثر استخداماً) يكتب الصورة المرجعية الجديدة فقط إذا كانت مختلفة ويمر بنجاح. |
--update-snapshots=all |
يُعيد كتابة الصور جميعاً بلا استثناء (سواء اختلفت أو تطابقت). |
--update-snapshots=none |
لا يكتب أي صورة مطلقاً (الوضع الأمثل في الـ CI لمنع كتابة صور جديدة عن طريق الخطأ). |
ملاحظة: الخيار المُبسط -u يُطبق كأنه changed وليس all. كلاهما يعمل على تحديث الصورة في حال اختلافها.
ركز على المكون، وليس الشاشة الكاملة
Section titled “ركز على المكون، وليس الشاشة الكاملة”كلما كان ذلك ممكناً، وجه دالة المقارنة على عنصر أو مكون بعينه بدلاً من الشاشة كاملة:
await expect(app.home.heroCard).toHaveScreenshot('home-hero-card.png');فصورة الشاشة كاملة تتضمن الـ Status Bar (شريط حالة الهاتف) الذي يُظهر الساعة والتنبيهات. وبما أن ساعة الجهاز تتغير باستمرار، فإن ذلك كفيل بإفشال اختبار الشاشة بأكملها لأسباب خارجة عن التطبيق!
كما أن Astur يتكفل عنك بتحويل إحداثيات العنصر الى بكسلات دقيقة ليتوافق مع حجم الشاشة؛ وهي عملية مهمة حيث يتعامل Android بالبكسلات الفعلية، بينما يعتمد iOS النقاط (Points) لشاشات تدعم مقياس 3x.
إخفاء العناصر الدائمة التغير (Masking)
Section titled “إخفاء العناصر الدائمة التغير (Masking)”إذا احتوت شاشتك على عدادات وقت (Timers)، طوابع زمنية (Timestamps)، أو صور متغيرة فتجنب توسيع التسامح (Threshold) عوضاً عن ذلك يُمكنك ببساطة تطبيق قناع (mask):
await expect(card).toHaveScreenshot('forms-fields-card.png', { mask: [app.forms.textInput, app.forms.mirror]});يتم تظليل المناطق المحددة في القناع باللون الأرجواني الفاقع قبل الاختبار. هذه الطريقة تسهل اكتشاف ما إذا كان القناع في المكان الخاطئ، وتحميك من إخفاء الخلل البصري. (اعلم أن العناصر غير الموجودة في الشاشة ستتم تجاهلها بشكل طبيعي). استخدم القناع دائماً قبل اللجوء لتوسيع التسامح، فالتسامح الكبير كفيل بتجاهل الأخطاء الفعلية، بينما القناع يركز على عناصر محددة بعناية.
تنظيم الصور المرجعية (لكل بيئة)
Section titled “تنظيم الصور المرجعية (لكل بيئة)”يقوم النظام بحفظ الصور ضمن مجلدات تحمل اسم المنصة، محرك العرض، وأبعاد الشاشة:
visual-comparison.test.ts-snapshots/ android-native-1080x2424/ android-flutter-1080x2424/ ios-native-393x852/ ios-flutter-393x852/الأبعاد وحدها ليست معياراً كافياً؛ فتطبيق React Native أو Flutter على المحاكي نفسه عادة ما ينتج رسومات بصرية مختلفة، فلذلك كل منهما يحتاج سجلاً منفصلاً.
في iOS يتم التفريق بينهما من خلال لقطة ملف الـ .app المثبت، لعدم وجود مؤشر صريح أثناء التشغيل يميز بين React Native و Flutter في XCUITest. الجلسة لـ Flutter تستمر بالإبلاغ عن أن الـ uiEngine: 'native' في حين أن الصور ستحفظ في مجلد ios-flutter-….
في حال المقارنة في محاكي مختلف، يظهر الـ Astur رسالة صريحة:
screenshot size does not match the baseline — baseline is 996x790, this runcaptured 1083x1191. This usually means the baseline was recorded on a differentdevice rather than that the UI changed.فرض التثبيت لالتقاط المرجعية في iOS
Section titled “فرض التثبيت لالتقاط المرجعية في iOS”في iOS يتخطى Astur عملية التثبيت إذا كان التطبيق موجوداً فعلياً. وبما أن بناء Flutter و React Native يتشاركان المعرف com.astur.demo في التطبيق التجريبي، فسيستمر البناء المتواجد مسبقاً في السيطرة، مما يؤدي إلى تسجيل الصور المرجعية إلى البناء الخاطئ.
لحل هذه المشكلة، يرجى استخدام المعامل ASTUR_IOS_APP_FORCE_INSTALL=1 عند التسجيل:
ASTUR_IOS_APP_FORCE_INSTALL=1 npm run test:ios -- specs/visual-comparison.test.tsASTUR_IOS_APP_FORCE_INSTALL=1 npm run test:ios:flutter -- specs/visual-comparison.test.tsأما في Android فلا داعي لهذا الأمر نظراً لأن الإعدادات تفرض التثبيت تلقائياً.
مستويات التسامح (Tolerance)
Section titled “مستويات التسامح (Tolerance)”بشكل افتراضي، ستفشل المقارنة عند رصد اختلاف بكسل واحد فقط، تماماً كما في Playwright. يتم امتصاص التشويش اللوني البسيط (Noise) من خلال مُعامل threshold قبل حساب الفروق.
| الخيار (Option) | الوظيفة |
|---|---|
threshold |
نسبة التسامح اللوني لكل بكسل، من 0 إلى 1. (الافتراضي 0.2). |
maxDiffPixels |
الحد الأقصى لعدد البكسلات المسموح باختلافها. |
maxDiffPixelRatio |
نسبة البكسلات المسموح باختلافها للشاشة ككل، من 0 إلى 1. |
mask |
مصفوفة من المُحددات التي سيتم تقنيعها وتجاهلها أثناء المقارنة. |
stabilizeTimeout |
مدة الانتظار لضمان استقرار الشاشة قبل أخذ الصورة. (الافتراضي 1500 مللي ثانية). |
ملاحظة: إذا قمت بتعيين maxDiffPixels و maxDiffPixelRatio معاً، فسيتم تطبيق الشرط المشترك بينهما.
ميزانية التسامح أساسية في اختبارات الجوال. التمرير أو ظهور لوحة المفاتيح قد يزيح النصوص بنسب ضئيلة (كـ 0.2%). لذا يفضل قياس الشاشة الفعلية ووضع ميزانية دقيقة بدلاً من التخمين:
await expect(card).toHaveScreenshot('card.png', { maxDiffPixelRatio: 0.01 });هذه الميزانية الدقيقة أكثر أماناً من توسيع الـ threshold. فالتغيير البصري الحقيقي يختلف بنسبة أكبر من 1% بكثير.
استقرار الشاشة قبل الالتقاط (Animations)
Section titled “استقرار الشاشة قبل الالتقاط (Animations)”أثناء المقارنة، يلتقط Astur عدة صور متتابعة للتحقق من تطابق صورتين (استقرار الشاشة)، ويستمر بذلك حتى ينقضي الـ stabilizeTimeout. وبدون هذه الآلية، قد تُسجل الرسوم المتحركة، أشرطة التحميل، أو التلاشي بالخطأ كصورة مرجعية، مما يؤدي لفشل الاختبار مستقبلاً.
إذا أردت التقاط الصورة فوراً بدون انتظار، عيّن القيمة stabilizeTimeout: 0.
نصيحة احترافية
Section titled “نصيحة احترافية”المقارنات البصرية هي الأكثر حساسية لبيئة النظام، حيث قد تتأثر بتحديثات إصدار النظام، نوع خط الجهاز، أو طريقة رسم الشاشة من المنصة نفسها. لذلك ننصحك بتضييق نطاق استخدامها بدلاً من الاعتماد عليها بشكل عشوائي. ركز على المكون الأساسي، استخدم أقنعة للأجزاء المتغيرة، وحافظ على ملفات مرجعية للمنصات الفعلية، فهذه الآلية هي التي تلتقط الخلل البصري بكفاءة دون الوقوع في الأخطاء الكاذبة.

