يبدأ الأمر برسالة على Slack. عملية البناء (build) تظهر باللون الأحمر. تتصفح سجل الإخفاقات، وتعبس، ثم تعيد تشغيل الاختبار نفسه على جهازك المحمول. النتيجة: أخضر. تعيد محاولة مهمة الـ CI. ربما كانت مجرد هفوة. لكن الإخفاق يعود، عنيداً ومتكرراً على الخادم، لكنه غير مرئي بالنسبة لك.

اختبار المتصفح الذي يفشل في الـ CI بينما ينجح محلياً هو أكثر من مجرد إزعاج؛ إنه يزرع عدم الثقة. تبدأ الفرق في إلقاء اللوم على التوقيت (timing). يلجأون إلى إصلاحات مؤقتة لا تُحذف أبداً. setTimeout هنا، و .wait(5000) هناك. تتباطأ مجموعة الاختبارات، وتستمر الإخفاقات في العودة. تتحول تلك الاختبارات المتقلبة (flaky tests) إلى عناصر ثابتة، وفي النهاية، يبدأ الجميع في التعامل مع خط الأنابيب (pipeline) الأحمر كأنه مجرد ضجيج في الخلفية.

هذا أمر خطير. فأنت لا تريد مجموعة اختبارات تطلق إنذارات كاذبة.

الـ CI ليس معطلاً؛ إنه مختلف فحسب

بيئات الـ CI ليست عشوائية، بل هي حتمية (deterministic). المشكلة تكمن في أنها حتمية تجاه نظام ليس هو جهاز MacBook الخاص بك أو محطة عمل Linux. إعداداتك المحلية تخفي اختلافات يكشفها مشغل الـ CI (CI runner) النظيف على الفور.

فكر في عدد الأجزاء المتحركة التي تختلف. قد يعمل جهازك المحلي بخادم تطوير يدعم خاصية hot module reloading، بينما يقوم الـ CI ببناء نسخة إنتاج (production artifact) مع تقنيات tree shaking و minification. هذا وحده كفيل بحذف مسارات برمجية أو تغيير ترتيب التنفيذ. تتغير أشجار التبعيات (dependency trees). ملف القفل (lockfile) الذي يبدو متطابقاً قد يتم حله بشكل مختلف إذا اختلف إصدار مدير الحزم (package manager) بمقدار إصدار فرعي واحد. تتغير تسلسلات الشبكة؛ فقد يصل الـ Wi-Fi في مكتبك إلى واجهة برمجة تطبيقات (API) تجريبية (staging) في قفزة واحدة، بينما قد يتصل مشغل الـ CI بعنقود (cluster) مختلف خلف موازن تحميل (load balancer)، مما يتسبب في تأخير (latency) لا تراه أبداً.

تتصرف المتصفحات نفسها بشكل مختلف عبر البيئات. متصفح Chrome المحلي الخاص بك يحتوي على إضافات، وبيانات اعتماد مخزنة مؤقتاً، وتخزين محلي دائم، ومعالج رسومات (GPU) مع تسريع الأجهزة. أما الـ CI فيبدأ بملف تعريف فارغ في كل مرة. تختلف دورات حياة المتصفح، وتختلف مسارات التصيير (rendering paths). الخطوط الموجودة على نظامك قد تُستبدل في الـ CI. كما تختلف أحجام نافذة العرض (viewport) ونسب بكسلات الجهاز، مما قد يؤدي إلى قلب نقاط التوقف للتصميم المتجاوب (responsive breakpoints) أو تغيير سلوك التحميل الكسول (lazy-loading).

هذه الفجوات حقيقية، وهي ميكانيكية. والتظاهر بأنها عشوائية لن يجعلها تختفي.

بيئات المعاينة تخدع

تزيد بيئات المعاينة (Preview environments) من تفاقم المشكلة. فهي مفيدة للمراجعة البشرية، لكنها ليست بيئة إنتاج. غالباً ما تشير إلى api-staging بدلاً من مضيف الـ API الحقيقي. وتكون أعلام الميزات (feature flags) مفعلة لكل تجربة، مما يخفي المنطق الشرطي الذي ينفذه نظام الإنتاج. قد تتخطى عملية المصادقة (authentication) خطوة ما أو تحقن رمزاً وهمياً (mock token). قد تستخدم ملفات تعريف الارتباط (cookies) سياسات مخففة. وقد تكون مجموعة البيانات مجرد شريحة ضئيلة، عشرة صفوف بدلاً من عشرة آلاف، مما يعني أن منطق الترقيم (pagination)، أو ترتيب البحث، أو المحاكاة الافتراضية (virtualization) لن يتم اختباره أبداً.

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

