أداة Cypress
Cypress أداة مفتوحة المصدر للاختبار الشامل (End-to-End) لتطبيقات الويب. تكتب الاختبارات بـ JavaScript أو TypeScript، ويشغّلها Cypress داخل متصفح حقيقي بجوار التطبيق قيد الاختبار. مثلاً، cy.get("[data-cy=cart-count]").should("have.text", "3") يجد شارة السلة ويواصل فحصها حتى تعرض 3 أو تنتهي المهلة.
لماذا يهم
تصميم الأداة يحدد أيّ الاختبارات سهل وأيّها مرهق. يعيد Cypress محاولة الاستعلامات والتأكيدات تلقائياً ويعرض كل أمر على لقطة من الصفحة، فيسهل تشخيص كثير من حالات الفشل. والتصميم نفسه يفرض حدوداً: علامة تبويب واحدة لكل اختبار، وJavaScript فقط، وأوامر لا تتصرف كشيفرة JavaScript عادية. ومن يتجاهلها يكتب انتظاراً ثابتاً واختبارات تفشل لسبب خاطئ.
بالنسبة إلى المنتج، تكشف حزمة Cypress تعمل مع كل طلب سحب (Pull Request) حالات الانحدار في المسارات المهمة قبل أن يصادفها المستخدمون. وبالنسبة إليك، فإن توسيع حزمة Cypress قائمة مهارة عملية في أي فريق تُبنى واجهته الأمامية بـ JavaScript.
كيف يعمل Cypress: داخل المتصفح، عبر طابور أوامر
يرسل Selenium (بلغات Java وPython وC# وJavaScript وRuby) أوامر W3C WebDriver عبر HTTP إلى مشغّل مثل chromedriver أو safaridriver، وتكتب أنت الانتظار الصريح وتنتقل بين النوافذ بمعرّفاتها (handles). ويقود Playwright (بلغات TypeScript/JavaScript وPython وJava و.NET) المتصفحات Chromium وFirefox وWebKit عبر اتصاله الخاص، بإجراءات تنتظر تلقائياً وتأكيدات تعيد المحاولة، ويستطيع استخدام عدة صفحات في اختبار واحد. وكلاهما يشغّل الاختبارات في عملية (process) منفصلة. أما Cypress فيشغّل ملف المواصفة (spec) داخل المتصفح، في حلقة تشغيل التطبيق نفسها، وخلفه عملية Node.js تشغّل المتصفح وتنفّذ مهام Node المعرَّفة في ملف الإعدادات، والتي يستدعيها الاختبار بـ cy.task(). ويتحكم Cypress أيضاً في الشبكة، وهذا ما يجعل cy.intercept() ممكناً.
Selenium WebDriver
شيفرة الاختبار خارج المتصفح
- 1عملية الاختبار لديكأوامر W3C WebDriver عبر HTTP
- 2مشغّل المتصفح
chromedriver, safaridriver - 3المتصفح
- لغات الاختبار:
- Java, Python, C#, JavaScript, Ruby
- الانتظار:
- انتظار صريح تكتبه أنت
- علامات التبويب في الاختبار الواحد:
- عدة نوافذ
- المتصفحات المذكورة في هذا الدرس:
- Chrome وSafari وغيرهما، كلٌّ عبر مشغّله
Playwright
شيفرة الاختبار خارج المتصفح
- 1عملية الاختبار لديكاتصال Playwright الخاص
- 2المتصفحChromium, Firefox, WebKit
- لغات الاختبار:
- TypeScript/JavaScript, Python, Java, .NET
- الانتظار:
- إجراءات تنتظر تلقائياً وتأكيدات تعيد المحاولة
- علامات التبويب في الاختبار الواحد:
- عدة صفحات وسياقات متصفح
- المتصفحات المذكورة في هذا الدرس:
- Chromium, Firefox, WebKit
Cypressهذا الدرس
شيفرة الاختبار داخل المتصفح
- 1عملية Node.js في Cypressتشغّل المتصفح وتنفّذ المهام التي يستدعيها الاختبار بـ
cy.task() - 2المتصفح: ملف المواصفة بجوار التطبيقحلقة التشغيل نفسها، والشبكة تحت تحكم Cypress
- لغات الاختبار:
- JavaScript, TypeScript
- الانتظار:
- الاستعلامات والتأكيدات يُعاد تنفيذها حتى
defaultCommandTimeout - علامات التبويب في الاختبار الواحد:
- علامة تبويب واحدة؛ وإضافة لعلامة ثانية
- المتصفحات المذكورة في هذا الدرس:
- Chrome, Edge, Firefox
توضع الأوامر في طابور ولا تُنفَّذ لحظة تنفيذ سطرها: تنضم cy.visit() وcy.get() و.click() إلى طابور يشغّله Cypress بعد انتهاء دالة الاختبار. وهي ليست وعوداً (Promises)، فلا يمكنك انتظارها بـ await ولا حفظ نتيجتها؛ أضف .then() أو .should() إلى السلسلة بدلاً من ذلك.
والفكرة المحورية هي قابلية إعادة المحاولة (Retry-ability): يُعاد تنفيذ الاستعلامات مثل cy.get() وcy.contains() مع التأكيدات المتصلة بها حتى تنجح التأكيدات أو تنتهي مهلة defaultCommandTimeout (4 ثوانٍ افتراضياً). أما الإجراءات مثل .click() فتُنفَّذ مرة واحدة بعد أن تنتظر حتى يصبح العنصر قابلاً للتفاعل: ظاهراً ومفعّلاً وغير مغطّى. أكّد الحالة التي تتوقعها بدلاً من إضافة cy.wait(5000).
cy.visit('/cart')
cy.get('[data-cy=add-item]').click() // action: waits until actionable, runs once
cy.get('[data-cy=cart-count]') // query: retried...
.should('have.text', '1') // ...until this passes or the timeout expires
// Wrong: commands are queued, they return no value you can store
// const count = cy.get('[data-cy=cart-count]').text()
// Right: work with the yielded value inside .then()
cy.get('[data-cy=cart-count]').invoke('text').then((text) => {
cy.log(`The badge shows ${text}`)
})التثبيت وبنية المشروع وملف الإعدادات
ثبّت Cypress في كل مشروع بالأمر npm install --save-dev cypress؛ وأول تشغيل لـ npx cypress open يفتح شاشة Launchpad، واختيار E2E Testing فيها ينشئ ملف الإعدادات ومجلد cypress/. ويستخدم ملف الإعدادات أدناه صيغة CommonJS في Node، أي require() وmodule.exports، وهي المقابل الأقدم لـimport وexport، لأن Node هو الذي يحمّله؛ أما ملفات المواصفات وملفات الدعم (support) وكائنات الصفحات فيمكنها استخدام import وexport، لأن Cypress يحزّمها (bundle) قبل تشغيلها في المتصفح.
في ملف الإعدادات، يوجّه baseUrl الأمرَ cy.visit("/orders") إلى بيئة معيّنة؛ وتحدّ المهلات مدة الاستعلامات وتحميل الصفحات والاستجابات؛ ويُضبط retries بقيمة مستقلة لكل من cypress run وcypress open؛ ويختار reporter صيغة المخرجات، مثل JUnit XML لخادم CI. ويُختار المتصفح عند التشغيل بـ --browser chrome أو edge أو firefox؛ ويحدد defaultBrowser المتصفح المستخدم دون هذا الخيار (وإلا استخدم cypress run متصفح Electron المضمَّن، وقد صار مُهمَلاً (deprecated) منذ Cypress 16)، ويمكن تصفية قائمة browsers داخل setupNodeEvents.
cypress/e2e/: ملفات المواصفات مثلorders.cy.js.cypress/fixtures/: بيانات الاستجابات البديلة مثلorders.json، والملفات المراد رفعها.cypress/support/: يعملe2e.jsقبل كل مواصفة ويستوردcommands.jsالذي يضم الأوامر المخصصة مثلcy.login()؛ وكثيراً ما توضع كائنات الصفحات بجواره.cypress/screenshots/وcypress/downloads/: مخرجات التشغيل، وتبقى خارج نظام التحكم في الإصدارات.
// cypress.config.js
const { defineConfig } = require('cypress')
module.exports = defineConfig({
e2e: {
baseUrl: 'http://localhost:3000', // cy.visit('/orders') opens this host
specPattern: 'cypress/e2e/**/*.cy.js',
},
defaultBrowser: 'chrome', // used when --browser is not given
defaultCommandTimeout: 6000, // queries and assertions (default 4000)
pageLoadTimeout: 60000, // page loads after cy.visit()
responseTimeout: 30000, // cy.request() and cy.wait() responses
retries: { runMode: 1, openMode: 0 }, // one rerun in cypress run only
reporter: 'junit',
reporterOptions: { mochaFile: 'results/junit-[hash].xml' },
screenshotOnRunFailure: true,
video: false,
})شغّل اختباراً واحداً، وصحّحه، وشغّل الحزمة في CI
أثناء الكتابة استخدم npx cypress open: اختر متصفحاً وملف مواصفة، فيُعاد التشغيل مع كل حفظ. والنقر على أمر في سجل الأوامر (Command Log) يعرض الصفحة كما كانت في تلك اللحظة، وهو «السفر عبر الزمن» (time travel). ومن سطر الأوامر وفي CI:
- ملف مواصفة واحد:
npx cypress run --spec cypress/e2e/orders.cy.js --browser chrome. - اختبار واحد:
it.only(أثناء العمل، واحذفه قبل الإيداع (commit). - الوضع المرئي (headed): يخفي
cypress runالمتصفح؛ أضف--headedلتشاهد التشغيل. - التصحيح: يوقف
cy.pause()الطابور في المشغّل التفاعلي، ويتوقف.debug()عند نقطة توقف (breakpoint) إذا كانت أدوات المطوّر (DevTools) مفتوحة، ويطبعDEBUG=cypress:*(صيغة Linux وmacOS) سجلات Cypress الداخلية حين لا يبدأ أو لا يشغّل المتصفح. - CI: تحتوي الصورة الرسمية
cypress/browsersعلى Node.js ومتصفحات (وتضيفcypress/includedإصداراً ثابتاً من Cypress). تشغّل المهمة أدناه كل المواصفات في Chrome، وتأخذ كلمة المرور من أسرار المستودع، وتحفظ نتائج JUnit ولقطات الفشل.
# .github/workflows/e2e.yml
name: e2e
on: [pull_request]
jobs:
cypress:
runs-on: ubuntu-latest
container:
image: cypress/browsers:latest # pin an exact tag in a real pipeline
steps:
- uses: actions/checkout@v7
- run: npm ci
- run: npx cypress run --browser chrome --config baseUrl=https://test.example.com
env:
CYPRESS_BUYER_PASSWORD: ${{ secrets.BUYER_PASSWORD }}
- uses: actions/upload-artifact@v7
if: always()
with:
name: cypress-results
path: |
results
cypress/screenshotsالجلسات واستجابات الشبكة البديلة ونوافذ الحوار وعلامات التبويب ورفع الملفات
عزل الاختبارات مفعّل افتراضياً: يبدأ كل اختبار بصفحة فارغة دون ملفات تعريف الارتباط (cookies) أو التخزين، لذا يجب أن يسجّل كل اختبار الدخول. يشغّل cy.session() الدخول مرة واحدة، ويحفظ ملفات تعريف الارتباط والتخزين الناتجة، ويستعيدها للاختبارات اللاحقة التي تستخدم المعرّف نفسه؛ وتتحقق validate من أن الجلسة المستعادة ما زالت صالحة. وزُر صفحة بعد ذلك، لأن الصفحة تكون فارغة.
يطابق cy.intercept() الطلبات بالطريقة (method) والعنوان. ومع استجابة ثابتة مثل { fixture: "orders.json" } أو { statusCode: 500 } يستبدل الخادم باستجابة بديلة (stub)، فتصل إلى حالات القائمة الفارغة والأخطاء متى شئت؛ ومن دونها يراقب فقط، وينتظر cy.wait("@alias") الطلب لتتحقق منه.
- نوافذ الحوار: يقبل Cypress
alert()وconfirm()تلقائياً. مرِّر إلىcy.on("window:alert", …)دالة بديلة (stub)، أي دالة تحلّ محل الأصلية وتسجّل استدعاءاتها، ثم أكّد على اسمها المستعار، فيفشل الاختبار أيضاً إذا لم يظهر التنبيه؛ والدالة البديلة لـwindow:confirmالتي تعيدfalseتختار «Cancel». - علامات التبويب الجديدة: يقود Cypress علامة تبويب واحدة. للرابط الذي يحمل
target="_blank"تحقّق منhrefوtarget، ثم زُر العنوان في علامة التبويب نفسها أو احذفtargetقبل النقر؛ ويشير التوثيق إلى الإضافة@cypress/puppeteerحين لا مفرّ من علامة تبويب ثانية. - رفع الملفات:
.selectFile("cypress/fixtures/receipt.pdf")على حقل الملف؛ وأضف{ action: "drag-drop" }لمنطقة الإفلات.
// cypress/support/commands.js: sign in once per user, reuse the session
Cypress.Commands.add('login', (email) => {
cy.session(email, () => {
cy.env(['BUYER_PASSWORD']).then(({ BUYER_PASSWORD }) => {
cy.request({
method: 'POST',
url: '/api/login',
body: { email, password: BUYER_PASSWORD },
log: false, // keep the password out of the log
})
})
}, {
validate() {
cy.request('/api/me').its('status').should('eq', 200)
},
})
})
// In a spec: watch a request, check an alert, upload a file
cy.login('buyer@example.com')
cy.intercept('POST', '/api/receipts').as('upload') // watch only, no stub
cy.on('window:alert', cy.stub().as('alert')) // record every alert
cy.visit('/account/receipts')
cy.get('[data-cy=receipt-file]').selectFile('cypress/fixtures/receipt.pdf')
cy.get('[data-cy=upload]').click()
cy.wait('@upload').its('response.statusCode').should('eq', 201)
cy.get('@alert').should('have.been.calledOnceWith', 'Receipt uploaded')جرّبها: مولّد استجابات API وهمية جرّبها: مولّد استجابات خطأ وهمية
مثال تطبيقي: سجل الطلبات في أربعة اختبارات مع كائن صفحة
كثيراً ما يسجّل أول اختبار Cypress الدخول ويفحص القائمة ويلغي طلباً ويفحص رسالة خطأ، كل ذلك في it واحدة. وعندما تفشل خطوة مبكرة لا تُنفَّذ الفحوص اللاحقة. قسّمه حسب السلوك.
المتطلب: «تعرض صفحة سجل الطلبات طلبات المشتري. وإذا لم توجد طلبات تعرض You have no orders yet؛ وإذا فشل التحميل تعرض Could not load your orders. ويطلب الإلغاء تأكيداً، واختيار Cancel يُبقي الطلب».
كائن الصفحة يضم المحددات ولا يحتوي على تأكيدات. ويضم ملف البيانات orders.json ثلاثة طلبات، حالة الطلب A-1002 منها Processing. وكل اختبار يستبدل بياناته بنفسه، ويعيد استخدام الجلسة المحفوظة، ويفحص سلوكاً واحداً، فيسمّي عنوانُ الاختبار الفاشل ما انكسر.
// cypress/support/pages/ordersPage.js: knows the screen, makes no assertions
export const ordersPage = {
visit: () => cy.visit('/account/orders'),
rows: () => cy.get('[data-cy=order-row]'),
row: (id) => cy.get(`[data-cy=order-row][data-order-id="${id}"]`),
message: () => cy.get('[data-cy=orders-message]'),
cancel: (id) => ordersPage.row(id).find('[data-cy=cancel-order]').click(),
}
// cypress/e2e/orders.cy.js: one behaviour per test
import { ordersPage } from '../support/pages/ordersPage'
describe('Order history', () => {
beforeEach(() => {
cy.login('buyer@example.com')
})
it('lists the orders returned by the API', () => {
cy.intercept('GET', '/api/orders', { fixture: 'orders.json' }).as('orders')
ordersPage.visit()
cy.wait('@orders')
ordersPage.rows().should('have.length', 3)
})
it('shows the empty state when there are no orders', () => {
cy.intercept('GET', '/api/orders', { body: [] })
ordersPage.visit()
ordersPage.message().should('have.text', 'You have no orders yet')
})
it('shows an error when the orders cannot be loaded', () => {
cy.intercept('GET', '/api/orders', { statusCode: 500, body: {} })
ordersPage.visit()
ordersPage.message().should('have.text', 'Could not load your orders')
})
it('keeps the order when the buyer cancels the confirmation', () => {
cy.intercept('GET', '/api/orders', { fixture: 'orders.json' })
cy.on('window:confirm', cy.stub().as('confirm').returns(false)) // choose Cancel
ordersPage.visit()
ordersPage.cancel('A-1002')
cy.get('@confirm').should('have.been.calledOnceWith', 'Cancel order A-1002?')
ordersPage.row('A-1002').should('contain', 'Processing')
})
})أخطاء شائعة
- إضافة
cy.wait(5000)بدلاً من تأكيد الحالة المتوقعة أو انتظار طلب له اسم مستعار (alias). - معاملة الأوامر كشيفرة JavaScript عادية: حفظ
cy.get()في متغير، أو استخدامawait، أو التفرّع بـifعلى حالة لم يصل إليها الطابور بعد. - تسجيل الدخول عبر الواجهة في كل اختبار بدلاً من استخدام
cy.session(). - استبدال كل الطلبات، فتبقى الحزمة خضراء بينما الواجهة الأمامية والواجهة البرمجية غير متوافقتين.
- استخدام
retriesلإخفاء الاختبارات المتذبذبة (Flaky)؛ فإعادة المحاولة احتواء لا إصلاح. - إيداع
it.onlyالذي يتخطى بقية المواصفة بصمت، أو كلمات مرور حقيقية بدلاً من أسرار CI.
من منظور ضمان الجودة
يتعامل المختبِر مع Cypress حين يختار المسارات التي تستحق اختباراً شاملاً (قليل منها وذو قيمة، والباقي في مستويات أدنى من الهرم)، وحين يكتب المواصفات ويراجعها مع المطورين، وحين يقرأ حالات الفشل في CI. واتفق مبكراً على سمات data-cy كما يوصي دليل أفضل الممارسات في Cypress.
عندما يفشل اختبار في CI، ابدأ من رسالة الخطأ ولقطة الفشل، وقرّر هل هو عيب في المنتج أم في الاختبار أم مشكلة في البيئة قبل إعادة التشغيل. ويذكر تقرير الخطأ ملف المواصفة والاختبار والمتصفح وإصدار Cypress والبناء (build)، وهل كانت البيانات بديلة.
تمرين عملي
خطّط مواصفة Cypress لحقل القسيمة
إجمالي السلة 200.00 ريال قبل أي قسيمة. المتطلب: «الرمز الصالح يطبّق خصماً بنسبة 10% ويعرض Coupon applied. والرمز المنتهي يعرض This coupon has expired. وإذا تعطلت خدمة القسائم تعرض الصفحة Coupons are unavailable, try again later ولا يتغير الإجمالي». وترسل الصفحة الطلب الموضح أدناه.
صمّم كائن صفحة للسلة واختباراً واحداً لكل سلوك، يستبدل كل منها استجابته بـ cy.intercept(). تكفي الورقة.
POST /api/coupons/apply
Content-Type: application/json
{"code": "SAVE10"}
Possible responses
200 OK {"code": "SAVE10", "discountPercent": 10, "total": 180.00}
422 Unprocessable Content {"error": "COUPON_EXPIRED"}
503 Service Unavailable {"error": "SERVICE_UNAVAILABLE"}- اكتب السلوكيات والاستجابة التي يحتاجها كل منها.
- اكتب كائن الصفحة: محددات وإجراءات، بلا تأكيدات.
- اكتب
itلكل سلوك مع الاستجابة البديلة والتأكيدات، بما فيها الإجمالي. - دوّن ما لا تثبته الاستجابات البديلة وكيف ستغطيه.
إظهار الإجابة النموذجية
السلوكيات: رمز صالح (200، total: 180.00)، ورمز منتهٍ (422)، وتعطل الخدمة (503). والرمز الفارغ لا يحتاج إلى استبدال: يبقى زر Apply معطلاً أو تظهر رسالة تحقق، بحسب اتفاق الفريق.
كائن الصفحة: visit() وapply(code) (يكتب في [data-cy=coupon-code] وينقر [data-cy=apply-coupon]) وmessage() وtotal()؛ ولا يحتوي على should().
الاختبارات: (1) استبدل استجابة 200 باسم @apply، وطبّق SAVE10، وتحقق أن cy.wait("@apply").its("request.body") يساوي { code: "SAVE10" }، وأن الرسالة Coupon applied والإجمالي 180.00. (2) استبدل استجابة 422، وتوقّع This coupon has expired و200.00. (3) استبدل استجابة 503، وتوقّع Coupons are unavailable, try again later و200.00.
لا تثبت الاستجابات البديلة أن الخدمة الحقيقية تقبل SAVE10 وتحسب 10% بشكل صحيح؛ فأضف اختبار API واختباراً شاملاً واحداً ببيانات قسائم مُهيّأة مسبقاً. واسأل مالك المنتج: هل يُطبَّق الخصم قبل ضريبة القيمة المضافة والتوصيل، وهل يمكن الجمع بين القسائم؟
أسئلة شائعة
هل أتعلّم Cypress أم Playwright؟
تعلّم الأداة التي يستخدمها فريقك؛ فالمحددات والانتظار والتأكيدات وكائنات الصفحات تنتقل بينهما. يناسب Cypress الفرق التي تكتب JavaScript أو TypeScript وتفضّل مشغّله التفاعلي. ويدعم Playwright لغات أكثر ويستطيع قيادة عدة صفحات وسياقات متصفح في اختبار واحد، وهذا مهم حين تمتد مسارات المنتج عبر علامات تبويب أو نوافذ.
لماذا لا أستطيع استخدام async/await مع أوامر Cypress؟
لأن الأوامر توضع في طابور ولا تُنفَّذ عند تشغيل سطرها، وليست وعوداً (Promises). يشغّل Cypress الطابور بعد انتهاء دالة الاختبار، ويعيد محاولة الاستعلامات أثناء ذلك. استخدم .then() للتعامل مع القيمة الناتجة، والأسماء المستعارة لإعادة استخدامها، و.should() لتأكيدها مع إعادة المحاولة.
هل يختبر Cypress الواجهات البرمجية (API)؟
يرسل cy.request() طلبات HTTP من داخل الاختبار. وهو مفيد للتجهيز، مثل إنشاء البيانات أو تسجيل الدخول، ولبضعة فحوص API بجوار اختبارات الواجهة. لكن Cypress يحتاج إلى متصفح ليعمل، لذا تكون حزمة API الكبيرة عادةً أبسط في أداة أو مكتبة مخصصة لاختبار API.
هل أفعّل إعادة المحاولة (retries)؟
محاولة أو محاولتان في cypress run قد تُبقيان خط CI قابلاً للاستخدام ريثما يُصلَح اختبار متذبذب، لكن كل نجاح بعد إعادة محاولة يجب أن يظهر في التقرير ويُعامل كعيب في الاختبار أو المنتج. وأبقِ قيمة openMode عند 0 لترى التذبذب أثناء كتابة الاختبارات.
المراجع
- Cypress Documentationالتوثيق الرسمي لاختبار الويب الشامل واختبار المكونات في المتصفح باستخدام Cypress.
- Best Practices — Cypressإرشادات Cypress لاختيار العناصر بسمات مخصّصة، وتجنّب الانتظار الثابت، وإبقاء الاختبارات مستقلة.
- GitHub Actions documentationكيف تُعرَّف مسارات العمل والمهام والمشغّلات والمخرجات في GitHub Actions — حيث تعمل الاختبارات الآلية عند كل دفع أو طلب دمج.
تدرّب بأدوات متقن
ولّد استجابات API وهمية كاملة (الحالة والترويسات وجسم JSON) لحالات النجاح والقوائم والتفاصيل والأخطاء والنتائج الفارغة.
ولّد استجابات خطأ منظّمة (التحقق والمصادقة وعدم العثور وتحديد المعدّل وخطأ الخادم) بالصيغ الشائعة.
التحقق من صحة صياغة JSON مع تحديد مواقع الأخطاء بدقة واقتراحات مفيدة للإصلاح.
تنسيق YAML والتحقق من صحته بمسافات بادئة وعلامات اقتباس متسقة.
تجميل كود JavaScript وTypeScript بمسافات بادئة وتباعد متسقين.
للتعمّق أكثر
تقدّمك في هذا الموضوع
سجّل الدخول لتتابع تقدّمك
علّم الموضوعات مكتملة، وتابع من حيث توقفت، وافتح امتحانات التخصص.