تخطَّ إلى المحتوى الرئيسي
خصم ٥٠٪ جميع الخطط، لفترة محدودة. تبدأ من $2.48/mo
13 min left
أدوات المطورين وDevOps

كيف تستضيف اختبارات الانحدار البصري ذاتيًا داخل خط CI لديك

S بواسطة Sajjad 13 دقيقة قراءة
Self-hosted visual regression testing running in a CI pipeline with BackstopJS and Docker

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

تلك الفجوة بين “يعمل محليًا” و“يعمل في CI” هي في الغالب مشكلة بنية تحتية، لا مشكلة اختبارات. ولكي تستضيف اختبارات الانحدار البصري ذاتيًا داخل خط CI بشكل موثوق، تحتاج إلى أربعة أجزاء موصولة ببعضها بشكل صحيح: مشغّل CI بمنفّذ Docker، وحاوية مضبوطة بحيث لا تنفد الذاكرة المشتركة من Chromium في الوضع بلا واجهة، وأداة مقارنة مضبوطة لإصدار تقارير يقرأها CI، وVPS بحجم يناسب الذاكرة التي تستهلكها المتصفحات بلا واجهة فعليًا.

يبني هذا الدليل تلك المنظومة من أولها إلى آخرها. وفي النهاية سيكون لديك ملف .gitlab-ci.yml عامل (أو ما يعادله في Gitea)، وبيئة Docker تمنع انهيار Chromium، وBackstopJS يُصدر تقارير JUnit يستطيع خط الأنابيب قراءتها، وVPS بحجم مناسب لتشغيل ذلك كله.

الخلاصة السريعة

  • شغّل المتصفح داخل صورة Docker نفسها محليًا وفي CI. معظم الفروق الإيجابية الكاذبة تأتي من اختلاف الخطوط وطريقة العرض بين البيئات. ثبّت الصورة فتختفي.
  • أصلح القيمة الافتراضية 64MB لـ /dev/shm. حجم الذاكرة المشتركة الافتراضي في Docker لا يكفي محرّك العرض في Chromium، فينهار بصمت داخل CI. اضبط shm_size: '2gb' في docker-compose.yml، أو مرّر --disable-dev-shm-usage إلى Chromium.
  • استخدم BackstopJS كأداة أساسية قابلة للاستضافة الذاتية. رخصته MIT (الإصدار v6.3.25)، ويعتمد Docker أولًا عبر الراية --docker، ويُصدر تقارير JUnit. وإن كنت تشغّل Playwright أصلًا، فإنّ toHaveScreenshot() المدمجة فيه نقطة انطلاق جيدة بلا أي تثبيت إضافي.
  • اجعل 4GB من RAM هي الأساس عند تحديد الحجم. خصّص الذاكرة بسخاء. في تشغيلات CI الواقعية، قد تستهلك مهمة التقاط الصور في Chromium ذاكرة أكبر بكثير أثناء التقاط صفحة كاملة مما تبدو عليه في وضع الخمول، لذا فإنّ 4GB من RAM هي الأساس الأكثر أمانًا لمهمة أو مهمتين متسلسلتين أو متوازيتين بشكل خفيف.

ما لا يغطيه هذا الدليل

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

  • ليس مقارنة كاملة بين الأدوات. يركّز هذا الدليل على المنظومة التي يمكنك تشغيلها فعليًا في بيئة CI مستضافة ذاتيًا: BackstopJS وDocker ومشغّلك الخاص.
  • لا يغطّي استضافة Argos ذاتيًا. ‏Argos مفتوح المصدر، لكن مسار منتجه العلني ووثائقه يركّزان على تطبيق Argos المستضاف وتكاملات CI، لا على مسار استضافة ذاتية بسيط للإنتاج. ولهدف هذا الدليل، يبقى BackstopJS المثال العملي الأكثر أمانًا.
  • لا يوصي بـ Lost Pixel. أُرشِف ذلك المشروع في أبريل 2026؛ استخدم BackstopJS لعمليات النشر الجديدة.
  • لا يعيد النقاش بين الاستضافة الذاتية وخدمات SaaS المُدارة. إن كنت هنا، فقد استبعدت Percy أو Chromatic من قبل.

المتطلبات الأساسية