سجل البيانات قبل أن تخمن

عندما يظهر الإخفاق لأول مرة، قاوم غريزة تعديل الاختبار والأمل في النجاح. توقف عن التخمين. أنت بحاجة إلى تجميد السياق (context) حتى تتمكن من مقارنة عملية تشغيل ناجحة بأخرى فاشلة.

قم بتسجيل المشتبه بهم الواضحين. سجل رابط الصفحة (URL) لحظة الإخفاق، ومعرف البناء (build ID)، ورمز الالتزام (commit SHA). سجل أعلام الميزات النشطة. سجل مضيف الـ API، وإصدار المتصفح الدقيق، وحجم نافذة العرض (viewport). هذه التفاصيل تحول الإخفاق الغامض إلى حالة يمكن إعادة إنتاجها.

لا تعتمد فقط على لقطات الشاشة (screenshots). فقد تبدو صفحتان متطابقتين تماماً في الصورة بينما تعملان بـ JavaScript مختلف تماماً. لن تخبرك لقطة الشاشة أن حزمة الـ CI تضمنت polyfill إضافياً، أو أن الحزمة المحلية تخطت جزءاً لأنها كانت موجودة بالفعل في ذاكرة التخزين المؤقت لمتصفحك.

تذكر أيضاً أن فتح أدوات المطور (DevTools) يغير التوقيت. يمكن لـ DevTools أن تؤخر عملية جمع المهملات (garbage collection)، وتغير أولويات الشبكة، وتعطل بعض تحسينات التصيير. الاختبار الذي ينجح أثناء فحصك للـ DOM قد يفشل في اللحظة التي تغلق فيها اللوحة وتشغله في وضع headless. المصحح (debugger) أداة مفيدة، لكنه ليس مراقباً محايداً.

أعد تمثيل مسرح الجريمة

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

قم ببناء نفس الأثر (artifact) الذي أنتجه نظام CI تماماً. قم بتنزيله إذا لزم الأمر. قم بتشغيل هذا الأثر محلياً باستخدام خادم ملفات ثابت بسيط، وليس باستخدام Vite أو Webpack dev middleware. استخدم نفس متغيرات البيئة التي حقنها نظام CI. طابق إصدار المتصفح بدقة. قم بتشغيله بنفس الوضع، سواء كان بواجهة (headed) أو بدون واجهة (headless)، لأن أحداث التركيز (focus events)، واستعلامات الوسائط (media queries)، وسياسات التشغيل التلقائي (autoplay policies) لا تزال تختلف بين الاثنين بطرق دقيقة. إذا كان نظام CI الخاص بك يستخدم حاوية Docker، فقم بتشغيل نفس الصورة محلياً. قم بإزالة ملف تعريف المتصفح الشخصي الخاص بك تماماً.

عندما يفشل إعادة الإنتاج المحلي أخيراً، ستكون أمام جلسة تصحيح أخطاء (debugging) حقيقية. وحتى ذلك الحين، فأنت تطارد الأوهام.

توقف عن النوم، وابدأ في الانتظار

الاستجابة الأكثر شيوعاً لاختبار المتصفح غير المستقر (flaky) هي إضافة تأخير. انتظر خمس ثوانٍ. انتظر عشر ثوانٍ. هذا ليس حلاً، بل هو استسلام. التأخيرات العشوائية تبطئ مجموعة اختباراتك، وتخلق ثقة زائفة، ومع ذلك ستفشل تحت الضغط عند حدوث اضطرابات في الشبكة.

بدلاً من ذلك، انتظر دليلاً على الحالة. إذا كان من المفترض أن يظهر إشعار بعد إرسال نموذج، فلا تنتظر مرور الوقت. انتظر وجود معرف إشعار (notification ID) محدد في الـ DOM. إذا كان من المفترض أن يزداد عداد، فانتظر حتى تتغير قيم النص. إذا كانت حالة التحميل تمنع التفاعل، فانتظر حتى يختفي مؤشر التحميل. إذا كنت تعمل مع WebSocket أو أحداث مرسلة من الخادم (server-sent events)، فانتظر حتى ينتج تدفق الشبكة حدثاً معيناً.

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

عملية الـ Hydration والزر المختفي

