Playwright E2E Testing: Page Objects and Best Practices

Build robust Playwright E2E test suites with page objects, data-testid selectors, multi-browser testing, CI integration, and accessibility assertions for Next.js applications.

E
ECOSIRE Research and Development Team
|19 मार्च 20269 मिनट पढ़ें2.0k शब्द|

नाटककार E2E परीक्षण: पेज ऑब्जेक्ट और सर्वोत्तम अभ्यास

यूनिट परीक्षण सत्यापित करते हैं कि व्यक्तिगत कार्य सही ढंग से व्यवहार करते हैं। एकीकरण परीक्षण सत्यापित करते हैं कि सेवाएँ एक साथ काम करती हैं। लेकिन जब कोई उपयोगकर्ता वास्तव में आपके एप्लिकेशन पर क्लिक करता है तो यह नहीं पता चलता है कि क्या टूटता है - टूटा हुआ नेविगेशन प्रवाह, जावास्क्रिप्ट हाइड्रेशन त्रुटियां, नेटवर्क-निर्भर रेंडरिंग विफलताएं, और क्रॉस-ब्राउज़र रेंडरिंग बग। प्लेराइट के साथ एंड-टू-एंड परीक्षण इस अंतर को भरते हैं, और वे अपने साइप्रस या सेलेनियम पूर्ववर्तियों की तुलना में कहीं अधिक विश्वसनीय हैं।

नाटककार वास्तविक क्रोमियम, फ़ायरफ़ॉक्स और वेबकिट ब्राउज़र को समानांतर में चलाता है। यह इंटरैक्ट करने से पहले तत्वों के दृश्यमान और स्थिर होने की स्वतः प्रतीक्षा करता है। यह विफलता पर स्क्रीनशॉट, वीडियो और नेटवर्क ट्रेस कैप्चर करता है। और इसका टाइपस्क्रिप्ट-प्रथम एपीआई रखरखाव योग्य परीक्षणों को लिखने को आनंददायक बनाता है। यह मार्गदर्शिका नेक्स्ट.जेएस अनुप्रयोगों के लिए कॉन्फ़िगरेशन से लेकर पेज ऑब्जेक्ट तक सीआई एकीकरण तक सब कुछ कवर करती है।

मुख्य बातें

  • data-testid चयनकर्ताओं का विशेष रूप से उपयोग करें - सीएसएस चयनकर्ता और पाठ चयनकर्ता यूआई परिवर्तनों पर ब्रेक लगाते हैं
  • पेज ऑब्जेक्ट मॉडल (पीओएम) चयनकर्ताओं को पुन: प्रयोज्य कक्षाओं में समाहित करता है - प्रति यूआई तत्व में एक स्थान परिवर्तन
  • हमेशा await page.goto() + await expect(page).toHaveURL() का उपयोग करें - यह कभी न मानें कि नेविगेशन समकालिक रूप से पूरा होता है
  • तेज सीआई रन के लिए test.describe.parallel() और --workers ध्वज का उपयोग करें
  • playwright codegen के साथ परीक्षण रिकॉर्ड करें लेकिन कमिट करने से पहले हमेशा पेज ऑब्जेक्ट पर रिफैक्टर करें
  • कॉन्फ़िगरेशन में screenshot: 'only-on-failure' के माध्यम से विफलता पर स्क्रीनशॉट कैप्चर करें
  • @axe-core/playwright के साथ व्यवहार के साथ-साथ पहुंच का परीक्षण करें
  • परतदार तृतीय-पक्ष कॉल के लिए नकली एपीआई प्रतिक्रियाएँ; महत्वपूर्ण खुशहाल रास्तों के लिए वास्तविक एपीआई का उपयोग करें

विन्यास

// apps/web/playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  timeout: 30_000,
  expect: { timeout: 5_000 },
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 2 : undefined,
  reporter: [
    ['html', { outputFolder: 'playwright-report' }],
    ['json', { outputFile: 'playwright-report/results.json' }],
    ...(process.env.CI ? [['github'] as ['github']] : []),
  ],
  use: {
    baseURL: process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:3000',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
    video: 'on-first-retry',
  },
  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 7'] },
    },
    {
      name: 'mobile-safari',
      use: { ...devices['iPhone 14'] },
    },
  ],
  webServer: {
    command: 'pnpm dev',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
    timeout: 120_000,
  },
});

