تخطَّ إلى المقالة

القياس نُشِر في زمن القراءة 11 د

قِس أرضية الضجيج قبل أن تضبط عتبة الفرق البصري

جولتان من البناء نفسه، على الجهاز نفسه، دون أن يتغيّر شيء بينهما: لقطات الصفحة لم تختلف على شيء البتّة، ولقطات canvas اختلفت على ما يصل إلى 81 بالمئة من بكسلاتها. أحد هذين الرقمين شدّ عتبةً عشرة أضعاف. والآخر أسقط بوابة.

باختصار

شغّل مجموعة الاختبارات على نفسها: التقط البناء نفسه مرّتين على الجهاز نفسه، وقارن الجولة A بالجولة B، واعتبر كل فرق يظهر ضجيجًا خالصًا من أداة القياس. أي عتبة دون ذلك الرقم ترسب عشوائيًا؛ وأي عتبة تعلوه كثيرًا لا تثبت شيئًا. هنا شدّت هذه الطريقة عتبة الصفحة عشرة أضعاف، من 0.001 إلى 0.0001، وأسقطت بوابة canvas تمامًا — جولتان متطابقتان اختلفتا على ما يصل إلى 81 بالمئة من بكسلات canvas، وهبط SSIM إلى 0.018148 حيث كانت البوابة المخمّنة تطلب 0.98. والنصف الثاني من الطريقة لا يقلّ أهمية عن الأول: تحقّق من أنّ أداة القياس كانت مصوَّبة نحو موضوعها، فستّ عشرة من اللقطات التسع عشرة التي وراء ذلك الصفر المقيس ليست إلا مستطيلًا مصمتًا بلون ماجنتا.

ما ينبغي أن تخرج به

  1. التقط البناء نفسه مرّتين، وقارنه بنفسه، واعتبر كل فرق يظهر هو الأرضية التي لا تعني تحتها أي عتبة شيئًا.
  2. القياس جعل عتبة الصفحة في هذا المستودع أشدّ عشر مرّات لا أرخى — من 0.001 إلى 0.0001 — لأنّ أداة القياس تبيّن أنها متطابقة بايت ببايت.
  3. أرضية ضجيج قيمتها صفر ليست دليلًا على أداة قياس جيّدة: ستّ عشرة من اللقطات التسع عشرة التي وراء هذه الأرضية هي 1,296,000 بكسل بلون واحد.
  4. حين لا تجتاز أي عتبة الأرضية، علّم البوابة بأنها موقوفة، واستمرّ في تسجيل الأرقام، وسمِّ التغيير المحدّد الذي يرفع الوقف.
  5. اطبع المقارنات التي لم تحدث إلى جانب تلك التي نجحت، فعددٌ أخضر يبتلع نقاطه العمياء نجاحٌ مصنوع.
  6. أرضية الضجيج قابلة للتلف، والسبب الوحيد في أنّ أحدًا يستطيع أن يعرف ذلك هو أنها مودَعة ومؤرّخة وصغيرة بما يكفي لتُقرأ.

tests/golden/thresholds.json · 46f81ba, 2026-08-19

ما الذي تقيسه عتبة الفرق فعلًا؟

العتبة تجيب عن سؤال واحد — كم مقدار الفرق ذي المعنى — ولا يمكن الإجابة عن ذلك السؤال قبل أن تعرف كم يظهر من فرق حين لا يتغيّر شيء. ومعظم النصائح المنشورة تتخطّى هذه الخطوة تمامًا: ابدأ دون واحد بالمئة، تقول، ثم اضبطها إن صارت المجموعة كثيرة الضجيج.

والضبط استنادًا إلى حالات الرسوب يحسّن الأمور لصالح مجموعة خضراء، وهو الشيء الوحيد الذي يجب ألّا تُحسَّن العتبة لصالحه أبدًا. بدأ هذا المستودع من حيث يبدأ الجميع، بأرقام مختارة بالنظر، وقد اعترف ملف إعداده بذلك في نصّه: أرقام canvas كانت موسومة بـ PROVISIONAL، أي مؤقّتة إلى أن تُثبِت جولةُ مرجعٍ في مواجهة مرجع على الجهاز نفسه أرضيةَ الضجيج الحقيقية.

جرت تلك الجولة في المساء نفسه، في الإيداع b5175ae، وحرّكت كل رقم في الملف. حرّكتها في الاتجاه الذي لا يكاد أحد يتوقّعه — القياس جعل المجموعة أشدّ لا أرخى، وأزال بوابة كاملة.

