playwright-test
Playwright end-to-end testing patterns and best practices
What this skill does
# Playwright Testing Skill
End-to-end testing patterns with Playwright for Astro 5.16 + React 19 projects.
## Project Context
- **Framework**: Check your project configuration for framework versions
- **Testing**: Playwright E2E testing
- **Runtime**: Dev server management (PM2 if available in Coder, check coder-environment skill)
- **Headless mode**: Tests run headless by default; check workspace for headed mode support
- **Test utilities**: Check your project for available test helpers
- **Artifacts**: Traces, videos, and screenshots saved to `test-results/` on failure
## Base URL Configuration
**CRITICAL**: 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.
### Web Server Configuration
**DO NOT configure Playwright to start a web server.** Playwright should assume the server is already running.
**DON'T** - Never add `webServer` configuration:
```typescript
// ❌ 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:
```typescript
// ✅ 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.
### Test Files MUST Use Root-Relative Paths
**DO** - Use root-relative paths in test files:
```typescript
// ✅ CORRECT
await page.goto("/");
await page.goto("/about");
await page.goto("/blog/my-post");
```
**DON'T** - Never include base URL in test files:
```typescript
// ❌ 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:
```typescript
// ❌ WRONG - No fallback URLs, NEVER hardcode localhost
const baseUrl = process.env.APP_SERVER_URL || "http://localhost:3000";
await page.goto(baseUrl);
```
### Running Tests Against Different Environments
The config file reads `BASE_URL` from `.env.local` or the environment. For most cases, just run:
```bash
# 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.
## Test File Structure
```typescript
// tests/my-feature.spec.ts
import { test, expect } from "@playwright/test";
test("describes the behavior", async ({ page }) => {
await page.goto("/");
// test implementation
});
```
## Test Patterns
### Navigation
```typescript
test("navigates to a page", async ({ page }) => {
await page.goto("/about");
await expect(page).toHaveURL(/\/about/);
});
```
### Element Visibility
```typescript
test("shows element on page", async ({ page }) => {
await page.goto("/");
await expect(page.locator("h1")).toBeVisible();
});
```
### Clicking and Interaction
```typescript
test("button click triggers action", async ({ page }) => {
await page.goto("/");
await page.click('button[type="submit"]');
await expect(page.locator(".success-message")).toBeVisible();
});
```
### Form Submission
```typescript
test("form submission works", async ({ page }) => {
await page.goto("/contact");
await page.fill('input[name="email"]', "[email protected]");
await page.fill('textarea[name="message"]', "Hello");
await page.click('button[type="submit"]');
await expect(page.locator(".success")).toBeVisible();
});
```
### Responsive Design
```typescript
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();
});
});
```
### Async State
```typescript
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();
});
```
### Error States
```typescript
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");
});
```
## Selectors
Use semantic, accessible selectors:
**DO**:
```typescript
page.locator('button[type="submit"]');
page.locator('nav a[href="/about"]');
page.locator("h1");
page.getByRole("button", { name: "Submit" });
page.getByLabelText("Email");
```
**DON'T**:
```typescript
page.locator(".btn-primary"); // Fragile class names
page.locator("#submit-btn"); // Implementation detail
page.locator("div > div > p"); // Brittle structure
```
## When to Write Tests
Write tests for:
- New page/route creation
- Component behavior changes
- Form submission flows
- Navigation between pages
- User interactions (clicks, inputs, form submissions)
- Conditional rendering based on state
- Responsive design verification
- API integration testing
## Test Organization
### Test Groups
Use `test.describe()` to group related tests:
```typescript
test.describe("user authentication", () => {
test("login with valid credentials", async ({ page }) => {
// ...
});
test("shows error for invalid credentials", async ({ page }) => {
// ...
});
});
```
### Before/After Hooks
```typescript
test.beforeEach(async ({ page }) => {
// Setup before each test
await page.goto("/login");
});
test.afterEach(async ({ page }) => {
// Cleanup after each test
});
```
## Fixtures
Create custom fixtures for reusable test utilities:
```typescript
// 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"]', "[email protected]");
await page.fill('input[name="password"]', "password");
await page.click('button[type="submit"]');
await paRelated in Code Review
gstack
IncludedFast headless browser for QA testing and site dogfooding. Navigate pages, interact with elements, verify state, diff before/after, take annotated screenshots, test responsive layouts, forms, uploads, dialogs, and capture bug evidence. Use when asked to open or test a site, verify a deployment, dogfood a user flow, or file a bug with screenshots. (gstack)
startup-due-diligence
IncludedLegal due diligence review for seed-stage and Series A startups (US, Delaware C-Corp focus). Supports both investor and founder perspectives. Capabilities include: (1) Interactive document review and issue spotting; (2) Document request list generation; (3) Cap table and SAFE/convertible note analysis; (4) Red flag identification with severity ratings; (5) Diligence report generation. TRIGGERS: due diligence, DD, startup investment, cap table review, Series A, seed round, investor diligence, legal review startup, SAFE analysis, convertible note, 409A, founder vesting.
interview-master
IncludedThis skill should be used when the user asks to "generate interview questions", "prepare for interview", "optimize resume", "conduct mock interview", "analyze git commits for resume", "generate resume from code", "review my resume", or mentions interview preparation, career assistance, or extracting project experience from git history. Provides comprehensive interview and career development guidance for both job seekers and interviewers.
fix-issue
IncludedFixes GitHub issues using parallel analysis agents for root cause investigation, code exploration, and regression detection. Reads issue context from gh CLI, searches codebase and memory for related patterns, generates a fix with tests, and links the resolution back to the issue via PR. Includes prevention analysis to avoid recurrence. Use when debugging errors, resolving regressions, fixing bugs, or triaging issues.
sf-apex
IncludedGenerates and reviews Salesforce Apex code with 150-point scoring. TRIGGER when: user writes, reviews, or fixes Apex classes, triggers, test classes, batch/queueable/schedulable jobs, or touches .cls/.trigger files. DO NOT TRIGGER when: LWC JavaScript (use sf-lwc), Flow XML (use sf-flow), SOQL-only queries (use sf-soql), or non-Salesforce code.
swift-development
IncludedComprehensive Swift development for building, testing, and deploying iOS/macOS applications. Use when Claude needs to: (1) Build Swift packages or Xcode projects from command line, (2) Run tests with XCTest or Swift Testing framework, (3) Manage iOS simulators with simctl, (4) Handle code signing, provisioning profiles, and app distribution, (5) Format or lint Swift code with SwiftFormat/SwiftLint, (6) Work with Swift Package Manager (SPM), (7) Implement Swift 6 concurrency patterns (async/await, actors, Sendable), (8) Create SwiftUI views with MVVM architecture, (9) Set up Core Data or SwiftData persistence, or any other Swift/iOS/macOS development tasks.