seed-skills/e2e-testing-patterns/SKILL.md
Comprehensive end-to-end testing methodologies and best practices covering architecture, test design, data management, flakiness prevention, and cross-browser strategies.
npx skillsauth add PramodDutta/qaskills E2E Testing PatternsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
You are an expert QA architect specializing in end-to-end testing patterns and methodologies. When the user asks you to design, review, or improve E2E testing strategies, follow these detailed instructions.
/\
/ \ E2E Tests (10-20%)
/____\ - Critical user journeys
/ \ - High-value scenarios
/ \ - Smoke tests
/__________\ Integration Tests (20-30%)
/ \
/ \ Unit Tests (50-70%)
/________________\
E2E tests should focus on:
E2E tests should NOT test:
Structure:
pages/
base.page.ts # Shared base functionality
login.page.ts # Login page actions and selectors
dashboard.page.ts # Dashboard page actions
components/
header.component.ts # Reusable header component
modal.component.ts # Reusable modal component
Implementation:
// base.page.ts
export abstract class BasePage {
constructor(protected page: Page) {}
async navigate(path: string): Promise<void> {
await this.page.goto(path);
}
async waitForLoad(): Promise<void> {
await this.page.waitForLoadState('networkidle');
}
async takeScreenshot(name: string): Promise<void> {
await this.page.screenshot({ path: `screenshots/${name}.png` });
}
}
// login.page.ts
export class LoginPage extends BasePage {
private readonly emailInput = this.page.getByLabel('Email');
private readonly passwordInput = this.page.getByLabel('Password');
private readonly submitButton = this.page.getByRole('button', { name: 'Sign in' });
async goto(): Promise<void> {
await this.navigate('/login');
}
async login(email: string, password: string): Promise<void> {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.submitButton.click();
await this.waitForLoad();
}
async expectError(message: string): Promise<void> {
await expect(this.page.getByRole('alert')).toContainText(message);
}
}
Pros:
Cons:
Structure:
// actors/user.actor.ts
export class User {
constructor(private page: Page) {}
async attemptsTo(...tasks: Task[]): Promise<void> {
for (const task of tasks) {
await task.perform(this.page);
}
}
async shouldSee(...assertions: Assertion[]): Promise<void> {
for (const assertion of assertions) {
await assertion.verify(this.page);
}
}
}
// tasks/login.task.ts
export class Login implements Task {
constructor(
private email: string,
private password: string
) {}
async perform(page: Page): Promise<void> {
await page.getByLabel('Email').fill(this.email);
await page.getByLabel('Password').fill(this.password);
await page.getByRole('button', { name: 'Sign in' }).click();
}
}
// Usage
test('user can login and view dashboard', async ({ page }) => {
const user = new User(page);
await user.attemptsTo(
new NavigateTo('/login'),
new Login('[email protected]', 'password123')
);
await user.shouldSee(
new PageTitle('Dashboard'),
new Element('welcome-message').isVisible()
);
});
Pros:
Cons:
Organize tests by complete user journeys rather than by pages:
describe('Purchase Journey', () => {
test('guest user can complete full purchase flow', async ({ page }) => {
// Journey: Browse → Add to Cart → Checkout → Payment → Confirmation
// Step 1: Browse products
await page.goto('/products');
await page.getByRole('link', { name: 'Laptops' }).click();
// Step 2: Add to cart
const product = page.getByTestId('product-123');
await product.getByRole('button', { name: 'Add to Cart' }).click();
await expect(page.getByTestId('cart-count')).toHaveText('1');
// Step 3: Checkout
await page.getByRole('button', { name: 'Checkout' }).click();
await fillCheckoutForm(page, guestUserData);
// Step 4: Payment
await fillPaymentForm(page, testPaymentData);
await page.getByRole('button', { name: 'Place Order' }).click();
// Step 5: Confirmation
await expect(page.getByRole('heading', { name: 'Order Confirmed' })).toBeVisible();
const orderNumber = await page.getByTestId('order-number').textContent();
expect(orderNumber).toMatch(/^ORD-\d{6}$/);
});
});
Pros:
Cons:
// factories/user.factory.ts
export class UserFactory {
private static counter = 0;
static createUser(overrides: Partial<User> = {}): User {
const id = ++this.counter;
return {
id: `user-${id}`,
email: `testuser${id}@example.com`,
name: `Test User ${id}`,
role: 'user',
...overrides,
};
}
static createAdmin(): User {
return this.createUser({ role: 'admin' });
}
}
// Usage in tests
test('admin can delete users', async ({ page }) => {
const admin = UserFactory.createAdmin();
await loginAs(page, admin);
// ... rest of test
});
// fixtures/db-seed.fixture.ts
export async function seedDatabase(): Promise<SeedData> {
const users = await db.users.createMany([
{ email: '[email protected]', name: 'User 1' },
{ email: '[email protected]', name: 'User 2' },
]);
const products = await db.products.createMany([
{ name: 'Product A', price: 29.99 },
{ name: 'Product B', price: 49.99 },
]);
return { users, products };
}
export async function cleanDatabase(): Promise<void> {
await db.orders.deleteMany();
await db.products.deleteMany();
await db.users.deleteMany();
}
// Use in test setup
test.beforeEach(async () => {
await cleanDatabase();
await seedDatabase();
});
// helpers/test-data.ts
export async function createUserViaAPI(userData: CreateUserDto): Promise<User> {
const response = await request.post('/api/users', {
data: userData,
});
return response.json();
}
test('user can update profile', async ({ page }) => {
// Setup: Create user via API (faster than UI)
const user = await createUserViaAPI({
email: '[email protected]',
password: 'password123',
});
// Test: Update profile via UI
await page.goto('/profile');
await page.getByLabel('Name').fill('Updated Name');
await page.getByRole('button', { name: 'Save' }).click();
// Assertion
await expect(page.getByText('Updated Name')).toBeVisible();
});
// ❌ BAD: Hardcoded wait
await page.waitForTimeout(5000);
// ✅ GOOD: Wait for specific condition
await page.waitForSelector('[data-testid="results"]');
await page.waitForLoadState('networkidle');
// ✅ BETTER: Use auto-waiting assertions
await expect(page.getByTestId('results')).toBeVisible();
// ✅ Automatically retries until condition is met (or timeout)
await expect(page.getByRole('alert')).toHaveText('Success', { timeout: 10000 });
// ✅ Wait for element count to stabilize
await expect(page.getByRole('listitem')).toHaveCount(5);
// ✅ Wait for element to be in the right state
await expect(page.getByRole('button', { name: 'Submit' })).toBeEnabled();
// Wait for specific API call to complete
test('should load user data', async ({ page }) => {
const responsePromise = page.waitForResponse(
(response) => response.url().includes('/api/users') && response.status() === 200
);
await page.goto('/users');
await responsePromise;
await expect(page.getByRole('heading')).toContainText('Users');
});
// ❌ BAD: Assumes element exists immediately
await page.click('button');
await page.fill('input', 'text');
// ✅ GOOD: Wait for element before interaction
await page.waitForSelector('button');
await page.click('button');
await page.waitForSelector('input');
await page.fill('input', 'text');
// ✅ BETTER: Use built-in auto-waiting
await page.getByRole('button').click();
await page.getByRole('textbox').fill('text');
// playwright.config.ts
export default defineConfig({
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
{
name: 'mobile-chrome',
use: { ...devices['Pixel 5'] },
},
{
name: 'mobile-safari',
use: { ...devices['iPhone 13'] },
},
],
});
test('should support advanced CSS features', async ({ page, browserName }) => {
test.skip(browserName === 'webkit', 'Safari does not support this CSS feature yet');
await page.goto('/advanced-styles');
// ... test advanced CSS behavior
});
test('homepage renders consistently across browsers', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveScreenshot('homepage.png', {
fullPage: true,
maxDiffPixels: 100, // Allow minor rendering differences
});
});
tests/
e2e/
auth/
login.spec.ts
signup.spec.ts
password-reset.spec.ts
shopping/
browse-products.spec.ts
cart-operations.spec.ts
checkout.spec.ts
admin/
user-management.spec.ts
analytics.spec.ts
// Tag tests by priority
test('user can login @smoke', async ({ page }) => {
// Critical path
});
test('user can reset password @regression', async ({ page }) => {
// Less critical, run in nightly builds
});
test('admin can export analytics @full', async ({ page }) => {
// Run only in full test suite
});
// Run subsets
// npx playwright test --grep @smoke
// npx playwright test --grep @regression
// Run tests in parallel (default)
test.describe.configure({ mode: 'parallel' });
// Run tests serially when they share state
test.describe.configure({ mode: 'serial' });
test.describe('User onboarding flow', () => {
test.describe.configure({ mode: 'serial' });
test('step 1: create account', async ({ page }) => {
// ...
});
test('step 2: verify email', async ({ page }) => {
// ...
});
test('step 3: complete profile', async ({ page }) => {
// ...
});
});
// auth.setup.ts
import { test as setup } from '@playwright/test';
setup('authenticate', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill('[email protected]');
await page.getByLabel('Password').fill('admin123');
await page.getByRole('button', { name: 'Sign in' }).click();
await page.context().storageState({ path: 'playwright/.auth/user.json' });
});
// playwright.config.ts
export default defineConfig({
projects: [
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
{
name: 'chromium',
use: {
storageState: 'playwright/.auth/user.json',
},
dependencies: ['setup'],
},
],
});
// fixtures/auth.fixture.ts
export const test = base.extend<{
authenticatedPage: Page;
adminPage: Page;
}>({
authenticatedPage: async ({ browser }, use) => {
const context = await browser.newContext({
storageState: 'playwright/.auth/user.json',
});
const page = await context.newPage();
await use(page);
await context.close();
},
adminPage: async ({ browser }, use) => {
const context = await browser.newContext({
storageState: 'playwright/.auth/admin.json',
});
const page = await context.newPage();
await use(page);
await context.close();
},
});
// Usage
test('admin can access admin panel', async ({ adminPage }) => {
await adminPage.goto('/admin');
await expect(adminPage.getByRole('heading')).toHaveText('Admin Dashboard');
});
test('homepage loads within 3 seconds', async ({ page }) => {
const startTime = Date.now();
await page.goto('/');
await page.waitForLoadState('networkidle');
const loadTime = Date.now() - startTime;
expect(loadTime).toBeLessThan(3000);
});
import { playAudit } from 'playwright-lighthouse';
test('homepage meets performance standards', async ({ page }) => {
await page.goto('/');
await playAudit({
page,
thresholds: {
performance: 90,
accessibility: 95,
'best-practices': 90,
seo: 90,
},
});
});
sleep(5000) is a code smell.// playwright.config.ts
export default defineConfig({
reporter: [
['html', { open: 'never', outputFolder: 'test-results/html' }],
['json', { outputFile: 'test-results/results.json' }],
['junit', { outputFile: 'test-results/junit.xml' }],
],
});
// Enable tracing on failure
export default defineConfig({
use: {
trace: 'retain-on-failure',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
});
// View trace:
// npx playwright show-trace trace.zip
test('critical payment flow', async ({ page }) => {
test.info().annotations.push({ type: 'priority', description: 'critical' });
test.info().annotations.push({ type: 'ticket', description: 'JIRA-1234' });
// ... test implementation
});
E2E testing is an investment in confidence. Done well, it catches critical bugs before production. Done poorly, it wastes time and erodes trust in automation.
testing
Teaches the agent to migrate a Jest suite to Vitest — vi.mock and the globals shim, vitest.config workspaces/projects, coverage, browser mode, and Vitest v4 breaking changes.
testing
Teaches the agent to speed up Node integration tests with Testcontainers reuse — withReuse(true), TESTCONTAINERS_REUSE_ENABLE, the .testcontainers.properties opt-in, stable hashing for Postgres/MySQL/Kafka, and Ryuk/CI caveats.
development
Port a Java Selenium suite to Playwright TypeScript - locator mapping, WebDriverWait to auto-wait, Grid to workers, Page Object port, with before/after code and a phased checklist.
development
Gate RAG pipelines in CI with versioned golden eval sets, per-metric thresholds, baseline drift detection, and a build that fails when retrieval or answer quality regresses.