كل عتبة في الملف، قبل القياس وبعده. القياس لم يُرخِ شيئًا.
العتبة بالتخمين، 46f81ba بالقياس، b5175ae
pixel.default.maxDiffRatio 0.001 0.0001
pixel.byViewport.mobile-390 0.002 0.0005، ما زالت مبدئية
pixel.byViewport.tablet-768 لا صفّ 0.0005، ما زالت مبدئية
ssim.page.meanMin 0.995 0.999
ssim.page.worstMin 0.9 0.95
ssim.canvas.meanMin 0.98 null، البوابة مُسقَطة
ssim.canvas.worstMin 0.85 null، البوابة مُسقَطة
observation listenerTotal لا صفّ تسامح maxDelta 24

tools/golden/noise-floor.mjs:75-93

كيف تقيس أرضية الضجيج؟

للأداة غرض واحد مكتوب في سطرها الأول: قياس مقدار اختلاف أداة القياس مع نفسها. تقدّم بناءً واحدًا، وتلتقط المرجع مرّتين عبر سكربت الالتقاط نفسه الذي تستعمله المجموعة الحقيقية، وتقارن الجولة A بالجولة B. وكل ما يعود ضجيج، لأنّ شيئًا لم يتغيّر بين الاثنتين.

والمقارنة نحيلة عن عمد — رقمان لكل زوج صور، بلا صورة فرق وبلا تأويل. تعطي pixelmatch عدد البكسلات التي تجاوزت حدًّا لونيًا، ويعطي SSIM مكتوب هنا مباشرةً، بنافذة ثمانية بكسلات وخطوة أربعة، الرؤيةَ الإدراكية. ويُحفَظ المتوسّط وأسوأ نافذة معًا، لأنّ الانحدار في shader يكون موضعيًا عادةً، ومتوسّط 0.99 قادر على إخفاء ركن معطوب.

تخرج ثلاث مجموعات لا تُدمج أبدًا: لقطات بنيوية، والتقاطات canvas الخام، وانحراف الرصد — أي حقل غير بكسلي في ملخّص كل نقطة تحقّق اختلف بين جولتين للصفحة نفسها. وتُذكر النسب المئوية بصيغة min وp50 وp95 وmax على كل مجموعة مرتّبة.

المقارنة كلّها، من tools/golden/noise-floor.mjs. مقياسان لكل زوج، ولا شيء غير ذلك.
function comparePair(a, b) {
  if (a.width !== b.width || a.height !== b.height) return { ok: false, reason: 'size-mismatch' }
  const total = a.width * a.height
  // لا حاجة إلى صورة فرق هنا — العدد وحده. وpixelmatch تقبل
  // مخزن إخراج undefined لهذه الحالة بالذات.
  const changed = pixelmatch(a.data, b.data, undefined, a.width, a.height, {
    threshold: 0.1,
    includeAA: false,
  })
  const score = ssim(a, b, { windowSize: 8, stride: 4 })
  return {
    ok: true,
    diffRatio: changed / total,
    changedPixels: changed,
    totalPixels: total,
    ssimMean: score.mean,
    ssimWorst: score.worst,
  }
}

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

noise-floor.json · 2026-08-19 · n=19 and n=89

على أيّ شيء اختلفت جولتان متطابقتان؟

الجهاز نفسه، والمتصفّح نفسه، والرابط نفسه، وجولتان للصفحة الرئيسية في Golden Master عند 1440 × 900، بختم زمني 2026-08-19T19:33:49.368Z على Node v22.23.1. عادت من كل جهة تسع عشرة لقطة بنيوية وتسع وثمانون التقاطة canvas خام.

المجموعة البنيوية لم تختلف على أي شيء إطلاقًا: نسبة فرق البكسل min 0 وp50 0 وp95 0 وmax 0، مع ثمانية عشر زوجًا من التسعة عشر متطابقة بايت ببايت، والتاسع عشر شريط marquee يُلتقط حيًّا عن قصد، لأنّ الحركة المستمرّة هي السلوك قيد الاختبار. أما مجموعة canvas فاختلفت على كل شيء تقريبًا: p95 عند 0.025829475308641974، وmax عند 0.8112244897959183، ومتوسّط SSIM يهبط إلى 0.0181484156493548، وأسوأ نافذة تبلغ -0.886465409531798.

وتلك القراءة السالبة قيمة حقيقية لا خلل. يمتدّ SSIM من -1 إلى 1 كما عرّفه Wang وBovik وSheikh وSimoncelli سنة 2004، واختبار الوحدة في هذا المستودع يثبّت هذه الحقيقة بالتأكيد على أنّ صورة مقلوبة تسجّل أقل من الصفر. والمقياس الذي يستطيع أن يصير سالبًا بوجه مشروع إنما يخبرك أنّ النافذتين متعاكستان بنيويًا، وهذا هو شكل canvas يُلتقط في لحظتين لا رابط بينهما.

  • 0 نسبة فرق البكسل البنيوي min وp50 وp95 وmax، على 19 زوج لقطات
  • 81% بكسلات canvas التي اختلفت الحدّ الأقصى على 89 زوجًا من canvas الخام
  • 0.018148 أسوأ متوسّط SSIM في canvas البوابة المخمّنة كانت تطلب 0.98
  • 447 مقابل 431 listenerTotal، نقطة تحقّق واحدة الحقل غير البكسلي الوحيد الذي تحرّك
