e2e-testing
When writing E2E/client tests for this codebase, follow the established patterns using Playwright and the BasePage pattern.
Install
mkdir -p .claude/skills/e2e-testing-coldstartlabs-ca && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/14476" && unzip -o skill.zip -d .claude/skills/e2e-testing-coldstartlabs-ca && rm skill.zipInstalls to .claude/skills/e2e-testing-coldstartlabs-ca
Activation
This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.
When writing E2E/client tests for this codebase, follow the established patterns using Playwright and the BasePage pattern.About this skill
E2E Testing Skill
When writing E2E/client tests for this codebase, follow the established patterns using Playwright and the BasePage pattern.
Overview
- Framework: Playwright E2E tests (
tests/e2e/*.e2e.spec.ts) - Pattern: Enhanced Page Object Model with
BasePageclass - Test Context:
TestContext- centralized resource management - Fixtures: Extended Playwright fixtures in
tests/test-fixtures.ts
Directory Structure
tests/
├── e2e/ # End-to-end browser tests (.e2e.spec.ts)
├── pages/ # Page object models
│ └── BasePage.ts # Base class with common UI patterns
├── helpers/
│ └── TestContext.ts # User factory and cleanup
└── test-fixtures.ts # Playwright fixture extensions
BasePage Pattern
All page objects extend BasePage for common functionality:
import { BasePage } from '@tests/pages/BasePage';
export class LoginPage extends BasePage {
// Locators
get googleSignInButton(): Locator {
return this.page.getByRole('button', { name: 'Continue with Google' });
}
get emailInput(): Locator {
return this.page.getByPlaceholder(/email/i);
}
// Actions
async openLoginModal(): Promise<void> {
await this.signInButton.click();
await this.waitForModal();
}
async fillLoginForm(email: string, password: string): Promise<void> {
await this.fillField(/email/i, email);
await this.fillField(/password/i, password);
}
async submitForm(): Promise<void> {
await this.clickButton('Sign In');
}
// Assertions
async assertModalVisible(): Promise<void> {
await expect(this.modal).toBeVisible();
}
}
BasePage Available Methods
Navigation
await page.goto('/'); // Navigate to path
await page.waitForURL('/dashboard'); // Wait for URL
Modal Handling
await this.waitForModal(); // Wait for modal to appear
await this.closeModal(); // Close active modal
await this.clickModalButton('Submit'); // Click button in modal
Toast/Notifications
await this.waitForToast('Success!'); // Wait for toast with text
await this.dismissToast(); // Dismiss active toast
Loading States
await this.waitForLoadingComplete(); // Wait for page load
await this.waitForAuthLoadingComplete(); // Wait for auth load
Network Handling
await this.waitForApiResponse('/api/user'); // Wait for API response
await this.waitForApiRequest('/api/upscale'); // Wait for API request
Form Helpers
await this.fillField(/email/i, '[email protected]');
await this.selectOption(/country/i, 'United States');
Accessibility
await this.checkBasicAccessibility(); // Basic a11y checks
await this.checkAriaLabels(); // Verify ARIA labels
Writing E2E Tests
Basic Test Structure
import { test, expect } from '@tests/e2e/fixtures';
import { LoginPage } from '@tests/pages/LoginPage';
test.describe('Authentication', () => {
test('should show login form when clicking sign in', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto('/');
await loginPage.openLoginModal();
await loginPage.assertModalVisible();
});
});
TestContext Setup
import { TestContext } from '@tests/helpers/TestContext';
let ctx: TestContext;
test.beforeAll(async () => {
ctx = new TestContext();
});
test.afterAll(async () => {
await ctx.cleanup();
});
User Factory
// Simple user
const user = await ctx.createUser();
// Fluent builder API
const user = await ctx.createUser().withSubscription('active', 'pro').withCredits(500).build();
// Preset configurations
const proUser = await ctx.userFactory.proUser(500);
const businessUser = await ctx.userFactory.businessUser(1000);
Common E2E Test Patterns
Form Submission
test('should submit login form', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto('/');
await loginPage.openLoginModal();
await loginPage.fillLoginForm('[email protected]', 'password123');
await loginPage.submitForm();
await loginPage.waitForToast('Welcome back!');
});
Navigation
test('should navigate to dashboard', async ({ page }) => {
const dashboardPage = new DashboardPage(page);
await dashboardPage.goto('/');
await dashboardPage.clickNavLink('Dashboard');
await dashboardPage.waitForURL('/dashboard');
});
Loading States
test('should show loading state during upload', async ({ page }) => {
const uploadPage = new UploadPage(page);
await uploadPage.goto('/upload');
await uploadPage.uploadImage(testImage);
await uploadPage.waitForLoadingComplete();
await expect(uploadPage.resultPreview).toBeVisible();
});
Network Requests
test('should handle API errors gracefully', async ({ page }) => {
const uploadPage = new UploadPage(page);
// Intercept and mock API response
await page.route('**/api/upscale', route =>
route.fulfill({ status: 500, body: '{"error":"Server error"}' })
);
await uploadPage.goto('/upload');
await uploadPage.uploadImage(testImage);
await uploadPage.waitForToast('Something went wrong');
});
Keyboard Navigation
test('should handle keyboard navigation properly', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto('/');
await loginPage.openLoginModal();
await page.keyboard.press('Tab');
await expect(loginPage.emailInput).toBeFocused();
await page.keyboard.press('Tab');
await expect(loginPage.passwordInput).toBeFocused();
});
Accessibility Testing
Always include accessibility checks in E2E tests:
test('should have proper accessibility attributes', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto('/');
await loginPage.openLoginModal();
// Check basic accessibility
await loginPage.checkBasicAccessibility();
// Check ARIA labels
await loginPage.checkAriaLabels();
});
Manual Accessibility Checks
test('should be keyboard accessible', async ({ page }) => {
const page = new BasePage(page);
await page.goto('/');
// Test Tab navigation
await page.keyboard.press('Tab');
// Verify focus indicator visible
});
test('should have proper ARIA labels', async ({ page }) => {
await page.goto('/');
const button = page.getByRole('button', { name: /submit/i });
await expect(button).toHaveAttribute('aria-label', 'Submit form');
});
Mobile Testing
The Playwright config includes mobile device projects. Tests automatically run on:
- Desktop (chromium)
- Mobile iPhone (iPhone 12)
- Mobile Android (Pixel 5)
- Tablet (iPad)
Write mobile-specific tests:
test.describe('Mobile Navigation', () => {
test('should show hamburger menu on mobile', async ({ page }) => {
// This test runs on mobile devices
const nav = new Navigation(page);
await nav.goto('/');
await expect(nav.hamburgerMenu).toBeVisible();
await nav.openMobileMenu();
await expect(nav.mobileMenuItems).toBeVisible();
});
});
Common Pitfalls to Avoid
1. Fragile Selectors
Bad: Text-based selectors break easily
await page.click('Submit');
Good: Role-based selectors
await page.getByRole('button', { name: 'Submit' }).click();
Good: Test IDs (most stable)
await page.getByTestId('submit-button').click();
2. Hardcoded Waits
Bad: Fixed timeouts are flaky
await page.waitForTimeout(2000);
Good: Wait for specific condition
await page.waitForURL('/dashboard');
await expect(element).toBeVisible();
await this.waitForLoadingComplete();
3. Not Cleaning Up
Always clean up test data:
let ctx: TestContext;
test.beforeAll(async () => {
ctx = new TestContext();
});
test.afterAll(async () => {
await ctx.cleanup(); // Don't forget this!
});
4. Skipping Accessibility
Don't skip accessibility checks:
test('should work', async ({ page }) => {
// Missing accessibility checks!
await page.goto('/');
});
test('should work with accessibility', async ({ page }) => {
await page.goto('/');
await new BasePage(page).checkBasicAccessibility();
});
5. Ignoring Mobile
Don't only test desktop - tests run on all devices:
// This runs on desktop AND mobile/tablet
test('should work on all devices', async ({ page }) => {
await page.goto('/');
// Test works on all screen sizes
});
Best Practices
Test Naming
- Use descriptive names:
"should show login form when clicking sign in" - Group by feature:
test.describe('Authentication') - Test user flows:
"should complete checkout flow"
Page Object Structure
export class FeaturePage extends BasePage {
// Locators (getters for lazy evaluation)
get element(): Locator {
return this.page.locator('...');
}
// Actions (async methods)
async doSomething(): Promise<void> {}
// Assertions (async methods with 'assert' prefix)
async assertSomething(): Promise<void> {
await expect(this.element).toBeVisible();
}
}
Selector Priority
- Test IDs - Most stable:
getByTestId() - Role + Name - Semantic:
getByRole('button', { name }) - Labels/Placeholders - Form inputs:
getByPlaceholder(),getByLabel() - Text - Only when necessary:
getByText() - CSS - Last resort:
locator()
Testing Commands
# Run all E2E tests
yarn test:e2e
# Run specific test file
yarn test:e2e tests/e2e/auth.e2e.spec.ts
# Playwright UI mode (interactive)
yarn test:e2e:ui
# Debug mode with inspector
yarn test:e2e:debug
# Run on specific device
yarn test:e2e --project=mobile-iphone
#
---
*Content truncated.*