e2e-testing

End-to-End Testing Skill

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "e2e-testing" with this command: npx skills add sgcarstrends/sgcarstrends/sgcarstrends-sgcarstrends-e2e-testing

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("john@example.com");
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("test@example.com");

  1. 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" });

  1. 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 });

  1. 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

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

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

e2e-testing

No summary provided by upstream source.

Repository SourceNeeds Review
General

framer-motion-animations

No summary provided by upstream source.

Repository SourceNeeds Review
General

e2e-testing

No summary provided by upstream source.

Repository SourceNeeds Review