الأرضية المقيسة، مقروءة من tests/golden/noise-floor.json. جولتان، ولم يتغيّر بينهما شيء.

وهذا الأخير هو سبب إجراء هذا القياس على أكثر من اللقطات. قرأ listenerTotal 447 في جولة و431 في الأخرى عند استقرار واجهة الافتتاح، بانحراف قدره 16، بينما ثبت rootFontSizePx وdocumentHeight وglobalsLeaked تمامًا عبر الجولتين وعبر نقاط التحقّق الـ21 كلّها في مصفوفة لاحقة. فخُفِّض التأكيد من تامّ إلى متسامح بقيمة maxDelta 24 — فوق الـ16 المرصودة ودون أي قيمة تُقرأ تسرّبًا في المستمعين — وبقيت البقيّة تامّة، لأنّ القياس قال إنها تستطيع.

الانضباط نفسه — قِس الشرط قبل أن تثق بالرقم — هو ما أنتج رقم سهولة الوصول المنشور على هذا الموقع، حيث شرط غير مضبوط ضخّم العدد نحو 140 مرّة.

thresholds.json ssim.canvas · enforced false

حين لا تجتاز أي عتبة الأرضية، أوقف البوابة

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

لذلك لم تُرخَ البوابة، بل أُسقطت. enforced false، وmeanMin null، وworstMin null، ونصّ حالة يسمّي الشرط بدل أن يتهرّب: BLOCKED، تحتاج إلى تحكّم زمني حتمي من جهة المشهد. والمقارن يحترم ذلك في الكود لا في النثر وحده — فهو يحسب SSIM في كل جولة، ويسجّل المتوسّط وأسوأ نافذة وإحداثيات أسوأ نافذة، ويعيد ok مهما كانت النتيجة.

والجولة الحقيقية عبر الهدفين تؤكّد الحكم من الجهة الأخرى. عبر صفوف SSIM الـ306 الخاصة بـ canvas والمقارِنة بين البناءين، وسيط المتوسّط 0.7284514638837428 ووسيط أسوأ نافذة -0.20839572391449632. والبوابة التي يكون وسيط أسوأ نافذة فيها سالبًا بين بناءين يُعتقد أنهما سليمان لا تستطيع فصل الانحدار عن الضجيج.

PASS
حدثت المقارنة واتّفقت نقطة التحقّق. في الجولة المودَعة، تعود 120 نقطة تحقّق من 132 بنسبة 0.000000 بالضبط.
FAIL
حدثت المقارنة وتجاوز الفرق ما تسمح به الأرضية المقيسة. والرسوب الوحيد في تلك الجولة صفّ قديم في السجلّ، لا بكسل.
BLOCKED
لم تحدث المقارنة — إمّا لأنّ أداة القياس لم تستطع تثبيت موضوعها، وإمّا لأنّ لا عتبة تجتاز ضجيجها. تُعدّ على حدة، ولا تُبتلع أبدًا داخل عدد الناجحات.
informational
الرقم يُحسب ويُسجَّل في كل جولة ولا يُرسِب البناء أبدًا. وهو الدليل الذي سيبرّر تشغيل البوابة لاحقًا.
النتائج الأربع التي يفرّق بينها هذا المقارن. الأوليان وحدهما تقولان شيئًا عن الصفحة.

ولا يمكن الدفاع عن BLOCKED إلا إذا حملت شرط رفعٍ معلومًا بالاسم، وهنا تحقّق ذلك الشرط. فمشهد WebGL في البناء الجديد يتيح خطّافًا اختباريًا يرسم إطارًا واحدًا بالضبط عند زمن صريح، وبه صار المحتوى نفسه قابلًا للبوابة بايتًا ببايت: إطار مثبَّت يُؤكَّد الآن أنه مطابق عبر تحميلين مستقلّين للصفحة. أما التكافؤ مع الموقع القديم فيبقى مراجعة بشرية على صور ملتقطة، بحكم البنية، لأنّ القديم لا يمكن تثبيته على إطار.

واشتراط الحتمية كشف أيضًا عيبين ما كان النظر إلى الصورة ليكشفهما. فقد تركت مرحلة تركيب إطارًا مستقرًّا ممزوجًا فوق مخزن لم يُمسح، وضُبط ذلك لأنّ إطارين مثبّتين عند 1.5 ثانية وعند 9 ثوانٍ عادا متطابقين بايت ببايت — والزمن المثبَّت الذي لا يغيّر شيئًا يعني أنّ متغيّرًا موحّدًا لا يصل إلى shader. ذلك الفرق الصفري كان هو العلّة، والمقارن المضبوط على كشف التغيّر الزائد وحده كان سيقول أخضر.