تحتاج إلى بضعة أمور جاهزة قبل أول أمر:

  • مشغّل CI مستضاف ذاتيًا، أو خطة لنشر واحد (نغطّيها أدناه).
  • ‏Docker مثبّت على مضيف المشغّل.
  • مشروع Node.js له هدف اختبار قائم (رابط تطبيق يعمل أو مجموعة مسارات مكوّنات لالتقاطها).
  • وصول إلى صدفة الأوامر على VPS أو المضيف الذي يعمل عليه المشغّل.

لماذا تنجح الاختبارات البصرية محليًا وتفشل في CI

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

تلك التوصية وحدها هي سبب وجود بقية هذا الإعداد. وثلاثة أوجه عدم تطابق تفسّر كل فشل كاذب تقريبًا:

  • اختلافات الخطوط وتنعيم الحواف. جهاز التطوير لديك وحاوية CI يأتيان بحزم خطوط مختلفة وطرق عرض دون البكسل مختلفة. لذا تُظهر الصفحات الغنية بالنص فروقًا في كل تشغيل حتى لو لم يتغيّر شيء.
  • العرض بلا واجهة مقابل العرض بواجهة. قد يرتّب المتصفح ذو الواجهة والمتصفح بلا واجهة الصفحة نفسها بشكل مختلف قليلًا.
  • استنفاد الذاكرة المشتركة. هذا هو العطل الصامت. قيمة /dev/shm الافتراضية في Docker هي 64MB، وChromium بلا واجهة يستخدم الذاكرة المشتركة لعمليات العرض لديه. وحين تنفد، ينهار Chromium دون رسالة خطأ مفيدة. فتموت مهمتك ببساطة أو تتعلّق.

يُحلّ الأولان بتثبيت صورة Docker واحدة لتوليد المرجع وللمقارنة معًا. ويُحلّ الثالث في قسم إعداد Docker أدناه. أصلح الثلاثة، فتتحوّل الاختبارات التي كانت “متذبذبة في CI” إلى اختبارات حتمية.

Three reasons visual regression tests pass locally but fail in CI: font and anti-aliasing differences between the dev machine and the CI container, headless versus headed rendering differences, and shared-memory exhaustion from Docker's 64MB /dev/shm default crashing headless Chromium

اختيار أداتك: BackstopJS أم المقارنة المدمجة في Playwright

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

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

BackstopJS هو الأداة المخصّصة المبنية لتلك الحلقة تحديدًا. رخصته MIT (حاليًا v6.3.25)، ويعتمد Docker أولًا عبر الراية --docker، ويُصدر JUnit XML يقرأه مشغّل CI أصلًا. وسير عمل المرجع لديه (توليد مرجع، ثم الاختبار مقابله، ثم اعتماد الفروق المقبولة كمرجع جديد) هو النموذج التشغيلي الذي تنتهي إليه معظم إعدادات الانحدار البصري المستضافة ذاتيًا. وفي بقية هذا الدليل، سيكون BackstopJS هو المثال العملي.

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

المحورBackstopJSPlaywright toHaveScreenshot()
سير عمل المراجعة داخل PRتقرير مدمج وأمر approveإعادة توليد اللقطات يدويًا
إدارة المرجع الأساسيأوامر reference / test / approveملفات لقطات لكل اختبار
عرض موحّد عبر Dockerالراية --dockerأحضر صورتك الخاصة
كلفة التثبيتاعتمادية منفصلةموجودة سلفًا إن كنت تستخدم Playwright

ولمقارنة أعمق تشمل أدوات ومحاور أكثر، تغطّي مقارنتنا بين BackstopJS وArgos وLost Pixel قرار الاختيار كاملًا.

نشر مشغّل CI مستضاف ذاتيًا على VPS

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

‏GitLab CE المستضاف ذاتيًا هو الخيار الأثقل: مستودعات، وCI/CD، وسجلّ صور، وقضايا، وطلبات دمج في نسخة واحدة. وما يهمّنا في هذا الدليل هو منفّذ Docker في GitLab Runner، الذي يتيح لكل مهمة انحدار بصري أن تعمل داخل صورة الحاوية المثبّتة نفسها في كل مرة. وGitLab وGitea وJenkins وForgejo وPortainer وDocker متاحة جميعًا كعمليات نشر بنقرة واحدة على سوق Cloudzy، ما يختصر الإعداد كثيرًا.

