مقدمة إلى Cypress
Cypress هي أداة اختبار أمامية من الجيل التالي مبنية للويب الحديث. على عكس أدوات E2E التقليدية مثل Selenium، يعمل Cypress مباشرة في المتصفح جنباً إلى جنب مع تطبيقك، مما يوفر اختباراً سريعاً وموثوقاً وخالياً من العيوب.
لماذا Cypress؟
- السفر عبر الزمن: التقاط لقطات أثناء تشغيل الاختبارات، والعودة في الوقت إلى أي أمر
- قابلية التصحيح: أخطاء قابلة للقراءة وتتبع المكدس، تكامل Chrome DevTools
- الانتظار التلقائي: لا حاجة للانتظار الصريح أو التوقفات
- إعادة التحميل في الوقت الفعلي: مشاهدة تنفيذ الاختبارات في الوقت الفعلي أثناء التطوير
- لقطات الشاشة ومقاطع الفيديو: التقاط الفشل تلقائياً وتسجيل التشغيل بالكامل
- التحكم في الشبكة: محاكاة والتحكم في حركة مرور الشبكة بسهولة
ملاحظة: تم تصميم Cypress لاختبار أي شيء يعمل في متصفح. يعمل مع أي إطار عمل أمامي أو بدون إطار على الإطلاق.
تثبيت Cypress
تثبيت Cypress عبر npm:
# تثبيت Cypress
npm install --save-dev cypress
# فتح مشغل اختبار Cypress
npx cypress open
# تشغيل اختبارات Cypress بدون رأس
npx cypress run
بعد التثبيت، ينشئ Cypress هيكل المجلدات هذا:
cypress/
├── e2e/ # ملفات الاختبار (.cy.js)
├── fixtures/ # بيانات الاختبار (JSON، CSV، إلخ)
├── support/ # أوامر وتكوين قابلة لإعادة الاستخدام
│ ├── commands.js # أوامر مخصصة
│ └── e2e.js # خطافات وتكوين عالمية
└── downloads/ # الملفات المحملة أثناء الاختبارات
كتابة أول اختبار لك
أنشئ ملف اختبار في cypress/e2e/:
// cypress/e2e/first-test.cy.js
describe('My First Test', () => {
it('visits the app', () => {
cy.visit('https://example.com');
cy.contains('Example Domain');
});
it('has the correct title', () => {
cy.visit('https://example.com');
cy.title().should('eq', 'Example Domain');
});
});
نصيحة: استخدم امتداد .cy.js أو .cy.ts لملفات اختبار Cypress. هذا يساعد بيئات التطوير على توفير إكمال تلقائي مناسب ويمنع أدوات فحص الأكواد من الشكوى من Cypress العالمية.
أساسيات Cypress
اختيار العناصر
يوفر Cypress محددات قوية مستوحاة من jQuery:
// عن طريق محدد CSS
cy.get('.btn-primary');
cy.get('#username');
// عن طريق سمة البيانات (موصى به)
cy.get('[data-cy="submit-button"]');
// عن طريق محتوى النص
cy.contains('Submit');
cy.contains('button', 'Submit');
// استعلامات علائقية
cy.get('form').find('input[type="email"]');
cy.get('.header').within(() => {
cy.get('nav').should('be.visible');
});
// الأول، الأخير، الترتيب
cy.get('li').first();
cy.get('li').last();
cy.get('li').eq(2); // العنصر الثالث (مفهرس من الصفر)
أفضل ممارسة: استخدم سمات data-cy أو data-test أو data-testid لاختيار العناصر. تجنب أسماء فئات CSS أو المعرفات التي قد تتغير أثناء إعادة هيكلة التصميم.
التفاعل مع العناصر
// الكتابة
cy.get('[data-cy="email-input"]').type('user@example.com');
cy.get('[data-cy="password"]').type('password123{enter}'); // الكتابة والضغط على Enter
// النقر
cy.get('[data-cy="submit-btn"]').click();
cy.get('[data-cy="menu-item"]').dblclick();
cy.get('[data-cy="context-menu"]').rightclick();
// مربعات الاختيار والأزرار الدائرية
cy.get('[data-cy="terms-checkbox"]').check();
cy.get('[data-cy="terms-checkbox"]').uncheck();
cy.get('[data-cy="gender-male"]').check();
// اختيار القوائم المنسدلة
cy.get('[data-cy="country-select"]').select('United States');
cy.get('[data-cy="country-select"]').select('US'); // حسب القيمة
// مسح المدخل
cy.get('[data-cy="search"]').clear();
// التركيز والضبابية
cy.get('[data-cy="input"]').focus();
cy.get('[data-cy="input"]').blur();
التأكيدات
يتضمن Cypress تأكيدات Chai مدمجة:
// التأكيدات الضمنية (إعادة المحاولة تلقائياً)
cy.get('[data-cy="title"]').should('be.visible');
cy.get('[data-cy="title"]').should('have.text', 'Welcome');
cy.get('[data-cy="input"]').should('have.value', 'test');
cy.get('[data-cy="btn"]').should('be.disabled');
cy.get('[data-cy="list"]').should('have.length', 5);
// تسلسل التأكيدات
cy.get('[data-cy="error"]')
.should('be.visible')
.and('contain', 'Invalid')
.and('have.class', 'error-message');
// التأكيدات السلبية
cy.get('[data-cy="modal"]').should('not.exist');
cy.get('[data-cy="spinner"]').should('not.be.visible');
// التأكيدات الصريحة (استخدم then() لمزيد من التحكم)
cy.get('[data-cy="count"]').then($el => {
const count = parseInt($el.text());
expect(count).to.be.greaterThan(0);
expect(count).to.be.lessThan(100);
});
العمل مع طلبات الشبكة
يسهل Cypress اختبار ومحاكاة طلبات الشبكة:
describe('API Tests', () => {
it('waits for API call', () => {
// اعتراض طلب HTTP
cy.intercept('GET', '/api/users').as('getUsers');
cy.visit('/users');
// انتظر اكتمال الطلب
cy.wait('@getUsers').then((interception) => {
expect(interception.response.statusCode).to.eq(200);
expect(interception.response.body).to.have.length.greaterThan(0);
});
});
it('stubs API response', () => {
// محاكاة مع استجابة مخصصة
cy.intercept('GET', '/api/users', {
statusCode: 200,
body: [
{ id: 1, name: 'John Doe' },
{ id: 2, name: 'Jane Smith' }
]
}).as('getUsers');
cy.visit('/users');
cy.wait('@getUsers');
cy.contains('John Doe');
cy.contains('Jane Smith');
});
it('simulates network error', () => {
cy.intercept('GET', '/api/users', {
statusCode: 500,
body: { error: 'Server error' }
});
cy.visit('/users');
cy.contains('Failed to load users');
});
});
استخدام التركيبات
قم بتخزين بيانات الاختبار في التركيبات لإعادة الاستخدام:
// cypress/fixtures/users.json
[
{
"id": 1,
"name": "John Doe",
"email": "john@example.com"
},
{
"id": 2,
"name": "Jane Smith",
"email": "jane@example.com"
}
]
تحميل واستخدام التركيبات في الاختبارات:
describe('Using Fixtures', () => {
it('loads user data from fixture', () => {
cy.fixture('users').then((users) => {
cy.intercept('GET', '/api/users', users);
});
cy.visit('/users');
cy.contains('John Doe');
});
// البديل: استخدم التركيبة في الاعتراض مباشرة
it('uses fixture directly', () => {
cy.intercept('GET', '/api/users', { fixture: 'users.json' });
cy.visit('/users');
cy.contains('Jane Smith');
});
});
الأوامر المخصصة
أنشئ أوامر قابلة لإعادة الاستخدام لتجنب التكرار:
// cypress/support/commands.js
Cypress.Commands.add('login', (email, password) => {
cy.visit('/login');
cy.get('[data-cy="email"]').type(email);
cy.get('[data-cy="password"]').type(password);
cy.get('[data-cy="submit"]').click();
// انتظر إعادة التوجيه
cy.url().should('include', '/dashboard');
});
Cypress.Commands.add('logout', () => {
cy.get('[data-cy="user-menu"]').click();
cy.get('[data-cy="logout"]').click();
cy.url().should('include', '/login');
});
// أضف تأكيداً مخصصاً
Cypress.Commands.add('shouldBeLoggedIn', () => {
cy.get('[data-cy="user-menu"]').should('exist');
cy.getCookie('auth_token').should('exist');
});
استخدم الأوامر المخصصة في الاختبارات:
describe('User Authentication', () => {
it('logs in successfully', () => {
cy.login('user@example.com', 'password123');
cy.shouldBeLoggedIn();
});
it('logs out successfully', () => {
cy.login('user@example.com', 'password123');
cy.logout();
cy.url().should('include', '/login');
});
});
العمل مع النماذج
اختبر التفاعلات المعقدة للنماذج:
describe('Contact Form', () => {
beforeEach(() => {
cy.visit('/contact');
});
it('submits form with valid data', () => {
cy.get('[data-cy="name"]').type('John Doe');
cy.get('[data-cy="email"]').type('john@example.com');
cy.get('[data-cy="message"]').type('Hello, this is a test message.');
cy.intercept('POST', '/api/contact').as('submitForm');
cy.get('[data-cy="submit"]').click();
cy.wait('@submitForm').its('response.statusCode').should('eq', 200);
cy.contains('Thank you for your message');
});
it('shows validation errors', () => {
cy.get('[data-cy="submit"]').click();
cy.get('[data-cy="name-error"]').should('contain', 'Name is required');
cy.get('[data-cy="email-error"]').should('contain', 'Email is required');
});
it('validates email format', () => {
cy.get('[data-cy="email"]').type('invalid-email');
cy.get('[data-cy="email"]').blur();
cy.get('[data-cy="email-error"]').should('contain', 'Invalid email');
});
});
التعامل مع السلوك غير المتزامن
ينتظر Cypress تلقائياً حتى تمر الأوامر والتأكيدات:
describe('Automatic Waiting', () => {
it('waits for element to appear', () => {
cy.visit('/loading-example');
// يعيد Cypress المحاولة لمدة تصل إلى 4 ثوانٍ (المهلة الافتراضية)
cy.get('[data-cy="content"]').should('be.visible');
});
it('uses custom timeout', () => {
cy.visit('/slow-loading');
// تجاوز المهلة الافتراضية
cy.get('[data-cy="content"]', { timeout: 10000 })
.should('be.visible');
});
it('waits for conditions', () => {
cy.visit('/counter');
cy.get('[data-cy="increment"]').click().click().click();
// يعيد Cypress المحاولة حتى يمر التأكيد
cy.get('[data-cy="count"]').should('have.text', '3');
});
});
ملاحظة: يتم وضع أوامر Cypress في قائمة انتظار وتنفيذها بشكل غير متزامن. لا تحتاج إلى async/await أو وعود - يتعامل Cypress مع التوقيت تلقائياً.
اختبار تدفقات المصادقة
describe('Authentication', () => {
it('completes full login flow', () => {
// ابدأ في الصفحة الرئيسية
cy.visit('/');
// انقر على رابط تسجيل الدخول
cy.get('[data-cy="login-link"]').click();
cy.url().should('include', '/login');
// املأ نموذج تسجيل الدخول
cy.get('[data-cy="email"]').type('user@example.com');
cy.get('[data-cy="password"]').type('password123');
// اعترض طلب تسجيل الدخول
cy.intercept('POST', '/api/auth/login').as('loginRequest');
cy.get('[data-cy="submit"]').click();
// تحقق من الطلب والاستجابة
cy.wait('@loginRequest').then((interception) => {
expect(interception.request.body).to.have.property('email');
expect(interception.response.statusCode).to.eq(200);
});
// تحقق من إعادة التوجيه وحالة المصادقة
cy.url().should('include', '/dashboard');
cy.getCookie('auth_token').should('exist');
cy.get('[data-cy="user-name"]').should('contain', 'Welcome');
});
});
اختبار منفذ العرض والجهاز
اختبر التصميمات المتجاوبة:
describe('Responsive Design', () => {
it('displays mobile menu on small screens', () => {
cy.viewport(375, 667); // iPhone SE
cy.visit('/');
cy.get('[data-cy="mobile-menu-toggle"]').should('be.visible');
cy.get('[data-cy="desktop-nav"]').should('not.be.visible');
});
it('displays desktop nav on large screens', () => {
cy.viewport(1920, 1080);
cy.visit('/');
cy.get('[data-cy="desktop-nav"]').should('be.visible');
cy.get('[data-cy="mobile-menu-toggle"]').should('not.be.visible');
});
// اختبار منافذ العرض المحددة مسبقاً
['iphone-x', 'ipad-2', 'macbook-15'].forEach((device) => {
it(`displays correctly on ${device}`, () => {
cy.viewport(device);
cy.visit('/');
cy.get('[data-cy="main-content"]').should('be.visible');
});
});
});
تمرين:
أنشئ مجموعة اختبارات E2E شاملة لتدفق الخروج في التجارة الإلكترونية:
- انتقل إلى صفحة قائمة المنتجات
- ابحث عن منتج محدد
- انقر على المنتج لعرض التفاصيل
- أضف المنتج إلى السلة
- اعرض السلة وتحقق من وجود المنتج
- حدّث الكمية وتحقق من حساب السعر
- تابع إلى الخروج
- املأ معلومات الشحن والدفع
- أرسل الطلب (محاكاة API الدفع)
- تحقق من صفحة تأكيد الطلب تعرض التفاصيل الصحيحة
استخدم التركيبات لبيانات المنتج، وأنشئ أوامر مخصصة للإجراءات القابلة لإعادة الاستخدام، واعترض جميع استدعاءات API.
أفضل الممارسات
- استخدم سمات data-cy: لا تعتمد على فئات CSS أو المعرفات التي قد تتغير
- أنشئ أوامر مخصصة: احصر الإجراءات الشائعة (تسجيل الدخول، تسجيل الخروج، إلخ)
- استخدم التركيبات: قم بتخزين بيانات الاختبار بشكل منفصل عن منطق الاختبار
- محاكاة طلبات الشبكة: اجعل الاختبارات أسرع وأكثر موثوقية
- اختبر رحلات المستخدم: ركز على سير العمل الكامل، وليس الميزات المعزولة
- احتفظ بالاختبارات مستقلة: يجب أن يعمل كل اختبار بشكل منفصل
- استخدم beforeEach بحكمة: قم بإعداد حالة مشتركة، لكن احتفظ بالاختبارات قابلة للقراءة
أخطاء شائعة:
- استخدام
.then() مثل الوعود - يتم وضع أوامر Cypress في قائمة انتظار بشكل مختلف
- عدم الانتظار لطلبات الشبكة قبل التأكيدات
- اختيار العناصر عن طريق النص الذي قد يتغير مع الترجمات
- إنشاء عدد كبير جداً من الاختبارات الصغيرة بدلاً من اختبار التدفقات الكاملة
- عدم تنظيف الحالة بين الاختبارات
في الدرس التالي، سنستكشف Playwright، أداة اختبار E2E أخرى قوية مع دعم ممتاز عبر المتصفحات.