परीक्षण निर्देशिका संरचना

tests/
  public/           # Tests for unauthenticated public pages
    homepage.spec.ts
    blog.spec.ts
    services.spec.ts
    sitemap.spec.ts
    i18n.spec.ts
    seo.spec.ts
  admin/            # Tests for authenticated admin pages
    contacts.spec.ts
    orders.spec.ts
    dashboard.spec.ts
  fixtures/
    auth.fixture.ts   # Authentication fixtures
    data.fixture.ts   # Test data factories
  pages/            # Page Object Model classes
    HomePage.ts
    BlogPage.ts
    ContactsPage.ts
    LoginPage.ts
  helpers/
    api.helper.ts    # API call helpers for test setup

पेज ऑब्जेक्ट मॉडल

पेज ऑब्जेक्ट मॉडल रखरखाव योग्य E2E परीक्षणों के लिए सबसे महत्वपूर्ण पैटर्न है। जब कोई चयनकर्ता बदलता है, तो आप एक स्थान को अपडेट करते हैं - इसका उपयोग करने वाले प्रत्येक परीक्षण में नहीं।

// tests/pages/LoginPage.ts
import { Page, Locator, expect } from '@playwright/test';

export class LoginPage {
  readonly page: Page;
  readonly emailInput: Locator;
  readonly passwordInput: Locator;
  readonly submitButton: Locator;
  readonly errorMessage: Locator;
  readonly forgotPasswordLink: Locator;

  constructor(page: Page) {
    this.page = page;
    // Always use data-testid selectors
    this.emailInput       = page.getByTestId('login-email');
    this.passwordInput    = page.getByTestId('login-password');
    this.submitButton     = page.getByTestId('login-submit');
    this.errorMessage     = page.getByTestId('login-error');
    this.forgotPasswordLink = page.getByTestId('forgot-password-link');
  }

  async goto() {
    await this.page.goto('/auth/login');
    await expect(this.page).toHaveURL('/auth/login');
    await expect(this.submitButton).toBeVisible();
  }

  async login(email: string, password: string) {
    await this.emailInput.fill(email);
    await this.passwordInput.fill(password);
    await this.submitButton.click();
  }

  async loginAndWaitForRedirect(email: string, password: string, expectedUrl: string) {
    await this.login(email, password);
    await expect(this.page).toHaveURL(expectedUrl, { timeout: 10_000 });
  }

  async expectError(message: string) {
    await expect(this.errorMessage).toBeVisible();
    await expect(this.errorMessage).toContainText(message);
  }
}
// tests/pages/ContactsPage.ts
import { Page, Locator, expect } from '@playwright/test';

export class ContactsPage {
  readonly page: Page;
  readonly addButton: Locator;
  readonly searchInput: Locator;
  readonly table: Locator;
  readonly dialog: Locator;
  readonly nameInput: Locator;
  readonly emailInput: Locator;
  readonly saveButton: Locator;
  readonly successToast: Locator;

  constructor(page: Page) {
    this.page        = page;
    this.addButton   = page.getByTestId('contacts-add-button');
    this.searchInput = page.getByTestId('contacts-search');
    this.table       = page.getByTestId('contacts-table');
    this.dialog      = page.getByTestId('contact-dialog');
    this.nameInput   = page.getByTestId('contact-name-input');
    this.emailInput  = page.getByTestId('contact-email-input');
    this.saveButton  = page.getByTestId('contact-save-button');
    this.successToast = page.getByTestId('toast-success');
  }

  async goto() {
    await this.page.goto('/dashboard/contacts');
    await expect(this.table).toBeVisible();
  }

  async openAddDialog() {
    await this.addButton.click();
    await expect(this.dialog).toBeVisible();
  }

  async createContact(name: string, email: string) {
    await this.openAddDialog();
    await this.nameInput.fill(name);
    await this.emailInput.fill(email);
    await this.saveButton.click();
    await expect(this.successToast).toBeVisible();
  }

  async searchFor(query: string) {
    await this.searchInput.fill(query);
    await this.page.waitForTimeout(500); // Debounce delay
  }

  async getRowCount() {
    return this.table.getByRole('row').count();
  }