Gitea مع act-runner هو البديل الخفيف. فـ Gitea خدمة Git مبنية بلغة Go مع محرّك سير عمل متوافق مع GitHub Actions (عبر act-runner)، لذا إن كان فريقك مرتاحًا لصيغة Actions، فهو مشغّل صغير وسريع الإقامة.

أيًّا كان اختيارك، فهدف الإعداد واحد: مشغّل يقبل المهام وينفّذها بمنفّذ Docker. وكل ما يأتي بعد ذلك (إعداد الحاوية، وBackstopJS، وخط الأنابيب النموذجي) يفترض وجود ذلك المنفّذ.

ضبط Docker حتى لا ينهار Chromium

هذا هو العطل الذي يكلّف الناس أكبر قدر من الوقت. يركّب Docker المسار /dev/shm بحجم 64MB افتراضيًا. وChromium بلا واجهة يستخدم قسم الذاكرة المشتركة هذا لعمليات العرض لديه، وأثناء التقاط صفحة كاملة يحتاج إلى أكثر بكثير من 64MB. وحين تنفد، يموت محرّك العرض، غالبًا دون أي رسالة خطأ تشير إلى الذاكرة المشتركة. فتتعلّق المهمة، أو تنتهي مهلتها، أو تُبلّغ عن انهيار عام في المتصفح.

هناك إصلاحان، وأيّهما يفي بالغرض.

الإصلاح أ: ارفع حجم الذاكرة المشتركة في Compose. يحدد مفتاح shm_size حجم قسم /dev/shm داخل الحاوية. مرجع ملف Docker Compose أنّ shm_size يحدّد حجم الذاكرة المشتركة المسموح بها لحاوية الخدمة. اضبطه داخل كتلة الخدمة:

# docker-compose.yml
services:
  vrt:
    image: backstopjs/backstopjs:6.3.25
    shm_size: '2gb'          # override the 64MB default
    volumes:
      - ./:/src
    working_dir: /src

الإصلاح ب: اطلب من Chromium ألّا يستخدم /dev/shm أصلًا. تجعل الراية --disable-dev-shm-usage متصفح Chromium يكتب ملفات الذاكرة المشتركة في /tmp بدلًا من قسم الذاكرة المشتركة. مرّرها ضمن وسائط تشغيل المتصفح. في إعداد backstop.json تكون تحت engineOptions؛ وفي Playwright تكون تحت launchOptions:

// backstop.json (fragment)
{
  "engine": "puppeteer",
  "engineOptions": {
    "args": ["--disable-dev-shm-usage", "--no-sandbox"]
  }
}

الإصلاح أ أنظف حين تتحكّم في ملف Compose؛ والإصلاح ب هو الخيار المحمول حين لا تملك سوى رايات تشغيل المتصفح. وتطبيقهما معًا لا يضرّ.

نصيحة احترافية: استخدم صورة Docker نفسها تمامًا محليًا وفي CI. يمنحك BackstopJS هذا مجانًا عبر الراية --docker، التي تشغّل عملية الالتقاط داخل صورة مرجعية مثبّتة بحيث يعرض جهازك والمشغّل النتيجة نفسها. وهذا ما يقضي على الإيجابيات الكاذبة الناتجة عن الخطوط وتنعيم الحواف: ثبّت الصورة مرة واحدة وتوقّف عن مطاردة فروق وهمية.

Two fixes for headless Chromium crashing in Docker CI: raising the container's shared memory with shm_size set to 2gb in docker-compose.yml, or passing the --disable-dev-shm-usage flag so Chromium writes shared-memory files to /tmp instead of the 64MB /dev/shm partition

إعداد BackstopJS للعمل مع CI

يُنتج BackstopJS تقرير JUnit XML يقرأه مشغّل CI ليُنجح البناء أو يُفشله، وهذا هو مغزى ربطه بخط أنابيب بدل تشغيله يدويًا. ثبّته وهيّئ ملف إعداد:

npm install --save-dev backstopjs
npx backstop init

ثم وجّه backstop.json إلى الصفحات التي تريد التقاطها وفعّل تقرير CI:

// backstop.json (fragment)
{
  "id": "my_app_vrt",
  "viewports": [
    { "label": "desktop", "width": 1440, "height": 900 }
  ],
  "scenarios": [
    {
      "label": "Homepage",
      "url": "http://app:3000/",
      "selectors": ["document"]
    }
  ],
  "report": ["CI"],
  "engine": "puppeteer",
  "engineOptions": {
    "args": ["--disable-dev-shm-usage", "--no-sandbox"]
  }
}

