לדלג לתוכן

10.8 בדיקות E2E פתרון

פתרון - בדיקות E2E עם Playwright

פתרון תרגיל 1

// e2e/navigation.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Navigation and Homepage', () => {
  test('should display homepage content', async ({ page }) => {
    await page.goto('/');

    await expect(page).toHaveTitle(/דף הבית/);
    await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
    await expect(page.getByRole('button', { name: /צור קשר|התחילו/ })).toBeVisible();
    await expect(page.getByText('השירותים שלנו')).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).toHaveTitle(/אודות/);
    await expect(page.getByRole('heading', { level: 1 })).toContainText('אודות');
  });

  test('should navigate to contact page', async ({ page }) => {
    await page.goto('/');

    await page.getByRole('link', { name: 'צור קשר' }).click();

    await expect(page).toHaveURL('/contact');
    await expect(page).toHaveTitle(/צור קשר/);
  });

  test('should navigate to all pages from menu', async ({ page }) => {
    const pages = [
      { name: 'אודות', url: '/about' },
      { name: 'שירותים', url: '/services' },
      { name: 'צור קשר', url: '/contact' },
    ];

    for (const p of pages) {
      await page.goto('/');
      await page.getByRole('link', { name: p.name }).click();
      await expect(page).toHaveURL(p.url);
    }
  });

  test('should return to homepage when clicking logo', async ({ page }) => {
    await page.goto('/about');

    await page.getByRole('link', { name: /לוגו|דף הבית/ }).click();

    await expect(page).toHaveURL('/');
  });
});

פתרון תרגיל 2

// e2e/auth.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Authentication', () => {
  test('should login successfully', 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');
    await expect(page.getByText('שלום, דני')).toBeVisible();
  });

  test('should show error for wrong password', async ({ page }) => {
    await page.goto('/login');

    await page.getByLabel('אימייל').fill('user@example.com');
    await page.getByLabel('סיסמה').fill('wrongpassword');
    await page.getByRole('button', { name: 'התחבר' }).click();

    await expect(page.getByText('אימייל או סיסמה שגויים')).toBeVisible();
    await expect(page).toHaveURL('/login');
  });

  test('should show error for invalid email', async ({ page }) => {
    await page.goto('/login');

    await page.getByLabel('אימייל').fill('notanemail');
    await page.getByLabel('סיסמה').fill('password123');
    await page.getByRole('button', { name: 'התחבר' }).click();

    await expect(page.getByText(/אימייל.*תקין/)).toBeVisible();
  });

  test('should logout successfully', 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');

    // התנתקות
    await page.getByRole('button', { name: 'התנתק' }).click();

    await expect(page).toHaveURL('/');
    await expect(page.getByText('שלום, דני')).not.toBeVisible();
  });

  test('should redirect to login when accessing protected route', async ({ page }) => {
    await page.goto('/dashboard');

    await expect(page).toHaveURL(/\/login/);
  });

  test('should redirect back after login', async ({ page }) => {
    // ניסיון גישה ל-dashboard
    await page.goto('/dashboard');
    await expect(page).toHaveURL(/\/login/);

    // התחברות
    await page.getByLabel('אימייל').fill('user@example.com');
    await page.getByLabel('סיסמה').fill('password123');
    await page.getByRole('button', { name: 'התחבר' }).click();

    // אמור לחזור ל-dashboard
    await expect(page).toHaveURL('/dashboard');
  });
});

פתרון תרגיל 3

