دليل شامل: دمج اختبارات لقطات الشاشة المرئية باستخدام Cypress في مشاريعك
مقدمة: ضمان الجودة المرئية لتطبيقات الويب
يهتم المطورون بشكل دائم بجودة الكود الذي يكتبونه. توجد أنواع مختلفة من الاختبارات التي تساعدنا على تجنب الأعطال عند إضافة ميزات جديدة إلى المشروع. ولكن، ماذا يمكننا أن نفعل لضمان أن المكونات لا تبدو مختلفة بمرور الوقت؟ هذا هو التحدي الذي تواجهه اختبارات الانحدار المرئي.
في هذا المقال، ستتعلم كيفية استخدام أداة Cypress لالتقاط أجزاء من صفحات الويب. بعد ذلك، ستقوم بدمج أداة الاختبار هذه في بيئة التكامل المستمر (CI) لضمان عدم قيام أي شخص في المستقبل بإجراء تغييرات غير مرغوبة على مشروعك تؤثر على المظهر البصري.
جاء دافعي لإنشاء استراتيجية الاختبار هذه من تجربتي في العمل. في شركة Thinkific، لدينا نظام تصميم داخلي (Design System)، وقد أضفنا Cypress لتجنب المفاجآت غير السارة عند العمل على ملفات CSS/JS. بنهاية هذا المقال، سنمتلك طلبات سحب (PRs) تتضمن اختبارات Cypress تظهر نتائجها في تعليقات البوت:

إعداد بيئة الاختبار الأولية
قبل أن نبدأ، قمت بإنشاء موقع ويب نموذجي لمحاكاة مكتبة مكونات (Component Library). إنه موقع بسيط للغاية تم إنشاؤه باستخدام TailwindCSS ومستضاف على Vercel. يوثق هذا الموقع مكونين رئيسيين: badge و button. يمكنك الاطلاع على الكود المصدري على GitHub.
الموقع ثابت ويقع داخل مجلد public. يمكنك رؤية الموقع محليًا عن طريق تشغيل الأمر npm run serve والتحقق منه في المتصفح على العنوان http://localhost:8000.

دمج Cypress و Cypress Image Snapshot
ابدأ باستنساخ المستودع (repository) المثال. بعد ذلك، أنشئ فرعًا جديدًا وقم بتثبيت حزمة cypress-image-snapshot، وهي الحزمة المسؤولة عن التقاط ومقارنة لقطات الشاشة.
git checkout -b add-cypress
npm install -D cypress cypress-image-snapshot
بعد إضافة الحزم، نحتاج إلى بضع خطوات إضافية لدمج Cypress Image Snapshot في Cypress. أنشئ ملفًا باسم cypress/plugins/index.js بالمحتوى التالي:
const { addMatchImageSnapshotPlugin } = require('cypress-image-snapshot/plugin');
module.exports = (on, config) => {
addMatchImageSnapshotPlugin(on, config);
};
بعد ذلك، أنشئ ملفًا باسم cypress/support/index.js يحتوي على ما يلي:
import { addMatchImageSnapshotCommand } from 'cypress-image-snapshot/command';
addMatchImageSnapshotCommand();
إنشاء اختبار لقطة الشاشة
حان الوقت لإنشاء اختبار لقطة الشاشة. الخطة هي كالتالي:
- ستقوم
Cypressبزيارة كل صفحة (badgeوbutton) من صفحات المشروع. - ستلتقط
Cypressلقطة شاشة لكل مثال في الصفحة. - تحتوي صفحة
Badgeعلى مثالين (DefaultوPill)، بينما تحتوي صفحةButtonعلى ثلاثة أمثلة (DefaultوPillوOutline).
جميع هذه الأمثلة موجودة داخل عنصر <div> يحمل الفئة cypress-wrapper. أُضيفت هذه الفئة بهدف وحيد هو تحديد ما يحتاج إلى الاختبار.
تكوين Cypress
الخطوة الأولى هي إنشاء ملف تكوين Cypress (cypress.json):
{
"baseUrl": "http://localhost:8000/",
"video": false
}
يمثل baseUrl عنوان الموقع الذي يعمل محليًا. كما ذكرت سابقًا، سيقوم الأمر npm run serve بتقديم محتوى مجلد public. الخيار الثاني، video، يعطل تسجيل الفيديو في Cypress، والذي لن نستخدمه في هذا المشروع.
كتابة الاختبار الفعلي
حان وقت إنشاء الاختبار. في الملف cypress/integration/screenshot.spec.js، أضف ما يلي:
const routes = [
'badge.html',
'button.html'
];
describe('Component screenshot', () => {
routes.forEach((route) => {
const componentName = route.replace('.html', '');
const testName = `${componentName} should match previous screenshot`;
it(testName, () => {
cy.visit(route);
cy.get('.cypress-wrapper').each((element, index) => {
const name = `${componentName} - ${index}`;
cy.wrap(element).matchImageSnapshot(name);
});
});
});
});
في الكود أعلاه، أقوم بإنشاء الاختبارات ديناميكيًا بناءً على مصفوفة routes. سيقوم الاختبار بإنشاء صورة واحدة لكل عنصر .cypress-wrapper موجود في الصفحة.
تشغيل الاختبارات
أخيرًا، داخل ملف package.json، لنقم بإنشاء الأمر لتشغيل الاختبارات:
{
"test": "cypress"
}
من هنا، هناك خياران: تشغيل Cypress في الوضع الصامت (headless mode) باستخدام npm run cypress run أو استخدام واجهة تشغيل اختبارات Cypress (Cypress Test Runner) مع npm run cypress open.
الخيار الأول: الوضع الصامت (Headless)
باستخدام الأمر npm run cypress run، يجب أن يكون الإخراج مشابهًا للصورة التالية:

