chore: add playwright cursor skill

This commit is contained in:
Anish Sarkar 2026-05-10 04:19:55 +05:30
parent 25aad38ca4
commit d52225c18d
57 changed files with 25244 additions and 0 deletions

View file

@ -0,0 +1,453 @@
# Performance & Parallelization
## Table of Contents
1. [Parallel Execution](#parallel-execution)
2. [Sharding](#sharding)
3. [Test Optimization](#test-optimization)
4. [Network Optimization](#network-optimization)
5. [Isolation and Parallel Execution](#isolation-and-parallel-execution)
6. [Resource Management](#resource-management)
7. [Benchmarking](#benchmarking)
## Parallel Execution
### Configuration
```typescript
// playwright.config.ts
export default defineConfig({
// Run test files in parallel
fullyParallel: true,
// Number of worker processes
workers: process.env.CI ? 1 : undefined, // undefined = half CPU cores
// Or explicit count
// workers: 4,
// workers: '50%', // Percentage of CPU cores
});
```
### Serial Execution When Needed
```typescript
// Entire file serial
test.describe.configure({ mode: "serial" });
test.describe("Sequential Tests", () => {
test("first", async ({ page }) => {
// Runs first
});
test("second", async ({ page }) => {
// Runs after first
});
});
```
```typescript
// Single describe block serial
test.describe("Parallel Tests", () => {
test("a", async () => {}); // Parallel
test("b", async () => {}); // Parallel
});
test.describe.serial("Serial Tests", () => {
test("c", async () => {}); // Serial
test("d", async () => {}); // Serial
});
```
### Parallel Projects
```typescript
// playwright.config.ts
export default defineConfig({
projects: [
{ name: "chromium", use: { ...devices["Desktop Chrome"] } },
{ name: "firefox", use: { ...devices["Desktop Firefox"] } },
{ name: "webkit", use: { ...devices["Desktop Safari"] } },
],
});
```
```bash
# Run all projects in parallel
npx playwright test
# Run specific project
npx playwright test --project=chromium
```
## Sharding
### Basic Sharding
```bash
# Split tests across 4 machines
# Machine 1:
npx playwright test --shard=1/4
# Machine 2:
npx playwright test --shard=2/4
# Machine 3:
npx playwright test --shard=3/4
# Machine 4:
npx playwright test --shard=4/4
```
### Sharding Strategy
Tests are distributed evenly by file. For optimal sharding:
- Keep test files similar in size
- Use `fullyParallel: true` for even distribution
- Balance slow tests across files
### CI Sharding Pattern
```yaml
# GitHub Actions
jobs:
test:
strategy:
matrix:
shard: [1, 2, 3, 4]
steps:
- run: npx playwright test --shard=${{ matrix.shard }}/4
```
> **For comprehensive CI sharding** (blob reports, merging sharded results, full workflows), see [ci-cd.md](ci-cd.md#sharding).
## Test Optimization
### Reuse Authentication
Avoid logging in for every test. Use setup projects with storage state to authenticate once and reuse the session.
> **For authentication patterns** (storage state, multiple auth states, setup projects), see [fixtures-hooks.md](fixtures-hooks.md#authentication-patterns).
### Reuse Page State (serial only — trade-off with isolation)
Sharing a single page/context across tests with `beforeAll`/`afterAll` is **not recommended** for most suites: it breaks test isolation, causes state leak between tests, and makes failures harder to debug. Prefer a fresh `page` per test (Playwright default). Use shared page only when you explicitly need serial execution and accept no isolation.
```typescript
// ⚠️ Serial only, no isolation: state from one test leaks into the next.
// Prefer test.describe.configure({ mode: 'serial' }) + fresh page per test, or beforeEach + page.goto().
test.describe.configure({ mode: "serial" });
test.describe("Dashboard", () => {
let page: Page;
test.beforeAll(async ({ browser }) => {
const context = await browser.newContext({
storageState: ".auth/user.json",
});
page = await context.newPage();
await page.goto("/dashboard");
});
test.afterAll(async () => {
await page?.close();
});
test("shows stats", async () => {
await expect(page.getByTestId("stats")).toBeVisible();
});
test("shows chart", async () => {
await expect(page.getByTestId("chart")).toBeVisible();
});
});
```
### Lazy Navigation
```typescript
// Bad: Navigate in every test
test("check header", async ({ page }) => {
await page.goto("/products");
await expect(page.getByRole("heading")).toBeVisible();
});
test("check footer", async ({ page }) => {
await page.goto("/products");
await expect(page.getByRole("contentinfo")).toBeVisible();
});
// Good: Share navigation
test.describe("Products Page", () => {
test.beforeEach(async ({ page }) => {
await page.goto("/products");
});
test("check header", async ({ page }) => {
await expect(page.getByRole("heading")).toBeVisible();
});
test("check footer", async ({ page }) => {
await expect(page.getByRole("contentinfo")).toBeVisible();
});
});
```
### Skip Unnecessary Setup
```typescript
// Use test.skip for conditional execution
test("admin feature", async ({ page }) => {
test.skip(!process.env.ADMIN_ENABLED, "Admin features disabled");
// ...
});
// Use test.fixme for known broken tests
test.fixme("broken feature", async ({ page }) => {
// Skipped but tracked
});
```
## Network Optimization
### Mock APIs
```typescript
test.beforeEach(async ({ page }) => {
// Mock slow/heavy endpoints
await page.route("**/api/analytics", (route) =>
route.fulfill({ json: { views: 1000 } }),
);
await page.route("**/api/recommendations", (route) =>
route.fulfill({ json: [] }),
);
});
```
### Block Unnecessary Resources
```typescript
test.beforeEach(async ({ page }) => {
// Block analytics, ads, tracking
await page.route("**/*", (route) => {
const url = route.request().url();
if (
url.includes("google-analytics") ||
url.includes("facebook") ||
url.includes("hotjar")
) {
return route.abort();
}
return route.continue();
});
});
```
### Block Resource Types
```typescript
// Block images and fonts for faster tests
await page.route("**/*", (route) => {
const resourceType = route.request().resourceType();
if (["image", "font", "stylesheet"].includes(resourceType)) {
return route.abort();
}
return route.continue();
});
```
### Cache API Responses
```typescript
const apiCache = new Map<string, object>();
test.beforeEach(async ({ page }) => {
await page.route("**/api/**", async (route) => {
const url = route.request().url();
if (apiCache.has(url)) {
return route.fulfill({ json: apiCache.get(url) });
}
const response = await route.fetch();
const json = await response.json();
apiCache.set(url, json);
return route.fulfill({ json });
});
});
```
## Isolation and Parallel Execution
### Default: one context per test
Playwright gives each test its own browser context (and page). That gives isolation: no shared cookies, storage, or DOM between tests, so failures dont carry over and you can run tests in any order or in parallel. Keep this default unless you have a clear reason to share state.
### Avoiding state leak in parallel runs
- **Do not** rely on shared mutable state (e.g. a single `page` or `context` in `beforeAll`) when tests can run in parallel. State from one test can leak into another and cause flaky, order-dependent failures.
- Use **fixtures** for setup/teardown and **`beforeEach`** for per-test navigation so each test gets a fresh page or a clean slate.
- For **backend or DB state** shared across tests, isolate per worker so parallel workers dont collide. Use a worker-scoped fixture and `testInfo.workerIndex` (or `process.env.TEST_WORKER_INDEX`) to create unique data per worker (e.g. unique user or DB prefix). See [fixtures-hooks.md](../core/fixtures-hooks.md) for worker-scoped fixtures and [debugging.md](../debugging/debugging.md) for debugging flaky parallel runs.
### Debugging flaky parallel runs
If a test is flaky only with multiple workers:
1. **Reproduce**: Run with default workers and `--repeat-each=10` (or `--repeat-each=100 --max-failures=1`).
2. **Confirm parallel-specific**: Run with `--workers=1`. If the failure disappears, the cause is likely shared state or non-isolated backend/DB data.
3. **Fix**: Remove shared page/context; use per-test fixtures and `beforeEach`; isolate test data per worker with `workerIndex` in a worker-scoped fixture.
Workers are restarted after a test failure so subsequent tests in that worker get a clean environment; fixing isolation still prevents the initial flakiness.
## Resource Management
### Browser Contexts
```typescript
// Recommended: One context per test (default) — full isolation
test("isolated test", async ({ page }) => {
// Fresh context automatically
});
// Manual context for specific needs
test("multiple tabs", async ({ browser }) => {
const context = await browser.newContext();
const page1 = await context.newPage();
const page2 = await context.newPage();
// Clean up
await context.close();
});
```
### Memory Management
```typescript
// playwright.config.ts
export default defineConfig({
// Limit concurrent workers
workers: 2,
// Limit parallel tests per worker
use: {
// Lower memory usage
launchOptions: {
args: ["--disable-dev-shm-usage"],
},
},
});
```
### Timeouts
```typescript
// playwright.config.ts
export default defineConfig({
// Global test timeout
timeout: 30000,
// Assertion timeout
expect: {
timeout: 5000,
},
// Navigation timeout
use: {
navigationTimeout: 15000,
actionTimeout: 10000,
},
});
```
## Benchmarking
### Measure Test Duration
```typescript
test("performance test", async ({ page }, testInfo) => {
const startTime = Date.now();
await page.goto("/");
const loadTime = Date.now() - startTime;
console.log(`Page load: ${loadTime}ms`);
// Add to test report
testInfo.annotations.push({
type: "performance",
description: `Load time: ${loadTime}ms`,
});
});
```
### Performance Metrics
```typescript
test("collect metrics", async ({ page }) => {
await page.goto("/");
const metrics = await page.evaluate(() => ({
// Navigation timing
loadTime:
performance.timing.loadEventEnd - performance.timing.navigationStart,
domContentLoaded:
performance.timing.domContentLoadedEventEnd -
performance.timing.navigationStart,
// Performance entries
resources: performance.getEntriesByType("resource").length,
// Memory (Chrome only)
// @ts-ignore
memory: performance.memory?.usedJSHeapSize,
}));
console.log("Metrics:", metrics);
expect(metrics.loadTime).toBeLessThan(3000);
});
```
### Lighthouse Integration
```typescript
import { playAudit } from "playwright-lighthouse";
test("lighthouse audit", async ({ page }) => {
await page.goto("/");
const audit = await playAudit({
page,
thresholds: {
performance: 80,
accessibility: 90,
"best-practices": 80,
seo: 80,
},
port: 9222,
});
expect(audit.lhr.categories.performance.score * 100).toBeGreaterThanOrEqual(
80,
);
});
```
## Performance Checklist
| Optimization | Impact |
| ------------------------------ | ---------- |
| Enable `fullyParallel` | High |
| Reuse authentication | High |
| Mock heavy APIs | High |
| Block tracking scripts | Medium |
| Use sharding in CI | High |
| Reduce workers if memory-bound | Medium |
| Cache API responses | Medium |
| Skip unnecessary tests | Low-Medium |
## Related References
- **CI/CD sharding**: See [ci-cd.md](ci-cd.md) for CI configuration
- **Test organization**: See [test-suite-structure.md](../core/test-suite-structure.md) for structuring tests
- **Fixtures for reuse**: See [fixtures-hooks.md](../core/fixtures-hooks.md) for authentication patterns