// e2e/contact.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Contact Form', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('/contact');
  });

  test('should show validation errors for empty form', async ({ page }) => {
    await page.getByRole('button', { name: 'שלח' }).click();

    await expect(page.getByText('שם הוא שדה חובה')).toBeVisible();
    await expect(page.getByText('אימייל הוא שדה חובה')).toBeVisible();
    await expect(page.getByText('הודעה היא שדה חובה')).toBeVisible();
  });

  test('should show error for invalid email', async ({ page }) => {
    await page.getByLabel('שם').fill('דני');
    await page.getByLabel('אימייל').fill('notanemail');
    await page.getByLabel('נושא').selectOption('support');
    await page.getByLabel('הודעה').fill('הודעה ארוכה מספיק לבדיקה בלבד');
    await page.getByRole('button', { name: 'שלח' }).click();

    await expect(page.getByText('אימייל לא תקין')).toBeVisible();
  });

  test('should show error for short message', async ({ page }) => {
    await page.getByLabel('שם').fill('דני');
    await page.getByLabel('אימייל').fill('dani@example.com');
    await page.getByLabel('נושא').selectOption('support');
    await page.getByLabel('הודעה').fill('קצר');
    await page.getByRole('button', { name: 'שלח' }).click();

    await expect(page.getByText(/הודעה חייבת להכיל לפחות 20 תווים/)).toBeVisible();
  });

  test('should submit successfully with valid data', async ({ page }) => {
    await page.getByLabel('שם').fill('דני כהן');
    await page.getByLabel('אימייל').fill('dani@example.com');
    await page.getByLabel('נושא').selectOption('support');
    await page.getByLabel('הודעה').fill('שלום, יש לי שאלה לגבי השירות שלכם. אשמח לקבל מענה.');
    await page.getByRole('button', { name: 'שלח' }).click();

    await expect(page.getByText('ההודעה נשלחה בהצלחה')).toBeVisible();
  });

  test('should clear form after successful submission', async ({ page }) => {
    await page.getByLabel('שם').fill('דני כהן');
    await page.getByLabel('אימייל').fill('dani@example.com');
    await page.getByLabel('נושא').selectOption('support');
    await page.getByLabel('הודעה').fill('הודעה ארוכה מספיק לבדיקה של הטופס הזה.');
    await page.getByRole('button', { name: 'שלח' }).click();

    await expect(page.getByText('ההודעה נשלחה בהצלחה')).toBeVisible();
    await expect(page.getByLabel('שם')).toHaveValue('');
    await expect(page.getByLabel('אימייל')).toHaveValue('');
  });
});

פתרון תרגיל 4

// e2e/products.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Products Page', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('/products');
  });

  test('should display product list', async ({ page }) => {
    const products = page.getByTestId('product-card');
    await expect(products.first()).toBeVisible();
    const count = await products.count();
    expect(count).toBeGreaterThan(0);
  });

  test('should filter products by search', async ({ page }) => {
    await page.getByPlaceholder('חיפוש מוצרים...').fill('אוזניות');

    const products = page.getByTestId('product-card');
    const count = await products.count();

    for (let i = 0; i < count; i++) {
      await expect(products.nth(i)).toContainText(/אוזניות/i);
    }
  });

  test('should show no results for invalid search', async ({ page }) => {
    await page.getByPlaceholder('חיפוש מוצרים...').fill('xyzabc123');

    await expect(page.getByText('לא נמצאו מוצרים')).toBeVisible();
  });

  test('should filter by category', async ({ page }) => {
    await page.getByLabel('קטגוריה').selectOption('electronics');

    const products = page.getByTestId('product-card');
    await expect(products.first()).toBeVisible();
  });

  test('should sort by price ascending', async ({ page }) => {
    await page.getByLabel('מיון').selectOption('price-asc');

    const prices = page.locator('[data-testid="product-price"]');
    const firstPrice = await prices.first().textContent();
    const secondPrice = await prices.nth(1).textContent();

    const price1 = parseFloat(firstPrice!.replace(/[^\d.]/g, ''));
    const price2 = parseFloat(secondPrice!.replace(/[^\d.]/g, ''));

    expect(price1).toBeLessThanOrEqual(price2);
  });

  test('should navigate between pages', async ({ page }) => {
    const firstPageFirstProduct = await page.getByTestId('product-card').first().textContent();

    await page.getByRole('button', { name: /עמוד 2|הבא/ }).click();

    await expect(page).toHaveURL(/page=2/);
    const secondPageFirstProduct = await page.getByTestId('product-card').first().textContent();
    expect(secondPageFirstProduct).not.toBe(firstPageFirstProduct);
  });

  test('should combine search and category filter', async ({ page }) => {
    await page.getByLabel('קטגוריה').selectOption('electronics');
    await page.getByPlaceholder('חיפוש מוצרים...').fill('Sony');

    const products = page.getByTestId('product-card');
    const count = await products.count();
    expect(count).toBeGreaterThan(0);

    for (let i = 0; i < count; i++) {
      await expect(products.nth(i)).toContainText(/Sony/i);
    }
  });
});