ستنجح الاختبارات وسيتم إنشاء 5 صور تحت المجلد /snapshots/screenshot.spec.js.
الخيار الثاني: واجهة تشغيل الاختبارات (Test Runner)
باستخدام الأمر npm run cypress open، سيتم فتح Cypress Test Runner ويمكنك متابعة خطوات الاختبار خطوة بخطوة.

لقد أنجزنا معلمنا الأول، لذا لنقم بدمج هذا الفرع مع الفرع الرئيسي (master). إذا كنت ترغب في رؤية العمل المنجز حتى الآن، يمكنك الانتقال إلى طلب السحب الخاص بي.
تشغيل Cypress داخل Docker لتجنب التناقضات
إذا قمت بتشغيل الاختبار أعلاه بالتناوب بين الوضع الصامت و Test Runner، قد تلاحظ أن لقطة الشاشة ستختلف. عند استخدام Test Runner على جهاز كمبيوتر بشاشة Retina، قد تحصل على صور عالية الدقة (2x)، بينما لا يوفر الوضع الصامت لقطات شاشة عالية الجودة.
من المهم أيضًا الإشارة إلى أن لقطات الشاشة قد تختلف وفقًا لنظام التشغيل الخاص بك. على سبيل المثال، تحتوي أنظمة Linux و Windows على تطبيقات ذات أشرطة تمرير مرئية (scrollbars)، بينما يخفي نظام macOS شريط التمرير. إذا لم يتناسب المحتوى الملتقط في لقطة الشاشة مع مكون معين، فقد يظهر شريط تمرير أو لا يظهر.
إذا كان مشروعك يعتمد على الخطوط الافتراضية لنظام التشغيل، فستكون لقطات الشاشة مختلفة أيضًا وفقًا للبيئة. لتجنب هذه التناقضات، سيتم تشغيل الاختبارات داخل Docker حتى لا يؤثر جهاز المطور على لقطات الشاشة الملتقطة.
لنبدأ بإنشاء فرع جديد:
git checkout -b add-docker
توفر Cypress صور Docker مختلفة – يمكنك الاطلاع على التفاصيل في وثائقهم و مدونتهم. لهذا المثال، سأستخدم الصورة cypress/included، والتي تتضمن Electron وجاهزة للاستخدام.
نحتاج إلى إجراء تغييرين:
- تغيير
baseUrlفي ملفcypress.json:
{
"baseUrl": "http://host.docker.internal:8000/"
}
- تغيير أمر
testفي ملفpackage.json:
{
"test": "docker run -it -e CYPRESS_updateSnapshots=$CYPRESS_updateSnapshots --ipc=host -v $PWD:/e2e -w /e2e cypress/included:4.11.0"
}
تشغيل npm run test سيواجهنا بمشكلة:

الصور مختلفة قليلاً، ولكن لماذا؟ دعنا نرى ما بداخل مجلد __diff_output__:

