مقدمة إلى Playwright
Playwright هو إطار عمل اختبار شامل حديث تم تطويره بواسطة Microsoft يمكّن من الاختبار الموثوق عبر جميع محركات المتصفح الرئيسية: Chromium و Firefox و WebKit. يوفر واجهة برمجة تطبيقات موحدة لأتمتة المتصفح مع تجربة مطور ممتازة وميزات قوية.
لماذا Playwright؟
- الاختبار عبر المتصفحات: اختبر على Chromium و Firefox و WebKit بواجهة برمجة تطبيقات واحدة
- محاكاة الهاتف المحمول: اختبر التصميمات المتجاوبة مع محاكاة الجهاز
- الانتظار التلقائي: انتظار ذكي للعناصر لتكون جاهزة
- التنفيذ المتوازي: تشغيل الاختبارات بالتوازي عبر متصفحات متعددة
- اعتراض الشبكة: محاكاة وتعديل طلبات الشبكة
- التتبع والتصحيح: تسجيل تنفيذ الاختبار مع لقطات الشاشة ومقاطع الفيديو
- دعم TypeScript: دعم TypeScript من الدرجة الأولى خارج الصندوق
ملاحظة: بينما يركز Cypress على تجربة المطور، يتفوق Playwright في الاختبار عبر المتصفحات وسيناريوهات الأتمتة المتقدمة.
تثبيت Playwright
تثبيت Playwright وتهيئة التكوين:
# تثبيت Playwright
npm init playwright@latest
# أو التثبيت يدوياً
npm install --save-dev @playwright/test
# تثبيت المتصفحات
npx playwright install
ينشئ التثبيت هذا الهيكل:
tests/ # ملفات الاختبار
playwright.config.ts # التكوين
.github/workflows/ # تكوين CI/CD
التكوين
قم بتكوين Playwright في playwright.config.ts:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
// دليل الاختبار
testDir: './tests',
// تشغيل الاختبارات بالتوازي
fullyParallel: true,
// فشل البناء على CI إذا تركت test.only عن طريق الخطأ
forbidOnly: !!process.env.CI,
// إعادة محاولة الاختبارات الفاشلة
retries: process.env.CI ? 2 : 0,
// عدد العمال (التنفيذ المتوازي)
workers: process.env.CI ? 1 : undefined,
// المراسل
reporter: 'html',
// الإعدادات المشتركة لجميع الاختبارات
use: {
// عنوان URL الأساسي
baseURL: 'http://localhost:3000',
// جمع الأثر عند إعادة المحاولة الأولى
trace: 'on-first-retry',
// لقطة الشاشة عند الفشل
screenshot: 'only-on-failure',
},
// تكوين المشاريع للمتصفحات الرئيسية
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
// منافذ عرض الهاتف المحمول
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] },
},
{
name: 'Mobile Safari',
use: { ...devices['iPhone 12'] },
},
],
// تشغيل خادم التطوير المحلي قبل الاختبارات
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
});
كتابة أول اختبار لك
أنشئ ملف اختبار في دليل tests/:
// tests/example.spec.ts
import { test, expect } from '@playwright/test';
test('has title', async ({ page }) => {
await page.goto('https://playwright.dev/');
// توقع أن يحتوي العنوان على سلسلة فرعية
await expect(page).toHaveTitle(/Playwright/);
});
test('get started link', async ({ page }) => {
await page.goto('https://playwright.dev/');
// انقر على رابط البدء
await page.getByRole('link', { name: 'Get started' }).click();
// يتوقع أن تحتوي الصفحة على عنوان باسم التثبيت
await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible();
});
تشغيل الاختبارات
# تشغيل جميع الاختبارات
npx playwright test
# تشغيل الاختبارات في وضع برأس (رؤية المتصفح)
npx playwright test --headed
# تشغيل الاختبارات في متصفح محدد
npx playwright test --project=firefox
# تشغيل ملف اختبار محدد
npx playwright test tests/example.spec.ts
# تشغيل الاختبارات في وضع التصحيح
npx playwright test --debug
# عرض تقرير الاختبار
npx playwright show-report
المحددات: العثور على العناصر
يوفر Playwright استراتيجيات محدد قوية:
// حسب الدور (مفضل - صديق لإمكانية الوصول)
await page.getByRole('button', { name: 'Submit' });
await page.getByRole('heading', { level: 1 });
await page.getByRole('textbox', { name: 'Email' });
await page.getByRole('checkbox', { name: 'I agree' });
// حسب نص التسمية (لمدخلات النماذج)
await page.getByLabel('Email address');
// حسب العنصر النائب
await page.getByPlaceholder('Enter your email');
// حسب محتوى النص
await page.getByText('Welcome back');
await page.getByText(/sign in/i); // تعبير عادي، غير حساس لحالة الأحرف
// حسب معرف الاختبار
await page.getByTestId('submit-button');
// حسب محدد CSS
await page.locator('.btn-primary');
await page.locator('#username');
// دمج المحددات
await page.locator('form').getByRole('button', { name: 'Submit' });
// الترشيح
await page.getByRole('listitem').filter({ hasText: 'Product 1' });
// العنصر الترتيبي
await page.getByRole('listitem').nth(2);
نصيحة: محددات Playwright صارمة افتراضياً. إذا تطابقت عناصر متعددة، فسيفشل الاختبار. استخدم first() أو last() أو nth() عند التحديد عمداً من تطابقات متعددة.
التفاعلات
تنفيذ إجراءات المستخدم:
// النقر
await page.getByRole('button', { name: 'Submit' }).click();
// النقر المزدوج
await page.getByText('Edit').dblclick();
// النقر بزر الماوس الأيمن
await page.getByText('File').click({ button: 'right' });
// الكتابة
await page.getByLabel('Email').fill('user@example.com');
await page.getByLabel('Password').type('password123'); // يكتب حرفاً بحرف
// الضغط على مفاتيح لوحة المفاتيح
await page.getByLabel('Search').press('Enter');
await page.keyboard.press('Control+A');
// الاختيار من القائمة المنسدلة
await page.getByLabel('Country').selectOption('United States');
await page.getByLabel('Country').selectOption({ value: 'us' });
// التحديد/إلغاء التحديد
await page.getByLabel('I agree').check();
await page.getByLabel('Subscribe').uncheck();
// رفع الملف
await page.getByLabel('Upload').setInputFiles('path/to/file.pdf');
await page.getByLabel('Photos').setInputFiles(['1.jpg', '2.jpg']); // ملفات متعددة
// التحويم
await page.getByText('Tooltip').hover();
// السحب والإفلات
await page.getByText('Item 1').dragTo(page.getByText('Drop Zone'));
التأكيدات
يوفر Playwright تأكيدات إعادة المحاولة التلقائية:
// الرؤية
await expect(page.getByText('Welcome')).toBeVisible();
await expect(page.getByText('Loading')).toBeHidden();
// محتوى النص
await expect(page.getByRole('heading')).toHaveText('Dashboard');
await expect(page.getByRole('alert')).toContainText('Error occurred');
// القيمة
await expect(page.getByLabel('Email')).toHaveValue('user@example.com');
// السمات
await expect(page.getByRole('link')).toHaveAttribute('href', '/about');
await expect(page.getByRole('button')).toHaveClass(/btn-primary/);
// العد
await expect(page.getByRole('listitem')).toHaveCount(5);
// URL
await expect(page).toHaveURL(/.*dashboard/);
await expect(page).toHaveTitle('Dashboard');
// ممكّن/معطّل
await expect(page.getByRole('button')).toBeEnabled();
await expect(page.getByRole('button')).toBeDisabled();
// محدد
await expect(page.getByLabel('Terms')).toBeChecked();
await expect(page.getByLabel('Newsletter')).not.toBeChecked();
// مقارنة لقطة الشاشة
await expect(page).toHaveScreenshot('homepage.png');
الانتظار التلقائي
ينتظر Playwright تلقائياً حتى تصبح العناصر قابلة للتنفيذ:
test('auto-waiting example', async ({ page }) => {
await page.goto('/');
// ينتظر Playwright أن يكون الزر:
// - مرفقاً بـ DOM
// - مرئياً
// - مستقراً (لا يتحرك)
// - ممكّناً
// - غير مغطى بعناصر أخرى
await page.getByRole('button', { name: 'Submit' }).click();
// انتظر الشروط المحددة
await page.getByText('Loading').waitFor({ state: 'hidden' });
await page.getByText('Success').waitFor({ state: 'visible' });
// انتظر تغيير URL
await page.waitForURL('**/dashboard');
// انتظر طلب الشبكة
await page.waitForResponse(response =>
response.url().includes('/api/users') && response.status() === 200
);
// مهلة مخصصة
await page.getByText('Slow Element').click({ timeout: 30000 });
});
ملاحظة: الانتظار التلقائي لـ Playwright يزيل معظم الاختبارات المتقلبة. نادراً ما تكون الانتظارات الصريحة مطلوبة.
اعتراض الشبكة
محاكاة وتعديل طلبات الشبكة:
test('mock API response', async ({ page }) => {
// محاكاة استجابة API
await page.route('**/api/users', route => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([
{ id: 1, name: 'John Doe' },
{ id: 2, name: 'Jane Smith' }
])
});
});
await page.goto('/users');
await expect(page.getByText('John Doe')).toBeVisible();
await expect(page.getByText('Jane Smith')).toBeVisible();
});
test('modify API response', async ({ page }) => {
await page.route('**/api/user', async route => {
// جلب الاستجابة الأصلية
const response = await route.fetch();
const json = await response.json();
// تعديل الاستجابة
json.name = 'Modified Name';
// الإيفاء بالاستجابة المعدلة
await route.fulfill({ response, json });
});
await page.goto('/profile');
await expect(page.getByText('Modified Name')).toBeVisible();
});
test('abort requests', async ({ page }) => {
// حظر طلبات التحليلات
await page.route('**/analytics/**', route => route.abort());
await page.goto('/');
});
نموذج كائن الصفحة
تنظيم الاختبارات مع نمط كائن الصفحة:
// pages/LoginPage.ts
import { Page, Locator } from '@playwright/test';
export class LoginPage {
readonly page: Page;
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly submitButton: Locator;
readonly errorMessage: Locator;
constructor(page: Page) {
this.page = page;
this.emailInput = page.getByLabel('Email');
this.passwordInput = page.getByLabel('Password');
this.submitButton = page.getByRole('button', { name: 'Login' });
this.errorMessage = page.getByRole('alert');
}
async goto() {
await this.page.goto('/login');
}
async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
async expectErrorMessage(message: string) {
await expect(this.errorMessage).toContainText(message);
}
}
استخدام كائن الصفحة في الاختبارات:
// tests/login.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
test('successful login', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('user@example.com', 'password123');
await expect(page).toHaveURL(/.*dashboard/);
});
test('failed login shows error', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('invalid@example.com', 'wrong');
await loginPage.expectErrorMessage('Invalid credentials');
});
التركيبات لإعداد الاختبار
استخدم التركيبات لإعداد متطلبات الاختبار المسبقة:
// fixtures/auth.ts
import { test as base } from '@playwright/test';
export const test = base.extend({
// تركيبة صفحة معتمدة
authenticatedPage: async ({ page }, use) => {
// انتقل إلى تسجيل الدخول
await page.goto('/login');
// تنفيذ تسجيل الدخول
await page.getByLabel('Email').fill('user@example.com');
await page.getByLabel('Password').fill('password123');
await page.getByRole('button', { name: 'Login' }).click();
// انتظر المصادقة
await page.waitForURL('**/dashboard');
// توفير صفحة معتمدة للاختبار
await use(page);
// التنظيف (تسجيل الخروج)
await page.getByRole('button', { name: 'Logout' }).click();
}
});
export { expect } from '@playwright/test';
استخدم التركيبة في الاختبارات:
// tests/dashboard.spec.ts
import { test, expect } from '../fixtures/auth';
test('displays user dashboard', async ({ authenticatedPage }) => {
// تم تسجيل الدخول بالفعل عبر التركيبة
await expect(authenticatedPage.getByText('Welcome back')).toBeVisible();
});
الاختبار عبر المتصفحات
تشغيل الاختبارات عبر متصفحات متعددة:
// التشغيل على جميع المتصفحات المكونة
npx playwright test
// التشغيل على متصفح محدد
npx playwright test --project=firefox
npx playwright test --project=webkit
// التشغيل على متصفحات محددة متعددة
npx playwright test --project=chromium --project=firefox
محاكاة الهاتف المحمول
اختبر منافذ عرض الهاتف المحمول وتفاعلات اللمس:
test('mobile menu', async ({ page }) => {
// محاكاة iPhone 12
await page.setViewportSize({ width: 390, height: 844 });
await page.goto('/');
// يجب أن تكون قائمة الهاتف المحمول مرئية
await expect(page.getByRole('button', { name: 'Menu' })).toBeVisible();
// انقر على قائمة الهاتف المحمول
await page.getByRole('button', { name: 'Menu' }).tap();
// يجب أن تظهر عناصر القائمة
await expect(page.getByRole('navigation')).toBeVisible();
});
تصحيح الاختبارات
يوفر Playwright أدوات تصحيح قوية:
# تشغيل الاختبارات في وضع التصحيح (يفتح Playwright Inspector)
npx playwright test --debug
# تصحيح اختبار محدد
npx playwright test tests/example.spec.ts --debug
# التشغيل في وضع برأس لرؤية المتصفح
npx playwright test --headed
# إبطاء التنفيذ
npx playwright test --headed --slow-mo=1000
استخدم مساعدات التصحيح في الكود:
test('debugging example', async ({ page }) => {
await page.goto('/');
// إيقاف التنفيذ (يفتح Playwright Inspector)
await page.pause();
// التقاط لقطة شاشة
await page.screenshot({ path: 'screenshot.png' });
// الحصول على محتوى HTML للتصحيح
const html = await page.content();
console.log(html);
// تقييم JavaScript
const title = await page.evaluate(() => document.title);
console.log(title);
});
عارض التتبع
تسجيل وتحليل تنفيذ الاختبار:
# تسجيل الأثر
npx playwright test --trace on
# عرض الأثر
npx playwright show-trace trace.zip
يعرض عارض التتبع:
- الجدول الزمني الكامل لتنفيذ الاختبار
- لقطات الشاشة في كل خطوة
- طلبات الشبكة
- سجلات وحدة التحكم
- لقطات DOM
- تفاصيل الإجراء
تمرين:
أنشئ مجموعة اختبارات شاملة لتطبيق مدونة باستخدام Playwright:
- أنشئ كائنات صفحة لـ: الصفحة الرئيسية، صفحة تسجيل الدخول، صفحة منشور المدونة، صفحة المسؤول
- قم بتنفيذ تركيبة المصادقة لمستخدم المسؤول
- اختبر تدفقات المستخدم:
- تصفح منشورات المدونة (الترقيم، الترشيح)
- قراءة منشور المدونة الكامل
- إرسال تعليق على منشور المدونة
- تسجيل دخول المسؤول
- إنشاء منشور مدونة جديد (مع رفع الصورة)
- تحرير منشور مدونة موجود
- حذف منشور المدونة
- اختبر عبر Chromium و Firefox و WebKit
- اختبر منفذ عرض الهاتف المحمول (قائمة الهامبورجر، الصور المتجاوبة)
- محاكاة استجابات API لمنشورات المدونة
- تحقق من لقطة الشاشة لبطاقات منشور المدونة
أفضل الممارسات
- استخدم محددات قائمة على الأدوار: أعط الأولوية لـ
getByRole() لإمكانية الوصول والاستقرار
- نموذج كائن الصفحة: احصر تفاعلات الصفحة في فئات قابلة لإعادة الاستخدام
- عزل الاختبار: يجب أن يكون كل اختبار مستقلاً ولا يعتمد على الآخرين
- استخدم التركيبات: قم بإعداد متطلبات الاختبار المسبقة المشتركة بالتركيبات
- محاكاة التبعيات الخارجية: محاكاة واجهات برمجة التطبيقات والخدمات الخارجية
- استفد من الانتظار التلقائي: ثق بالانتظار التلقائي لـ Playwright، تجنب الانتظارات اليدوية
- اختبر عبر المتصفحات: قم بتشغيل الاختبارات على Chromium و Firefox و WebKit
- استخدم عارض التتبع: قم بتمكين التتبع لتصحيح الاختبارات الفاشلة
أخطاء شائعة:
- عدم استخدام تسلسل المحدد - استخدم دائماً المحدد الأكثر تحديداً
- استخدام
waitForTimeout() بدلاً من الاعتماد على الانتظار التلقائي
- عدم الاختبار عبر متصفحات متعددة
- إنشاء اختبارات مترابطة تفشل عند التشغيل بشكل منفصل
- عدم استخدام نموذج كائن الصفحة للتطبيقات المعقدة
- اختيار العناصر عن طريق النص الذي يتغير مع الترجمة
Playwright مقابل Cypress
الاختلافات الرئيسية لمساعدتك في الاختيار:
| الميزة |
Playwright |
Cypress |
| دعم المتصفح |
Chromium، Firefox، WebKit |
قائم على Chromium، Firefox |
| اختبار الهاتف المحمول |
محاكاة الجهاز الأصلية |
محاكاة منفذ العرض |
| مشغل الاختبار |
تنفيذ متوازٍ مدمج |
تسلسلي (متوازٍ في النسخة المدفوعة) |
| API |
قائم على الوعود (async/await) |
قائمة انتظار الأوامر (التسلسل) |
| تجربة المطور |
أدوات ممتازة، آثار |
تصحيح ممتاز، السفر عبر الزمن |
في الدرس التالي، سنستكشف تقنيات وأدوات اختبار API لاختبار خدمات الخلفية.