Next.js: اختبارات E2E: Playwright

آخر تحديث: 2026-08-26

اختبار E2E هو "محاكاة شاملة" تحاكي تفاعلات المستخدم الحقيقية — يخبرك بالضبط بمدى جودة عمل تطبيقك في المتصفح.

1. ما ستتعلمه



2. قصة حقيقية لمهندسة اختبار

(1) نقطة الألم: تغطية اختبار يدوي غير مكتملة وحوادث إنتاج متكررة

تعمل Diana كمهندسة اختبار في فريق TaskFlow. يحافظ الفريق على منصة SaaS تخدم أكثر من 10,000 مستخدم، وقبل كل إصدار، يجب على ثلاثة مهندسي QA اختبار أكثر من 200 حالة اختبار يدويا — وهي عملية تستغرق يومين كاملين.

في إصدار الأسبوع الماضي، عملت ميزة "تعديل اسم المشروع" بشكل جيد في Chrome، لكن حقل الإدخال لم يكن قابلا للتركيز في Safari. اختبر فريق QA في Chrome فقط، وبعد الإصدار، تلقوا أكثر من 50 شكوى.

مشكلة الاختبار اليدوي واضحة:

المشكلة التأثير
تغطية متصفح غير مكتملة تم الاختبار على Chrome فقط؛ تم إغفال Safari و Firefox
دورة تطوير طويلة كل إصدار يتطلب يومين من الاختبار اليدوي
نتائج غير متسقة يستخدم مختبرون مختلفون طرقا مختلفة
عدم القدرة على إعادة الإنتاج الحاجة إلى إعادة الاختبار بعد إصلاح الخطأ

(2) حل Playwright E2E

أدخلت Diana Playwright لاستبدال العمليات اليدوية بالكود:

TS
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.

100%
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) التثبيت والتهيئة

BASH
npm init playwright@latest
# اختر:
#   ✓ TypeScript
#   ✓ E2E tests
#   ✓ Add GitHub Actions workflow
#   ✓ Install browsers (Chromium, Firefox, WebKit)

(2) الإعداد الأساسي لـ playwright.config.ts

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) هيكل المجلدات

TEXT 📖 للعرض فقط
├── 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

TS
// 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')
})
BASH
npx playwright test --project=chromium --headed
💻 المخرجات:

TEXT 📖 للعرض فقط
Running 1 test using 1 worker
  ✓ e2e/example.spec.ts:3:1 › homepage has correct title (2.3s)

المخرجات:

TEXT 📖 للعرض فقط
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 منطق تفاعل الصفحة في فئات منفصلة، بحيث يركز كود الاختبار فقط على "ماذا تفعل" بدلا من "كيف تفعل".

100%
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 لصفحة تسجيل الدخول

TS
// 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 لصفحة المشاريع

TS
// 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

المخرجات:

TEXT 📖 للعرض فقط
Defines TypeScript type(s): Page, Locator.
TS
// 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')
  })
})

المخرجات:

TEXT 📖 للعرض فقط
TypeScript code executed successfully.


5. اختبار المسار الحرج

المسار الحرج هو سير العمل التجاري الأكثر استخداما من قبل المستخدمين ويجب إعطاؤه الأولوية.

(1) اختبار عملية المصادقة

TS
// 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) اختبار إرسال النموذج

TS
// 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) اختبار التنقل والتوجيه

TS
// 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().

100%
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) إعداد الاختبار البصري

TS
// مقتطف من playwright.config.ts
export default defineConfig({
  use: {
    screenshot: 'only-on-failure',
    viewport: { width: 1280, height: 720 }
  },
  expect: {
    toHaveScreenshot: {
      maxDiffPixels: 100,
      animations: 'disabled'
    }
  }
})

(2) إنشاء لقطة مرجعية

BASH
# إنشاء لقطة أساسية عند التشغيل الأول
npx playwright test --update-snapshots

# مقارنة العمليات اللاحقة مع الأساس
npx playwright test

▶ مثال: اختبار الانحدار البصري

المخرجات:

TEXT 📖 للعرض فقط
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)
TS
// 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"]')]
    })
  })
})

المخرجات:

TEXT 📖 للعرض فقط
TypeScript code executed successfully.


7. محاكاة API واعتراض الطلبات

يمكن لـ page.route() في Playwright اعتراض طلبات الشبكة على مستوى المتصفح، مما يتيح لك اختبار تفاعلات الواجهة الأمامية/الخلفية دون خادم خلفي حقيقي.