في تطبيقات React الحديثة، تسبب عملية الـ hydration فئة معينة من الإخفاقات التي غالباً ما تخفيها خوادم التطوير المحلية. يرسل الخادم ملف HTML، ثم يبدأ React بالعمل في المتصفح ويربط مستمعي الأحداث (event listeners). خلال تلك النافذة الزمنية، قد ينقر اختبارك على زر ما. بعد ذلك، يقوم React باستبدال عقدة الـ DOM تلك أو إعادة هيكلتها أثناء عملية الـ hydration. تصبح مرجعية العنصر (element handle) التي كان يمسك بها إطار عمل الاختبار الخاص بك تشير الآن إلى عقدة منفصلة (detached node)، وتحصل على خطأ يتعلق بالتفاعل مع عنصر تمت إزالته.

الحل ليس في كتابة محدد (selector) أكثر تعقيداً يبحث بعمق أكبر في شجرة المكونات. الحل هو البحث عن إشارات الجاهزية. انتظر حتى يكتسب عنصر جذري سمة (attribute) تدل على الـ hydration أو خاصية بيانات معروفة. انتظر حتى يختفي مؤشر التحميل الهيكلي (skeleton loader). انتظر حتى يصبح معالج الأحداث في جانب العميل (client-side event handler) نشطاً. اترك التطبيق يعلن أنه مستقر قبل أن ترسل نقراتك.

الجناة الخفيون: التبعيات والسكربتات الخارجية

أحياناً تتغير البيئة حتى لو لم يتغير كود تطبيقك. تحديث غير مباشر (transitive update) في مكتبة أدوات صغيرة، في المستوى الثالث داخل node_modules الخاص بك، يمكن أن يغير سلوك المتصفح. قد يغير كيفية حل الوعود (promises)، أو كيفية حقن الأنماط (styles)، أو كيفية اعتراض المحاكاة (mocks) للطلبات. عندما تبدأ الاختبارات في الفشل بعد تحديث روتيني للتبعية، قم بتسجيل إصدار مدير الحزم (package manager) ورمز التحقق (checksum) لملف القفل (lockfile). يجب أن تعرف ما إذا كنت تنظر إلى نفس الشجرة التي كنت تعمل عليها الأسبوع الماضي.

السكربتات الخارجية هي مخرب آخر متكرر. أدوات تتبع التحليلات، وحزم تطوير البرمجيات (SDKs) الخاصة بالدفع، وأدوات الدردشة، يتم تحميلها بشكل غير متزامن. تقوم هذه السكربتات بحقن iframes، أو تغيير التخطيط (layout)، أو سرقة التركيز (focus) في لحظات لا يتوقعها اختبارك. في نظام CI، قد يتم تحميل هذه السكربتات ببطء أكبر، أو قد تفشل في التحميل تماماً بسبب قيود الشبكة، مما يجعل تطبيقك يتبع مساراً مختلفاً لمعالجة الأخطاء. قم بتسجيل الموارد الخارجية التي تم تحميلها وحالة HTTP الخاصة بها. إذا استغرق إطار دفع (payment iframe) ثلاث ثوانٍ للظهور في CI ولكنه يتم تحميله فوراً على اتصالك المحلي السريع، فإن خطأ "العنصر غير قابل للنقر" (element not clickable) سيكون له فجأة سبب واضح.

وخطأ "العنصر غير قابل للنقر" ليس تشخيصاً أبداً، بل هو عرض. عالج السبب.

ابنِ مجموعة أدلة

يجب أن يكون كل فشل في نظام CI قابلاً لاتخاذ إجراء. تتبع المكدس (stack trace) وحده لا يكفي. أنت بحاجة إلى مجموعة أدلة تسمح لمهندس آخر، أو لنفسك في الشهر القادم، بإعادة بناء ما حدث.

احتفظ بلقطات الشاشة وتسجيلات الفيديو من عملية التشغيل الفاشلة. التقط مخرجات وحدة تحكم المتصفح (browser console) بالكامل، ليس فقط الأخطاء بل التحذيرات أيضاً. سجل إخفاقات الشبكة، بما في ذلك أخطاء 404، ورفض CORS، والاتصالات المقطوعة. احتفظ بمعرفات البناء (build IDs) وأعلام الميزات (feature flags) التي كانت نشطة. التقط لقطة (snapshot) للـ DOM في اللحظة التي فشل فيها التحقق (assertion). تتيح لك اللقطة فحص هيكل HTML بعد وقوع الحدث، بدلاً من