  async expectContactInTable(name: string) {
    await expect(this.table).toContainText(name);
  }
}

प्रमाणीकरण फिक्स्चर

// tests/fixtures/auth.fixture.ts
import { test as base, Page } from '@playwright/test';

type AuthFixture = {
  adminPage: Page;
  userPage: Page;
};

export const test = base.extend<AuthFixture>({
  adminPage: async ({ browser }, use) => {
    const context = await browser.newContext();
    const page = await context.newPage();

    // Login via API to bypass UI (faster, more reliable)
    const response = await page.request.post('/auth/login', {
      data: {
        email: process.env.TEST_ADMIN_EMAIL || '[email protected]',
        password: process.env.TEST_ADMIN_PASSWORD || 'testpassword123',
      },
    });

    if (!response.ok()) {
      throw new Error(`Auth failed: ${response.status()}`);
    }

    // Cookies are set automatically on the context
    await use(page);
    await context.close();
  },

  userPage: async ({ browser }, use) => {
    const context = await browser.newContext();
    const page = await context.newPage();

    await page.request.post('/auth/login', {
      data: {
        email: process.env.TEST_USER_EMAIL || '[email protected]',
        password: process.env.TEST_USER_PASSWORD || 'testpassword123',
      },
    });

    await use(page);
    await context.close();
  },
});

export { expect } from '@playwright/test';

लेखन परीक्षण

// tests/admin/contacts.spec.ts
import { test, expect } from '../fixtures/auth.fixture';
import { ContactsPage } from '../pages/ContactsPage';

test.describe('Contacts Management', () => {
  test('admin can create, search, and delete a contact', async ({ adminPage }) => {
    const contactsPage = new ContactsPage(adminPage);
    const testName = `Test User ${Date.now()}`;
    const testEmail = `test-${Date.now()}@example.com`;

    await contactsPage.goto();

    // Create
    await contactsPage.createContact(testName, testEmail);
    await contactsPage.expectContactInTable(testName);

    // Search
    await contactsPage.searchFor(testName);
    const rowCount = await contactsPage.getRowCount();
    expect(rowCount).toBe(2); // 1 header + 1 result

    // Delete
    await adminPage.getByTestId(`contact-delete-${testEmail}`).click();
    await adminPage.getByTestId('confirm-delete').click();
    await expect(contactsPage.successToast).toBeVisible();
    await expect(contactsPage.table).not.toContainText(testName);
  });

  test('shows validation errors for invalid contact data', async ({ adminPage }) => {
    const contactsPage = new ContactsPage(adminPage);
    await contactsPage.goto();
    await contactsPage.openAddDialog();

    // Submit without filling required fields
    await contactsPage.saveButton.click();

    await expect(adminPage.getByTestId('error-name')).toContainText('required');
    await expect(adminPage.getByTestId('error-email')).toContainText('required');
  });
});

सार्वजनिक पृष्ठ परीक्षण

// tests/public/homepage.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Homepage', () => {
  test('renders hero section with correct heading', async ({ page }) => {
    await page.goto('/');
    await expect(page).toHaveTitle(/ECOSIRE/);

    const hero = page.getByTestId('hero-heading');
    await expect(hero).toBeVisible();
    await expect(hero).not.toBeEmpty();
  });

  test('navigation links are all accessible', async ({ page }) => {
    await page.goto('/');

    const navLinks = page.getByRole('navigation').getByRole('link');
    const count = await navLinks.count();

    for (let i = 0; i < count; i++) {
      const link = navLinks.nth(i);
      const href = await link.getAttribute('href');
      // All nav links should have valid hrefs
      expect(href).toBeTruthy();
      expect(href).not.toBe('#');
    }
  });

  test('CTA button links to services page', async ({ page }) => {
    await page.goto('/');
    await page.getByTestId('hero-cta').click();
    await expect(page).toHaveURL(/\/services/);
  });
});

i18n टेस्ट

// tests/public/i18n.spec.ts
import { test, expect } from '@playwright/test';

const testLocales = ['zh', 'es', 'ar', 'fr'];