פתרון תרגיל 5

// e2e/pages/ProductsPage.ts
import { Page, Locator } from '@playwright/test';

export class ProductsPage {
  private page: Page;

  constructor(page: Page) {
    this.page = page;
  }

  async goto() {
    await this.page.goto('/products');
  }

  async search(query: string) {
    await this.page.getByPlaceholder('חיפוש מוצרים...').fill(query);
  }

  async clickProduct(name: string) {
    await this.page.getByText(name).click();
  }

  getProducts() {
    return this.page.getByTestId('product-card');
  }
}

// e2e/pages/ProductDetailPage.ts
export class ProductDetailPage {
  private page: Page;

  constructor(page: Page) {
    this.page = page;
  }

  async setQuantity(qty: number) {
    await this.page.getByLabel('כמות').fill(qty.toString());
  }

  async addToCart() {
    await this.page.getByRole('button', { name: 'הוסף לסל' }).click();
  }
}

// e2e/pages/CartPage.ts
export class CartPage {
  private page: Page;

  constructor(page: Page) {
    this.page = page;
  }

  async goto() {
    await this.page.goto('/cart');
  }

  async proceedToCheckout() {
    await this.page.getByRole('button', { name: 'לתשלום' }).click();
  }

  getItems() {
    return this.page.getByTestId('cart-item');
  }

  getTotal() {
    return this.page.getByTestId('cart-total');
  }
}
// e2e/purchase.spec.ts
import { test, expect } from '@playwright/test';
import { ProductsPage } from './pages/ProductsPage';
import { ProductDetailPage } from './pages/ProductDetailPage';
import { CartPage } from './pages/CartPage';

test('should complete full purchase flow', async ({ page }) => {
  // שלב 1: חיפוש ובחירת מוצר ראשון
  const productsPage = new ProductsPage(page);
  await productsPage.goto();
  await productsPage.search('אוזניות');
  await productsPage.clickProduct('אוזניות Sony');

  // שלב 2: בחירת כמות והוספה לסל
  const productPage = new ProductDetailPage(page);
  await productPage.setQuantity(2);
  await productPage.addToCart();
  await expect(page.getByText('המוצר נוסף לסל')).toBeVisible();

  // שלב 3: חזרה למוצרים והוספת מוצר נוסף
  await page.getByRole('link', { name: 'המשך קניות' }).click();
  await productsPage.search('כבל');
  await productsPage.clickProduct('כבל USB-C');

  const productPage2 = new ProductDetailPage(page);
  await productPage2.addToCart();

  // שלב 4: מעבר לסל ובדיקת סיכום
  const cartPage = new CartPage(page);
  await cartPage.goto();
  await expect(cartPage.getItems()).toHaveCount(2);

  // שלב 5: מעבר לתשלום
  await cartPage.proceedToCheckout();
  await expect(page).toHaveURL('/checkout');

  // שלב 6: פרטי משלוח
  await page.getByLabel('שם מלא').fill('דני כהן');
  await page.getByLabel('אימייל').fill('dani@example.com');
  await page.getByLabel('טלפון').fill('050-1234567');
  await page.getByLabel('כתובת').fill('הרצל 1');
  await page.getByLabel('עיר').fill('תל אביב');
  await page.getByLabel('מיקוד').fill('6120101');

  // שלב 7: המשך לתשלום
  await page.getByRole('button', { name: 'המשך לתשלום' }).click();

  // שלב 8: פרטי תשלום
  await page.getByLabel('מספר כרטיס').fill('4111111111111111');
  await page.getByLabel('תוקף').fill('12/28');
  await page.getByLabel('CVV').fill('123');

  // שלב 9: אישור הזמנה
  await page.getByRole('button', { name: 'בצע הזמנה' }).click();

  // שלב 10: בדיקת דף אישור
  await expect(page).toHaveURL(/\/order\/.*/);
  await expect(page.getByText('ההזמנה בוצעה בהצלחה')).toBeVisible();
  await expect(page.getByText(/מספר הזמנה/)).toBeVisible();
  await expect(page.getByText('אוזניות Sony')).toBeVisible();
  await expect(page.getByText('כבל USB-C')).toBeVisible();
});