وتثبيت canvas على إطار واحد هو القدرة نفسها التي يحتاجها canvas ليستجيب بصدق لطلب تقليل الحركة — ما يدين به WebGL وRive لـ prefers-reduced-motion.

sha256 d8ca0518 · 5,851 B · one colour

أرضية ضجيج صفرية قد تعني أنّ غطاء العدسة موضوع

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

الصفحة الرئيسية المرجعية عند initial-load مقاسها 1440 × 900، وحجمها 5,851 بايت، ولونها واحد: 1,296,000 بكسل بلون #ff00ff. وكذلك hero-settled، وhero-pointer-corner، وotot-section، وfooter، واثنتا عشرة أخرى. ستّ عشرة نقطة تحقّق، عند ستّة عشر موضع تمرير مختلفًا من y 0 إلى y 17809، تتشارك بصمة sha256 واحدة.

وهذه سياسة الأقنعة الموثّقة تعمل تمامًا كما وُصفت. فالصفحة المرجعية تحمل عنصري canvas بملء العرض، وقبل كل لقطة تُثبَّت طبقة ماجنتا ثابتة فوق مستطيل كل canvas في أعلى ترتيب التكديس — ولا تُرسم داخل canvas نفسه أبدًا، لأنّ ملأه عبر سياق 2D يمحو محتواه ويفسد سطح WebGL إفسادًا تامًّا. وعنصرا canvas بملء العرض ينتجان طبقة بملء العرض عند كل موضع تمرير.

ما تحتويه فعلًا اللقطات البنيوية التسع عشرة التي وراء ذلك الصفر، بفكّ ترميز كل ملف PNG وعدّ ثلاثيات RGB المتمايزة فيه.
نقطة التحقّق سياسة canvas الألوان المتمايزة
initial-load mask 1
hero-settled mask 1
أربع عشرة أخرى، ببصمة sha256 نفسها mask 1
marquee capture، حيّ 797
menu-open hide 14,060
menu-closed-again hide 14,080

ولا يمكن اكتشاف هذا إلا لأنّ أداة القياس تدوّن ما رفضت النظر إليه. فكل سجلّ لقطة يحمل المستطيلات التي استبعدها، ووثيقة البنية تسمّي ذلك الحقل واحدًا من الشيئين اللذين يجب قراءتهما قبل الثقة بأي نتيجة بكسلية. لا شيء في المستودع كاذب؛ غاية الأمر أنّ حجم العيّنة وراء جملة واحدة أصغر ممّا توحي به الجملة.

أما نقطتا التحقّق اللتان تحملان بكسلات صفحة حقيقية فتحملانها بمحض مصادفة في السياسة. فـ menu-open وmenu-closed-again تُخفيان عنصري canvas بدل تقنيعهما، لأنّ canvas القائمة يقع داخل حاوية ما زالت تتحوّل حين يُحسب مستطيل القناع، فطبقة ثابتة كانت ستنزاح عنه. وكلتاهما كانت متطابقة بايت ببايت عبر الجولتين A وB، وكلتاهما ما زالت متطابقة بايت ببايت في جولة مصفوفة كاملة مستقلّة بعد خمسة أيام — وهذا دليل حقيقي، وهو سبب صحّة الـ0.0001 المشدودة. لكنها تستند إلى صورتين، والملف يقول ثماني عشرة من تسع عشرة.

وليست حالة فريدة. ففكّ ترميز كل لقطة بنيوية مذكورة في بياني المصفوفة الكاملة يعطي 108 من 126 على Golden Master و114 من 132 على البناء الجديد بلون واحد بنسبة 100 بالمئة، عند العروض الستّة كلّها، وعلى الهدفين. وأرخص فحص متاح هنا سطر واحد: فُكّ ترميز لقطة مرجعية وعُدّ قيم RGB المتمايزة فيها قبل أن تثق بصفر.

menu-open vs menu-closed-again · 3 of 6

الطريقة الثانية التي تجعل نقطة تحقّق خضراء عن لا شيء

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

عند ثلاثة من العروض الستّة — breakpoint-991 وtablet-768 وmobile-390 — تحمل اللقطة المسمّاة menu-open واللقطة المسمّاة menu-closed-again بصمة sha256 نفسها، على الهدفين كليهما. والنقرة التي تفتح القائمة مسجَّلة ناجحة، وfailedInteractions صفر في كل مكان، ومقارنة البكسل تنجح بنسبة 0 في الاتجاهين لأنّ الهدفين فارغان بالقدر نفسه. والتكافؤ بين البناءين محفوظ فعلًا. أما نقطة التحقّق التي غرضها المعلن إثبات أنّ مسار الإغلاق لا يترك أثرًا فلا تثبت شيئًا في نصف المصفوفة.

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

