10.8 בדיקות E2E הרצאה
בדיקות E2E עם Playwright¶
בשיעור זה נלמד כיצד לכתוב בדיקות מקצה לקצה (End-to-End) עם Playwright - כלי שמדמה את השימוש באפליקציה בדפדפן אמיתי.
מה זה בדיקות E2E¶
- בדיקות E2E בודקות את האפליקציה כמו שמשתמש אמיתי היה משתמש בה
- רצות בדפדפן אמיתי (Chromium, Firefox, WebKit)
- בודקות תהליכים מלאים: התחברות, מילוי טופס, ניווט בין דפים
- האיטיות מבין סוגי הבדיקות, אבל מספקות את הביטחון הגבוה ביותר
מתי להשתמש ב-E2E¶
- תהליכים קריטיים (הרשמה, התחברות, רכישה)
- ניווט בין דפים
- אינטגרציה עם שירותים חיצוניים
- בדיקת responsive ו-cross-browser
- בדיקות regression
הגדרת Playwright¶
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI, // מונע test.only ב-CI
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry', // שומר trace לבדיקות שנכשלו
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'] },
},
],
// הרצת שרת הפיתוח לפני הבדיקות
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
},
});
כתיבת בדיקות בסיסיות¶
ניווט ובדיקת תוכן¶
// e2e/home.spec.ts
import { test, expect } from '@playwright/test';
test('should display homepage', async ({ page }) => {
await page.goto('/');
// בדיקת כותרת הדף
await expect(page).toHaveTitle(/דף הבית/);
// בדיקת כותרת ראשית
await expect(page.getByRole('heading', { level: 1 })).toHaveText('ברוכים הבאים');
// בדיקת ניווט
await expect(page.getByRole('navigation')).toBeVisible();
});
test('should navigate to about page', async ({ page }) => {
await page.goto('/');
await page.getByRole('link', { name: 'אודות' }).click();
await expect(page).toHaveURL('/about');
await expect(page.getByRole('heading', { level: 1 })).toHaveText('אודות');
});
Locators - מציאת אלמנטים¶
// לפי role - הדרך המומלצת
page.getByRole('button', { name: 'שלח' });
page.getByRole('heading', { level: 1 });
page.getByRole('link', { name: 'אודות' });
page.getByRole('textbox', { name: 'אימייל' });
page.getByRole('checkbox', { name: 'אני מסכים' });
// לפי טקסט
page.getByText('ברוכים הבאים');
page.getByText(/שלום/); // regex
// לפי label
page.getByLabel('אימייל');
page.getByLabel('סיסמה');
// לפי placeholder
page.getByPlaceholder('הקלידו כאן...');
// לפי alt text
page.getByAltText('לוגו');
// לפי test-id
page.getByTestId('submit-button');
// לפי CSS selector (שימוש אחרון)
page.locator('.product-card');
page.locator('#main-content');
Assertions - בדיקות¶
// בדיקת נראות
await expect(page.getByText('שלום')).toBeVisible();
await expect(page.getByText('הודעת שגיאה')).not.toBeVisible();
await expect(page.getByText('שלום')).toBeHidden();
// בדיקת טקסט
await expect(page.getByRole('heading')).toHaveText('כותרת');
await expect(page.getByRole('heading')).toContainText('כותר');
// בדיקת ערכים
await expect(page.getByLabel('אימייל')).toHaveValue('user@example.com');
// בדיקת URL
await expect(page).toHaveURL('/about');
await expect(page).toHaveURL(/\/products\/.*/);
// בדיקת כותרת דף
await expect(page).toHaveTitle('דף הבית');
// בדיקת כמות אלמנטים
await expect(page.getByRole('listitem')).toHaveCount(5);
// בדיקת CSS
await expect(page.getByRole('button')).toHaveCSS('background-color', 'rgb(59, 130, 246)');
// בדיקת attributes
await expect(page.getByRole('button')).toBeDisabled();
await expect(page.getByRole('button')).toBeEnabled();
await expect(page.getByRole('checkbox')).toBeChecked();
אינטראקציה עם אלמנטים¶
לחיצה¶
// לחיצה רגילה
await page.getByRole('button', { name: 'שלח' }).click();
// לחיצה כפולה
await page.getByText('פריט').dblclick();
// לחיצה ימנית
await page.getByText('פריט').click({ button: 'right' });
מילוי טופס¶
// הקלדה
await page.getByLabel('שם').fill('דני כהן');
await page.getByLabel('אימייל').fill('dani@example.com');
// ניקוי + הקלדה
await page.getByLabel('חיפוש').clear();
await page.getByLabel('חיפוש').fill('React');
// בחירה מ-select
await page.getByLabel('מדינה').selectOption('IL');
// checkbox
await page.getByRole('checkbox', { name: 'אני מסכים' }).check();
await page.getByRole('checkbox', { name: 'אני מסכים' }).uncheck();
הקלדה תו-תו¶
// כשצריך לדמות הקלדה אמיתית (עם אירועים)
await page.getByLabel('חיפוש').pressSequentially('React', { delay: 100 });
// לחיצת מקשים
await page.getByLabel('חיפוש').press('Enter');
await page.keyboard.press('Escape');
await page.keyboard.press('Tab');
אסטרטגיות המתנה - Waiting Strategies¶
- Playwright ממתין אוטומטית לאלמנטים לפני שמבצע פעולות
- אין צורך ב-sleep או waitFor ברוב המקרים
// Playwright ממתין אוטומטית שהכפתור יהיה גלוי ומופעל
await page.getByRole('button', { name: 'שלח' }).click();
// המתנה מפורשת כשצריך
await page.waitForURL('/dashboard');
await page.waitForResponse('**/api/users');
// המתנה לאלמנט
await page.getByText('נטען בהצלחה').waitFor();
await page.getByText('טוען...').waitFor({ state: 'hidden' });
// המתנה לבקשת רשת
const responsePromise = page.waitForResponse('**/api/products');
await page.getByRole('button', { name: 'טען מוצרים' }).click();
const response = await responsePromise;
expect(response.status()).toBe(200);
בדיקת ניווט ותהליכים מרובי דפים¶
// e2e/shopping.spec.ts
import { test, expect } from '@playwright/test';
test('should complete purchase flow', async ({ page }) => {
// שלב 1: כניסה לחנות
await page.goto('/');
await expect(page.getByRole('heading')).toContainText('החנות שלנו');
// שלב 2: בחירת מוצר
await page.getByRole('link', { name: 'מוצרים' }).click();
await expect(page).toHaveURL('/products');
await page.getByText('אוזניות Sony').click();
await expect(page).toHaveURL(/\/products\/.*/);
// שלב 3: הוספה לסל
await page.getByRole('button', { name: 'הוסף לסל' }).click();
await expect(page.getByText('המוצר נוסף לסל')).toBeVisible();
// שלב 4: מעבר לסל
await page.getByRole('link', { name: 'סל קניות' }).click();
await expect(page).toHaveURL('/cart');
await expect(page.getByText('אוזניות Sony')).toBeVisible();
// שלב 5: מעבר לתשלום
await page.getByRole('button', { name: 'לתשלום' }).click();
await expect(page).toHaveURL('/checkout');
// שלב 6: מילוי פרטים
await page.getByLabel('שם מלא').fill('דני כהן');
await page.getByLabel('אימייל').fill('dani@example.com');
await page.getByLabel('כתובת').fill('הרצל 1, תל אביב');
// שלב 7: ביצוע הזמנה
await page.getByRole('button', { name: 'בצע הזמנה' }).click();
// שלב 8: אישור
await expect(page).toHaveURL(/\/order\/.*/);
await expect(page.getByText('ההזמנה בוצעה בהצלחה')).toBeVisible();
});
צילומי מסך ו-Visual Regression¶
// צילום מסך ידני
test('should match homepage screenshot', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveScreenshot('homepage.png');
});
// צילום מסך של אלמנט ספציפי
test('should match product card screenshot', async ({ page }) => {
await page.goto('/products');
const card = page.getByTestId('product-card-1');
await expect(card).toHaveScreenshot('product-card.png');
});
// השוואה עם tolerance
test('should match with threshold', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveScreenshot('homepage.png', {
maxDiffPixelRatio: 0.01, // עד 1% הבדל
});
});
הרצה ב-CI¶
# .github/workflows/e2e.yml
name: E2E Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Install dependencies
run: npm ci
- name: Install Playwright browsers
run: npx playwright install --with-deps
- name: Build app
run: npm run build
- name: Run E2E tests
run: npx playwright test
- name: Upload test report
uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30
טיפים ו-Best Practices¶
ארגון בדיקות¶
// e2e/auth.spec.ts - קיבוץ בדיקות קשורות
import { test, expect } from '@playwright/test';
test.describe('Authentication', () => {
test('should login with valid credentials', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('אימייל').fill('user@example.com');
await page.getByLabel('סיסמה').fill('password123');
await page.getByRole('button', { name: 'התחבר' }).click();
await expect(page).toHaveURL('/dashboard');
});
test('should show error for invalid credentials', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('אימייל').fill('user@example.com');
await page.getByLabel('סיסמה').fill('wrong');
await page.getByRole('button', { name: 'התחבר' }).click();
await expect(page.getByText('אימייל או סיסמה שגויים')).toBeVisible();
await expect(page).toHaveURL('/login');
});
test('should logout', async ({ page }) => {
// login ראשון
await page.goto('/login');
await page.getByLabel('אימייל').fill('user@example.com');
await page.getByLabel('סיסמה').fill('password123');
await page.getByRole('button', { name: 'התחבר' }).click();
await expect(page).toHaveURL('/dashboard');
// logout
await page.getByRole('button', { name: 'התנתק' }).click();
await expect(page).toHaveURL('/');
});
});
Page Object Model - תבנית עיצוב¶
// e2e/pages/LoginPage.ts
import { Page, Locator } from '@playwright/test';
export class LoginPage {
private page: Page;
private emailInput: Locator;
private passwordInput: Locator;
private submitButton: Locator;
private errorMessage: Locator;
constructor(page: Page) {
this.page = page;
this.emailInput = page.getByLabel('אימייל');
this.passwordInput = page.getByLabel('סיסמה');
this.submitButton = page.getByRole('button', { name: 'התחבר' });
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 getError() {
return this.errorMessage;
}
}
// שימוש בבדיקה
test('should login', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('user@example.com', 'password123');
await expect(page).toHaveURL('/dashboard');
});
הרצת בדיקות¶
# הרצת כל הבדיקות
npx playwright test
# הרצת בדיקות של קובץ ספציפי
npx playwright test e2e/home.spec.ts
# הרצת בדיקה ספציפית
npx playwright test -g "should login"
# הרצה בדפדפן ספציפי
npx playwright test --project=chromium
# הרצה עם UI
npx playwright test --ui
# הרצה בראש פתוח (לא headless)
npx playwright test --headed
# צפייה בדוח
npx playwright show-report
# דיבוג בדיקה
npx playwright test --debug
סיכום¶
- בדיקות E2E בודקות את האפליקציה כמו שמשתמש אמיתי היה משתמש
- Playwright תומך ב-Chromium, Firefox ו-WebKit
- Locators: getByRole (מומלץ), getByText, getByLabel, getByTestId
- אינטראקציות: click, fill, check, selectOption, press
- Playwright ממתין אוטומטית לאלמנטים - אין צורך ב-sleep
- Visual regression עם toHaveScreenshot
- Page Object Model מפשט תחזוקה של בדיקות
- הרצה ב-CI עם GitHub Actions