| name | swe-developing-e2e-test-with-playwright |
| description | Playwright E2E testing standards from authoritative docs/explanation/software-engineering/automation-testing/tools/playwright/ documentation |
Playwright E2E Testing Standards
Purpose
Progressive disclosure of Playwright end-to-end testing standards for agents writing E2E tests.
Authoritative Source: docs/explanation/software-engineering/automation-testing/tools/playwright/README.md
Usage: Auto-loaded for agents when writing Playwright E2E tests. Provides quick reference to test organization, selectors, assertions, page objects, and debugging patterns.
Quick Standards Reference
Test Organization
File Structure: Group tests by feature or page
tests/
โโโ e2e/
โ โโโ auth/
โ โ โโโ login.spec.ts
โ โ โโโ register.spec.ts
โ โโโ payments/
โ โ โโโ murabaha.spec.ts
โ โ โโโ zakat.spec.ts
โ โโโ navigation.spec.ts
โโโ page-objects/
โ โโโ pages/
โ โ โโโ LoginPage.ts
โ โ โโโ DashboardPage.ts
โ โโโ components/
โ โโโ Header.ts
โ โโโ Sidebar.ts
โโโ fixtures/
โโโ test-data.ts
Naming Conventions:
- Test files:
*.spec.ts (e.g., login.spec.ts)
- Page objects:
PascalCase (e.g., LoginPage.ts)
- Test descriptions: Behavior-focused (e.g., "successful login redirects to dashboard")
Selectors (Accessibility-First)
Priority Order: Role โ Label โ Text โ TestID โ CSS
page.getByRole("button", { name: "Submit" });
page.getByLabel("Email");
page.getByText("Welcome");
page.getByTestId("submit-button");
page.locator("css=.button");
Avoid:
- Overly specific CSS selectors
- XPath unless necessary
- Element IDs that change frequently
- Position-based selectors
Assertions (Web-First)
Auto-Waiting Assertions: Use web-first assertions with automatic retries
await expect(page).toHaveTitle("Dashboard");
await expect(page.getByRole("heading")).toContainText("Welcome");
await expect(page.getByLabel("Email")).toBeVisible();
await expect(page.getByTestId("status")).toHaveText("Success");
const text = await page.getByRole("heading").textContent();
expect(text).toBe("Welcome");
Assertion Types:
- Visibility:
toBeVisible(), toBeHidden()
- Text:
toHaveText(), toContainText()
- Values:
toHaveValue(), toHaveAttribute()
- States:
toBeEnabled(), toBeDisabled(), toBeChecked()
- URL:
toHaveURL(), URL patterns with regex
Page Object Model
Class-Based Pattern: Encapsulate page locators and actions
import { Page, Locator } from "@playwright/test";
export class LoginPage {
readonly page: Page;
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly submitButton: Locator;
constructor(page: Page) {
this.page = page;
this.emailInput = page.getByRole("textbox", { name: "Email" });
this.passwordInput = page.getByRole("textbox", { name: "Password" });
this.submitButton = page.getByRole("button", { name: "Sign in" });
}
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();
}
}
Usage in Tests:
test("successful login", async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login("user@example.com", "password123");
await expect(page).toHaveURL("/dashboard");
});
Configuration Standards
playwright.config.ts: Environment-specific settings
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: process.env.CI ? [["html"], ["junit"]] : "html",
use: {
baseURL: process.env.BASE_URL || "http://localhost:3000",
trace: "on-first-retry",
screenshot: "only-on-failure",
},
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
{
name: "firefox",
use: { ...devices["Desktop Firefox"] },
},
{
name: "webkit",
use: { ...devices["Desktop Safari"] },
},
],
});
Best Practices
Test Isolation: Each test independent
test.describe("User Management", () => {
test.beforeEach(async ({ page }) => {
await page.goto("/users");
await page.getByRole("button", { name: "Add User" }).click();
});
test("creates new user", async ({ page }) => {
await page.getByLabel("Name").fill("John Doe");
await page.getByRole("button", { name: "Save" }).click();
await expect(page.getByText("User created")).toBeVisible();
});
});
API Testing Integration: Combine UI and API
test("user sees their data after login", async ({ page, request }) => {
const response = await request.post("/api/users", {
data: { name: "Test User", email: "test@example.com" },
});
const userId = (await response.json()).id;
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login("test@example.com", "password");
await expect(page.getByText("Test User")).toBeVisible();
await request.delete(`/api/users/${userId}`);
});
Anti-Patterns to Avoid
โ Manual Waits:
await page.click("button");
await page.waitForTimeout(2000);
await page.click("button");
await expect(page.getByText("Success")).toBeVisible();
โ Overly Specific Selectors:
await page.locator("div.container > div:nth-child(2) > button.primary").click();
await page.getByRole("button", { name: "Submit" }).click();
โ Test Interdependence:
test("1. create user", async ({ page }) => {
});
test("2. edit user", async ({ page }) => {
});
test.describe("User Management", () => {
test.beforeEach(async ({ request }) => {
await request.post("/api/users", { data: testUser });
});
test("creates user", async ({ page }) => {
});
test("edits user", async ({ page }) => {
});
});
Debugging Tools
Trace Viewer: Post-failure debugging
npx playwright show-trace trace.zip
Inspector: Step-through debugging
npx playwright test login.spec.ts --debug
Headed Mode: Visual debugging
use: {
headless: false,
slowMo: 500,
},
OSE Platform Context
Islamic Finance Testing
Zakat Calculator Tests:
test("calculates zakat correctly", async ({ page }) => {
await page.goto("/zakat-calculator");
await page.getByLabel("Wealth Amount").fill("100000");
await page.getByRole("button", { name: "Calculate" }).click();
await expect(page.getByTestId("zakat-amount")).toHaveText("RM 2,500.00");
});
Murabaha Contract Tests:
test("murabaha contract workflow", async ({ page }) => {
const murabaha = new MurabahaPage(page);
await murabaha.goto();
await murabaha.createContract({
asset: "Vehicle",
cost: 50000,
profitRate: 5,
});
await expect(page.getByText("Contract Created")).toBeVisible();
await expect(page.getByTestId("total-payment")).toContainText("52,500");
});
Test-Driven Development for E2E
TDD applies to E2E test authoring: write the failing Playwright spec โ or a failing Playwright-MCP
manual verification script โ before the feature implementation exists. Both forms follow
RedโGreenโRefactor:
- Red: Author the
.spec.ts or manual verification script and run it. It must fail because the
feature does not yet exist, not because of a misconfigured test environment.
- Green: The feature implementation makes every Playwright assertion or manual observation pass.
- Refactor: Improve locators, page objects, and fixture composition while keeping all assertions
green.
Manual verification scripts are TDD-compliant when they are written, dated, repeatable, and contain
discrete expected observations (e.g., "Navigate to /products โ snapshot shows product list with 3
items"). Informal "tested manually" notes are not TDD-compliant. Promote manual scripts to
automated Playwright specs whenever the behavior recurs.
Canonical references:
Related Standards
See Authoritative Documentation: