EduFlow
اختبار تطبيقات JavaScript
⁦0%⁩

الوحدة 4 · اختبارات end-to-end مع Playwright

الـ Locators والـ Assertions الموجّهة للويب والانتظار التلقائي

20 دقراءة 4 داختر بحكمة

الدرس في لمحة

تُعدّ Playwright لتطبيق Vite، وتجد العناصر بـ locators موجّهة للمستخدم، وتكتب assertions موجّهة للويب تنتظر الصفحة بدل أن تتسابق معها.

  1. 1الإعداد
  2. 2أول اختبار end-to-end
  3. 3الـ locators كسولة وصارمة
  4. 4الانتظار التلقائي: الأفعال
  5. 5الـ assertions الموجّهة للويب: assertions تنتظر
  6. 6ما الذي ينتمي إلى اختبار end-to-end

لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.

كل ما سبق عمل في Node، مع jsdom الذي يتظاهر بأنه متصفّح. هذا يغطّي معظم المنطق ومعظم سلوك الواجهة، لكن ليس كله: التخطيط الحقيقي، والتوجيه الحقيقي، والخادم الحقيقي، وخطأ CSS حقيقي يُخفي زر “Settle up” على Safari. وللمسارات القليلة التي يهمّ فيها ذلك، تقود متصفّحًا حقيقيًا. وفي 2026 الأداة الافتراضية لذلك في JavaScript هي Playwright.

الإعداد

Terminal
npm init playwright@latest

يضيف المعالج @playwright/test، وملف playwright.config.ts، واختبارًا نموذجيًا، و(اختياريًا) workflow لـ GitHub Actions، وينزّل المتصفّحات. وجّهه إلى تطبيق Vite لديك ودعه يشغّل خادم التطوير نيابةً عنك:

playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './e2e',
  fullyParallel: true,
  retries: process.env.CI ? 2 : 0,
  use: {
    baseURL: 'http://localhost:5173',
    trace: 'on-first-retry',
  },
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'webkit', use: { ...devices['Desktop Safari'] } },
  ],
  webServer: {
    command: 'npm run dev',
    url: 'http://localhost:5173',
    reuseExistingServer: !process.env.CI,
  },
});

يشغّل webServer الأمر npm run dev، وينتظر حتى يستجيب الـ URL، ثم يشغّل الاختبارات، ثم يوقفه. ويتيح baseURL للاختبارات استدعاء page.goto('/groups/lisbon'). شغّل المجموعة بـ npx playwright test.

أول اختبار end-to-end

e2e/settle-up.spec.ts
import { test, expect } from '@playwright/test';

test('adding an expense updates the balances', async ({ page }) => {
  await page.goto('/groups/lisbon');

  await page.getByLabel('Description').fill('Dinner');
  await page.getByLabel('Amount').fill('30.00');
  await page.getByRole('button', { name: 'Add expense' }).click();

  const balances = page.getByRole('list', { name: 'Balances' });
  await expect(balances.getByRole('listitem')).toHaveText([
    'Ana is owed $15.00',
    'Ben owes $15.00',
  ]);
});

page تبويب متصفّح جديد، يُعطى للاختبار على شكل fixture (تجهيزة اختبار؛ المزيد عنها في الدرس القادم). يُفترض أن تبدو الـ locators (محدِّدات العناصر) مألوفة: getByRole وgetByLabel وgetByText وgetByPlaceholder وgetByAltText وgetByTitle وgetByTestId في Playwright تتبع فكرة الأولوية نفسها في Testing Library، وللسبب نفسه.

الـ locators كسولة وصارمة

الـ locator وصف لطريقة إيجاد عنصر، لا العنصر نفسه. لا يجد page.getByRole('button', { name: 'Add expense' }) شيئًا حتى تتعامل معه، ويجده من جديد في كل مرة، فلا تبقى ممسكًا بمرجع قديم حين تستبدل إعادة الـ render الزر.

والـ locators أيضًا صارمة: إن طابق locator فعلٍ ما عنصرين، ترمي Playwright خطأً بدل أن تنقر الأول. ضيّقه بالتسلسل وبـ filter:

TypeScript
const taxiRow = page.getByRole('listitem').filter({ hasText: 'Taxi' });
await taxiRow.getByRole('button', { name: 'Remove' }).click();

هذا يُقرأ كالتعليمات التي ستعطيها لإنسان (“احذف صف التاكسي”)، ويصمد أمام إعادة الترتيب. توجد nth(2) وfirst()، لكنهما تنكسران لحظة تغيّر الترتيب؛ فلا تستخدمهما إلا حين يكون الترتيب هو المقصود.

الانتظار التلقائي: الأفعال

قبل click()، تنتظر Playwright حتى يصبح العنصر ظاهرًا، ومستقرًا (لا في منتصف حركة)، ومفعّلًا، ويستقبل الأحداث فعلًا (غير مغطّى بمؤشّر تحميل أو بشريط ملفات تعريف الارتباط). وتنتظر fill() أن يكون ظاهرًا ومفعّلًا وقابلًا للتحرير. ثم تنفّذ. وإن لم تتحقّق الشروط أبدًا، يفشل الفعل بعد انتهاء المهلة برسالة تذكر أيّ فحص فشل.

لهذا نادرًا ما تحتاج اختبارات end-to-end في Playwright إلى انتظارات صريحة. فالزر الذي يظهر بعد تحميل المجموعة يُنقر حين يظهر.