interactions
نقرة على زرّ القائمة، ok true، ثم انتظار 1200 مللي ثانية، ok true. والملخّص يذكر failedInteractions 0 عند كل عرض على الهدفين.
navGroups.opacity
1 — وهو ما يُقرأ على أنّ القائمة مفتوحة، وهو الحقل الذي سيفحصه أولًا أي شخص يصحّح هذه المشكلة.
navGroups.rect
x 0، y 0، width 0، height 0. لا صندوق للعنصر أصلًا. وعند desktop-1440 يقيس العنصر نفسه 128.794 في 60 عند x 16.656، بتحويل تحجيم 1.2.
pixel
نسبة 0 في مواجهة الهدف الآخر، أي دون الأرضية المقيسة بمسافة مريحة. المقارنة سليمة إحصائيًا، ومضبوطة الإعداد، وعن لا شيء.
سجلّ mobile-390 للقائمة المفتوحة، حقلًا حقلًا. الحقل الذي يفحصه القارئ أولًا هو الذي يضلّله.

والقاعدة العامة نصفان لا تستطيع أي عتبة دمجهما. قِس مقدار اختلاف أداة القياس مع نفسها، وأكّد بشكل منفصل أنّ كل نقطة تحقّق بلغت الحالة التي يدّعيها اسمها — عنصر ظاهر، وصندوق غير صفري، وحقل في الملخّص تحرّك فعلًا بين القبل والبعد. النصف الأول حساب على الصور. أما الثاني فتأكيد مختلف تمامًا، وهو الذي يمنع مجموعة اختبارات من تصحيح ورقتها البيضاء بنفسها.

tests/.golden-diff/report.json · 2026-08-24

عُدّ المقارنات التي لم تحدث

الجولة المودَعة عبر الهدفين تقرأ 132 مقارَنة و132 ناجحة. وتقرأ أيضًا 162 مقارنة canvas لم تجرِ قط، والمقارن يطبع ذلك الرقم إلى جانب عدد الناجحات عن عمد: فالجولة التي تقول 132 من 132 وهي تحتفظ صامتةً بمئة مقارنة لم تُجرَ هي شكل النجاح المصنوع.

أكبر الفئات أثرٌ من آثار القصّ، أي أنّ الصفحة تتّفق مع نفسها بينما لا يتّفق قاصّ اللقطات. عنصر عرضه 13.328125 بكسل CSS عاد مقصوصًا إلى 15 في 14 من جهة و14 في 15 من الأخرى، لأنّ لقطة العنصر تمرّر العنصر إلى مجال الرؤية أولًا فينزاح المنشأ دون البكسل تحته. والعلاج ليس تسامحًا: صندوق CSS مضبوط ومفروض، بنجاح 468 من 468 فحص صندوق، بينما يُسجَّل حجم PNG للعلم فقط — فالصندوق ما تقرّره الصفحة، والقصّ ما يقرّره القاصّ، والأول وحده قول عن البناء.

أما الفئتان الأخريان فأرخص في الشرح وأغلى كثيرًا في التشغيل. 192 محاولة من 792 محاولة التقاط canvas لم تجد صندوقًا تصوّره أصلًا. و66 محاولة انتهت مهلتها عند 30 ثانية لكل منها، وكلّها على canvas واحد — انتقال الصفحة في المرجع، الذي لم يثبت قط في أيٍّ من محاولاته الـ66. وهذا 1,980 ثانية على الأقل من جولة مصفوفة طولها 2,620.6 ثانية: ثلاثة أرباع الزمن الفعلي أُنفقت في انتظار تصوير شيء لا يمكن تصويره.

مُلتقطة 534
لا صندوق للتصوير 192
انتهت المهلة عند 30 ثانية 66
792 محاولة التقاط canvas عبر جولة المصفوفة الكاملة المرجعية في 2026-08-24.الأشرطة بمقياس نسبي إلى الـ534 التي نجحت.

ومصفوفة البناء الجديد نفسه، وهي أكبر قليلًا عند 132 نقطة تحقّق، انتهت في 4.82 دقيقة مقابل 43.68 دقيقة للمرجع. وجُلّ هذا الفارق canvas واحد لا يمكن تثبيته على إطار، وهذه حقيقة أداء وحقيقة قياس في آن: فالشيء الذي لا تستطيع مجموعتك تصويره هو نفسه الذي تنفق عليه عصرها كلّه.