إعداد تقرير CI هو ما يُصدر JUnit XML. استخدم الراية --docker عند تشغيل هذه الأوامر محليًا من جهازك المضيف. أما داخل مهمة CI تستخدم صورة backstopjs/backstopjs أصلًا، فشغّل backstop test مباشرة بدلًا من ذلك. وسير عمل المرجع الأساسي ثلاثة أوامر:

npx backstop reference --docker   # capture the approved baseline
npx backstop test --docker        # run the comparison, emit the report
npx backstop approve              # accept the current diffs as the new baseline

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

إعداد خط أنابيب CI عامل

يعمل خط الأنابيب في جزأين منطقيين: توليد المرجع الأساسي مرة واحدة (أو عند الطلب)، ثم المقارنة معه عند كل تغيير. والمثال أدناه إعداد GitLab CI يستخدم منفّذ Docker، مع تطبيق إصلاح الذاكرة المشتركة مسبقًا عبر رايات التشغيل في backstop.json.

# .gitlab-ci.yml
stages:
  - visual-regression

visual_regression:
  stage: visual-regression
  image:
    name: backstopjs/backstopjs:6.3.25
    entrypoint: [""]
  script:
    - backstop test
  artifacts:
    when: always
    reports:
      junit: backstop_data/ci_report/xunit.xml
    paths:
      - backstop_data/bitmaps_test
      - backstop_data/html_report
    expire_in: 1 week

سطر junit يسلّم التقرير إلى GitLab كي تظهر حالات الفشل في واجهة طلب الدمج؛ وسطر paths يحتفظ بصور الفروق كي تتمكّن من رؤية ما تغيّر فعلًا. وعلى Gitea مع act-runner، تنعكس الخطوات نفسها في سير عمل بنمط Actions:

# .gitea/workflows/visual-regression.yaml
name: Visual Regression
on: [push, pull_request]

jobs:
  vrt:
    runs-on: ubuntu-latest
    container:
      image: backstopjs/backstopjs:6.3.25
      options: --shm-size=2gb          # raise /dev/shm for the job container
    steps:
      - uses: actions/checkout@v4
      - run: backstop test

لاحظ خيار الحاوية --shm-size=2gb في سير عمل Gitea. إنه إصلاح الذاكرة المشتركة نفسه الوارد في الإصلاح أ، مطبَّقًا على مستوى حاوية المهمة حيث لا يوجد ملف Compose منفصل لتحريره.

تحديد حجم VPS المناسب لـ Chromium بلا واجهة

القيد هنا هو RAM، لا CPU. فاستهلاك Chromium بلا واجهة لوحدة CPU متقطّع (يقفز أثناء الالتقاط ويخمد بينها)، لذا يكفي عدد أنوية متواضع. أما الذاكرة فهي السقف الذي يحدّد كم مهمة يمكنك تشغيلها في الوقت نفسه. تستقرّ عملية Chromium بلا واجهة عند نحو 300–500MB في الخمول وترتفع إلى 1–2GB أثناء التقاط صفحة كاملة، لذا فإنّ محرّك تحديد الحجم لديك هو RAM لكل مهمة متزامنة.

الذاكرة (RAM)مناسب لـ
2GBحد أدنى ضيّق: مهمة واحدة متسلسلة
4GBالأساس الموصى به: 1–2 مهمة متزامنة
8GB3–4 مهام متزامنة
16GBمجموعات اختبار كبيرة وخطوط أنابيب متوازية

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

خلاصة القسم: 4GB من RAM هي الأساس الموثوق؛ أضف نحو 2GB من الهامش لكل مهمة انحدار بصري متزامنة إضافية.

المشكلة في VRT ذاتي الاستضافة أن المشغّل الذي يفتقر إلى الذاكرة لا يفشل بصوت عالٍ، بل يُسقط Chromium بصمت، وهو تحديداً العَرَض الذي وُجد كل هذا الإعداد لتفاديه. وتوفير هامش كافٍ من الذاكرة هو أرخص تأمين يمكنك شراؤه. وإذا كنت تفضّل عدم بناء مضيف للمشغّل من الصفر، فإن سوق Cloudzy يوفّر عمليات نشر بنقرة واحدة لـ GitLab ذاتي الاستضافة و Gitea ذاتي الاستضافة ذاتيًا تضع بين يديك نسخة CI عاملة خلال دقائق، على VPS يمكنك تحديد حجمه بدقة ليوفّر الهامش الذي تحتاجه مجموعة اختباراتك مع Chromium بلا واجهة.

