DEV Community

kouana
kouana

Posted on

جعلنا موقعنا غير قابل للضغط مرتين، ولم يكن الخطأ في الكود

مرتين خلال أسابيع صار موقعنا يبدو سليمًا تمامًا ولا يستجيب للضغط. الصفحة تُحمَّل، والتصميم في مكانه، والكونسول نظيف، والزوار لا يستطيعون فتح أي رابط.

في المرتين لم يكن السبب خطأً برمجيًا بالمعنى المعتاد. كان سلوكًا موثّقًا في المتصفح يعمل كما صُمّم تمامًا، لكنه انطبق على نطاق أوسع مما توقّعنا. والأخطر أن اختباراتنا الآلية مرّت بنجاح في الحالتين.

الحادثة الأولى: إعداد واحد عطّل سبعين عنصرًا

أضفنا ويدجت مساعد ذكي للموقع، وفيه إعداد يفتح نافذة المحادثة تلقائيًا عند دخول الزائر. فعّلناه.

بعدها صارت الصفحة ميتة. الروابط لا تُفتح، والأزرار لا تستجيب، وحقول البحث لا تستقبل كتابة.

السبب أن الويدجت يعتمد نمطًا شائعًا في نوافذ الحوار: عند فتح النافذة، يضع السمة inert على كل ما عداها حتى لا يتشتت التركيز ولا يهرب مؤشر لوحة المفاتيح خارجها. سلوك صحيح ومطلوب في الحوارات.

المشكلة أن الفتح التلقائي يجعل هذه الحالة هي حالة الصفحة الافتراضية عند كل زيارة. سبعون عنصرًا في الصفحة ورثوا inert، وبقوا كذلك حتى يغلق الزائر نافذة لم يطلب فتحها أصلًا.

// ما يفعله الويدجت عند الفتح
document.querySelectorAll('body > *:not(.assistant-root)')
  .forEach((el) => el.setAttribute('inert', ''));
Enter fullscreen mode Exit fullscreen mode

وinert ليست سمة تجميلية. الفحص السريع يوضح مداها:

const el = document.querySelector('a.main-cta');

el.offsetParent !== null;                    // true  — العنصر مرئي
getComputedStyle(el).pointerEvents;          // 'auto' — لا شيء يمنع المؤشر
el.getBoundingClientRect().width;            // 180   — له مساحة حقيقية
el.matches(':disabled');                     // false — ليس معطّلًا

el.closest('[inert]') !== null;              // true  ← هنا الجواب
Enter fullscreen mode Exit fullscreen mode

كل فحص اعتدنا عليه يقول إن العنصر سليم. inert تعمل في طبقة أخرى: تُخرج العنصر وكل أبنائه من شجرة الوصول، وتلغي استقباله لأحداث المؤشر والتركيز، بلا أي أثر في الأنماط المحسوبة.

لماذا مرّت الاختبارات

اختباراتنا كانت تسأل الأسئلة المعتادة: هل العنصر موجود في الـDOM؟ هل هو مرئي؟ هل نصّه صحيح؟ الإجابات كلها نعم.

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

إن كنت تستخدم أي مكوّن يطبّق inert، أضف هذا التأكيد إلى اختباراتك:

test('لا شيء خارج الحوار يصير inert في الحالة الافتراضية', async () => {
  await page.goto('/');
  const inertCount = await page.$$eval('[inert]', (els) => els.length);
  expect(inertCount).toBe(0);
});
Enter fullscreen mode Exit fullscreen mode

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

الحادثة الثانية: pointer-events: none لا تفعل ما تظنه

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

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

/* ❌ لا يعمل: الأب هو من يعترض */
.wa-widget__button {
  pointer-events: none;
}
Enter fullscreen mode Exit fullscreen mode

pointer-events لا تورَّث صعودًا. وضعها على الابن لا يجعل الأب شفافًا للمس. الترتيب الصحيح معكوس: عطّل الأب واستثنِ الابن.

/* ✅ الأب لا يستقبل، والزر وحده يستقبل */
.wa-widget {
  pointer-events: none;
}
.wa-widget__button {
  pointer-events: auto;
}
Enter fullscreen mode Exit fullscreen mode

هذا النمط يصلح لكل طبقة عائمة: شريط إشعارات، وطبقة حركة، وخلفية ضبابية فوق المحتوى. الحاوية تُعطَّل والعنصر التفاعلي وحده يُستثنى.

للفحص السريع، اسأل المتصفح من يستقبل اللمسة فعلًا عند نقطة معينة بدل تفحّص الأنماط:

// في منتصف الشاشة على مقاس جوال
const el = document.elementFromPoint(195, 400);
console.log(el.className);   // إن كانت طبقة عائمة، فهي تسرق اللمس
Enter fullscreen mode Exit fullscreen mode

سطر واحد يجيب عن سؤال قضينا فيه وقتًا طويلًا في تفحّص z-index وposition.

ثلاث طبقات كاش تخفي أثر أي إصلاح

مضاعِف صعوبة في الحالتين: بين تعديلنا وما يراه الزائر ثلاث طبقات كاش — كاش CDN، وكاش الصفحات الثابتة في التطبيق، وكاش المتصفح.

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

القاعدة التي التزمناها بعدها: أي فحص بعد نشر لا يُقرأ قبل تفريغ الكاش صراحةً، والقياس الأول بعد التفريغ غير موثوق أيضًا لأن الحافة تكون باردة. انتظر طلبًا ثانيًا.

الخلاصة

العطلان يشتركان في أنهما غير مرئيين لأي فحص برمجي معتاد. الأول يعيش في طبقة الوصول، والثاني في هندسة التقاط الأحداث، وكلاهما يترك الـDOM والأنماط تبدو سليمة تمامًا.

بعد هذين، صار عندنا فحصان ثابتان بعد كل نشر: عدّ عناصر [inert] في الصفحة، وelementFromPoint في منتصف الشاشة على مقاس جوال. سطران يكشفان صنفًا كاملًا من الأعطال التي تكلّف كل زائر يصل إليك.

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

Top comments (0)