(1) أساسيات اعتراض المسارات

TS
// اعتراض عام
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

المخرجات:

TEXT 📖 للعرض فقط
TypeScript code executed successfully.
TS
// 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()
  })
})

المخرجات:

TEXT 📖 للعرض فقط
TypeScript code executed successfully.


8. تكامل CI: GitHub Actions

يتكامل Playwright بسلاسة مع GitHub Actions، ليقوم بتشغيل اختبارات E2E تلقائيا مع كل push.

YAML
# .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

TS
// 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()
  })
})
💻 المخرجات:

TEXT 📖 للعرض فقط
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)

❓ أسئلة شائعة

س ما الفرق بين Playwright و Cypress؟
ج تدير Microsoft Playwright (45k⭐)، ويدعم متصفحات متعددة (Chromium/Firefox/WebKit)، ويستخدم بروتوكول CDP للتحكم المباشر في المتصفح، مما يجعله أسرع. يتطلب Cypress حقن JavaScript في الصفحة للتشغيل ويدعم فقط المتصفحات المبنية على Chromium. كما أن إعداد webServer في Playwright更适合 لسيناريوهات التشغيل التلقائي لـ npm run dev في Next.js.
س كيف أختبر صفحة تتطلب تسجيل الدخول؟
ج نوصي باستخدام ميزة storageState في Playwright: بعد تسجيل الدخول في auth.setup.ts، احفظ cookies و localStorage في storageState.json، ثم ارجع إلى ذلك الملف في إعداد use للاختبارات الأخرى لتجنب تكرار تسجيل الدخول لكل اختبار.
س أين يجب حفظ اللقطات الأساسية لاختبار الانحدار البصري؟
ج يجب إرسال اللقطات الأساسية إلى مستودع Git (e2e/__screenshots__/) كجزء من الكود. سيقوم نظام CI بمقارنتها تلقائيا أثناء التشغيل. تأكد من استخدام وسم .gitattributes لتصنيف ملفات اللقطات كـ binary.
س كيف أختار بين page.route() و MSW لاختبار E2E؟
ج يعترض page.route() الطلبات على مستوى المتصفح ولا يتطلب اعتماديات إضافية، مما يجعله مناسبا للمحاكاة المؤقتة. يتطلب MSW تسجيل Service Worker وهو مناسب لاختبار المكونات (Vitest). بالنسبة لاختبار E2E، يوصى باستخدام page.route() افتراضيا لتقليل الاعتماديات الخارجية.
س ماذا أفعل إذا كانت اختبارات Playwright بطيئة في CI؟
ج (1) استخدم workers: 1 لتجنب تنافس الموارد؛ (2) قم بتخزين ثنائيات المتصفح مؤقتا (~/.cache/ms-playwright)؛ (3) استخدم --project=chromium لتشغيل المتصفحات الحرجة فقط في CI؛ (4) التوازي: قسم الاختبارات عبر وظائف متعددة.
س كيف أتعامل مع البيانات العشوائية في الاختبارات (مثل الطوابع الزمنية ومعرفات قاعدة البيانات)؟
ج استخدم خيار mask في Playwright لإخفاء العناصر الديناميكية في الاختبارات البصرية: toHaveScreenshot({ mask: [page.locator('[data-testid="timestamp"]')] }). بالنسبة لتأكيدات النص، استخدم أنماط التعبيرات النمطية بدلا من القيم الثابتة.

📖 ملخص


📝 تمارين

  1. سؤال أساسي (⭐): أنشئ كائن صفحة LoginPage يحتوي على ثلاث طرق — goto() و login(email, password) و expectLoggedIn() — واكتب حالة اختبار تستخدم هذا POM.

  2. تمرين متقدم (⭐⭐): اكتب خمسة اختبارات شاملة (E2E) لسير العمل الكامل "إنشاء-تعديل-حذف" (CRUD) في تطبيق Next.js الخاص بك، بما في ذلك محاكاة page.route() لأخطاء التحقق من النموذج وسيناريوهات أخطاء API.

  3. تحدّ (⭐⭐⭐): نفذ مجموعة اختبار انحدار بصري كاملة: (1) أنشئ لقطات أساسية لـ 5 صفحات رئيسية؛ (2) قم بإعداد حدود expect.toHaveScreenshot و maxDiffPixels؛ (3) اكتب سكريبت CI لمقارنة اللقطات تلقائيا في pull request وإضافة تعليق مع تقرير الفروق.

Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%