والزمن الفعلي مسألة قياس بذاتها، وطبقة التمرير في هذا الموقع نالت المعاملة نفسها — Lenis في مواجهة التمرير الأصلي، مقيسًا لا متجادَلًا فيه.

pixelmatch 7.2.0 · checked 2026-08-26

ما الذي تضبطه عتبات pixelmatch وPlaywright فعلًا؟

نصف الالتباس حول العتبات مصدره أنّ الكلمة تسمّي رقمين مختلفين. ففي pixelmatch — المكتبة التي تقوم عليها هذه المقارنة، والنموذج الذي يتبعه تأكيد اللقطات في Playwright نفسه — تعني threshold حدًّا لونيًا لكل بكسل لا تسامحًا لكل صورة. وفي الإصدار 7.2.0، وهو المثبّت هنا، تصير threshold 0.1 مسافةَ YIQ مربّعة قصوى قدرها 35215 × 0.1 × 0.1 = 352.15، مقابل قيمة قصوى ممكنة قدرها 35215. ثم تعيد الدالة عدد البكسلات التي تجاوزتها.

أما السماح لكل صورة فرقم منفصل، وهو الرقم الذي تخبرك به أرضية الضجيج. وPlaywright يُظهر هذا الانقسام ويترك نصفه فارغًا: قيمة threshold الافتراضية 0.2، بينما maxDiffPixels وmaxDiffPixelRatio بلا قيمة افتراضية أصلًا — فمن الصندوق مباشرةً، بكسل واحد مختلف بما يكفي يُرسِب التأكيد. وVitest لا يشحن سماحًا افتراضيًا هو الآخر ويقول ذلك صراحةً، وينصح بنسبة تُحسب على حجم اللقطة بدل عدد ثابت.

وثمّة أمر آخر تخفيه الكلمة: المقياس تحتها غير ثابت عبر الإصدارات. فقد استبدلت pixelmatch في فرعها الرئيسي مسافةَ YIQ بمقياس OKLab HyAB، فلم تعد threshold 0.1 كمّية واحدة ثابتة عبر الزمن. وتُقرأ includeAA عكس اسمها — فالقيمة false، وهي الافتراضية وهي إعداد هذا المستودع، تعني أنّ كشف تنعيم الحواف مفعَّل وأنّ بكسلات التنعيم لا تُحسب فروقًا.

ما تضبطه القيم الافتراضية لكل أداة، وأين يغيب الرقم الثاني ببساطة.
الأداة الحدّ لكل بكسل السماح لكل صورة
pixelmatch 7.2.0، هذا المستودع threshold 0.1، مسافة YIQ مربّعة maxDiffRatio 0.0001، مقيسة
pixelmatch، الفرع الرئيسي threshold، بمقياس OKLab HyAB لا شيء، تعيد عددًا
Playwright toHaveScreenshot threshold 0.2، بمقياس YIQ غير محدّدة افتراضيًا
Vitest، وضع المتصفّح غير متاح لا قيمة افتراضية، ويُنصح بنسبة

ولا شيء من هذه القيم الافتراضية يعرف شيئًا عن صفحتك، وهذا هو مغزى قياس صفحتك أنت. وكلٌّ من Playwright وVitest ينصحك أيضًا بإعادة توليد المراجع في البيئة التي ستقارنها فيها، وكل رقم ذُكر أعلاه هو جهاز واحد، وبناء Chromium واحد، بلا واجهة، ودون تمرير أي رايات حتمية عند الالتقاط — لا font-render-hinting ولا فرض لمعامل تحجيم الجهاز. والسماح 0.0001 موصوف في الملف بأنه هامش لتنقيط الحروف عبر الأجهزة، وهو هامش لحالة لم يقسها هذا المستودع بعد.

noise-floor.json · one commit, never re-run

كيف تضبط عتبة دون تخمين