في الأعلى: ينتظر click أن يكون العنصر ظاهرًا ومستقرًا ويستقبل الأحداث ومفعّلًا، ثم ينقر. في الأسفل: يعيد toHaveText قراءة العنصر مرارًا حتى يتطابق النص أو تمرّ 5 ثوانٍ. click() ظاهر مستقر يستقبل الأحداث مفعّل نقر toHaveText() إعادة قراءة، لا تطابق تطابق يستسلم عند 5 s
تنتظر الأفعال حتى يجهز العنصر؛ وتعيد الـ assertions المحاولة حتى تنجح

الـ assertions الموجّهة للويب: assertions تنتظر

تنطبق الفكرة نفسها على فحص النتائج. expect في Playwright حين يُستخدم على locator يكون غير متزامن ويعيد المحاولة:

TypeScript
await expect(page.getByRole('status')).toHaveText('Everyone is settled up');
await expect(page.getByRole('listitem')).toHaveCount(0);
await expect(page).toHaveURL(/\/groups\/lisbon\/settled/);
await expect(page.getByRole('button', { name: 'Settle up' })).toBeDisabled();

كل واحد منها يعيد فحص الصفحة حتى يتطابق، لمدة تصل إلى 5 ثوانٍ افتراضيًا. وtoHaveText مع مصفوفة (كما في الاختبار الأول) يفحص نص كل عنصر مطابق، بالترتيب، وهذه طريقة موجزة لتأكيد قائمة كاملة.

ما الذي ينتمي إلى اختبار end-to-end

لأن كل اختبار هنا يبدأ سياق متصفّح، ويحمّل التطبيق، ويتحدّث إلى خادم حقيقي، فإن اختبار الـ end-to-end يكلّف ثوانيَ حيث يكلّف الـ unit test أجزاءً من الثانية. أنفق هذه الميزانية على المسارات التي يكون فيها الاتصال بين القطع هو الخطر: تسجيل الدخول، وإضافة مصروف ورؤية الأرصدة تتغيّر، وتسوية الحسابات، وكل ما يتعلّق بالمال أو بفقدان البيانات. لا تستخدمها لفحص كل رسالة تحقّق في حقل المبلغ؛ فاختبارات المكوّنات من القسم 3 تفعل ذلك أسرع بمئة مرة. قد تحتوي مجموعة Splitwise-lite السليمة على خمسة إلى خمسة عشر اختبار end-to-end، وبضع مئات من اختبارات الوحدات والمكوّنات. وإن فشل اختبار end-to-end، فاسأل أولًا إن كان على اختبار في مستوى أدنى أن يلتقطه.

يطلب منك التمرين اختيار الـ locator الذي سيصمد أمام إعادة التصميم القادمة. الدرس القادم: إبقاء الاختبارات مستقلّة بعضها عن بعض.

أهم النقاط

  • تقود Playwright متصفّحات Chromium وFirefox وWebKit حقيقية، وخيارها webServer يشغّل خادم التطوير لديك قبل أن تعمل الاختبارات.
  • فضّل الـ locators الموجّهة للمستخدم (getByRole وgetByLabel وgetByText)، وضيّقها بـ filter أو بالتسلسل بدل مسارات CSS.
  • تنتظر الأفعال مثل click وfill تلقائيًا حتى يصبح العنصر ظاهرًا، ومستقرًا، ومفعّلًا، وقادرًا على استقبال الأحداث.
  • الـ assertions الموجّهة للويب مثل await expect(locator).toHaveText(...) تعيد المحاولة حتى تنجح أو تنتهي المهلة (5 ثوانٍ افتراضيًا)؛ أما قراءة قيمة ثم تأكيدها مرة واحدة فلا تفعل ذلك.

قراءات إضافية

تحقّق من فهمك

3 أسئلة

السؤال 1 من 3

أيّ assertion يظلّ يعمل حين يتحدّث الرصيد بعد 300 ms من النقر؟

السؤال 2 من 3

يفشل await page.getByRole('button', { name: 'Remove' }).click() بخطأ strict mode violation. ماذا يعني ذلك؟

السؤال 3 من 3

ما الذي تفحصه Playwright قبل أن تنفّذ click() على locator؟

تدريب

اختر بحكمة

اختر الـ locator الذي يصمد أمام إعادة التصميم

تُعرض قائمة المصروفات في Splitwise-lite هكذا:

HTML
<ul aria-label="Expenses" class="mt-4 space-y-2">
  <li class="row">Dinner: $45.50 <button class="btn btn-ghost">Remove</button></li>
  <li class="row">Taxi: $18.00 <button class="btn btn-ghost" data-testid="remove-e2">Remove</button></li>
  <li class="row">Museum: $24.00 <button class="btn btn-ghost">Remove</button></li>
</ul>

في الـ sprint القادم، ستحوّل مصمّمة القائمة إلى شبكة بطاقات، وتغيّر كل الأصناف، وترتّب المصروفات حسب التاريخ بدل ترتيب الإضافة. أيّ locator لـ زر Remove الخاص بمصروف Taxi هو الأرجح أن يظلّ يعمل، وألا يفشل إلا إن عجز المستخدمون فعلًا عن حذف التاكسي؟

اختر الـ locator الذي يصمد أمام إعادة التصميم