home / skills / fcakyon / claude-codex-settings / playwright-testing

This skill helps you implement reliable Playwright tests by teaching best practices for page objects, locators, authentication, file uploads, and flaky-test

npx playbooks add skill fcakyon/claude-codex-settings --skill playwright-testing

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
7.3 KB
---
name: playwright-testing
description: This skill should be used when user asks about "Playwright", "responsiveness test", "test with playwright", "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

```typescript
// 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("[email protected]", "password");
  await expect(page).toHaveURL("/dashboard");
});
```

## Locator Strategies

### Priority Order (Best to Worst)

1. **`getByRole`** - Accessible, resilient
2. **`getByLabel`** - Form inputs
3. **`getByPlaceholder`** - When no label
4. **`getByText`** - Visible text
5. **`getByTestId`** - When no better option
6. **CSS/XPath** - Last resort

### Examples

```typescript
// Preferred
await page.getByRole("button", { name: "Submit" }).click();
await page.getByLabel("Email address").fill("[email protected]");

// 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:

```typescript
// 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

```typescript
// 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

```typescript
// 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

```typescript
// 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

```typescript
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:

```typescript
// 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)

```typescript
// 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

```typescript
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

```typescript
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

```yaml
- 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

```typescript
// playwright.config.ts
export default defineConfig({
  workers: process.env.CI ? 2 : undefined,
  fullyParallel: true,
});
```

## Debugging Failed Tests

### Debug Tools

```bash
# Run with UI mode
npx playwright test --ui

# Run with inspector
npx playwright test --debug

# Show browser
npx playwright test --headed
```

### Trace Viewer

```typescript
// 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

```typescript
// 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".

Overview

This skill provides practical Playwright testing patterns, utilities, and best practices for reliable end-to-end tests. It focuses on test organization, robust locator strategies, authentication handling, file upload/download, network mocking, CI integration, and flaky-test mitigation. Use it to speed up writing maintainable tests and reduce intermittent failures.

How this skill works

It inspects common Playwright flows and recommends concrete code patterns: page objects, storageState authentication, preferred locator order, and effective waiting strategies. It also shows how to mock network responses, handle file uploads/downloads, capture traces for debugging, and configure CI/parallel runs. Guidance is actionable and ready to plug into Python/TypeScript Playwright projects.

When to use it

  • When writing new UI tests or refactoring brittle suites
  • When automating login flows or multi-user scenarios
  • When adding file upload/download or drag-and-drop tests
  • When tests fail intermittently and you need flaky-test fixes
  • When running tests in CI and needing parallelism or artifacts

Best practices

  • Organize tests by feature folders and use descriptive test names that state user behavior
  • Encapsulate UI actions in page objects to reduce duplication and improve readability
  • Prefer accessible locators: getByRole, getByLabel, getByPlaceholder, then getByTestId; avoid fragile CSS/XPath
  • Reuse authentication via storageState for fast, isolated tests and create separate auth states for different roles
  • Rely on Playwright auto-wait and assertions; use explicit waits only for navigation, network idle, or specific responses
  • Mock network responses to make tests deterministic and speed up CI runs

Example use cases

  • Test a login flow using a LoginPage object and storageState to skip repeated sign-ins
  • Upload single and multiple files and verify server-side behavior or client previews
  • Mock API responses to test error handling and edge cases without hitting production services
  • Capture traces on first retry to debug flaky tests and replay failing sessions locally
  • Run Playwright in CI with limited workers, upload reports, and enable trace capture for failed tests

FAQ

How do I avoid flaky tests caused by timing issues?

Prefer assertions that reflect the desired state and let Playwright auto-wait. Wait for navigation, network idle, or a specific response rather than using fixed timeouts.

What's the fastest way to run authenticated tests?

Perform a single auth flow in a global setup, save context.storageState to a file, and load it in tests with use.storageState. Create separate storageState files per role for multi-user tests.