פתרון תרגיל 6

// e2e/responsive.spec.ts
import { test, expect } from '@playwright/test';

const viewports = {
  mobile: { width: 375, height: 667 },
  tablet: { width: 768, height: 1024 },
  desktop: { width: 1440, height: 900 },
};

test.describe('Responsive Design', () => {
  for (const [device, viewport] of Object.entries(viewports)) {
    test.describe(`${device} (${viewport.width}x${viewport.height})`, () => {
      test.use({ viewport });

      test('should display homepage correctly', async ({ page }) => {
        await page.goto('/');

        // כותרת ראשית
        await expect(page.getByRole('heading', { level: 1 })).toBeVisible();

        // תמונת hero
        await expect(page.getByAltText(/hero/i)).toBeVisible();

        // צילום מסך
        await expect(page).toHaveScreenshot(`homepage-${device}.png`, {
          maxDiffPixelRatio: 0.02,
        });
      });

      test(`should display navigation correctly on ${device}`, async ({ page }) => {
        await page.goto('/');

        if (device === 'mobile') {
          // במובייל - כפתור hamburger
          await expect(page.getByRole('button', { name: /תפריט/ })).toBeVisible();

          // פתיחת תפריט
          await page.getByRole('button', { name: /תפריט/ }).click();
          await expect(page.getByRole('link', { name: 'אודות' })).toBeVisible();
        } else {
          // בדסקטופ - קישורים גלויים
          await expect(page.getByRole('link', { name: 'אודות' })).toBeVisible();
          await expect(page.getByRole('link', { name: 'צור קשר' })).toBeVisible();
        }
      });

      test(`should display product cards correctly on ${device}`, async ({ page }) => {
        await page.goto('/products');

        const cards = page.getByTestId('product-card');
        await expect(cards.first()).toBeVisible();

        // צילום מסך של אזור המוצרים
        const productsSection = page.locator('.products-grid');
        await expect(productsSection).toHaveScreenshot(`products-${device}.png`, {
          maxDiffPixelRatio: 0.02,
        });
      });
    });
  }
});

תשובות לשאלות

1. בדיקות אינטגרציה בודקות שמספר חלקים של הקוד עובדים יחד, אבל בסביבה מבוקרת (jsdom, mocks). בדיקות E2E רצות בדפדפן אמיתי ובודקות את כל ה-stack (פרונטאנד, שרת, מסד נתונים). כדאי להשתמש ב-E2E לתהליכים קריטיים (התחברות, רכישה) ובאינטגרציה לבדיקת קומפוננטות ולוגיקה.

2. Playwright ממתין אוטומטית שהאלמנט יהיה גלוי, מופעל, ויציב לפני שמבצע פעולה. sleep ידני הוא בעייתי כי זמן קבוע עלול להיות קצר מדי (בדיקה נכשלת) או ארוך מדי (בדיקות איטיות). ההמתנה האוטומטית מותאמת לכל מצב ומבטיחה בדיקות מהירות ואמינות.

3. Page Object Model הוא תבנית עיצוב שמקבצת את כל ה-locators והפעולות של דף אחד למחלקה. היתרון: אם ה-UI משתנה (למשל label של כפתור), צריך לעדכן רק מקום אחד במקום בכל הבדיקות. זה משפר תחזוקה ומקל על שימוש חוזר.

4. Visual Regression Testing שימושי כשחשוב שהעיצוב לא ישתנה (design system, landing pages). לא כדאי להשתמש בו כשהתוכן משתנה לעתים קרובות (דפים דינמיים) כי הבדיקות ייכשלו בכל שינוי תוכן ויצרכו עדכון snapshots תכוף.

5. חסרונות: (1) איטיות - כל בדיקה לוקחת שניות. (2) שבריריות (flaky) - עלולות להיכשל בגלל תזמון, רשת, או שינויי UI. (3) תחזוקה מורכבת - שינויים ב-UI דורשים עדכון בדיקות. (4) יקרות - דורשות תשתית (דפדפנים, CI). מתמודדים עם: מעט בדיקות E2E ממוקדות, auto-wait, retries, ו-Page Object Model.