| name | e2e-testing |
| description | Playwright E2E testing patterns, Page Object Model, configuration, CI/CD integration, artifact management, and flaky test strategies. |
| origin | ECC |
E2E Testing Patterns
Comprehensive Playwright patterns for building stable, fast, and maintainable E2E test suites.
Test File Organization
tests/
├── e2e/
│ ├── auth/
│ │ ├── login.spec.ts
│ │ ├── logout.spec.ts
│ │ └── register.spec.ts
│ ├── features/
│ │ ├── browse.spec.ts
│ │ ├── search.spec.ts
│ │ └── create.spec.ts
│ └── api/
│ └── endpoints.spec.ts
├── fixtures/
│ ├── auth.ts
│ └── data.ts
└── playwright.config.ts
Page Object Model (POM)
import { Page, Locator } from "@playwright/test";
export class ItemsPage {
readonly page: Page;
readonly searchInput: Locator;
readonly itemCards: Locator;
readonly createButton: Locator;
constructor(page: Page) {
this.page = page;
this.searchInput = page.locator('[data-testid="search-input"]');
this.itemCards = page.locator('[data-testid="item-card"]');
this.createButton = page.locator('[data-testid="create-btn"]');
}
async goto() {
await this.page.goto("/items");
await this.page.waitForLoadState("networkidle");
}
async search(query: string) {
await this.searchInput.fill(query);
await this.page.waitForResponse((resp) => resp.url().includes("/api/search"));
await this.page.waitForLoadState("networkidle");
}
async getItemCount() {
return await this.itemCards.count();
}
}
Test Structure
import { test, expect } from "@playwright/test";
import { ItemsPage } from "../../pages/ItemsPage";
test.describe("Item Search", () => {
let itemsPage: ItemsPage;
test.beforeEach(async ({ page }) => {
itemsPage = new ItemsPage(page);
await itemsPage.goto();
});
test("should search by keyword", async ({ page }) => {
await itemsPage.search("test");
const count = await itemsPage.getItemCount();
expect(count).toBeGreaterThan(0);
await expect(itemsPage.itemCards.first()).toContainText(/test/i);
await page.screenshot({ path: "artifacts/search-results.png" });
});
test("should handle no results", async ({ page }) => {
await itemsPage.search("xyznonexistent123");
await expect(page.locator('[data-testid="no-results"]')).toBeVisible();
expect(await itemsPage.getItemCount()).toBe(0);
});
});
Playwright Configuration
import { defineConfig, devices } from "@playwright/test";
export default defineConfig({
testDir: "./tests/e2e",
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: [
["html", { outputFolder: "playwright-report" }],
["junit", { outputFile: "playwright-results.xml" }],
["json", { outputFile: "playwright-results.json" }],
],
use: {
baseURL: process.env.BASE_URL || "http://localhost:3000",
trace: "on-first-retry",
screenshot: "only-on-failure",
video: "retain-on-failure",
actionTimeout: 10000,
navigationTimeout: 30000,
},
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,
timeout: 120000,
},
});
Flaky Test Patterns
Quarantine
test("flaky: complex search", async ({ page }) => {
test.fixme(true, "Flaky - Issue #123");
});
test("conditional skip", async ({ page }) => {
test.skip(process.env.CI, "Flaky in CI - Issue #123");
});
Identify Flakiness
npx playwright test tests/search.spec.ts --repeat-each=10
npx playwright test tests/search.spec.ts --retries=3
Common Causes & Fixes
Race conditions:
await page.click('[data-testid="button"]');
await page.locator('[data-testid="button"]').click();
Network timing:
await page.waitForTimeout(5000);
await page.waitForResponse((resp) => resp.url().includes("/api/data"));
Animation timing:
await page.click('[data-testid="menu-item"]');
await page.locator('[data-testid="menu-item"]').waitFor({ state: "visible" });
await page.waitForLoadState("networkidle");
await page.locator('[data-testid="menu-item"]').click();
Artifact Management
Screenshots
await page.screenshot({ path: "artifacts/after-login.png" });
await page.screenshot({ path: "artifacts/full-page.png", fullPage: true });
await page.locator('[data-testid="chart"]').screenshot({ path: "artifacts/chart.png" });
Traces
await browser.startTracing(page, {
path: "artifacts/trace.json",
screenshots: true,
snapshots: true,
});
await browser.stopTracing();
Video
use: {
video: 'retain-on-failure',
videosPath: 'artifacts/videos/'
}
CI/CD Integration
name: E2E Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npx playwright install --with-deps
- run: npx playwright test
env:
BASE_URL: ${{ vars.STAGING_URL }}
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30
Test Report Template
# E2E Test Report
**Date:** YYYY-MM-DD HH:MM
**Duration:** Xm Ys
**Status:** PASSING / FAILING
## Summary
- Total: X | Passed: Y (Z%) | Failed: A | Flaky: B | Skipped: C
## Failed Tests
### test-name
**File:** `tests/e2e/feature.spec.ts:45`
**Error:** Expected element to be visible
**Screenshot:** artifacts/failed.png
**Recommended Fix:** [description]
## Artifacts
- HTML Report: playwright-report/index.html
- Screenshots: artifacts/\*.png
- Videos: artifacts/videos/\*.webm
- Traces: artifacts/\*.zip
Wallet / Web3 Testing
test("wallet connection", async ({ page, context }) => {
await context.addInitScript(() => {
window.ethereum = {
isMetaMask: true,
request: async ({ method }) => {
if (method === "eth_requestAccounts") return ["0x1234567890123456789012345678901234567890"];
if (method === "eth_chainId") return "0x1";
},
};
});
await page.goto("/");
await page.locator('[data-testid="connect-wallet"]').click();
await expect(page.locator('[data-testid="wallet-address"]')).toContainText("0x1234");
});
Financial / Critical Flow Testing
test("trade execution", async ({ page }) => {
test.skip(process.env.NODE_ENV === "production", "Skip on production");
await page.goto("/markets/test-market");
await page.locator('[data-testid="position-yes"]').click();
await page.locator('[data-testid="trade-amount"]').fill("1.0");
const preview = page.locator('[data-testid="trade-preview"]');
await expect(preview).toContainText("1.0");
await page.locator('[data-testid="confirm-trade"]').click();
await page.waitForResponse((resp) => resp.url().includes("/api/trade") && resp.status() === 200, {
timeout: 30000,
});
await expect(page.locator('[data-testid="trade-success"]')).toBeVisible();
});