Skip to content
Back to skills

E2e Patterns

ASecurity

Write robust E2E tests with Playwright — page objects, resilient waiting, auth, and CI integration.

  • 19 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 19, 2026
ai-agentstypescriptgobashtestingapi

Works with

  • cli
  • api

Security analysis

A100/100

Scanned October 19, 2026

npx -y skills add sawrus/agent-guides --skill e2e-patterns --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of E2e Patterns?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for E2e Patterns
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/sawrus-e2e-patterns/badge)](https://www.skillsdirectory.com/skills/sawrus-e2e-patterns)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: e2e-patterns
type: skill
description: Write robust E2E tests with Playwright — page objects, resilient waiting, auth, and CI integration.
related-rules:
  - test-strategy.md
  - flakiness-policy.md
allowed-tools: Read, Write, Edit, Bash
---

# E2E Testing Patterns (Playwright) Skill

> **Expertise:** Playwright page objects, resilient locators, auth fixtures, parallel execution, CI configuration.

## Page Object Model

```typescript
// pages/OrderPage.ts
export class OrderPage {
  readonly page: Page;

  constructor(page: Page) {
    this.page = page;
  }

  // ✅ Role-based locators — resilient to CSS changes + tests a11y
  get orderItems() { return this.page.getByRole('listitem', { name: /order item/i }); }
  get submitButton() { return this.page.getByRole('button', { name: 'Place order' }); }
  get confirmationMessage() { return this.page.getByText('Order confirmed'); }

  async addItem(productName: string, quantity: number) {
    await this.page.getByLabel('Product').selectOption(productName);
    await this.page.getByLabel('Quantity').fill(String(quantity));
    await this.page.getByRole('button', { name: 'Add to cart' }).click();
  }

  async checkout(address: Address) {
    await this.page.getByLabel('Street').fill(address.street);
    await this.page.getByLabel('City').fill(address.city);
    await this.submitButton.click();
    await expect(this.confirmationMessage).toBeVisible({ timeout: 10000 });
  }
}
```

## Resilient Waiting — Never Use `sleep`

```typescript
// ❌ Arbitrary sleep — flaky and slow
await page.waitForTimeout(2000);

// ✅ Wait for specific condition
await page.waitForURL('**/dashboard');
await expect(page.getByText('Welcome back')).toBeVisible();

// ✅ Wait for network request to complete
await Promise.all([
  page.waitForResponse(resp => resp.url().includes('/api/orders') && resp.status() === 201),
  page.getByRole('button', { name: 'Place order' }).click(),
]);

// ✅ Wait for element to reach a specific state
await expect(page.getByRole('status')).toHaveText('Processing...', { timeout: 5000 });
await expect(page.getByRole('status')).toHaveText('Complete', { timeout: 30000 });
```

## Authentication Fixture (Reuse Across Tests)

```typescript
// fixtures/auth.ts — store session state, avoid re-logging in per test
import { test as base } from '@playwright/test';

export const test = base.extend<{ authenticatedPage: Page }>({
  authenticatedPage: async ({ browser }, use) => {
    // Load stored auth state (set up once with `playwright auth`)
    const context = await browser.newContext({
      storageState: 'tests/.auth/user.json',
    });
    const page = await context.newPage();
    await use(page);
    await context.close();
  },
});

// Set up auth state (run once before test suite)
// playwright.config.ts globalSetup points to this
export async function setupAuth() {
  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: 'tests/.auth/user.json' });
  await browser.close();
}
```

## Playwright Config (CI-Ready)

```typescript
// playwright.config.ts
export default defineConfig({
  testDir: './tests/e2e',
  fullyParallel: true,
  retries: process.env.CI ? 2 : 0,   // Retry on CI only
  workers: process.env.CI ? 4 : 2,
  timeout: 30000,

  reporter: [
    ['html', { outputFolder: 'test-results/html' }],
    ['junit', { outputFile: 'test-results/junit.xml' }],  // For CI parsing
  ],

  use: {
    baseURL: process.env.E2E_BASE_URL || 'http://localhost:3000',
    trace: 'on-first-retry',      // Capture trace on failure
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
  },

  projects: [
    { name: 'setup', testMatch: /global.setup.ts/ },
    {
      name: 'chromium',
      dependencies: ['setup'],
      use: { ...devices['Desktop Chrome'], storageState: 'tests/.auth/user.json' },
    },
  ],
});
```

## Flakiness Prevention Checklist

- [ ] No `waitForTimeout` — all waits target specific conditions
- [ ] Test data created fresh per test (factory functions, not shared fixtures)
- [ ] Tests don't depend on order (can run in any sequence)
- [ ] Network requests mocked when testing UI-only behavior
- [ ] Assertions use `toBeVisible()` not `isVisible()` — auto-waits
- [ ] `retries: 2` in CI config catches transient infrastructure failures (not logic bugs)

## Locator Priority (Most to Least Resilient)

```
1. getByRole()         — semantic, tests a11y
2. getByLabel()        — form elements
3. getByText()         — visible text
4. getByTestId()       — data-testid attribute (explicit contract)
5. locator('css')      — fragile, avoid
6. locator('xpath')    — most fragile, never use
```

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.