Playwright end-to-end testing patterns and best practices
Executes Playwright end-to-end tests for Astro 5.16 + React 19 projects with environment-based configuration.
/plugin marketplace add jovermier/claude-code-plugins-ip-labs/plugin install playwright@ip-labs-marketplaceThis skill inherits all available tools. When active, it can use any tool Claude has access to.
End-to-end testing patterns with Playwright for Astro 5.16 + React 19 projects.
test-results/ on failureCRITICAL: Base URLs should be configured in the Playwright config file(s), never in test files.
Ideally, the base URL is set via use.baseURL in the config file, which may read from environment variables.
For projects testing multiple environments, separate config files can be used (e.g., playwright.config.ci.ts, playwright.config.staging.ts) and selected via the --config flag.
Test files should always use root-relative paths (starting with /) and rely on the config to provide the full base URL.
DO NOT configure Playwright to start a web server. Playwright should assume the server is already running.
DON'T - Never add webServer configuration:
// ❌ WRONG - Do not configure webServer in playwright.config.ts
export default defineConfig({
webServer: {
command: "[package-manager] start",
url: "BASE_URL must be set via environment variable",
},
});
DO - The config file already handles base URL:
// ✅ CORRECT - Server managed externally, config reads from .env.local
// This is already implemented in playwright.config.ts
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { defineConfig, devices } from "@playwright/test";
import { config } from "dotenv";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Load environment variables from .env.local
const envResult = config({ path: resolve(__dirname, ".env.local") });
if (envResult.error) {
console.warn(
"Warning: .env.local not found, relying on existing environment variables."
);
}
const baseURL = process.env.APP_SERVER_URL;
if (!baseURL) {
throw new Error(
"Missing baseURL! Set BASE_URL, PLAYWRIGHT_BASE_URL, or ensure VITE_CONVEX_URL is defined in .env.local."
);
}
export default defineConfig({
use: { baseURL },
});
Rationale: This project uses PM2 to manage the dev server with fast refresh. Playwright tests should connect to the already-running server, not start a new one. The config file loads .env.local (which contains environment configuration) and falls back to shell environment variables. Set BASE_URL in .env.local or your shell environment to point to the running dev server.
DO - Use root-relative paths in test files:
// ✅ CORRECT
await page.goto("/");
await page.goto("/about");
await page.goto("/blog/my-post");
DON'T - Never include base URL in test files:
// ❌ WRONG - Base URL should not be hardcoded in tests
await page.goto("http://localhost:3000"); // NEVER use localhost
await page.goto("http://localhost:3000/about"); // ALWAYS use environment variables
DON'T - Never provide fallback URLs:
// ❌ WRONG - No fallback URLs, NEVER hardcode localhost
const baseUrl = process.env.APP_SERVER_URL || "http://localhost:3000";
await page.goto(baseUrl);
The config file reads BASE_URL from .env.local or the environment. For most cases, just run:
# Run tests - uses BASE_URL from .env.local or environment
[package-manager] run [test-script]
# Override for a different environment
BASE_URL=<your-dev-url> [package-manager] run [test-script]
# Using a specific config file for an environment
[package-manager] run [test-script] --config=playwright.config.staging.ts
CRITICAL: Never hardcode localhost URLs. Always use environment variables or the actual deployment URL.
// tests/my-feature.spec.ts
import { test, expect } from "@playwright/test";
test("describes the behavior", async ({ page }) => {
await page.goto("/");
// test implementation
});
test("navigates to a page", async ({ page }) => {
await page.goto("/about");
await expect(page).toHaveURL(/\/about/);
});
test("shows element on page", async ({ page }) => {
await page.goto("/");
await expect(page.locator("h1")).toBeVisible();
});
test("button click triggers action", async ({ page }) => {
await page.goto("/");
await page.click('button[type="submit"]');
await expect(page.locator(".success-message")).toBeVisible();
});
test("form submission works", async ({ page }) => {
await page.goto("/contact");
await page.fill('input[name="email"]', "test@example.com");
await page.fill('textarea[name="message"]', "Hello");
await page.click('button[type="submit"]');
await expect(page.locator(".success")).toBeVisible();
});
test.describe("mobile", () => {
test.use({ viewport: { width: 375, height: 667 } });
test("mobile layout works", async ({ page }) => {
await page.goto("/");
await expect(page.locator(".mobile-menu")).toBeVisible();
});
});
test("content loads asynchronously", async ({ page }) => {
await page.goto("/dashboard");
await page.waitForSelector('[data-testid="loaded-content"]');
await expect(page.locator('[data-testid="loaded-content"]')).toBeVisible();
});
test("shows error message on failure", async ({ page }) => {
await page.goto("/form");
await page.click('button[type="submit"]');
await expect(page.locator(".error-message")).toBeVisible();
await expect(page.locator(".error-message")).toContainText("required");
});
Use semantic, accessible selectors:
DO:
page.locator('button[type="submit"]');
page.locator('nav a[href="/about"]');
page.locator("h1");
page.getByRole("button", { name: "Submit" });
page.getByLabelText("Email");
DON'T:
page.locator(".btn-primary"); // Fragile class names
page.locator("#submit-btn"); // Implementation detail
page.locator("div > div > p"); // Brittle structure
Write tests for:
Use test.describe() to group related tests:
test.describe("user authentication", () => {
test("login with valid credentials", async ({ page }) => {
// ...
});
test("shows error for invalid credentials", async ({ page }) => {
// ...
});
});
test.beforeEach(async ({ page }) => {
// Setup before each test
await page.goto("/login");
});
test.afterEach(async ({ page }) => {
// Cleanup after each test
});
Create custom fixtures for reusable test utilities:
// tests/fixtures.ts
import { test as base } from "@playwright/test";
export const test = base.extend<{
authenticatedPage: Page;
}>({
authenticatedPage: async ({ page }, use) => {
// Perform login
await page.goto("/login");
await page.fill('input[name="email"]', "test@example.com");
await page.fill('input[name="password"]', "password");
await page.click('button[type="submit"]');
await page.waitForURL("/dashboard");
await use(page);
},
});
Organize page interactions into reusable classes:
// tests/pages/LoginPage.ts
export class LoginPage {
constructor(private page: Page) {}
async login(email: string, password: string) {
await this.page.fill('input[name="email"]', email);
await this.page.fill('input[name="password"]', password);
await this.page.click('button[type="submit"]');
}
async assertErrorMessage(message: string) {
await expect(this.page.locator(".error")).toContainText(message);
}
}
Mock or intercept network requests:
test("mocks API response", async ({ page }) => {
await page.route("**/api/data", (route) => {
route.fulfill({
status: 200,
body: JSON.stringify({ mock: "data" }),
});
});
await page.goto("/dashboard");
});
Compare screenshots against baseline:
test("visual regression", async ({ page }) => {
await page.goto("/");
await expect(page).toHaveScreenshot("homepage.png");
});
Run tests with a visible browser for debugging:
HEADED=true pnpm test
Add delays between actions in config:
use: {
launchOptions: {
slowMo: 100, // 100ms delay between actions
},
}
Use @axe-core/playwright for accessibility checks:
import { test, expect } from "@playwright/test";
import AxeBuilder from "@axe-core/playwright";
test("page is accessible", async ({ page }) => {
await page.goto("/");
const accessibilityScanResults = await new AxeBuilder({ page })
.include('[role="main"]')
.analyze();
expect(accessibilityScanResults.violations).toEqual([]);
});
waitForSelector() or assertionstoHaveText(), toBeVisible(), etc.# Run all tests
[package-manager] run [test-script]
# Run specific test file
[package-manager] run [test-script] tests/my-feature.spec.ts
# Run with trace for debugging
[package-manager] run [test-script] -- --trace on
# View trace
npx playwright show-trace test-results/trace.zip
# Run in headed mode (if supported)
HEADED=true [package-manager] run [test-script]
The project includes tests/test-utils.ts with powerful testing utilities:
.env.local)CONVEX_URL - Self-hosted Convex backend URL (required)CONVEX_DASHBOARD_URL - Convex dashboard URL (required)VITE_CONVEX_URL - Alternative Convex URL for devTests automatically track:
// Using test-utils.ts extended test fixture
import { test } from "./test-utils";
test("page has no console errors", async ({ page }) => {
await page.goto("/");
// Console tracking is automatic with test-utils.ts fixture
page.assertNoConsoleErrors();
});
// With filter pattern
test("page has no critical errors", async ({ page }) => {
await page.goto("/");
// Ignore CORS warnings, fail on others
page.assertNoConsoleErrors(/CORS/);
});
import { CONSOLE_FILTERS } from "./test-utils";
// Ignore common development warnings
page.assertNoConsoleErrors(CONSOLE_FILTERS.ALL_DEV);
// Ignore only CORS errors
page.assertNoConsoleErrors(CONSOLE_FILTERS.CORS);
// Ignore auth-redirect errors (common in Coder)
page.assertNoConsoleErrors(CONSOLE_FILTERS.AUTH_REDIRECT);
test("page loads quickly", async ({ page }) => {
await page.goto("/");
// Assert performance metrics
const metrics = await page.assertPerformanceMetrics({
maxLoadTime: 3000,
maxDomContentLoaded: 2000,
});
});
import { gotoAndCheckConsole, expectConsoleErrors } from "./test-utils";
// Navigate and check console in one call
await gotoAndCheckConsole(page, "/my-page", {
waitForState: "load",
ignoreErrorsPattern: /CORS/,
});
// Check for specific error patterns
expectConsoleErrors(page, [/expected-error-1/, /expected-error-2/]);
On test failure, Playwright automatically saves:
| Artifact | Location | When Saved |
|---|---|---|
| Traces | test-results/trace.zip | On failure (retain-on-failure) |
| Screenshots | test-results/ | On failure (only-on-failure) |
| Videos | test-results/ | On failure (retain-on-failure) |
# Open trace viewer
npx playwright show-trace test-results/trace.zip
# Run tests with trace always on
[package-manager] run [test-script] -- --trace on
Before running tests, verify the dev server is running:
# Check if dev server is running
# Use PM2 if available (check coder-environment skill), or check process manually
lsof -i :[dev-port]
# View logs to diagnose issues
# Use PM2 logs if available, or container logs for Docker
Server Ports: Check your project configuration for the development and production ports.
Creating algorithmic art using p5.js with seeded randomness and interactive parameter exploration. Use this when users request creating art using code, generative art, algorithmic art, flow fields, or particle systems. Create original algorithmic art rather than copying existing artists' work to avoid copyright violations.
Applies Anthropic's official brand colors and typography to any sort of artifact that may benefit from having Anthropic's look-and-feel. Use it when brand colors or style guidelines, visual formatting, or company design standards apply.
Create beautiful visual art in .png and .pdf documents using design philosophy. You should use this skill when the user asks to create a poster, piece of art, design, or other static piece. Create original visual designs, never copying existing artists' work to avoid copyright violations.