test.describe('Internationalization', () => {
  for (const locale of testLocales) {
    test(`/${locale}/ renders with correct lang attribute`, async ({ page }) => {
      await page.goto(`/${locale}/`);

      const html = page.locator('html');
      await expect(html).toHaveAttribute('lang', locale);
    });
  }

  test('Arabic locale sets dir="rtl"', async ({ page }) => {
    await page.goto('/ar/');
    const html = page.locator('html');
    await expect(html).toHaveAttribute('dir', 'rtl');
  });

  test('language switcher changes locale', async ({ page }) => {
    await page.goto('/');
    await page.getByTestId('language-switcher').click();
    await page.getByTestId('locale-option-fr').click();
    await expect(page).toHaveURL('/fr/');
    await expect(page.locator('html')).toHaveAttribute('lang', 'fr');
  });

  test('hreflang tags are present on all pages', async ({ page }) => {
    await page.goto('/');
    const hreflangs = await page.locator('link[rel="alternate"][hreflang]').all();
    // Should have 11 locale hreflangs + x-default
    expect(hreflangs.length).toBeGreaterThanOrEqual(12);
  });
});

अभिगम्यता परीक्षण

// tests/public/accessibility.spec.ts
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';

test.describe('Accessibility', () => {
  const pages = ['/', '/blog', '/services', '/about', '/contact'];

  for (const path of pages) {
    test(`${path} has no WCAG 2.1 AA violations`, async ({ page }) => {
      await page.goto(path);

      const results = await new AxeBuilder({ page })
        .withTags(['wcag2a', 'wcag2aa', 'wcag21aa'])
        .analyze();

      if (results.violations.length > 0) {
        const report = results.violations.map((v) =>
          `${v.id}: ${v.description} — ${v.nodes.length} element(s)`
        ).join('\n');
        expect.fail(`Accessibility violations on ${path}:\n${report}`);
      }
    });
  }

  test('skip link is the first focusable element', async ({ page }) => {
    await page.goto('/');
    await page.keyboard.press('Tab');
    const focused = page.locator(':focus');
    await expect(focused).toHaveAttribute('href', '#main-content');
  });
});

सीआई एकीकरण

# .github/workflows/ci.yml (E2E section)
e2e-tests:
  runs-on: ubuntu-latest
  needs: [unit-tests, build]
  steps:
    - uses: actions/checkout@v4
    - uses: pnpm/action-setup@v3
    - uses: actions/setup-node@v4
      with:
        node-version: 22
        cache: 'pnpm'
    - run: pnpm install --frozen-lockfile

    - name: Install Playwright browsers
      run: cd apps/web && npx playwright install --with-deps chromium firefox

    - name: Run E2E tests
      run: cd apps/web && npx playwright test --project=chromium --project=firefox
      env:
        PLAYWRIGHT_BASE_URL: ${{ secrets.STAGING_URL }}
        TEST_ADMIN_EMAIL: ${{ secrets.TEST_ADMIN_EMAIL }}
        TEST_ADMIN_PASSWORD: ${{ secrets.TEST_ADMIN_PASSWORD }}

    - uses: actions/upload-artifact@v4
      if: failure()
      with:
        name: playwright-report
        path: apps/web/playwright-report/
        retention-days: 7

अक्सर पूछे जाने वाले प्रश्न

मुझे data-testid बनाम ARIA भूमिकाओं बनाम पाठ चयनकर्ताओं का उपयोग कब करना चाहिए?

प्राथमिक चयनकर्ता के रूप में data-testid को प्राथमिकता दें - यह यूआई रीडिज़ाइन, कॉपी परिवर्तन और रिफैक्टर्स से बचता है। एक साथ पहुंच का परीक्षण करने के लिए ARIA भूमिका चयनकर्ताओं (getByRole('button', { name: 'Submit' })) का उपयोग करें। टेक्स्ट सामग्री चयनकर्ताओं (getByText('Submit')) से बचें क्योंकि कॉपी बदलने पर वे टूट जाते हैं। कभी भी सीएसएस वर्ग चयनकर्ताओं का उपयोग न करें - वे कार्यान्वयन विवरण हैं जो स्टाइलिंग कार्य के साथ बदलते हैं।

मुझे सीआई में कितने प्रोजेक्ट (ब्राउज़र) चलाने चाहिए?