كما ذكرت سابقًا، تناقضات في الخطوط! يستخدم مكون Button الخط الافتراضي لنظام التشغيل. نظرًا لأن Docker يعمل داخل Linux، فإن الخط المعروض لن يكون هو نفسه الذي قمت بتثبيته على macOS. بما أننا انتقلنا الآن إلى Docker، فإن لقطات الشاشة هذه قديمة. حان الوقت لتحديث اللقطات:
CYPRESS_updateSnapshots=true npm run test
يرجى ملاحظة أنني أضع متغير البيئة CYPRESS_updateSnapshots قبل أمر الاختبار. لقد أنجزنا المعلم الثاني. في حال احتجت إلى مساعدة، تحقق من طلب السحب الخاص بي. لنقم بدمج هذا الفرع ونمضي قدمًا.
إضافة التكامل المستمر (CI)
خطوتنا التالية هي إضافة الاختبارات في بيئة التكامل المستمر (CI). هناك حلول CI مختلفة في السوق، ولكن لهذا الدرس التعليمي، سأستخدم Semaphore. أنا لست تابعًا لهم وأستخدم منتجهم في العمل، لذلك كان خيارًا طبيعيًا بالنسبة لي. التكوين مباشر ويمكن تكييفه مع حلول أخرى مثل CircleCI أو Github Actions.
قبل إنشاء ملف تكوين Semaphore الخاص بنا، لنقم بإعداد مشروعنا للتشغيل في CI. الخطوة الأولى هي تثبيت start-server-and-test. كما يوحي اسم الحزمة، ستقوم بتشغيل خادم، والانتظار حتى يصبح URL جاهزًا، ثم تشغيل أمر الاختبار:
npm install -D start-server-and-test
ثانيًا، قم بتحرير ملف package.json:
{
"test": "docker run -it -e CYPRESS_baseUrl=$CYPRESS_baseUrl -e CYPRESS_updateSnapshots=$CYPRESS_updateSnapshots --ipc=host -v $PWD:/e2e -w /e2e cypress/included:4.11.0",
"test:ci": "start-server-and-test serve http://localhost:8000 test"
}
في السكريبت test، نضيف متغير البيئة CYPRESS_baseUrl. سيسمح لنا هذا بتغيير baseUrl الذي تستخدمه Cypress ديناميكيًا. كما نضيف السكريبت test:ci، والذي سيقوم بتشغيل الحزمة التي قمنا بتثبيتها للتو.
نحن جاهزون لـ Semaphore. أنشئ ملف .semaphore/semaphore.yml بالمحتوى التالي:
version: v1.0
name: Cypress example
agent:
machine:
type: e1-standard-2
os_image: ubuntu1804
blocks:
- name: Build Dependencies
dependencies: []
task:
jobs:
- name: NPM
commands:
- sem-version node 12
- checkout
- npm install
- name: Tests
dependencies: ['Build Dependencies']
task:
prologue:
commands:
- sem-version node 12
- checkout
jobs:
- name: Cypress
commands:
- export CYPRESS_baseUrl="http://$(ip route | grep -E '(default|docker0)' | grep -Eo '([0-9]+\.){3}[0-9]+' | tail -1 ):8000"
- npm run test:ci
لنحلل التكوين بالتفصيل:
- الأسطر 1-6 تحدد نوع المثيل الذي سنستخدمه في بيئتنا.
- الأسطر 8 و 16 تنشئ كتلتين: الكتلة الأولى،
"Build Dependencies"، ستقوم بتشغيلnpm install، لتنزيل التبعيات التي نحتاجها. - الكتلة الثانية،
"Tests"، ستقوم بتشغيلCypress، مع بعض الاختلافات. - في السطر 27، نقوم بتعيين متغير البيئة
CYPRESS_baseUrlديناميكيًا بناءً على عنوانIPالذي يستخدمهDockerفي الوقت الحالي. سيحل هذا محلhttp://host.docker.internal:8000/، والذي قد لا يعمل في جميع البيئات. - في السطر 28، نقوم أخيرًا بتشغيل الاختبار باستخدام
start-server-and-test: بمجرد أن يصبح الخادم جاهزًا للاتصالات، ستقومCypressبتشغيل مجموعة الاختبار.
لقد أنجزنا معلمًا آخر، حان الوقت لدمج فرعنا! يمكنك الاطلاع على طلب السحب الذي يحتوي على جميع الملفات من هذا القسم والتحقق من البناء داخل Semaphore.
تسجيل الاختبارات في cypress.io
قراءة إخراج الاختبارات في CI ليست سهلة للغاية. في هذه الخطوة، سنقوم بدمج مشروعنا مع cypress.io. تستند الخطوات التالية إلى وثائق Cypress.
لنبدأ بالحصول على معرف مشروع (projectId) ومفتاح تسجيل (record key). في الطرفية، أنشئ فرعًا جديدًا وقم بالتشغيل:
git checkout -b add-cypress-recording
CYPRESS_baseUrl=http://localhost:8000 ./node_modules/.bin/cypress open
لقد ذكرت سابقًا أننا سنستخدم Cypress داخل Docker. ولكن هنا نفتح Cypress محليًا لأن هذه هي الطريقة الوحيدة للاندماج مع لوحة تحكم الموقع.
داخل Cypress، دعنا نذهب إلى علامة التبويب Runs، انقر فوق "Set up project to record"، واختر اسمًا ورؤية. سنحصل على projectId يتم إضافته تلقائيًا في ملف cypress.json ومفتاح تسجيل خاص.
في Semaphore، أضفت مفتاح التسجيل كمتغير بيئة يسمى CYPRESS_recordKey. بعد ذلك، لنقم بتحديث سكريبت الاختبار الخاص بنا لاستخدام المتغير:
{
"test:ci": "start-server-and-test 'serve' 8000 'npm run test -- run --record --key $CYPRESS_recordKey'"
}
هذا كل ما يجب القيام به. في طلب السحب يمكننا رؤية تكامل cypress.io في التعليقات. يوجد حتى رابط عميق يأخذنا إلى لوحة القيادة الخاصة بهم ويعرض جميع لقطات الشاشة.
حان الوقت لدمج عملنا، وهذا هو نهاية تكاملنا.
الاختبار في سيناريو واقعي
تخيل أننا نعمل على تغيير يؤثر على حشوة الأزرار (padding): حان الوقت لاختبار ما إذا كانت Cypress ستلتقط الفرق. في الموقع النموذجي، لنضاعف الحشوة الأفقية من 16px إلى 32px. هذا التغيير بسيط للغاية لأننا نستخدم Tailwind CSS: يتم استبدال px-4 بـ px-8. إليك طلب السحب الذي يوضح هذا التغيير.
كما قد نتوقع، التقطت Cypress أن الزر لا يتطابق مع لقطات الشاشة السابقة. عند زيارة الصفحة، يمكننا التحقق من لقطة الشاشة للاختبار الفاشل:
يعرض ملف الاختلاف (diff file) لقطة الشاشة الأصلية على اليسار، والنتيجة الحالية على اليمين، ويتم دمجها في المنتصف. لدينا أيضًا خيار تنزيل الصورة حتى نتمكن من رؤية المشكلة بشكل أفضل:

