| name | playwright-testing |
| description | This skill should be used when user asks about "Playwright test", "E2E testing", "browser automation", "test login flow", "file upload test", "handle authentication in tests", or "fix flaky tests". |
Playwright 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();