اختبارات الانحدار البصري تنجح على حاسوبك المحمول. ثم تدفع الكود، فيعمل خط الأنابيب، وتنهار المتصفحات بصمت في الاختبارات نفسها، أو تُبلّغ عن خمسين “تغييرًا” ليست في الحقيقة سوى فروق في تنعيم الحواف. لم يتغيّر شيء في كودك، لكن 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” إلى اختبارات حتمية.

اختيار أداتك: BackstopJS أم المقارنة المدمجة في Playwright
إن كنت تشغّل Playwright أصلًا لاختبارات الطرف إلى الطرف، فأسرع مسار هو تأكيده المدمج toHaveScreenshot(): صفر اعتماديات إضافية، ومقارنة على مستوى البكسل، ومراجع أساسية بلواحق حسب المنصّة جاهزة فورًا. وهي نقطة توقّف مشروعة لمجموعة اختبارات صغيرة، وقد تكون كل ما تحتاجه للفرق التي تعمل على أقل من خمسين شاشة تقريبًا بلا مكتبة مكوّنات مشتركة.
لكنه يصطدم بجدار عند سير عمل المراجعة. فمقارنة Playwright تعمل على البكسل فقط وليست لها واجهة مراجعة داخل PR، ما يعني أنّ اعتماد دفعة تغييرات بصرية مشروعة يتطلّب إعادة توليد اللقطات يدويًا. وكلما ارتفع عدد مراجعك الأساسية، صارت حلقة الاعتماد اليدوية هذه مؤلمة.
BackstopJS هو الأداة المخصّصة المبنية لتلك الحلقة تحديدًا. رخصته MIT (حاليًا v6.3.25)، ويعتمد Docker أولًا عبر الراية --docker، ويُصدر JUnit XML يقرأه مشغّل CI أصلًا. وسير عمل المرجع لديه (توليد مرجع، ثم الاختبار مقابله، ثم اعتماد الفروق المقبولة كمرجع جديد) هو النموذج التشغيلي الذي تنتهي إليه معظم إعدادات الانحدار البصري المستضافة ذاتيًا. وفي بقية هذا الدليل، سيكون BackstopJS هو المثال العملي.
أداتان قد تجد من يوصي بهما في مواضع أخرى مستبعدتان من هذا الشرح: لا نغطّي Argos هنا لأنّ مسار منتجه العلني يتمحور حول التطبيق المستضاف وتكاملات CI، بينما Lost Pixel مؤرشف. وBackstopJS هو المثال العملي الأكثر أمانًا لإعداد ذاتي الاستضافة عملي.
| المحور | BackstopJS | Playwright 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، التي تشغّل عملية الالتقاط داخل صورة مرجعية مثبّتة بحيث يعرض جهازك والمشغّل النتيجة نفسها. وهذا ما يقضي على الإيجابيات الكاذبة الناتجة عن الخطوط وتنعيم الحواف: ثبّت الصورة مرة واحدة وتوقّف عن مطاردة فروق وهمية.

إعداد 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 مهمة متزامنة |
| 8GB | 3–4 مهام متزامنة |
| 16GB | مجموعات اختبار كبيرة وخطوط أنابيب متوازية |
هذه الأرقام إرشادات عملية مستخلَصة من تشغيلات CI حقيقية، لا معيار أداء من مزوّد، فتعامل معها كنقاط بداية تقريبية وراقب ذروة استهلاك الذاكرة لديك.
خلاصة القسم: 4GB من RAM هي الأساس الموثوق؛ أضف نحو 2GB من الهامش لكل مهمة انحدار بصري متزامنة إضافية.
المشكلة في VRT ذاتي الاستضافة أن المشغّل الذي يفتقر إلى الذاكرة لا يفشل بصوت عالٍ، بل يُسقط Chromium بصمت، وهو تحديداً العَرَض الذي وُجد كل هذا الإعداد لتفاديه. وتوفير هامش كافٍ من الذاكرة هو أرخص تأمين يمكنك شراؤه. وإذا كنت تفضّل عدم بناء مضيف للمشغّل من الصفر، فإن سوق Cloudzy يوفّر عمليات نشر بنقرة واحدة لـ GitLab ذاتي الاستضافة و Gitea ذاتي الاستضافة ذاتيًا تضع بين يديك نسخة CI عاملة خلال دقائق، على VPS يمكنك تحديد حجمه بدقة ليوفّر الهامش الذي تحتاجه مجموعة اختباراتك مع Chromium بلا واجهة.

الأسئلة الشائعة
كيف أُعدّ اختبارات الانحدار البصري داخل خط 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 لإعداد اختبارات انحدار بصري مستضافة ذاتيًا.