إذا لم تكن هذه مشكلة (أي أن التغيير مقصود)، فقم بتحديث لقطات الشاشة:
CYPRESS_updateSnapshots=true npm run test
الخلاصة التقنية
لقد تعلمنا في هذا المقال كيف يمكن لأداة Cypress، بالتعاون مع cypress-image-snapshot، أن تكون أداة لا غنى عنها في ترسانة المطورين لضمان جودة الواجهة الأمامية. من خلال أتمتة اختبارات الانحدار المرئي، يمكننا حماية مشاريعنا من التغييرات غير المقصودة في المظهر البصري، والتي غالبًا ما تمر دون ملاحظة في دورات التطوير السريعة. إن دمج هذه الاختبارات في خطوط التكامل المستمر (CI/CD) يضمن أن كل تغيير في الكود يخضع لفحص بصري دقيق، مما يقلل من الأخطاء ويحسن تجربة المستخدم النهائية. استخدام Docker يوفر بيئة اختبار متسقة، متغلبًا على تحديات التباين بين أنظمة التشغيل المختلفة، بينما توفر لوحة تحكم cypress.io رؤية شاملة لنتائج الاختبارات، مما يسهل عملية المراجعة والتصحيح. إن الاستثمار في هذه الاستراتيجية يعزز الثقة في الكود ويساهم في تقديم منتج نهائي عالي الجودة.