الطريقة قصيرة ومعظمها مسك دفاتر. وما يجعلها تصمد أنّ مخرجها مودَع إلى جانب الأرقام التي يبرّرها، فيصير عمره ظاهرًا في سجلّ git لا في ذاكرة أحدهم.

  1. قدّم بناءً واحدًا، والتقطه مرّتين لا يجوز أن يختلف بين الجولتين شيء إلا اللحظة التي جرتا فيها. استعمل أداة الالتقاط نفسها التي تستعملها المجموعة الحقيقية لا بديلًا مبسّطًا عنها، وإلا تكون قد قِست أداة قياس أخرى.
  2. قارن A بـ B في كل ما تؤكّده عدٌّ لكل بكسل، ومقياس إدراكي مع حفظ أسوأ نافذة إلى جانب المتوسّط، وكل حقل غير بكسلي يسجّله ملخّصك. تحرّك listenerTotal هنا بمقدار 16؛ ولم يتحرّك documentHeight إطلاقًا.
  3. اضبط كل عتبة فوق p95 الخاصة بها بهامش تختاره عن عمد، ومع كتابة القياس بجانب الرقم تعليقًا. فالعتبة التي لا يستطيع أحد تتبّعها عتبة يستطيع أي أحد إرخاءها.
  4. حيث لا شيء يجتاز الأرضية، أوقف البوابة سجّل الأرقام، ولا تُرسِب البناء عليها أبدًا، وسمِّ التغيير المحدّد الذي يرفع الوقف. البوابة الموقوفة ومعها طريق عودة صادقة؛ والمرخاة زينة.
  5. أودِع القياس مؤرّخًا وصغيرًا نصًّا، في المستودع، إلى جانب العتبات التي أنتجها. هذا ما يتيح للقارئ التالي أن يلاحظ أنه قديم بدل أن يثق به.
  6. تحقّق من أنّ أداة القياس رأت الموضوع فُكّ ترميز لقطة مرجعية وعُدّ قيم RGB المتمايزة فيها. اللون الواحد يعني أنك صوّرت القناع. ثم اقرأ ما تسجّله أدواتك عن المناطق التي استبعدتها.
  7. أكّد أنّ كل نقطة تحقّق بلغت ما يقوله اسمها عنصر ظاهر، وصندوق غير صفري، وحقل في الملخّص تغيّر بين القبل والبعد. لا عتبة تعني أيًّا من هذا، والمجموعة الخضراء بدونه تصحّح ورقة بيضاء.
الطريقة كلّها، بما فيها الخطوتان اللتان لا علاقة لهما بالبكسلات أصلًا.

تحذيران يبرهن هذا المستودع عليهما في نفسه، وكلاهما ظاهر في إعداده. عرضان من عروضه الستّة، وكلاهما عند deviceScaleFactor 2، لم تُقَس لهما أرضية قط، وعتبتاهما تحملان الاعتراف حيث سيلقاه القارئ التالي: not yet measured، أي مبدئية. ووثيقة البنية تذكر النتيجة دون تلطيف — النجاح أو الرسوب عند هذين العرضين لا يثبت شيئًا عن أداة القياس، ولا ينبغي اعتماد أي مرشّح استنادًا إليهما.

والأرضية نفسها لها إيداع واحد لا غير. أُخذت في 2026-08-19 ولم يُعَد تشغيلها قط، بينما تغيّر السيناريو الذي قاسته مرّتين منذئذ ونما من 19 نقطة تحقّق إلى 21. أرضية الضجيج قابلة للتلف، تتحلّل بمجرّد أن تتحرّك الصفحة أو المتصفّح أو الجهاز، والسبب الوحيد في أنّ أحدًا يستطيع أن يعرف كم صار عمرها هو ملف صغير مؤرّخ قابع في نظام الإصدارات إلى جانب الأرقام التي أنتجها.

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

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

أسئلة

كم جولة أحتاج للحصول على أرضية ضجيج صالحة؟

كفت هنا جولتان، لأنّ الإشارة كانت قاطعة في الاتجاهين: عاد المسار البنيوي متطابقًا بايت ببايت، وعاد مسار canvas بحدّ أقصى قدره 81 بالمئة. جولتان تكفيان لإسقاط أداة قياس، وتكفيان لتبرير شدّ عتبة تقرأ صفرًا أصلًا. لكنهما لا تكفيان لوصف توزيع — فالنسب المئوية في الملف المودَع محسوبة على 19 و89 عيّنة مأخوذة من زوج جولات واحد، ولا شيء في المستودع يدّعي أكثر من ذلك. أما الأرضية التي تحطّ في مكان ما في الوسط فهي الحالة التي تحتاج فيها إلى جولات أكثر لا أقلّ.

عادت أرضية الضجيج عندي صفرًا. هل هذا جيّد؟

تحقّق ممّا في الصورة قبل أن تحتفل. الأرضية البنيوية هنا صفر تمامًا، وستّ عشرة من اللقطات التسع عشرة التي وراءها هي 1440 × 900 بلون واحد، لأنّ أداة الالتقاط تُقنّع عناصر canvas، والصفحة تحمل اثنين منها بملء العرض. والعتبة المشتقّة من تلك الجولة ما زالت صحيحة، لكنها تستند إلى لقطتين حقيقيتين لا إلى تسع عشرة. فُكّ ترميز لقطة مرجعية، وعُدّ قيم RGB المتمايزة فيها، واقرأ ما تسجّله أدواتك عن المناطق التي استبعدتها.

ماذا أفعل حين لا تجتاز أي عتبة أرضية الضجيج؟

