E2e testing automation
Skill yigityildiz0/universal-ai-skill-library/skills/common/e2e-testing-automation
531 searchable AI Agent Skills for Claude Code, OpenAI Codex, and OpenCode — EN/TR catalog, platform and risk notes, direct ZIPs, and curated bundles.
npx -y skills add yigityildiz0/universal-ai-skill-library --skill e2e-testing-automationAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
3 things to look at
- 18 days oldThe repository was created 18 days ago. New is not bad, but a brand new repository carrying a familiar-sounding name is the shape a typosquat arrives in, and there has been no time for anyone else to find a problem with it.
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
End-to-end testing automation with Playwright, Cypress, and Selenium including page objects, visual regression, and CI integration. Use when implementing.
SKILL.md
21.0 KB, as published. Nobody here has run it
E2E Testing Automation
Comprehensive guidance for implementing end-to-end browser testing using modern frameworks. Covers framework selection, page object patterns, waiting strategies, visual regression, API mocking, authentication fixtures, parallel execution, CI/CD integration, flaky test management, and accessibility testing.
When to Use This Skill
Use this skill for:
- Choosing between Playwright, Cypress, and Selenium for a project
- Implementing the page object model for maintainable test suites
- Writing reliable selectors using data-testid and ARIA roles
- Configuring auto-wait and explicit wait strategies to eliminate flakiness
- Setting up visual regression testing with screenshot comparison
- Mocking or intercepting API responses in browser tests
- Handling authentication in E2E tests without repeated login flows
- Running tests in parallel across multiple browsers
- Integrating E2E tests into CI/CD pipelines (GitHub Actions, GitLab CI)
- Diagnosing and fixing flaky tests
- Adding accessibility checks to existing E2E suites
Trigger phrases: "E2E testing", "end-to-end test", "Playwright", "Cypress", "Selenium", "page object", "visual regression", "flaky test", "browser testing", "test automation", "data-testid", "accessibility testing"
What This Skill Does
Provides production-ready E2E testing patterns including:
- Framework Selection: Decision matrix for Playwright vs Cypress vs Selenium
- Architecture: Page object model, test fixtures, configuration management
- Selectors: Priority hierarchy (ARIA roles, data-testid, CSS, XPath)
- Reliability: Auto-wait, retry strategies, network idle detection
- Visual Testing: Screenshot comparison, threshold tuning, baseline management
- CI Integration: Pipeline configs, parallelization, artifact collection
- Debugging: Trace viewers, video recording, flaky test quarantine
Instructions
Step 1: Select the Right Framework
Framework Decision Matrix:
| Feature | Playwright | Cypress | Selenium |
|---|---|---|---|
| Language support | JS/TS, Python, Java, C# | JS/TS only | All major languages |
| Browser support | Chromium, Firefox, WebKit | Chromium, Firefox, WebKit | All browsers |
| Auto-wait | Built-in | Built-in | Manual |
| Network interception | Native | Native | Requires proxy |
| Multi-tab/window | Yes | Limited | Yes |
| iframes | Easy | Difficult | Moderate |
| Parallel execution | Built-in | Via CI parallelization | Grid/Selenoid |
| Mobile emulation | Device profiles | Viewport only | Appium integration |
| Speed | Fast | Fast | Moderate |
| Debugging | Trace viewer, codegen | Time-travel debugger | Logs only |
Recommendation: Use Playwright for new projects. It provides the best combination of speed, reliability, cross-browser support, and developer experience.
Step 2: Set Up Playwright Project
Installation and Configuration:
# Initialize Playwright project
npm init playwright@latest
# Project structure after setup:
# tests/
# example.spec.ts
# playwright.config.ts
# package.json
Playwright Configuration (playwright.config.ts):
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
outputDir: './test-results',
/* Fail the build on CI if test.only is left in source */
forbidOnly: !!process.env.CI,
/* Retry flaky tests in CI only */
retries: process.env.CI ? 2 : 0,
/* Parallel workers */
workers: process.env.CI ? 4 : undefined,
/* Shared settings for all projects */
use: {
baseURL: process.env.BASE_URL || 'http://localhost:3000',
trace: 'on-first-retry', // Capture trace on failure
screenshot: 'only-on-failure',
video: 'retain-on-failure',
actionTimeout: 10_000,
navigationTimeout: 30_000,
},
/* Reporter configuration */
reporter: process.env.CI
? [['html', { open: 'never' }], ['junit', { outputFile: 'results.xml' }]]
: [['html', { open: 'on-failure' }]],
/* Browser projects */
projects: [
// Desktop browsers
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
// Mobile viewports
{ name: 'mobile-chrome', use: { ...devices['Pixel 7'] } },
{ name: 'mobile-safari', use: { ...devices['iPhone 14'] } },
],
/* Local dev server */
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
timeout: 120_000,
},
});
Step 3: Implement the Page Object Model
Page objects encapsulate page-specific selectors and actions, making tests resilient to UI changes.
Base Page Object:
// tests/pages/base.page.ts
import { Page, Locator } from '@playwright/test';
export abstract class BasePage {
readonly page: Page;
constructor(page: Page) {
this.page = page;
}
/* Navigation helpers */
async goto(path: string): Promise<void> {
await this.page.goto(path);
}
async waitForPageLoad(): Promise<void> {
await this.page.waitForLoadState('networkidle');
}
/* Common element interactions */
async clickAndWait(locator: Locator): Promise<void> {
await locator.click();
await this.page.waitForLoadState('domcontentloaded');
}
/* Toast / notification helpers */
async getToastMessage(): Promise<string> {
const toast = this.page.getByRole('alert');
await toast.waitFor({ state: 'visible' });
return toast.textContent() ?? '';
}
}
Login Page Object:
// tests/pages/login.page.ts
import { Page, Locator, expect } from '@playwright/test';
import { BasePage } from './base.page';
export class LoginPage extends BasePage {
/* Selectors: prefer role-based and data-testid locators */
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly submitButton: Locator;
readonly errorMessage: Locator;
readonly forgotPasswordLink: Locator;
constructor(page: Page) {
super(page);
this.emailInput = page.getByLabel('Email');
this.passwordInput = page.getByLabel('Password');
this.submitButton = page.getByRole('button', { name: 'Sign in' });
this.errorMessage = page.getByTestId('login-error');
this.forgotPasswordLink = page.getByRole('link', { name: 'Forgot password' });
}
async navigate(): Promise<void> {
await this.goto('/login');
}
async login(email: string, password: string): Promise<void> {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
}
async expectErrorVisible(message: string): Promise<void> {
await expect(this.errorMessage).toBeVisible();
await expect(this.errorMessage).toContainText(message);
}
}
Dashboard Page Object:
// tests/pages/dashboard.page.ts
import { Page, Locator, expect } from '@playwright/test';
import { BasePage } from './base.page';
export class DashboardPage extends BasePage {
readonly heading: Locator;
readonly orderTable: Locator;
readonly searchInput: Locator;
readonly filterDropdown: Locator;
readonly exportButton: Locator;
constructor(page: Page) {
super(page);
this.heading = page.getByRole('heading', { name: 'Dashboard' });
this.orderTable = page.getByRole('table', { name: 'Orders' });
this.searchInput = page.getByPlaceholder('Search orders...');
this.filterDropdown = page.getByTestId('status-filter');
this.exportButton = page.getByRole('button', { name: 'Export' });
}
async navigate(): Promise<void> {
await this.goto('/dashboard');
}
async searchOrders(query: string): Promise<void> {
await this.searchInput.fill(query);
// Wait for debounced search to trigger and table to update
await this.page.waitForResponse(resp =>
resp.url().includes('/api/orders') && resp.status() === 200
);
}
async getOrderCount(): Promise<number> {
const rows = this.orderTable.getByRole('row');
// Subtract 1 for the header row
return (await rows.count()) - 1;
}
async filterByStatus(status: string): Promise<void> {
await this.filterDropdown.selectOption(status);
await this.page.waitForLoadState('networkidle');
}
}
Step 4: Write Reliable Selectors
Selector Priority Hierarchy (most resilient to least):
// 1. ARIA roles (best: tied to accessibility semantics)
page.getByRole('button', { name: 'Submit' });
page.getByRole('heading', { name: 'Dashboard', level: 1 });
page.getByRole('link', { name: 'Sign out' });
page.getByRole('textbox', { name: 'Email' });
// 2. Accessible labels
page.getByLabel('Email address');
page.getByPlaceholder('Search...');
page.getByAltText('Company logo');
page.getByTitle('Close dialog');
// 3. data-testid (stable, decoupled from UI text)
page.getByTestId('order-total');
page.getByTestId('submit-payment');
// 4. Text content (fragile if text changes frequently)
page.getByText('Welcome back');
page.getByText(/total: \$[\d.]+/i);
// 5. CSS selectors (last resort)
page.locator('.order-card >> nth=0');
page.locator('#main-content');
// AVOID: XPath (brittle, hard to read)
// page.locator('xpath=//div[@class="card"]/span[2]');
Step 5: Handle Waiting Strategies
Playwright Auto-Wait (built-in, handles most cases):
// Playwright automatically waits for elements to be:
// - Attached to DOM
// - Visible
// - Stable (not animating)
// - Enabled
// - Receiving events
await page.getByRole('button', { name: 'Submit' }).click();
// No explicit wait needed; Playwright retries until actionable
Explicit Waits for Complex Scenarios:
// Wait for a specific API response
const responsePromise = page.waitForResponse(
resp => resp.url().includes('/api/orders') && resp.status() === 200
);
await page.getByRole('button', { name: 'Load orders' }).click();
const response = await responsePromise;
const data = await response.json();
// Wait for element state
await page.getByTestId('loading-spinner').waitFor({ state: 'hidden' });
// Wait for a specific number of elements
await expect(page.getByTestId('order-row')).toHaveCount(10);
// Wait for navigation
await Promise.all([
page.waitForURL('**/dashboard'),
page.getByRole('button', { name: 'Sign in' }).click(),
]);
Step 6: Implement Visual Regression Testing
Playwright Visual Comparisons:
import { test, expect } from '@playwright/test';
test.describe('Visual Regression', () => {
test('dashboard renders correctly', async ({ page }) => {
await page.goto('/dashboard');
await page.waitForLoadState('networkidle');
// Full-page screenshot comparison
await expect(page).toHaveScreenshot('dashboard-full.png', {
fullPage: true,
maxDiffPixelRatio: 0.01, // Allow 1% pixel difference
});
});
test('order card component renders correctly', async ({ page }) => {
await page.goto('/dashboard');
// Component-level screenshot
const orderCard = page.getByTestId('order-card').first();
await expect(orderCard).toHaveScreenshot('order-card.png', {
maxDiffPixels: 50, // Allow up to 50 pixels difference
});
});
test('responsive layout at mobile width', async ({ page }) => {
await page.setViewportSize({ width: 375, height: 812 });
await page.goto('/dashboard');
await expect(page).toHaveScreenshot('dashboard-mobile.png', {
fullPage: true,
});
});
});
Updating Baselines:
# Update all visual snapshots after intentional UI changes
npx playwright test --update-snapshots
# Update snapshots for a specific test file
npx playwright test tests/visual.spec.ts --update-snapshots
Step 7: Mock and Intercept API Calls
import { test, expect } from '@playwright/test';
test.describe('API Mocking', () => {
test('displays orders from mocked API', async ({ page }) => {
// Intercept the API call and return mock data
await page.route('**/api/orders*', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
orders: [
{ id: '1', total: 99.99, status: 'pending' },
{ id: '2', total: 149.50, status: 'shipped' },
],
total: 2,
}),
});
});
await page.goto('/dashboard');
await expect(page.getByTestId('order-row')).toHaveCount(2);
});
test('handles API error gracefully', async ({ page }) => {
await page.route('**/api/orders*', (route) =>
route.fulfill({ status: 500, body: 'Internal Server Error' })
);
await page.goto('/dashboard');
await expect(page.getByText('Failed to load orders')).toBeVisible();
});
test('modifies real API response', async ({ page }) => {
await page.route('**/api/orders*', async (route) => {
const response = await route.fetch(); // Forward to real server
const json = await response.json();
json.orders[0].status = 'cancelled'; // Modify one field
await route.fulfill({ response, json });
});
await page.goto('/dashboard');
await expect(page.getByText('cancelled')).toBeVisible();
});
});
Step 8: Handle Authentication Efficiently
Storage State Pattern (login once, reuse across tests):
// tests/auth.setup.ts
import { test as setup, expect } from '@playwright/test';
const AUTH_FILE = 'tests/.auth/user.json';
setup('authenticate', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill('[email protected]');
await page.getByLabel('Password').fill('securepassword');
await page.getByRole('button', { name: 'Sign in' }).click();
// Wait for redirect to dashboard after login
await page.waitForURL('**/dashboard');
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
// Save authentication state (cookies, localStorage)
await page.context().storageState({ path: AUTH_FILE });
});
Use Saved Auth in Tests:
// playwright.config.ts (add setup project and dependency)
export default defineConfig({
projects: [
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
storageState: 'tests/.auth/user.json',
},
dependencies: ['setup'],
},
],
});
API-Based Authentication (faster, no browser login):
// tests/fixtures/auth.fixture.ts
import { test as base } from '@playwright/test';
type AuthFixture = {
authenticatedPage: Page;
};
export const test = base.extend<AuthFixture>({
authenticatedPage: async ({ page, request }, use) => {
// Get auth token via API (no browser login flow)
const response = await request.post('/api/auth/login', {
data: { email: '[email protected]', password: 'securepassword' },
});
const { token } = await response.json();
// Inject token into browser context
await page.goto('/');
await page.evaluate((t) => {
localStorage.setItem('auth_token', t);
}, token);
await use(page);
},
});
Step 9: Integrate into CI/CD
GitHub Actions Pipeline:
# .github/workflows/e2e-tests.yml
name: E2E Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
e2e:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium firefox
- name: Build application
run: npm run build
- name: Run E2E tests
run: npx playwright test --shard=${{ matrix.shard }}/${{ strategy.job-total }}
env:
BASE_URL: http://localhost:3000
CI: true
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-report-${{ matrix.shard }}
path: |
playwright-report/
test-results/
retention-days: 14
- name: Upload test results to PR
if: always() && github.event_name == 'pull_request'
uses: daun/playwright-report-summary@v3
with:
report-file: results.xml
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
Step 10: Manage Flaky Tests
Flaky Test Identification and Quarantine:
// Tag known flaky tests for separate tracking
test.describe('Flaky Test Quarantine', () => {
// Mark as flaky: will retry up to 3 times before failing
test('intermittent WebSocket connection', {
tag: '@flaky',
retries: 3,
}, async ({ page }) => {
// Test implementation
});
});
// Run only non-flaky tests in the main pipeline
// npx playwright test --grep-invert @flaky
// Run flaky tests separately for monitoring
// npx playwright test --grep @flaky
Common Flaky Test Causes and Fixes:
| Cause | Symptom | Fix |
|---|---|---|
| Race condition | Passes locally, fails in CI | Add explicit waits for API responses |
| Animation timing | Element not clickable | Wait for animation end or disable animations |
| Shared test state | Order-dependent failures | Reset state in beforeEach, use isolated contexts |
| Network latency in CI | Timeout errors | Increase timeouts, mock slow endpoints |
| Time-dependent logic | Fails at month/year boundaries | Mock Date.now() or use fixed test dates |
Step 11: Add Accessibility Testing
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test.describe('Accessibility', () => {
test('dashboard has no WCAG violations', async ({ page }) => {
await page.goto('/dashboard');
const results = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
.exclude('.third-party-widget') // Exclude elements you cannot control
.analyze();
expect(results.violations).toEqual([]);
});
test('login form is keyboard navigable', async ({ page }) => {
await page.goto('/login');
// Tab through form elements
await page.keyboard.press('Tab');
await expect(page.getByLabel('Email')).toBeFocused();
await page.keyboard.press('Tab');
await expect(page.getByLabel('Password')).toBeFocused();
await page.keyboard.press('Tab');
await expect(page.getByRole('button', { name: 'Sign in' })).toBeFocused();
// Submit with Enter
await page.keyboard.press('Enter');
});
});
Best Practices
- Prefer role-based selectors:
getByRoleties tests to accessibility semantics, improving both test resilience and a11y - One assertion per logical check: Keep tests focused on a single user flow or behavior
- Never use hard-coded waits: Replace
page.waitForTimeout(3000)with event-based waits - Use page objects for all pages: Centralizes selector maintenance when UI changes
- Mock external services: Do not depend on third-party APIs in E2E tests
- Run in CI on every PR: Catch regressions before merge, not after
- Record traces on failure: Playwright traces provide full replay for debugging
- Parallelize with sharding: Split tests across CI workers for faster feedback
- Quarantine flaky tests: Do not let flaky tests block the main pipeline
- Test critical paths first: Login, checkout, and core CRUD flows before edge cases
Quality Checklist
- Framework selected with documented rationale
- Page object model implemented for all tested pages
- Selectors use ARIA roles or data-testid (no fragile CSS/XPath)
- Authentication handled via storage state or API fixtures
- Visual regression baselines established and reviewed
- API mocking covers error states and edge cases
- Tests run in CI on every pull request
- Parallel execution configured (sharding or multiple workers)
- Test artifacts (traces, screenshots, video) uploaded on failure
- Flaky tests identified, tagged, and tracked separately
- Accessibility checks (axe-core) included in the test suite
- All tests pass consistently across Chromium, Firefox, and WebKit
Related Skills
unit-tests- Unit testing for individual functions and componentstest-cases- Integration and API testing patternscicd-architect- CI/CD pipeline design for test automationcode-coverage- Measuring and improving test coverageperformance-testing- Load testing and performance benchmarking
Version: 1.0.0 Last Updated: March 2026
Iterative Refinement Strategy
This skill is optimized for an iterative approach:
- Execute: Perform the core steps defined above.
- Review: Critically analyze the output (coverage, quality, completeness).
- Refine: If targets aren't met, repeat the specific implementation steps with improved context.
- Loop: Continue until the definition of done is satisfied.