playwright-testing
npx machina-cli add skill fcakyon/claude-codex-settings/playwright-testing --openclawPlaywright Testing Best Practices
Test Organization
File Structure
tests/
├── auth/
│ ├── login.spec.ts
│ └── signup.spec.ts
├── dashboard/
│ └── dashboard.spec.ts
├── fixtures/
│ └── test-data.ts
├── pages/
│ └── login.page.ts
└── playwright.config.ts
Naming Conventions
- Files:
feature-name.spec.ts - Tests: Describe user behavior, not implementation
- Good:
test('user can reset password via email') - Bad:
test('test reset password')
Page Object Model
Basic Pattern
// pages/login.page.ts
export class LoginPage {
constructor(private page: Page) {}
async goto() {
await this.page.goto("/login");
}
async login(email: string, password: string) {
await this.page.getByLabel("Email").fill(email);
await this.page.getByLabel("Password").fill(password);
await this.page.getByRole("button", { name: "Sign in" }).click();
}
}
// tests/login.spec.ts
test("successful login", async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login("user@example.com", "password");
await expect(page).toHaveURL("/dashboard");
});
Locator Strategies
Priority Order (Best to Worst)
getByRole- Accessible, resilientgetByLabel- Form inputsgetByPlaceholder- When no labelgetByText- Visible textgetByTestId- When no better option- CSS/XPath - Last resort
Examples
// Preferred
await page.getByRole("button", { name: "Submit" }).click();
await page.getByLabel("Email address").fill("user@example.com");
// Acceptable
await page.getByTestId("submit-button").click();
// Avoid
await page.locator("#submit-btn").click();
await page.locator('//button[@type="submit"]').click();
Authentication Handling
Storage State (Recommended)
Save logged-in state and reuse across tests:
// global-setup.ts
async function globalSetup() {
const browser = await chromium.launch();
const page = await browser.newPage();
await page.goto("/login");
await page.getByLabel("Email").fill(process.env.TEST_USER_EMAIL);
await page.getByLabel("Password").fill(process.env.TEST_USER_PASSWORD);
await page.getByRole("button", { name: "Sign in" }).click();
await page.waitForURL("/dashboard");
await page.context().storageState({ path: "auth.json" });
await browser.close();
}
// playwright.config.ts
export default defineConfig({
globalSetup: "./global-setup.ts",
use: {
storageState: "auth.json",
},
});
Multi-User Scenarios
// Create different auth states
const adminAuth = "admin-auth.json";
const userAuth = "user-auth.json";
test.describe("admin features", () => {
test.use({ storageState: adminAuth });
// Admin tests
});
test.describe("user features", () => {
test.use({ storageState: userAuth });
// User tests
});
File Upload Handling
Basic Upload
// Single file
await page.getByLabel("Upload file").setInputFiles("path/to/file.pdf");
// Multiple files
await page
.getByLabel("Upload files")
.setInputFiles(["path/to/file1.pdf", "path/to/file2.pdf"]);
// Clear file input
await page.getByLabel("Upload file").setInputFiles([]);
Drag and Drop Upload
// Create file from buffer
const buffer = Buffer.from("file content");
await page.getByTestId("dropzone").dispatchEvent("drop", {
dataTransfer: {
files: [{ name: "test.txt", mimeType: "text/plain", buffer }],
},
});
File Download
const downloadPromise = page.waitForEvent("download");
await page.getByRole("button", { name: "Download" }).click();
const download = await downloadPromise;
await download.saveAs("downloads/" + download.suggestedFilename());
Waiting Strategies
Auto-Wait (Preferred)
Playwright auto-waits for elements. Use assertions:
// Auto-waits for element to be visible and stable
await page.getByRole("button", { name: "Submit" }).click();
// Auto-waits for condition
await expect(page.getByText("Success")).toBeVisible();
Explicit Waits (When Needed)
// Wait for navigation
await page.waitForURL("**/dashboard");
// Wait for network idle
await page.waitForLoadState("networkidle");
// Wait for specific response
await page.waitForResponse((resp) => resp.url().includes("/api/data"));
Network Mocking
Mock API Responses
await page.route("**/api/users", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify([{ id: 1, name: "Test User" }]),
});
});
// Mock error response
await page.route("**/api/users", async (route) => {
await route.fulfill({ status: 500 });
});
Intercept and Modify
await page.route("**/api/data", async (route) => {
const response = await route.fetch();
const json = await response.json();
json.modified = true;
await route.fulfill({ response, json });
});
CI/CD Integration
GitHub Actions Example
- name: Run Playwright tests
run: npx playwright test
env:
CI: true
- name: Upload test results
if: always()
uses: actions/upload-artifact@v3
with:
name: playwright-report
path: playwright-report/
Parallel Execution
// playwright.config.ts
export default defineConfig({
workers: process.env.CI ? 2 : undefined,
fullyParallel: true,
});
Debugging Failed Tests
Debug Tools
# Run with UI mode
npx playwright test --ui
# Run with inspector
npx playwright test --debug
# Show browser
npx playwright test --headed
Trace Viewer
// playwright.config.ts
use: {
trace: 'on-first-retry', // Capture trace on failure
}
Flaky Test Fixes
Common Causes and Solutions
Race conditions:
- Use proper assertions instead of hard waits
- Wait for network requests to complete
Animation issues:
- Disable animations in test config
- Wait for animation to complete
Dynamic content:
- Use flexible locators (text content, not position)
- Wait for loading states to resolve
Test isolation:
- Each test should set up its own state
- Don't depend on other tests' side effects
Anti-Patterns to Avoid
// Bad: Hard sleep
await page.waitForTimeout(5000);
// Good: Wait for condition
await expect(page.getByText("Loaded")).toBeVisible();
// Bad: Flaky selector
await page.locator(".btn:nth-child(3)").click();
// Good: Semantic selector
await page.getByRole("button", { name: "Submit" }).click();
Responsive Design Testing
For comprehensive responsive testing across viewport breakpoints, use the responsive-tester agent. It automatically:
- Tests pages across 7 standard breakpoints (375px to 1536px)
- Detects horizontal overflow issues
- Verifies mobile-first design patterns
- Checks touch target sizes (44x44px minimum)
- Flags anti-patterns like fixed widths without mobile fallback
Trigger it by asking to "test responsiveness", "check breakpoints", or "test mobile/desktop layout".
Source
git clone https://github.com/fcakyon/claude-codex-settings/blob/main/plugins/playwright-tools/skills/playwright-testing/SKILL.mdView on GitHub Overview
Guides structuring Playwright tests with a solid file layout, a Page Object Model, and reliable locator strategies. It covers authentication handling, file uploads, and strategies to reduce flaky tests, helping teams build stable end-to-end tests.
How This Skill Works
Prescribes organizing tests in a clear directory structure (tests, fixtures, pages, and config). Demonstrates a Page Object Model with login.page.ts and tests/login.spec.ts, and defines locator strategies with a priority order (getByRole, getByLabel, getByPlaceholder, getByText, getByTestId) and storageState-based authentication.
When to Use It
- When implementing authentication flows and login tests with Playwright
- When testing file uploads (single, multiple, drag-and-drop)
- When you want stable tests across sessions using storageState
- When building scalable test suites with a clear file structure and naming conventions
- When fixing flaky tests and supporting multi-user scenarios
Quick Start
- Step 1: Create a structured project with tests/auth, tests/dashboard, fixtures/, pages/, and a basic playwright.config.ts.
- Step 2: Implement authentication using storageState by adding a global-setup.ts that logs in and saves auth.json, then configure storageState in playwright.config.ts.
- Step 3: Write a sample test (e.g., login) that uses a Page Object (LoginPage) and locator strategies like getByRole and getByLabel.
Best Practices
- Create a logical file structure under tests/ with auth/, dashboard/, fixtures/, pages/, and a dedicated playwright.config.ts.
- Name tests to describe user behavior, using feature-name.spec.ts and tests like test('user can reset password via email').
- Implement Page Object Model; place reusable page classes in pages/ and use them in tests.
- Follow Locator Strategies priority: getByRole, getByLabel, getByPlaceholder, getByText, getByTestId; avoid raw selectors.
- Use Storage State to save and reuse authentication across tests; set up globalSetup to log in and export auth.json; configure tests to use storageState; support multi-user states.
Example Use Cases
- Login test using a Page Object Model and a simple flow to verify navigation to the dashboard.
- LoginPage class example in pages/login.page.ts with methods goto and login for reusability in tests.
- Global setup that logs in and stores storageState to auth.json; playwright.config.ts configured to load storageState.
- Multi-user tests that switch between admin-auth.json and user-auth.json to cover different roles.
- File upload handling demonstrating single file, multiple files, and drag-and-drop upload methods.