e2e-testing
About
This skill enables developers to write and run comprehensive end-to-end tests using Playwright, focusing on complete user flows, page interactions, and visual regression testing. It's designed for verifying UI functionality, testing user journeys, and ensuring application reliability before deployments. The skill supports Playwright's key capabilities including cross-browser testing, auto-waiting, network interception, and parallel execution.
Documentation
End-to-End Testing Skill
This skill helps you write and run comprehensive end-to-end tests using Playwright.
When to Use This Skill
- Testing complete user flows
- Verifying page interactions and navigation
- Testing form submissions
- Checking API integrations from the UI
- Visual regression testing
- Cross-browser compatibility testing
- Mobile responsiveness testing
- Before production deployments
Playwright Overview
Playwright is a modern E2E testing framework that provides:
- Cross-browser: Chromium, Firefox, WebKit
- Auto-waiting: Intelligent element waiting
- Network interception: Mock API responses
- Screenshots & videos: Visual debugging
- Parallel execution: Fast test runs
- TypeScript support: Type-safe tests
Project Configuration
Installation
# Install Playwright (if not already installed)
pnpm add -D -w @playwright/test
# Install browsers
pnpm exec playwright install
Configuration File
// playwright.config.ts
import { defineConfig, devices } from "@playwright/test";
export default defineConfig({
testDir: "./apps/web/__tests__/e2e",
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: [
["html"],
["junit", { outputFile: "test-results/junit.xml" }],
],
use: {
baseURL: process.env.BASE_URL || "http://localhost:3001",
trace: "on-first-retry",
screenshot: "only-on-failure",
},
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 12"] },
},
],
webServer: {
command: "pnpm -F @sgcarstrends/web dev",
url: "http://localhost:3001",
reuseExistingServer: !process.env.CI,
},
});
Test Structure
File Organization
apps/web/
├── __tests__/
│ └── e2e/
│ ├── home.spec.ts # Homepage tests
│ ├── cars/
│ │ ├── makes.spec.ts # Car makes listing
│ │ └── models.spec.ts # Car models listing
│ ├── coe/
│ │ └── bidding.spec.ts # COE bidding results
│ ├── blog/
│ │ ├── list.spec.ts # Blog listing
│ │ └── post.spec.ts # Blog post detail
│ └── fixtures/
│ ├── mock-data.ts # Test data
│ └── page-objects.ts # Page object models
Basic Test Example
// apps/web/__tests__/e2e/home.spec.ts
import { test, expect } from "@playwright/test";
test.describe("Homepage", () => {
test("should load successfully", async ({ page }) => {
await page.goto("/");
// Check page title
await expect(page).toHaveTitle(/SG Cars Trends/);
// Check main heading
const heading = page.getByRole("heading", { name: /SG Cars Trends/ });
await expect(heading).toBeVisible();
});
test("should display navigation menu", async ({ page }) => {
await page.goto("/");
// Check nav links
await expect(page.getByRole("link", { name: "Cars" })).toBeVisible();
await expect(page.getByRole("link", { name: "COE" })).toBeVisible();
await expect(page.getByRole("link", { name: "Blog" })).toBeVisible();
});
test("should navigate to cars page", async ({ page }) => {
await page.goto("/");
// Click cars link
await page.getByRole("link", { name: "Cars" }).click();
// Verify URL
await expect(page).toHaveURL(/\/cars/);
// Verify page content
await expect(page.getByRole("heading", { name: /Cars/ })).toBeVisible();
});
});
Page Object Pattern
Create Page Objects
// apps/web/__tests__/e2e/fixtures/page-objects.ts
import { Page, Locator } from "@playwright/test";
export class HomePage {
readonly page: Page;
readonly heading: Locator;
readonly carsLink: Locator;
readonly coeLink: Locator;
readonly blogLink: Locator;
constructor(page: Page) {
this.page = page;
this.heading = page.getByRole("heading", { name: /SG Cars Trends/ });
this.carsLink = page.getByRole("link", { name: "Cars" });
this.coeLink = page.getByRole("link", { name: "COE" });
this.blogLink = page.getByRole("link", { name: "Blog" });
}
async goto() {
await this.page.goto("/");
}
async navigateToCars() {
await this.carsLink.click();
}
async navigateToCOE() {
await this.coeLink.click();
}
async navigateToBlog() {
await this.blogLink.click();
}
}
export class CarsPage {
readonly page: Page;
readonly heading: Locator;
readonly makeSelect: Locator;
readonly modelSelect: Locator;
readonly resultsTable: Locator;
constructor(page: Page) {
this.page = page;
this.heading = page.getByRole("heading", { name: /Cars/ });
this.makeSelect = page.getByLabel("Make");
this.modelSelect = page.getByLabel("Model");
this.resultsTable = page.getByRole("table");
}
async goto() {
await this.page.goto("/cars");
}
async selectMake(make: string) {
await this.makeSelect.click();
await this.page.getByRole("option", { name: make }).click();
}
async selectModel(model: string) {
await this.modelSelect.click();
await this.page.getByRole("option", { name: model }).click();
}
async getResultsCount(): Promise<number> {
const rows = await this.resultsTable.locator("tbody tr").count();
return rows;
}
}
Use Page Objects
// apps/web/__tests__/e2e/cars/makes.spec.ts
import { test, expect } from "@playwright/test";
import { HomePage, CarsPage } from "../fixtures/page-objects";
test.describe("Cars Page", () => {
test("should filter by make", async ({ page }) => {
const homePage = new HomePage(page);
const carsPage = new CarsPage(page);
// Navigate to cars page
await homePage.goto();
await homePage.navigateToCars();
// Select Toyota
await carsPage.selectMake("Toyota");
// Wait for results
await expect(carsPage.resultsTable).toBeVisible();
// Verify results contain Toyota
const firstRow = page.locator("tbody tr").first();
await expect(firstRow).toContainText("Toyota");
});
test("should filter by make and model", async ({ page }) => {
const carsPage = new CarsPage(page);
await carsPage.goto();
await carsPage.selectMake("Toyota");
await carsPage.selectModel("Corolla");
// Verify results
const count = await carsPage.getResultsCount();
expect(count).toBeGreaterThan(0);
});
});
API Mocking
Mock API Responses
// apps/web/__tests__/e2e/cars/mocked.spec.ts
import { test, expect } from "@playwright/test";
test.describe("Cars Page with Mocked API", () => {
test("should display mocked car data", async ({ page }) => {
// Mock API response
await page.route("**/api/v1/cars/makes", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify([
{ make: "Toyota", count: 1000 },
{ make: "Honda", count: 800 },
{ make: "BMW", count: 600 },
]),
});
});
await page.goto("/cars");
// Verify mocked data is displayed
await expect(page.getByText("Toyota")).toBeVisible();
await expect(page.getByText("1000")).toBeVisible();
});
test("should handle API errors gracefully", async ({ page }) => {
// Mock API error
await page.route("**/api/v1/cars/makes", async (route) => {
await route.fulfill({
status: 500,
contentType: "application/json",
body: JSON.stringify({ error: "Internal server error" }),
});
});
await page.goto("/cars");
// Verify error message is displayed
await expect(page.getByText(/error|failed/i)).toBeVisible();
});
});
Form Testing
Test Form Submissions
// apps/web/__tests__/e2e/blog/comment.spec.ts
import { test, expect } from "@playwright/test";
test.describe("Blog Comment Form", () => {
test("should submit comment successfully", async ({ page }) => {
await page.goto("/blog/test-post");
// Fill form
await page.getByLabel("Name").fill("John Doe");
await page.getByLabel("Email").fill("[email protected]");
await page.getByLabel("Comment").fill("Great article!");
// Submit
await page.getByRole("button", { name: "Submit" }).click();
// Verify success message
await expect(page.getByText(/comment submitted/i)).toBeVisible();
});
test("should validate required fields", async ({ page }) => {
await page.goto("/blog/test-post");
// Submit empty form
await page.getByRole("button", { name: "Submit" }).click();
// Verify validation errors
await expect(page.getByText(/name is required/i)).toBeVisible();
await expect(page.getByText(/email is required/i)).toBeVisible();
});
test("should validate email format", async ({ page }) => {
await page.goto("/blog/test-post");
await page.getByLabel("Email").fill("invalid-email");
await page.getByRole("button", { name: "Submit" }).click();
await expect(page.getByText(/invalid email/i)).toBeVisible();
});
});
Visual Testing
Screenshot Comparison
// apps/web/__tests__/e2e/visual/homepage.spec.ts
import { test, expect } from "@playwright/test";
test.describe("Visual Regression", () => {
test("homepage should match snapshot", async ({ page }) => {
await page.goto("/");
// Wait for page to be fully loaded
await page.waitForLoadState("networkidle");
// Take screenshot and compare
await expect(page).toHaveScreenshot("homepage.png", {
fullPage: true,
maxDiffPixels: 100,
});
});
test("cars page should match snapshot", async ({ page }) => {
await page.goto("/cars");
await page.waitForLoadState("networkidle");
await expect(page).toHaveScreenshot("cars-page.png", {
fullPage: true,
});
});
test("mobile homepage should match snapshot", async ({ page }) => {
await page.setViewportSize({ width: 375, height: 667 });
await page.goto("/");
await page.waitForLoadState("networkidle");
await expect(page).toHaveScreenshot("homepage-mobile.png", {
fullPage: true,
});
});
});
Accessibility Testing
Test Accessibility
// apps/web/__tests__/e2e/a11y/homepage.spec.ts
import { test, expect } from "@playwright/test";
import AxeBuilder from "@axe-core/playwright";
test.describe("Accessibility", () => {
test("homepage should not have accessibility violations", async ({ page }) => {
await page.goto("/");
const accessibilityScanResults = await new AxeBuilder({ page }).analyze();
expect(accessibilityScanResults.violations).toEqual([]);
});
test("should have proper heading hierarchy", async ({ page }) => {
await page.goto("/");
// Check h1 exists and is unique
const h1Count = await page.locator("h1").count();
expect(h1Count).toBe(1);
// Check heading order
const headings = await page.locator("h1, h2, h3, h4, h5, h6").all();
const headingLevels = await Promise.all(
headings.map((h) => h.evaluate((el) => el.tagName))
);
// H1 should come first
expect(headingLevels[0]).toBe("H1");
});
test("should have alt text on images", async ({ page }) => {
await page.goto("/");
const images = await page.locator("img").all();
for (const img of images) {
const alt = await img.getAttribute("alt");
expect(alt).toBeTruthy();
}
});
});
Running Tests
Common Commands
# Run all tests
pnpm exec playwright test
# Run specific test file
pnpm exec playwright test home.spec.ts
# Run tests in headed mode
pnpm exec playwright test --headed
# Run tests in specific browser
pnpm exec playwright test --project=chromium
pnpm exec playwright test --project=firefox
# Run tests in debug mode
pnpm exec playwright test --debug
# Run tests with UI
pnpm exec playwright test --ui
# Generate test report
pnpm exec playwright show-report
# Update screenshots
pnpm exec playwright test --update-snapshots
Package.json Scripts
{
"scripts": {
"test:e2e": "playwright test",
"test:e2e:headed": "playwright test --headed",
"test:e2e:debug": "playwright test --debug",
"test:e2e:ui": "playwright test --ui",
"test:e2e:report": "playwright show-report"
}
}
CI Configuration
GitHub Actions
# .github/workflows/e2e.yml
name: E2E Tests
on: [push, pull_request]
jobs:
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
- uses: actions/setup-node@v4
with:
node-version: 20
cache: "pnpm"
- run: pnpm install
- run: pnpm exec playwright install --with-deps
- run: pnpm test:e2e
env:
BASE_URL: http://localhost:3001
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
retention-days: 30
Best Practices
1. Use Locators Wisely
// ❌ Fragile CSS selectors
await page.locator(".btn-primary").click();
// ✅ Semantic selectors
await page.getByRole("button", { name: "Submit" }).click();
// ✅ Text content
await page.getByText("Welcome").click();
// ✅ Label
await page.getByLabel("Email").fill("[email protected]");
2. Auto-waiting
// ❌ Manual waiting
await page.waitForTimeout(1000);
await page.click("button");
// ✅ Auto-waiting
await page.getByRole("button").click();
// ✅ Wait for specific state
await page.getByRole("button").waitFor({ state: "visible" });
3. Isolate Tests
// ❌ Tests depend on each other
test("create user", async ({ page }) => {
// Creates user
});
test("login user", async ({ page }) => {
// Assumes user from previous test exists
});
// ✅ Independent tests
test("create user", async ({ page }) => {
// Creates user and cleans up
});
test("login user", async ({ page }) => {
// Creates its own user, logs in, cleans up
});
4. Use Fixtures
// apps/web/__tests__/e2e/fixtures/test.ts
import { test as base } from "@playwright/test";
import { HomePage, CarsPage } from "./page-objects";
type Fixtures = {
homePage: HomePage;
carsPage: CarsPage;
};
export const test = base.extend<Fixtures>({
homePage: async ({ page }, use) => {
await use(new HomePage(page));
},
carsPage: async ({ page }, use) => {
await use(new CarsPage(page));
},
});
export { expect } from "@playwright/test";
// Use in tests
import { test, expect } from "./fixtures/test";
test("test with fixtures", async ({ homePage, carsPage }) => {
await homePage.goto();
await homePage.navigateToCars();
// ...
});
Debugging
Debug Mode
# Run with debugger
pnpm exec playwright test --debug
# Debug specific test
pnpm exec playwright test home.spec.ts --debug
Inspector
// Add breakpoint
await page.pause();
// Log to console
console.log(await page.title());
// Take screenshot
await page.screenshot({ path: "debug.png" });
Trace Viewer
# Generate trace
pnpm exec playwright test --trace on
# View trace
pnpm exec playwright show-trace trace.zip
Troubleshooting
Tests Timing Out
// Increase timeout
test("slow test", async ({ page }) => {
test.setTimeout(60000); // 60 seconds
await page.goto("/slow-page");
});
// Or in config
export default defineConfig({
timeout: 30000, // 30 seconds
});
Flaky Tests
// Use waitForLoadState
await page.goto("/");
await page.waitForLoadState("networkidle");
// Wait for specific elements
await page.getByRole("button").waitFor({ state: "visible" });
// Retry assertions
await expect(page.getByText("Welcome")).toBeVisible({ timeout: 10000 });
Selector Not Found
// Check element exists
const button = page.getByRole("button", { name: "Submit" });
console.log(await button.count()); // 0 if not found
// Use has
await expect(page.getByRole("button")).toHaveCount(1);
// Debug selectors
await page.pause(); // Open inspector
References
- Playwright Documentation: https://playwright.dev
- Best Practices: https://playwright.dev/docs/best-practices
- Accessibility Testing: https://playwright.dev/docs/accessibility-testing
- Related files:
playwright.config.ts- Playwright configuration- Root CLAUDE.md - Testing guidelines
Best Practices Summary
- Use Semantic Selectors: Prefer role, text, label over CSS selectors
- Isolate Tests: Each test should be independent
- Auto-waiting: Let Playwright wait for elements
- Page Objects: Encapsulate page logic
- Mock APIs: Use route mocking for predictable tests
- Visual Testing: Compare screenshots for UI changes
- Accessibility: Test with axe-core
- CI Integration: Run tests in continuous integration
Quick Install
/plugin add https://github.com/sgcarstrends/sgcarstrends/tree/main/e2e-testingCopy and paste this command in Claude Code to install this skill
GitHub 仓库
Related Skills
evaluating-llms-harness
TestingThis Claude Skill runs the lm-evaluation-harness to benchmark LLMs across 60+ standardized academic tasks like MMLU and GSM8K. It's designed for developers to compare model quality, track training progress, or report academic results. The tool supports various backends including HuggingFace and vLLM models.
langchain
MetaLangChain is a framework for building LLM applications using agents, chains, and RAG pipelines. It supports multiple LLM providers, offers 500+ integrations, and includes features like tool calling and memory management. Use it for rapid prototyping and deploying production systems like chatbots, autonomous agents, and question-answering services.
Algorithmic Art Generation
MetaThis skill helps developers create algorithmic art using p5.js, focusing on generative art, computational aesthetics, and interactive visualizations. It automatically activates for topics like "generative art" or "p5.js visualization" and guides you through creating unique algorithms with features like seeded randomness, flow fields, and particle systems. Use it when you need to build reproducible, code-driven artistic patterns.
webapp-testing
TestingThis Claude Skill provides a Playwright-based toolkit for testing local web applications through Python scripts. It enables frontend verification, UI debugging, screenshot capture, and log viewing while managing server lifecycles. Use it for browser automation tasks but run scripts directly rather than reading their source code to avoid context pollution.
