الوحدة 4 · اختبارات end-to-end مع Playwright
الـ Locators والـ Assertions الموجّهة للويب والانتظار التلقائي
20 دقراءة 4 داختر بحكمة
الدرس في لمحة
تُعدّ Playwright لتطبيق Vite، وتجد العناصر بـ locators موجّهة للمستخدم، وتكتب assertions موجّهة للويب تنتظر الصفحة بدل أن تتسابق معها.
- 1الإعداد
- 2أول اختبار end-to-end
- 3الـ locators كسولة وصارمة
- 4الانتظار التلقائي: الأفعال
- 5الـ assertions الموجّهة للويب: assertions تنتظر
- 6ما الذي ينتمي إلى اختبار end-to-end
لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.
كل ما سبق عمل في Node، مع jsdom الذي يتظاهر بأنه متصفّح. هذا يغطّي معظم المنطق ومعظم سلوك الواجهة، لكن ليس كله: التخطيط الحقيقي، والتوجيه الحقيقي، والخادم الحقيقي، وخطأ CSS حقيقي يُخفي زر “Settle up” على Safari. وللمسارات القليلة التي يهمّ فيها ذلك، تقود متصفّحًا حقيقيًا. وفي 2026 الأداة الافتراضية لذلك في JavaScript هي Playwright.
الإعداد
npm init playwright@latestيضيف المعالج @playwright/test، وملف playwright.config.ts، واختبارًا نموذجيًا، و(اختياريًا) workflow لـ GitHub Actions، وينزّل المتصفّحات. وجّهه إلى تطبيق Vite لديك ودعه يشغّل خادم التطوير نيابةً عنك:
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
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:
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 إلى انتظارات صريحة. فالزر الذي يظهر بعد تحميل المجموعة يُنقر حين يظهر.
الـ assertions الموجّهة للويب: assertions تنتظر
تنطبق الفكرة نفسها على فحص النتائج. expect في Playwright حين يُستخدم على locator يكون غير متزامن ويعيد المحاولة:
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 ثوانٍ افتراضيًا)؛ أما قراءة قيمة ثم تأكيدها مرة واحدة فلا تفعل ذلك.