Comprehensive end-to-end testing methodologies and best practices covering architecture, test design, data management, flakiness prevention, and cross-browser strategies.
npx skills add https://github.com/PramodDutta/qaskills --skill E2E Testing Patterns
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,
},
});
});
10. Use visual regression wisely -- Critical UI only, not everything.
sleep(5000) is a code smell.10. Committing with .only or .skip -- Clean up before committing.
// 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.
Toolkit for interacting with and testing local web applications using Playwright. Supports verifying frontend functionality, debugging UI behavior, capturing browser screenshots, and viewing browser logs.
Use when implementation is complete, all tests pass, and you need to decide how to integrate the work - guides completion of development work by presenting structured options for merge, PR, or cleanup
Use when implementing any feature or bugfix, before writing implementation code
Use when encountering any bug, test failure, or unexpected behavior, before proposing fixes
Use when about to claim work is complete, fixed, or passing, before committing or creating PRs - requires running verification commands and confirming output before making any success claims; evidence before assertions always
Expert guidance for systematic backtesting of trading strategies. Use when developing, testing, stress-testing, or validating quantitative trading strategies. Covers "beating ideas to death" methodology, parameter robustness testing, slippage modeling, bias prevention, and interpreting backtest results. Applicable when user asks about backtesting, strategy validation, robustness testing, avoiding overfitting, or systematic trading development.
Cloud laboratory platform for automated protein testing and validation. Use when designing proteins and needing experimental validation including binding assays, expression testing, thermostability measurements, enzyme activity assays, or protein sequence optimization. Also use for submitting experiments via API, tracking experiment status, downloading results, optimizing protein sequences for better expression using computational tools (NetSolP, SoluProt, SolubleMPNN, ESM), or managing protein design workflows with wet-lab validation.
This skill should be used for time series machine learning tasks including classification, regression, clustering, forecasting, anomaly detection, segmentation, and similarity search. Use when working with temporal data, sequential patterns, or time-indexed observations requiring specialized algorithms beyond standard ML approaches. Particularly suited for univariate and multivariate time series analysis with scikit-learn compatible APIs.
Take pramoddutta/e2e testing patterns from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.
The instructions reference npx.
Without those the skill loads but fails at the first command.