| name | e2e-testing |
| description | Write comprehensive end-to-end tests that verify complete user workflows, integration between components, and system behavior from user perspective using Playwright, Cypress, or similar frameworks. Use when testing user flows, verifying feature integration, testing across real browsers, ensuring UI interactions work correctly, validating form submissions end-to-end, testing authentication flows, catching regressions, automating QA processes, testing API integrations, or building confidence in production deployments. |
End-to-End Testing - Testing Full User Workflows
When to use this skill
- Testing complete user workflows end-to-end
- Verifying integration between frontend and backend
- Testing critical user journeys (signup, checkout, etc.)
- Validating form submissions and data persistence
- Testing authentication and authorization flows
- Catching regressions before production deployment
- Automating manual QA testing processes
- Testing responsive behavior across devices
- Verifying third-party integrations work correctly
- Building CI/CD confidence with automated tests
- Testing error handling and edge cases
- Ensuring accessibility features work correctly
When to use this skill
- Verifying complete user flows, testing integrations, validating critical paths, or ensuring features work in production-like environments.
- When working on related tasks or features
- During development that requires this expertise
Use when: Verifying complete user flows, testing integrations, validating critical paths, or ensuring features work in production-like environments.
Core Principles
- Test User Journeys, Not Implementation - Focus on what users do
- Test Critical Paths First - Happy paths and key revenue flows
- Stable Selectors - Use data-testid, not brittle CSS selectors
- Independent Tests - No shared state between tests
- Fast Feedback - Parallel execution, smart retries
Playwright (Recommended)
1. Basic Setup
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: 'html',
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure'
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] }
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] }
},
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] }
}
],
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process..
}
});
2. Writing Tests
import { test, expect } from '@playwright/test';
test.describe('Authentication', () => {
test('user can sign up', async ({ page }) => {
await page.goto('/signup');
await page.getByLabel('Email').fill('user@example.com');
await page.getByLabel('Password').fill('SecurePass123!');
await page.getByLabel('Confirm Password').fill('SecurePass123!');
await page.getByRole('button', { name: 'Sign Up' }).click();
await expect(page).toHaveURL('/dashboard');
await expect(page.getByText('Welcome')).toBeVisible();
});
test('shows error for invalid email', async ({ page }) => {
page.();
page.().();
page.().();
page.(, { : }).();
(page.()).();
(page).();
});
(, ({ page }) => {
page.();
page.().();
page.().();
page.(, { : }).();
(page).();
});
});
3. Page Object Model
import { Page } from '@playwright/test';
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: 'Log In' }).click();
}
async getErrorMessage() {
return this..().();
}
}
(, ({ page }) => {
loginPage = (page);
loginPage.();
loginPage.(, );
(page).();
});
4. Fixtures for Setup/Teardown
import { test as base } from '@playwright/test';
type Fixtures = {
authenticatedPage: Page;
};
export const test = base.extend<Fixtures>({
authenticatedPage: async ({ page }, use) => {
await page.goto('/login');
await page.getByLabel('Email').fill('test@example.com');
await page.getByLabel('Password').fill('password123');
await page.getByRole('button', { name: 'Log In' }).click();
await page.waitForURL('/dashboard');
await use(page);
await page.getByRole('button', { name: 'Log Out' }).click();
}
});
(, ({ authenticatedPage }) => {
authenticatedPage.();
authenticatedPage.().();
authenticatedPage.(, { : }).();
(authenticatedPage.()).();
});
5. Waiting Strategies
test('search functionality', async ({ page }) => {
await page.goto('/search');
await page.getByPlaceholder('Search...').fill('playwright');
await page.waitForTimeout(2000);
await page.waitForLoadState('networkidle');
await page.waitForSelector('[data-testid="search-results"]');
await expect(page.getByTestId('search-results')).toBeVisible();
const results = page.getByTestId('result-item');
await expect(results).toHaveCount(10);
});
test('data loads correctly', async ({ page }) => {
const responsePromise = page.waitForResponse(
() => response.().() && response.() ===
);
page.();
response = responsePromise;
data = response.();
(data.).();
});
6. Testing Forms & Interactions
test('checkout flow', async ({ page }) => {
await page.goto('/cart');
await page.getByRole('button', { name: 'Checkout' }).click();
await page.getByLabel('Full Name').fill('John Doe');
await page.getByLabel('Address').fill('123 Main St');
await page.getByLabel('City').fill('San Francisco');
await page.getByLabel('State').selectOption('CA');
await page.getByLabel('ZIP Code').fill('94102');
await page.getByRole('button', { name: 'Continue' }).click();
await page.frameLocator('iframe[title="Payment"]')
.getByPlaceholder()
.();
page.()
.()
.();
page.()
.()
.();
page.(, { : }).();
(page.()).();
(page).();
});
7. Testing File Uploads
test('upload profile picture', async ({ page }) => {
await page.goto('/profile/edit');
const fileInput = page.getByLabel('Profile Picture');
await fileInput.setInputFiles('fixtures/avatar.png');
await expect(page.getByText('Upload complete')).toBeVisible();
await page.getByRole('button', { name: 'Save' }).click();
const avatar = page.getByRole('img', { name: 'Profile picture' });
await expect(avatar).toHaveAttribute('src', /avatar\.png/);
});
8. Visual Regression Testing
test('homepage looks correct', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveScreenshot('homepage.png');
});
test('button hover state', async ({ page }) => {
await page.goto('/');
const button = page.getByRole('button', { name: 'Get Started' });
await button.hover();
await expect(button).toHaveScreenshot('button-hover.png');
});
Cypress Alternative
1. Cypress Setup
describe('Authentication', () => {
beforeEach(() => {
cy.visit('/login');
});
it('allows user to log in', () => {
cy.get('[data-testid="email"]').type('user@example.com');
cy.get('[data-testid="password"]').type('password123');
cy.get('[data-testid="submit"]').click();
cy.url().should('include', '/dashboard');
cy.contains('Welcome back').should('be.visible');
});
it('shows error for wrong password', () => {
cy.get('[data-testid="email"]').type('user@example.com');
cy.get('[data-testid="password"]').type('wrongpassword');
cy.get('[data-testid="submit"]').click();
cy.contains('Invalid credentials').should('be.visible');
});
});
2. Cypress Commands
declare global {
namespace Cypress {
interface Chainable {
login(email: string, password: string): Chainable<void>;
createPost(title: string, content: string): Chainable<void>;
}
}
}
Cypress.Commands.add('login', (email, password) => {
cy.visit('/login');
cy.get('[data-testid="email"]').type(email);
cy.get('[data-testid="password"]').type(password);
cy.get('[data-testid="submit"]').click();
cy.url().should('include', '/dashboard');
});
Cypress.Commands.add('createPost', (title, content) => {
cy.visit('/posts/new');
cy.get().(title);
cy.().(content);
cy.().();
});
(, {
cy.(, );
cy.(, );
cy.().();
});
Best Practices
1. Use Stable Selectors
await page.locator('.btn-primary.large').click();
await page.locator('div > div > button:nth-child(2)').click();
await page.getByRole('button', { name: 'Submit' }).click();
await page.getByRole('textbox', { name: 'Email' }).fill('test@example.com');
await page.getByTestId('submit-button').click();
await page.getByTestId('email-input').fill('test@example.com');
<button data-testid="submit-button">Submit</button>
<input data-testid="email-input" type="email" />
2. Independent Tests
test('create user', async ({ page }) => {
globalUserId = await createUser();
});
test('update user', async ({ page }) => {
await updateUser(globalUserId);
});
test('update user', async ({ page }) => {
const user = await createTestUser();
await page.goto(`/users/${user.id}/edit`);
await page.getByLabel('Name').fill('New Name');
await page.getByRole('button', { name: 'Save' }).click();
await expect(page.getByText('Updated successfully')).toBeVisible();
await deleteTestUser(user.id);
});
3. Test Data Management
export function createTestUser(overrides = {}) {
return {
email: `test-${Date.now()}@example.com`,
password: 'password123',
name: 'Test User',
...overrides
};
}
test('create multiple users', async ({ page }) => {
const user1 = createTestUser({ name: 'Alice' });
const user2 = createTestUser({ name: 'Bob' });
});
test.beforeEach(async ({ page }) => {
await db.users.deleteMany();
await db.users.createMany([
createTestUser({ email: 'alice@test.com' }),
createTestUser({ email: 'bob@test.com' })
]);
});
4. Mocking External Services
test('handles API error', async ({ page }) => {
await page.route('**/api/users', (route) => {
route.fulfill({
status: 500,
body: JSON.stringify({ error: 'Server error' })
});
});
await page.goto('/users');
await expect(page.getByText('Failed to load users')).toBeVisible();
});
test('displays user list', async ({ page }) => {
await page.route('**/api/users', (route) => {
route.fulfill({
status: 200,
body: JSON.stringify({
users: [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' }
]
})
});
});
page.();
(page.()).();
(page.()).();
});
E2E Testing Checklist
Test Coverage:
□ Critical user paths tested (signup, login, purchase)
□ Happy paths covered
□ Error scenarios tested
□ Edge cases included
□ Mobile viewport tested
Test Quality:
□ Stable selectors (data-testid, roles)
□ No arbitrary timeouts
□ No test interdependencies
□ Proper waits for async operations
□ Clear test descriptions
Performance:
□ Tests run in parallel
□ Fast test data creation
□ Minimal unnecessary waits
□ Strategic use of mocks
□ Cleanup after tests
CI/CD:
□ Tests run on every PR
□ Failures block merges
□ Screenshots on failure
□ Test reports generated
□ Flaky tests identified and fixed
Resources
Remember: E2E tests verify what users actually experience. Keep them focused on critical paths, stable, and fast.