قل ذلك في ملف الإعداد وتوقّف عن البوّابة. يكتب هذا المستودع enforced false وmeanMin null وworstMin null وحالة BLOCKED مع سببها مرفقًا، ومقارنه يحسب SSIM على أي حال، ويسجّل المتوسّط وأسوأ نافذة وإحداثياتها، ويعيد ok مهما كان. والتعليل مكتوب في الملف: البوابة التي لا تستطيع تمييز الانحدار من الضجيج أسوأ من لا بوابة، لأنها تعلّم الناس تجاهل الأحمر. والأهم أنّ BLOCKED تسمّي التغيير الذي يرفع الوقف — ولمّا تحقّق ذلك التغيير في البناء الجديد، صار المحتوى نفسه قابلًا للبوابة بايتًا ببايت.

هل يمكن أصلًا مقارنة محتوى canvas أو WebGL بكسلًا ببكسل؟

نعم، وذلك بالضبط حين لا يكون للمحتوى اعتماد غير مثبَّت على الزمن. حقل نقاط مُبذَّر من بصمة إحداثيات شبكة صحيحة، بلا مصدر عشوائي وبلا بذرة زمنية وبلا تاريخ، قُورن بكسلًا ببكسل تحت تقليل الحركة فعاد بـ 365 بكسلًا مطليًّا في كل جهة، عند الإحداثيات الـ365 نفسها، بمتوسّط ألفا 53.742 في الاثنين وبفارق ألفا لكل بكسل قدره 0. والحقل نفسه متحرّكًا تراوح بين 630 و1,841 بكسلًا مطليًّا عبر خمس التقاطات تفصل بينها 400 مللي ثانية على هدف واحد، ولا سبب لذلك إلا لحظة الغالق. فالحتمية خاصّية في اعتماد المحتوى على الزمن، لا خاصّية في السطح.

هل تعني أرضية ضجيج مقيسة أنني أستطيع التوقّف عن تثبيت البيئة؟

العكس: الأرضية صالحة فقط للبيئة التي قيست فيها، وهي الشيء الذي يخبرك إن كان التثبيت قد نجح. وقد أُخذت هذه على جهاز واحد، في بناء Chromium واحد، دون تمرير أي رايات حتمية إطلاقًا، فهي لا تقول شيئًا عن جهاز ثانٍ. وعرضان من العروض الستّة في هذه المصفوفة لم تُقَس لهما أرضية قط، وعتبتاهما تقولان ذلك في ملف الإعداد. وينصح Playwright وVitest بالنصيحة نفسها في وثائقهما: أعِد توليد المراجع في البيئة التي ستقارنها فيها.

المصادر

مقيس في هذا المستودع

  • tests/golden/noise-floor.json القياس نفسه: 19 زوجًا بنيويًا، و89 زوج canvas، ونسب مئوية لكل مجموعة، والحقل الوحيد غير البكسلي الذي انحرف. إيداع واحد، 2026-08-19.
  • tests/golden/thresholds.json كل عتبة مع التعليل الذي اشتُقّت منه، وبوابة canvas المُسقَطة، والعرضان اللذان ما زالا موسومين بأنهما مبدئيان.
  • tools/golden/noise-floor.mjs الأداة: بناء واحد يُلتقط مرّتين، وpixelmatch للعدّ، وSSIM مكتوب هنا مباشرةً بنافذة 8 وخطوة 4، محفوظًا متوسّطًا وأسوأ نافذة.
  • tests/.golden-diff/report.json الجولة المودَعة عبر الهدفين: 132 مقارَنة، و132 ناجحة، و162 مقارنة canvas لم تُحكَم، و468 من 468 فحص صندوق مفروضة وناجحة.
  • docs/adr/ADR-013-golden-master-testing.md البدائل التي دُرست وسبب سقوط كلٍّ منها، ومنها لقطة واحدة لكل نقطة تحقّق مع ضبط التسامح إلى أن تنجح.

مُراجَع في مقابل

Alaa Abbod

بقلم

علاء عبّود

مطوّر إبداعي — هيرنه، ألمانيا

مصمّم ومطوّر يبني مواقع ويب سهلة الوصول وتطبيقات جوال ومتاجر إلكترونية وهويات بصرية كعملٍ واحد، وبيديه. هذا الموقع يُنشَر بالإنجليزية والألمانية والعربية من مصدر واحد، ومن هناك جاء معظم هذه الأسئلة.

Alaa Abbod

هل تقيس مجموعتك البصرية أي شيء؟

المجموعة الخضراء والمجموعة التي ترى الصفحة شيئان مختلفان، والفارق بينهما عادةً لقطة مرجعية واحدة مفكوكة الترميز. إن كانت لديك بوابة لقطات لا يثق بها أحد، أو canvas لا يستطيع أحد تثبيته على إطار، فتلك محادثة قصيرة تستحقّ أن تُجرى.

من فضلك أدر جهازك،
هذا الموقع مبني عموديًا.