पुल अनुरोधों के लिए, सीआई को तेज़ रखने के लिए केवल क्रोमियम चलाएँ। मुख्य शाखा विलय के लिए, सभी तीन डेस्कटॉप ब्राउज़र (क्रोमियम, फ़ायरफ़ॉक्स, वेबकिट) चलाएँ। रिलीज़ उम्मीदवारों के लिए, मोबाइल व्यूपोर्ट जोड़ें। वेबकिट सफ़ारी-विशिष्ट लेआउट बग पकड़ता है; फ़ायरफ़ॉक्स उन विचित्रताओं को पकड़ता है जिन्हें क्रोम और सफारी दोनों छिपाते हैं। रिलीज से पहले क्रॉस-ब्राउज़र बग को पकड़ने के दौरान टियरिंग सीआई मिनट बचाता है।

मैं समय संबंधी समस्याओं के कारण होने वाले ख़राब परीक्षणों को कैसे संभाल सकता हूँ?

नाटककार के ऑटो-वेट को अधिकांश समय संबंधी मुद्दों को संभालना चाहिए - page.waitForTimeout() के बजाय await expect(locator).toBeVisible() का उपयोग करें। यदि ऑटो-प्रतीक्षा पर्याप्त नहीं है, तो नेटवर्क-निर्भर स्थिति के लिए page.waitForResponse(), भारी पृष्ठों के लिए page.waitForLoadState('networkidle'), या कस्टम स्थितियों के लिए page.waitForFunction() का उपयोग करें। वास्तविक विफलताओं को बुनियादी ढांचे की शिथिलता से अलग करने के लिए CI में retries: 2 सेट करें।

क्या E2E परीक्षणों को वास्तविक डेटाबेस या वरीयता प्राप्त परीक्षण डेटाबेस का उपयोग करना चाहिए?

E2E परीक्षणों के लिए सीडेड परीक्षण डेटाबेस के साथ एक समर्पित स्टेजिंग वातावरण का उपयोग करें। उत्पादन के विरुद्ध कभी भी E2E परीक्षण न चलाएँ। सीआई के लिए, परीक्षण डेटा के साथ एक ताजा डेटाबेस के साथ डॉकर कंपोज़ स्टैक को स्पिन करें। प्री-सीडेड डेटा पर भरोसा करने के बजाय beforeAll/afterAll हुक में एपीआई कॉल के माध्यम से परीक्षण डेटा बनाएं और साफ़ करें जो बह सकता है।

मैं प्लेराइट में फ़ाइल डाउनलोड का परीक्षण कैसे करूं?

page.waitForDownload() पैटर्न का उपयोग करें: const [download] = await Promise.all([page.waitForEvent('download'), page.click('[data-testid="download-button"]')]); const path = await download.path(); expect(path).toBeTruthy();। आप download.suggestedFilename() भी जांच सकते हैं और सत्यापन के लिए फ़ाइल सामग्री पढ़ सकते हैं।


अगले चरण

उपयोगकर्ताओं द्वारा आपके एप्लिकेशन को देखने से पहले नाटककार E2E परीक्षण अंतिम सुरक्षा जाल हैं। ठीक से निवेश किया गया - पेज ऑब्जेक्ट्स, डेटा-टेस्टिड चयनकर्ताओं, ऑथ फिक्स्चर और सीआई एकीकरण के साथ - वे पकड़ते हैं कि यूनिट परीक्षण क्या चूक गए और आपको शुक्रवार को तैनात करने का विश्वास दिलाते हैं।

ECOSIRE 29 Playwright E2E परीक्षण फ़ाइलों का रखरखाव करता है, जिसमें सार्वजनिक पृष्ठों, व्यवस्थापक वर्कफ़्लो और सभी 11 स्थानों के लिए पहुंच में 416+ परीक्षण मामले शामिल हैं। हमारी फ्रंटएंड इंजीनियरिंग सेवाओं का अन्वेषण करें यह जानने के लिए कि हम गुणवत्ता-युक्त नेक्स्ट.जेएस एप्लिकेशन कैसे बनाते हैं।

शेयर करें:
E

लेखक

ECOSIRE Research and Development Team

ECOSIRE में एंटरप्राइज़-ग्रेड डिजिटल उत्पाद बना रहे हैं। Odoo एकीकरण, ई-कॉमर्स ऑटोमेशन, और AI-संचालित व्यावसायिक समाधानों पर अंतर्दृष्टि साझा कर रहे हैं।

WhatsApp पर चैट करें