Next.js: اختبارات E2E: Playwright
آخر تحديث: 2026-08-26
اختبار E2E هو "محاكاة شاملة" تحاكي تفاعلات المستخدم الحقيقية — يخبرك بالضبط بمدى جودة عمل تطبيقك في المتصفح.
1. ما ستتعلمه
- إعداد Playwright وضبط خادم تطوير Next.js ليبدأ تلقائيا (إعداد webServer)
- تصميم كود اختبار قابل للصيانة باستخدام نموذج كائن الصفحة (POM)
- تطوير اختبارات المسار الحرج: التنقل بين الصفحات، إرسال النماذج، وعملية المصادقة
- استخدام
toHaveScreenshot()لاختبار الانحدار البصري - تنفيذ اختبار معزول عن طريق اعتراض طلبات API عبر
page.route() - دمج Playwright في مسار GitHub Actions CI
2. قصة حقيقية لمهندسة اختبار
(1) نقطة الألم: تغطية اختبار يدوي غير مكتملة وحوادث إنتاج متكررة
تعمل Diana كمهندسة اختبار في فريق TaskFlow. يحافظ الفريق على منصة SaaS تخدم أكثر من 10,000 مستخدم، وقبل كل إصدار، يجب على ثلاثة مهندسي QA اختبار أكثر من 200 حالة اختبار يدويا — وهي عملية تستغرق يومين كاملين.
في إصدار الأسبوع الماضي، عملت ميزة "تعديل اسم المشروع" بشكل جيد في Chrome، لكن حقل الإدخال لم يكن قابلا للتركيز في Safari. اختبر فريق QA في Chrome فقط، وبعد الإصدار، تلقوا أكثر من 50 شكوى.
مشكلة الاختبار اليدوي واضحة:
| المشكلة | التأثير |
|---|---|
| تغطية متصفح غير مكتملة | تم الاختبار على Chrome فقط؛ تم إغفال Safari و Firefox |
| دورة تطوير طويلة | كل إصدار يتطلب يومين من الاختبار اليدوي |
| نتائج غير متسقة | يستخدم مختبرون مختلفون طرقا مختلفة |
| عدم القدرة على إعادة الإنتاج | الحاجة إلى إعادة الاختبار بعد إصلاح الخطأ |
(2) حل Playwright E2E
أدخلت Diana Playwright لاستبدال العمليات اليدوية بالكود:
test('user can create a project', async ({ page }) => {
await page.goto('/projects')
await page.getByRole('button', { name: 'New Project' }).click()
await page.getByPlaceholder('Project name').fill('E-commerce App')
await page.getByRole('button', { name: 'Create' }).click()
await expect(page.getByText('E-commerce App')).toBeVisible()
})
(3) النتائج
| البعد | الاختبار اليدوي | Playwright E2E |
|---|---|---|
| مدة الاختبار | يومان | 15 دقيقة |
| تغطية المتصفح | Chrome فقط | Chrome + Firefox + Safari + Edge |
| دورة الاستعادة | استعادة كاملة كل مرة | استعادة تدريجية كل مرة + CI تلقائي |
| معدل هروب الأخطاء | 15% | < 2% |
| قابلية تكرار الاختبار | منخفضة (تباين بشري) | مرتفعة (اتساق 100%) |
3. إعداد بيئة Playwright
Playwright هو إطار اختبار آلي متعدد المتصفحات تديره Microsoft ويدعم Chromium و Firefox و WebKit.
graph TB
A[playwright.config.ts] --> B[إعداد webServer]
A --> C[إعدادات المتصفح]
A --> D[دليل الاختبار]
B --> E[تشغيل تلقائي npm run dev]
B --> F[الاستماع على المنفذ 3000 متاح]
C --> G[Chromium / Firefox / WebKit]
C --> H[Viewport 1280x720]
style A fill:#cce5ff
style B fill:#d4edda
| ملف الإعداد | الغرض | الخيارات الرئيسية |
|---|---|---|
playwright.config.ts |
الإعداد الرئيسي لـ E2E | webServer إدارة دورة الحياة التلقائية |
playwright/index.html |
بوابة اختبار المكونات | ct.target الإعداد |
.github/workflows/playwright.yml |
تكامل CI | npx playwright install --with-deps |
(1) التثبيت والتهيئة
npm init playwright@latest
# اختر:
# ✓ TypeScript
# ✓ E2E tests
# ✓ Add GitHub Actions workflow
# ✓ Install browsers (Chromium, Firefox, WebKit)
(2) الإعداد الأساسي لـ playwright.config.ts
import { defineConfig, devices } from '@playwright/test'
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: [
['html'],
['json', { outputFile: 'playwright-report/results.json' }]
],
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure'
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] }
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] }
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] }
}
],
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
timeout: 30000
}
})
(3) هيكل المجلدات
├── e2e/
│ ├── auth.setup.ts # معالجة مسبقة للمصادقة
│ ├── models/
│ │ ├── LoginPage.ts # POM صفحة تسجيل الدخول
│ │ ├── ProjectPage.ts # POM صفحة المشروع
│ │ └── TaskPage.ts # POM صفحة المهام
│ ├── navigation.spec.ts # اختبار التنقل
│ ├── auth.spec.ts # اختبار المصادقة
│ ├── projects.spec.ts # CRUD المشاريع
│ └── visual.spec.ts # الانحدار البصري
├── playwright.config.ts
├── playwright-report/
└── test-results/
▶ مثال: التحقق من تشغيل Playwright
// e2e/example.spec.ts
import { test, expect } from '@playwright/test'
test('homepage has correct title', async ({ page }) => {
await page.goto('/')
await expect(page).toHaveTitle(/TaskFlow/)
await expect(page.locator('h1')).toContainText('Welcome')
})
npx playwright test --project=chromium --headed
Running 1 test using 1 worker
✓ e2e/example.spec.ts:3:1 › homepage has correct title (2.3s)
المخرجات:
Running 1 test using 1 worker
✓ e2e/example.spec.ts:3:1 › homepage has correct title (2.3s)
1 passed (2.3s)
4. نموذج كائن الصفحة (POM) كنمط تصميم
يغلّف POM منطق تفاعل الصفحة في فئات منفصلة، بحيث يركز كود الاختبار فقط على "ماذا تفعل" بدلا من "كيف تفعل".
graph TB
A[حالات الاختبار] --> B[طبقة كائن الصفحة]
B --> C[LoginPage]
B --> D[ProjectsPage]
B --> E[TaskPage]
C --> F[Browser API]
D --> F
E --> F
style B fill:#cce5ff
style C fill:#d4edda
style D fill:#d4edda
style E fill:#d4edda
(1) تنفيذ POM لصفحة تسجيل الدخول
// e2e/models/LoginPage.ts
import { type Page, type 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.getByPlaceholder('Enter your email address')
this.passwordInput = page.getByPlaceholder('Enter your password')
this.submitButton = page.getByRole('button', { name: 'Log In' })
this.errorMessage = page.getByTestId('login-error')
}
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 expectError(message: string) {
await expect(this.errorMessage).toContainText(message)
}
async expectLoggedIn() {
await expect(this.page).toHaveURL(/dashboard/)
}
}
(2) تنفيذ POM لصفحة المشاريع
// e2e/models/ProjectsPage.ts
import { type Page, type Locator } from '@playwright/test'
export class ProjectsPage {
readonly page: Page
readonly newProjectButton: Locator
readonly projectNameInput: Locator
readonly createButton: Locator
readonly projectList: Locator
readonly searchInput: Locator
constructor(page: Page) {
this.page = page
this.newProjectButton = page.getByRole('button', { name: 'New Project' })
this.projectNameInput = page.getByPlaceholder('Project name')
this.createButton = page.getByRole('button', { name: 'Create' })
this.projectList = page.getByTestId('project-list')
this.searchInput = page.getByPlaceholder('Search Items')
}
async goto() {
await this.page.goto('/projects')
}
async createProject(name: string) {
await this.newProjectButton.click()
await this.projectNameInput.fill(name)
await this.createButton.click()
}
async searchProject(name: string) {
await this.searchInput.fill(name)
}
async expectProjectVisible(name: string) {
await expect(this.projectList).toContainText(name)
}
async openProject(name: string) {
await this.page.getByRole('link', { name }).first().click()
}
}
▶ مثال: اختبار مدفوع بـ POM
المخرجات:
Defines TypeScript type(s): Page, Locator.
// e2e/projects.spec.ts
import { test, expect } from '@playwright/test'
import { LoginPage } from './models/LoginPage'
import { ProjectsPage } from './models/ProjectsPage'
test.describe('Project Management', () => {
let loginPage: LoginPage
let projectsPage: ProjectsPage
test.beforeEach(async ({ page }) => {
loginPage = new LoginPage(page)
projectsPage = new ProjectsPage(page)
await loginPage.goto()
await loginPage.login('alice@example.com', 'password123')
await loginPage.expectLoggedIn()
})
test('create a new project', async () => {
await projectsPage.goto()
await projectsPage.createProject('E-commerce Dashboard')
await projectsPage.expectProjectVisible('E-commerce Dashboard')
})
test('search for an existing project', async () => {
await projectsPage.goto()
await projectsPage.searchProject('E-commerce')
await projectsPage.expectProjectVisible('E-commerce Dashboard')
})
})
المخرجات:
TypeScript code executed successfully.
5. اختبار المسار الحرج
المسار الحرج هو سير العمل التجاري الأكثر استخداما من قبل المستخدمين ويجب إعطاؤه الأولوية.
(1) اختبار عملية المصادقة
// e2e/auth.spec.ts
import { test, expect } from '@playwright/test'
test.describe('Authentication Flow', () => {
test('redirects unauthenticated user to login', async ({ page }) => {
await page.goto('/dashboard')
await expect(page).toHaveURL(/login/)
})
test('shows validation errors on empty form', async ({ page }) => {
await page.goto('/login')
await page.getByRole('button', { name: 'Log In' }).click()
await expect(page.getByText(/Please enter your email address/i)).toBeVisible()
await expect(page.getByText(/Please enter your password/i)).toBeVisible()
})
test('successful login redirects to dashboard', async ({ page }) => {
await page.goto('/login')
await page.getByPlaceholder('Enter your email address').fill('alice@example.com')
await page.getByPlaceholder('Enter your password').fill('correct-password')
await page.getByRole('button', { name: 'Log In' }).click()
await expect(page).toHaveURL(/dashboard/)
await expect(page.getByText(/Welcome back, Alice/i)).toBeVisible()
})
test('logout clears session', async ({ page }) => {
await page.goto('/login')
await page.getByPlaceholder('Enter your email address').fill('alice@example.com')
await page.getByPlaceholder('Enter your password').fill('correct-password')
await page.getByRole('button', { name: 'Log In' }).click()
await page.getByRole('button', { name: /Exit/i }).click()
await expect(page).toHaveURL(/login/)
})
})
(2) اختبار إرسال النموذج
// e2e/task-creation.spec.ts
import { test, expect } from '@playwright/test'
test.describe('Task Creation Flow', () => {
test.beforeEach(async ({ page }) => {
// تسجيل الدخول والانتقال إلى المشروع
await page.goto('/login')
await page.getByPlaceholder('Enter your email address').fill('alice@example.com')
await page.getByPlaceholder('Enter your password').fill('password123')
await page.getByRole('button', { name: 'Log In' }).click()
await page.goto('/projects/p1')
})
test('creates a task with all fields', async ({ page }) => {
await page.getByRole('button', { name: 'Add Task' }).click()
await page.getByPlaceholder('Task title').fill('Implement user auth')
await page.getByLabel('Priority').selectOption('High')
await page.getByLabel('Assignee').selectOption('Bob')
await page.getByRole('button', { name: 'Save' }).click()
await expect(page.getByText('Implement user auth')).toBeVisible()
})
test('shows error for empty title', async ({ page }) => {
await page.getByRole('button', { name: 'Add Task' }).click()
await page.getByRole('button', { name: 'Save' }).click()
await expect(page.getByText(/The title cannot be left blank./i)).toBeVisible()
})
})
(3) اختبار التنقل والتوجيه
// e2e/navigation.spec.ts
import { test, expect } from '@playwright/test'
test.describe('Navigation Flow', () => {
test('sidebar links navigate correctly', async ({ page }) => {
await page.goto('/login')
await page.getByPlaceholder('Enter your email address').fill('alice@example.com')
await page.getByPlaceholder('Enter your password').fill('password123')
await page.getByRole('button', { name: 'Log In' }).click()
await page.getByRole('link', { name: 'Projects' }).click()
await expect(page).toHaveURL(/\/projects/)
await page.getByRole('link', { name: 'Dashboard' }).click()
await expect(page).toHaveURL(/\/dashboard/)
await page.getByRole('link', { name: 'Settings' }).click()
await expect(page).toHaveURL(/\/settings/)
})
test('breadcrumb shows current location', async ({ page }) => {
await page.goto('/login')
await page.getByPlaceholder('Enter your email address').fill('alice@example.com')
await page.getByPlaceholder('Enter your password').fill('password123')
await page.getByRole('button', { name: 'Log In' }).click()
await page.goto('/projects/p1/tasks/t1')
await expect(page.getByTestId('breadcrumb')).toContainText([
/Projects/, /Project 1/, /Task 1/
])
})
})
6. اختبار الانحدار البصري
يلتقط اختبار الانحدار البصري التغييرات الدقيقة في تنسيق واجهة المستخدم. يتيح Playwright مقارنة على مستوى البكسل من خلال toHaveScreenshot().
graph LR
A[تشغيل الاختبار] --> B[التقاط لقطة شاشة للحالة الحالية]
B --> C{المقارنة مع اللقطة المرجعية}
C -->|مطابقة| D[نجاح الاختبار]
C -->|الفروق > الحد المسموح| E[فشل الاختبار]
E --> F[إنشاء تقرير الفروق]
style A fill:#cce5ff
style C fill:#fff3cd
style D fill:#d4edda
style E fill:#f8d7da
| الخيار | الوصف | القيمة الموصى بها |
|---|---|---|
maxDiffPixels |
الحد الأقصى لعدد البكسلات المختلفة | 100 |
maxDiffPixelRatio |
الحد الأقصى لنسبة الاختلاف | 0.01 |
threshold |
حد مقارنة البكسل | 0.2 |
animations |
تعطيل الحركات | 'disabled' |
stylePath |
تجاوزات CSS إضافية | إخفاء العناصر العشوائية |
(1) إعداد الاختبار البصري
// مقتطف من playwright.config.ts
export default defineConfig({
use: {
screenshot: 'only-on-failure',
viewport: { width: 1280, height: 720 }
},
expect: {
toHaveScreenshot: {
maxDiffPixels: 100,
animations: 'disabled'
}
}
})
(2) إنشاء لقطة مرجعية
# إنشاء لقطة أساسية عند التشغيل الأول
npx playwright test --update-snapshots
# مقارنة العمليات اللاحقة مع الأساس
npx playwright test
▶ مثال: اختبار الانحدار البصري
المخرجات:
Running 5 tests using 1 worker
✓ navigation.spec.ts:3:1 › navigates to about page (1.2s)
✓ navigation.spec.ts:10:1 › displays product list (0.8s)
✓ form.spec.ts:5:1 › submits contact form (1.5s)
3 passed (3.5s)
Running 5 tests using 1 worker
✓ navigation.spec.ts:3:1 › navigates to about page (1.2s)
✓ navigation.spec.ts:10:1 › displays product list (0.8s)
✓ form.spec.ts:5:1 › submits contact form (1.5s)
3 passed (3.5s)
// e2e/visual.spec.ts
import { test, expect } from '@playwright/test'
test.describe('Visual Regression', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/login')
await page.getByPlaceholder('Enter your email address').fill('alice@example.com')
await page.getByPlaceholder('Enter your password').fill('password123')
await page.getByRole('button', { name: 'Log In' }).click()
})
test('dashboard page matches snapshot', async ({ page }) => {
await page.goto('/dashboard')
await page.waitForLoadState('networkidle')
await expect(page).toHaveScreenshot('dashboard.png', {
fullPage: true
})
})
test('project list matches snapshot', async ({ page }) => {
await page.goto('/projects')
await page.waitForSelector('[data-testid="project-list"]')
await expect(page).toHaveScreenshot('projects.png')
})
test('task detail matches snapshot', async ({ page }) => {
await page.goto('/projects/p1/tasks/t1')
await page.waitForLoadState('networkidle')
await expect(page).toHaveScreenshot('task-detail.png', {
mask: [page.locator('[data-testid="timestamp"]')]
})
})
})
المخرجات:
TypeScript code executed successfully.
7. محاكاة API واعتراض الطلبات
يمكن لـ page.route() في Playwright اعتراض طلبات الشبكة على مستوى المتصفح، مما يتيح لك اختبار تفاعلات الواجهة الأمامية/الخلفية دون خادم خلفي حقيقي.
(1) أساسيات اعتراض المسارات
// اعتراض عام
await page.route('**/api/**', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([{ id: 1, title: 'Mocked Task' }])
})
})
// اعتراض عنوان URL محدد
await page.route('https://api.example.com/products*', async route => {
const response = await route.fetch() // السماح بالوصول إلى API الحقيقي
const body = await response.json()
body.push({ id: 999, name: 'Injected Product' })
await route.fulfill({ response, body: JSON.stringify(body) })
})
(2) مقارنة أنماط محاكاة API
| النمط | الطريقة | السيناريوهات المناسبة |
|---|---|---|
| محاكاة كاملة | route.fulfill() |
اختبار الواجهة الأمامية عندما لا تكون الخلفية جاهزة |
| تعديل الوكيل | route.fetch() + تعديل |
حقن بيانات اختبار/خطأ |
| تمرير الوكيل | route.fetch() + route.fulfill({ response }) |
الاستماع للطلبات دون تعديل البيانات |
| مقاطعة الطلب | route.abort() |
اختبار حالة عدم الاتصال |
▶ مثال: اختبار كامل مع محاكاة API
المخرجات:
TypeScript code executed successfully.
// e2e/api-mock.spec.ts
import { test, expect } from '@playwright/test'
test.describe('API Mock Scenarios', () => {
test('shows empty state when no data', async ({ page }) => {
await page.route('**/api/projects', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([])
})
})
await page.goto('/projects')
await expect(page.getByText(/No projects yet/i)).toBeVisible()
})
test('handles 500 error gracefully', async ({ page }) => {
await page.route('**/api/projects', async route => {
await route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({ error: 'Server error' })
})
})
await page.goto('/projects')
await expect(page.getByText(/Failed to load/i)).toBeVisible()
})
test('shows loading state then data', async ({ page }) => {
// تأخير الاستجابات لاختبار ظروف التحميل
await page.route('**/api/projects', async route => {
await new Promise(resolve => setTimeout(resolve, 1000))
await route.fulfill({
status: 200,
body: JSON.stringify([
{ id: 'p1', name: 'Mock Project', taskCount: 5 }
])
})
})
await page.goto('/projects')
await expect(page.getByTestId('loading-skeleton')).toBeVisible()
await expect(page.getByText('Mock Project')).toBeVisible({ timeout: 5000 })
})
test('network offline scenario', async ({ page }) => {
await page.route('**/api/**', async route => {
await route.abort('internetdisconnected')
})
await page.goto('/projects')
await expect(page.getByText(/Network connection failed/i)).toBeVisible()
})
})
المخرجات:
TypeScript code executed successfully.
8. تكامل CI: GitHub Actions
يتكامل Playwright بسلاسة مع GitHub Actions، ليقوم بتشغيل اختبارات E2E تلقائيا مع كل push.
# .github/workflows/playwright.yml
name: Playwright Tests
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
strategy:
matrix:
browser: [chromium, firefox, webkit]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Cache Playwright browsers
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-${{ hashFiles('package-lock.json') }}
- name: Install Playwright browsers
run: npx playwright install --with-deps ${{ matrix.browser }}
- name: Run Playwright tests
run: npx playwright test --project=${{ matrix.browser }}
- uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report-${{ matrix.browser }}
path: playwright-report/
retention-days: 7
9. مثال كامل: مجموعة E2E لتطبيق TaskFlow
// e2e/taskflow-e2e.spec.ts
// ============================================
// اختبار E2E شامل: تدفق المستخدم الكامل لتطبيق TaskFlow
// ============================================
import { test, expect, type Page } from '@playwright/test'
// --- دوال مساعدة: تعيين حالة المصادقة ---
async function setupAuth(page: Page) {
await page.goto('/login')
await page.getByPlaceholder('Enter your email address').fill('alice@taskflow.io')
await page.getByPlaceholder('Enter your password').fill('Password123!')
await page.getByRole('button', { name: 'Log In' }).click()
await expect(page).toHaveURL(/dashboard/)
}
test.describe('TaskFlow E2E Suite', () => {
test.beforeEach(async ({ page }) => {
await setupAuth(page)
})
// --- 1. تحميل لوحة التحكم ---
test('dashboard displays key metrics', async ({ page }) => {
await page.goto('/dashboard')
await expect(page.getByTestId('total-projects')).toBeVisible()
await expect(page.getByTestId('total-tasks')).toBeVisible()
await expect(page.getByTestId('team-members')).toBeVisible()
const projectCount = await page.getByTestId('total-projects').textContent()
expect(Number(projectCount)).toBeGreaterThan(0)
})
// --- 2. عملية CRUD للمشاريع ---
test('complete project lifecycle', async ({ page }) => {
// إنشاء
await page.goto('/projects')
await page.getByRole('button', { name: 'New Project' }).click()
await page.getByPlaceholder('Project name').fill('E2E Test Project')
await page.getByPlaceholder('Description').fill('Created by Playwright')
await page.getByRole('button', { name: 'Create' }).click()
await expect(page.getByText('E2E Test Project')).toBeVisible()
// تعديل
await page.getByRole('button', { name: /Edit/i }).click()
await page.getByPlaceholder('Project name').fill('E2E Test Project v2')
await page.getByRole('button', { name: 'Save' }).click()
await expect(page.getByText('E2E Test Project v2')).toBeVisible()
// حذف
await page.getByRole('button', { name: /Delete/i }).click()
await page.getByRole('button', { name: /Confirm/i }).click()
await expect(page.getByText('E2E Test Project v2')).not.toBeVisible()
})
// --- 3. إدارة المهام ---
test('task drag and drop status change', async ({ page }) => {
await page.goto('/projects/p1')
// إنشاء مهمة
await page.getByRole('button', { name: 'Add Task' }).click()
await page.getByPlaceholder('Task title').fill('Setup Playwright tests')
await page.getByText('TODO').click()
await page.getByRole('button', { name: 'Save' }).click()
// التحقق من ظهور المهمة في عمود TODO
await expect(page.getByTestId('column-todo'))
.toContainText('Setup Playwright tests')
})
// --- 4. وظيفة البحث ---
test('search filters projects correctly', async ({ page }) => {
await page.goto('/projects')
await page.getByPlaceholder('Search Items').fill('Marketing')
await page.waitForTimeout(300) // debounce
const items = page.getByTestId('project-item')
const count = await items.count()
for (let i = 0; i < count; i++) {
await expect(items.nth(i)).toContainText(/Marketing/i)
}
})
// --- 5. التخطيط المتجاوب ---
test.describe('Responsive Design', () => {
test('sidebar collapses on mobile viewport', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 812 })
await page.goto('/dashboard')
await expect(page.getByTestId('sidebar')).not.toBeVisible()
await page.getByRole('button', { name: /menu/i }).click()
await expect(page.getByTestId('sidebar')).toBeVisible()
})
})
// --- 6. اختبار محاكاة API ---
test('handles API errors gracefully', async ({ page }) => {
await page.route('**/api/dashboard/metrics', async route => {
await route.fulfill({
status: 503,
body: JSON.stringify({ error: 'Service Unavailable' })
})
})
await page.goto('/dashboard')
await expect(page.getByText(/The service is temporarily unavailable/i)).toBeVisible()
})
})
Running 8 tests using 2 workers
✓ e2e/taskflow-e2e.spec.ts (8 tests) 34.2s
✓ chromium | 8 passed (34.2s)
✓ firefox | 8 passed (38.1s)
✓ webkit | 8 passed (41.5s)
❓ أسئلة شائعة
webServer في Playwright更适合 لسيناريوهات التشغيل التلقائي لـ npm run dev في Next.js.storageState في Playwright: بعد تسجيل الدخول في auth.setup.ts، احفظ cookies و localStorage في storageState.json، ثم ارجع إلى ذلك الملف في إعداد use للاختبارات الأخرى لتجنب تكرار تسجيل الدخول لكل اختبار.e2e/__screenshots__/) كجزء من الكود. سيقوم نظام CI بمقارنتها تلقائيا أثناء التشغيل. تأكد من استخدام وسم .gitattributes لتصنيف ملفات اللقطات كـ binary.page.route() و MSW لاختبار E2E؟page.route() الطلبات على مستوى المتصفح ولا يتطلب اعتماديات إضافية، مما يجعله مناسبا للمحاكاة المؤقتة. يتطلب MSW تسجيل Service Worker وهو مناسب لاختبار المكونات (Vitest). بالنسبة لاختبار E2E، يوصى باستخدام page.route() افتراضيا لتقليل الاعتماديات الخارجية.workers: 1 لتجنب تنافس الموارد؛ (2) قم بتخزين ثنائيات المتصفح مؤقتا (~/.cache/ms-playwright)؛ (3) استخدم --project=chromium لتشغيل المتصفحات الحرجة فقط في CI؛ (4) التوازي: قسم الاختبارات عبر وظائف متعددة.mask في Playwright لإخفاء العناصر الديناميكية في الاختبارات البصرية: toHaveScreenshot({ mask: [page.locator('[data-testid="timestamp"]')] }). بالنسبة لتأكيدات النص، استخدم أنماط التعبيرات النمطية بدلا من القيم الثابتة.📖 ملخص
- يسمح إعداد
webServerفي Playwright لخادم تطوير Next.js بالبدء والتوقف تلقائيا مع الاختبارات - يغلف نموذج كائن الصفحة (POM) تفاعلات الصفحة في فئات منفصلة، مما يجعل كود الاختبار أكثر إيجازا وقابلية للصيانة
- يغطي اختبار المسار الحرج تدفقات المستخدم الأساسية مثل المصادقة والتنقل وإرسال النماذج
- يتيح
toHaveScreenshot()مقارنات انحدار بصري على مستوى البكسل لاكتشاف تغييرات تنسيق واجهة المستخدم غير المقصودة - يعترض
page.route()طلبات API على مستوى المتصفح؛ مناسب لاختبار العزل ومحاكاة سيناريوهات الأخطاء - يتطلب دمج Playwright مع GitHub Actions تخزين المتصفح مؤقتا وإعداد مصفوفة متوازية ورفع تقارير الفشل
📝 تمارين
-
سؤال أساسي (⭐): أنشئ كائن صفحة
LoginPageيحتوي على ثلاث طرق —goto()وlogin(email, password)وexpectLoggedIn()— واكتب حالة اختبار تستخدم هذا POM. -
تمرين متقدم (⭐⭐): اكتب خمسة اختبارات شاملة (E2E) لسير العمل الكامل "إنشاء-تعديل-حذف" (CRUD) في تطبيق Next.js الخاص بك، بما في ذلك محاكاة
page.route()لأخطاء التحقق من النموذج وسيناريوهات أخطاء API. -
تحدّ (⭐⭐⭐): نفذ مجموعة اختبار انحدار بصري كاملة: (1) أنشئ لقطات أساسية لـ 5 صفحات رئيسية؛ (2) قم بإعداد حدود
expect.toHaveScreenshotوmaxDiffPixels؛ (3) اكتب سكريبت CI لمقارنة اللقطات تلقائيا في pull request وإضافة تعليق مع تقرير الفروق.