VPS memory sizing guide for headless Chromium visual regression jobs: 2GB for a single sequential job, 4GB as the recommended baseline for one to two concurrent jobs, 8GB for three to four concurrent jobs, and 16GB for large suites and parallel pipelines

الأسئلة الشائعة

كيف أُعدّ اختبارات الانحدار البصري داخل خط CI/CD؟

ثبّت BackstopJS، ووجّه backstop.json إلى صفحاتك المستهدفة، واضبط خيار تقرير CI ليُصدر JUnit XML. استخدم backstop reference --docker وbackstop test --docker عند التشغيل من جهازك المحلي كي تُلتقط اللقطات داخل صورة Docker المثبّتة. وفي CI، شغّل المهمة داخل صورة backstopjs/backstopjs واستدعِ backstop test مباشرة، ثم انشر تقرير JUnit من backstop_data/ci_report/xunit.xml. واستخدم مشغّلًا مستضافًا ذاتيًا بمنفّذ Docker، بحجم لا يقلّ عن 4GB من RAM لـ Chromium بلا واجهة.

كم من RAM يحتاج Chromium بلا واجهة داخل حاوية CI؟

تستهلك عملية Chromium بلا واجهة نحو 300–500MB في الخمول وتقفز إلى 1–2GB أثناء التقاط صفحة كاملة. ولتشغيل CI موثوق، خطّط لنحو 4GB من RAM لكل مهمة أو مهمتين متزامنتين، مع إضافة نحو 2GB لكل مهمة متوازية إضافية. واستهلاك CPU متقطّع وليس هو القيد؛ فـ RAM هي ما يحدّد كم مهمة يمكنك تشغيلها في الوقت نفسه.

لماذا تنجح اختبارات الانحدار البصري لديّ محليًا وتفشل في CI؟

يختلف عرض المتصفح بين البيئات: الخطوط، وتنعيم الحواف، والعمل بلا واجهة مقابل العمل بواجهة، ونظام OS، كلها تغيّر النتيجة. والحل هو تشغيل توليد المرجع والمقارنة داخل صورة Docker نفسها كي يتطابق العرض في الحالتين. والراية --docker في BackstopJS تفعل ذلك بتشغيل عمليات الالتقاط داخل صورة مرجعية مثبّتة.

كيف أُصلح خطأ /dev/shm في Chromium داخل Docker وCI؟

قيمة /dev/shm الافتراضية في Docker هي 64MB، وهي أصغر من أن تكفي محرّك العرض في Chromium بلا واجهة، فتسبّب انهيارات صامتة. إما أن ترفعها عبر shm_size: '2gb' في docker-compose.yml (أو --shm-size=2gb على حاوية المهمة)، أو تمرّر --disable-dev-shm-usage ضمن رايات تشغيل Chromium كي يكتب ملفات الذاكرة المشتركة في /tmp بدلًا من ذلك.

هل أستخدم toHaveScreenshot في Playwright أم أداة انحدار بصري مخصّصة؟

الدالة المدمجة toHaveScreenshot() في Playwright تكفي للمجموعات الصغيرة: أقل من خمسين شاشة تقريبًا، بلا مكتبة مكوّنات مشتركة وبلا حاجة إلى واجهة مراجعة داخل PR. وانتقل إلى أداة مخصّصة مثل BackstopJS حين تحتاج إلى سير عمل حقيقي لاعتماد المراجع، وتقرير مراجعة، وعرض موحّد عبر Docker، وهو ما يصبح مهمًا مع تزايد عدد مراجعك الأساسية.

هل ما زال Lost Pixel مصانًا؟

لا. أُرشِف Lost Pixel في أبريل 2026 ولم يعد مرشّحًا لعمليات نشر جديدة. استخدم BackstopJS لإعداد اختبارات انحدار بصري مستضافة ذاتيًا.

مشاركة

المزيد من المدونة

تابع القراءة.

جاهز للنشر؟ تبدأ من 2.48 $/شهر.

سحابة مستقلة منذ 2008. AMD EPYC، NVMe، 40 Gbps. استرداد خلال 14 يومًا.