دليل شامل: دمج اختبارات لقطات الشاشة المرئية باستخدام Cypress في مشاريعك

دقائق القراءة: 9

مقدمة: ضمان الجودة المرئية لتطبيقات الويب

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

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

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

مثال لتعليق بوت Cypress في طلب سحب يوضح نتائج اختبارات لقطات الشاشة

إعداد بيئة الاختبار الأولية

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

الموقع ثابت ويقع داخل مجلد public. يمكنك رؤية الموقع محليًا عن طريق تشغيل الأمر npm run serve والتحقق منه في المتصفح على العنوان http://localhost:8000.

لقطة شاشة لموقع ويب نموذجي بسيط يعرض مكونات Badge و Button

دمج 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، يجب أن يكون الإخراج مشابهًا للصورة التالية:

نتائج تشغيل اختبارات Cypress في الوضع الصامت (headless mode)

ستنجح الاختبارات وسيتم إنشاء 5 صور تحت المجلد /snapshots/screenshot.spec.js.

الخيار الثاني: واجهة تشغيل الاختبارات (Test Runner)

باستخدام الأمر npm run cypress open، سيتم فتح Cypress Test Runner ويمكنك متابعة خطوات الاختبار خطوة بخطوة.

واجهة 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 وجاهزة للاستخدام.

نحتاج إلى إجراء تغييرين:

  1. تغيير baseUrl في ملف cypress.json:
{
  "baseUrl": "http://host.docker.internal:8000/"
}
  1. تغيير أمر 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 سيواجهنا بمشكلة:

لقطة شاشة لخطأ في تشغيل اختبارات Cypress داخل Docker

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

صورة توضح الاختلافات المرئية في مكون الزر بعد تشغيل الاختبارات في Docker

كما ذكرت سابقًا، تناقضات في الخطوط! يستخدم مكون 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

إذا لم تكن هذه مشكلة (أي أن التغيير مقصود)، فقم بتحديث لقطات الشاشة:

CYPRESS_updateSnapshots=true npm run test

الخلاصة التقنية

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

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *