# Test Organization > **When to use**: Structuring test files, naming tests, grouping with `describe`, tagging, filtering, and deciding parallel vs serial execution. > **Prerequisites**: [core/configuration.md](foundations/configuration.md) ## Quick Reference | Concept | Rule | |---|---| | File suffix | `.spec.ts` / `.spec.js` — always | | Grouping | By feature/domain, not by page or URL | | Test names | `test('should ...')` or `test('user can ...')` — describe behavior | | Nesting | Max 2 levels of `test.describe()` | | Default execution | `fullyParallel: true` — tests run in parallel by default | | Serial tests | Almost never use `test.describe.serial()` | | Test dependencies | Avoid — each test sets up its own state | | Tags | `@smoke`, `@regression`, `@slow` — filter with `--grep` | ## Patterns ### Pattern 1: Feature-Based File Structure **Use when**: Any project — this is the default layout. **Avoid when**: Never. This is always correct. Group tests by feature or domain, not by page class or test type. **Small project (< 20 tests):** ``` tests/ ├── auth.spec.ts ├── dashboard.spec.ts ├── settings.spec.ts └── checkout.spec.ts playwright.config.ts ``` **Medium project (20–200 tests):** ``` tests/ ├── auth/ │ ├── login.spec.ts │ ├── signup.spec.ts │ ├── password-reset.spec.ts │ └── mfa.spec.ts ├── dashboard/ │ ├── widgets.spec.ts │ ├── filters.spec.ts │ └── export.spec.ts ├── checkout/ │ ├── cart.spec.ts │ ├── payment.spec.ts │ └── confirmation.spec.ts ├── settings/ │ ├── profile.spec.ts │ └── notifications.spec.ts └── fixtures/ ├── auth.fixture.ts └── test-data.ts playwright.config.ts ``` **Large project (200+ tests):** ``` tests/ ├── e2e/ │ ├── auth/ │ │ ├── login.spec.ts │ │ ├── signup.spec.ts │ │ ├── password-reset.spec.ts │ │ └── mfa.spec.ts │ ├── checkout/ │ │ ├── cart.spec.ts │ │ ├── payment.spec.ts │ │ ├── promo-codes.spec.ts │ │ └── confirmation.spec.ts │ ├── admin/ │ │ ├── user-management.spec.ts │ │ ├── reports.spec.ts │ │ └── audit-log.spec.ts │ └── inventory/ │ ├── product-list.spec.ts │ ├── product-detail.spec.ts │ └── stock-management.spec.ts ├── api/ │ ├── users.spec.ts │ ├── orders.spec.ts │ └── products.spec.ts ├── visual/ │ ├── homepage.spec.ts │ └── product-page.spec.ts ├── fixtures/ │ ├── auth.fixture.ts │ ├── db.fixture.ts │ └── base.fixture.ts ├── page-objects/ │ ├── login.page.ts │ ├── dashboard.page.ts │ └── checkout.page.ts └── helpers/ ├── test-data.ts └── api-client.ts playwright.config.ts ``` --- ### Pattern 2: Naming Conventions **Use when**: Writing any test or file. **Avoid when**: Never. **TypeScript:** ```typescript // tests/checkout/cart.spec.ts import { test, expect } from '@playwright/test'; // Good: group by feature, describe behavior test.describe('Shopping Cart', () => { test('should add item to empty cart', async ({ page }) => { await page.goto('/products/widget-a'); await page.getByRole('button', { name: 'Add to cart' }).click(); await expect(page.getByTestId('cart-count')).toHaveText('1'); }); test('should update quantity when same item added twice', async ({ page }) => { await page.goto('/products/widget-a'); await page.getByRole('button', { name: 'Add to cart' }).click(); await page.getByRole('button', { name: 'Add to cart' }).click(); await expect(page.getByTestId('cart-count')).toHaveText('2'); }); test('user can remove item from cart', async ({ page }) => { await page.goto('/cart'); // ... setup item in cart via API or fixture await page.getByRole('button', { name: 'Remove' }).first().click(); await expect(page.getByText('Your cart is empty')).toBeVisible(); }); }); ``` **JavaScript:** ```javascript // tests/checkout/cart.spec.js const { test, expect } = require('@playwright/test'); test.describe('Shopping Cart', () => { test('should add item to empty cart', async ({ page }) => { await page.goto('/products/widget-a'); await page.getByRole('button', { name: 'Add to cart' }).click(); await expect(page.getByTestId('cart-count')).toHaveText('1'); }); test('should update quantity when same item added twice', async ({ page }) => { await page.goto('/products/widget-a'); await page.getByRole('button', { name: 'Add to cart' }).click(); await page.getByRole('button', { name: 'Add to cart' }).click(); await expect(page.getByTestId('cart-count')).toHaveText('2'); }); test('user can remove item from cart', async ({ page }) => { await page.goto('/cart'); await page.getByRole('button', { name: 'Remove' }).first().click(); await expect(page.getByText('Your cart is empty')).toBeVisible(); }); }); ``` **Naming rules:** | Element | Convention | Example | |---|---|---| | File name | `kebab-case.spec.ts` | `password-reset.spec.ts` | | `test.describe()` | Title Case, feature name | `'Password Reset'` | | `test()` | Sentence starting with `should` or `user can` | `'should send reset email'` | | Page objects | `PascalCase.page.ts` | `login.page.ts` / `LoginPage` | | Fixtures | `kebab-case.fixture.ts` | `auth.fixture.ts` | --- ### Pattern 3: `test.describe()` Grouping **Use when**: A file has multiple related tests that share context or setup. **Avoid when**: A file has only 1–3 tests with no shared setup — skip the `describe` wrapper. **TypeScript:** ```typescript // tests/auth/login.spec.ts import { test, expect } from '@playwright/test'; test.describe('Login', () => { // Shared setup for all tests in this describe block test.beforeEach(async ({ page }) => { await page.goto('/login'); }); test('should login with valid credentials', async ({ page }) => { await page.getByLabel('Email').fill('user@example.com'); await page.getByLabel('Password').fill('securepass123'); await page.getByRole('button', { name: 'Sign in' }).click(); await expect(page).toHaveURL('/dashboard'); }); test('should show error for invalid password', async ({ page }) => { await page.getByLabel('Email').fill('user@example.com'); await page.getByLabel('Password').fill('wrongpassword'); await page.getByRole('button', { name: 'Sign in' }).click(); await expect(page.getByRole('alert')).toHaveText('Invalid credentials'); }); // One level of nesting — acceptable test.describe('Rate Limiting', () => { test('should lock account after 5 failed attempts', async ({ page }) => { for (let i = 0; i < 5; i++) { await page.getByLabel('Email').fill('user@example.com'); await page.getByLabel('Password').fill('wrong'); await page.getByRole('button', { name: 'Sign in' }).click(); } await expect(page.getByRole('alert')).toContainText('Account locked'); }); }); }); ``` **JavaScript:** ```javascript // tests/auth/login.spec.js const { test, expect } = require('@playwright/test'); test.describe('Login', () => { test.beforeEach(async ({ page }) => { await page.goto('/login'); }); test('should login with valid credentials', async ({ page }) => { await page.getByLabel('Email').fill('user@example.com'); await page.getByLabel('Password').fill('securepass123'); await page.getByRole('button', { name: 'Sign in' }).click(); await expect(page).toHaveURL('/dashboard'); }); test('should show error for invalid password', async ({ page }) => { await page.getByLabel('Email').fill('user@example.com'); await page.getByLabel('Password').fill('wrongpassword'); await page.getByRole('button', { name: 'Sign in' }).click(); await expect(page.getByRole('alert')).toHaveText('Invalid credentials'); }); test.describe('Rate Limiting', () => { test('should lock account after 5 failed attempts', async ({ page }) => { for (let i = 0; i < 5; i++) { await page.getByLabel('Email').fill('user@example.com'); await page.getByLabel('Password').fill('wrong'); await page.getByRole('button', { name: 'Sign in' }).click(); } await expect(page.getByRole('alert')).toContainText('Account locked'); }); }); }); ``` **Nesting limit: 2 levels max.** If you need a third level, split into a separate file. --- ### Pattern 4: Tags and Annotations **Use when**: You need to categorize tests for selective runs (smoke suites, CI pipelines, skip known issues). **Avoid when**: All tests always run together and you have < 20 tests. **TypeScript:** ```typescript // tests/checkout/payment.spec.ts import { test, expect } from '@playwright/test'; // Tag in the test title — simplest approach test('should process credit card payment @smoke', async ({ page }) => { await page.goto('/checkout'); await page.getByLabel('Card number').fill('4242424242424242'); await page.getByLabel('Expiry').fill('12/28'); await page.getByLabel('CVC').fill('123'); await page.getByRole('button', { name: 'Pay now' }).click(); await expect(page.getByText('Payment successful')).toBeVisible(); }); test('should handle declined card @regression', async ({ page }) => { await page.goto('/checkout'); await page.getByLabel('Card number').fill('4000000000000002'); await page.getByLabel('Expiry').fill('12/28'); await page.getByLabel('CVC').fill('123'); await page.getByRole('button', { name: 'Pay now' }).click(); await expect(page.getByRole('alert')).toContainText('Card declined'); }); // Tag via test.describe for group-level tagging test.describe('Payment Edge Cases @regression', () => { test('should handle network timeout during payment', async ({ page }) => { await page.goto('/checkout'); // ... test implementation await expect(page.getByText('Please try again')).toBeVisible(); }); }); // Annotations for test lifecycle control test('should render 3D Secure iframe @slow', async ({ page }) => { test.slow(); // Triples the default timeout await page.goto('/checkout/3ds'); // ... long-running 3D Secure flow await expect(page.getByText('Verified')).toBeVisible(); }); test('should apply loyalty points discount', async ({ page }) => { test.skip(process.env.CI !== 'true', 'Only run in CI — needs loyalty service'); await page.goto('/checkout'); // ... }); test('should handle Apple Pay on Safari', async ({ page, browserName }) => { test.skip(browserName !== 'webkit', 'Apple Pay only works in Safari'); await page.goto('/checkout'); // ... }); test('should display PayPal button', async ({ page }) => { test.fixme(); // Known broken — tracked in JIRA-1234 await page.goto('/checkout'); await expect(page.getByRole('button', { name: 'PayPal' })).toBeVisible(); }); test('should fail when submitting empty card form', async ({ page }) => { test.fail(); // This test is expected to fail — documents known bug await page.goto('/checkout'); await page.getByRole('button', { name: 'Pay now' }).click(); // Bug: currently no validation shown — this assertion will fail await expect(page.getByRole('alert')).toBeVisible(); }); ``` **JavaScript:** ```javascript // tests/checkout/payment.spec.js const { test, expect } = require('@playwright/test'); test('should process credit card payment @smoke', async ({ page }) => { await page.goto('/checkout'); await page.getByLabel('Card number').fill('4242424242424242'); await page.getByLabel('Expiry').fill('12/28'); await page.getByLabel('CVC').fill('123'); await page.getByRole('button', { name: 'Pay now' }).click(); await expect(page.getByText('Payment successful')).toBeVisible(); }); test('should handle declined card @regression', async ({ page }) => { await page.goto('/checkout'); await page.getByLabel('Card number').fill('4000000000000002'); await page.getByLabel('Expiry').fill('12/28'); await page.getByLabel('CVC').fill('123'); await page.getByRole('button', { name: 'Pay now' }).click(); await expect(page.getByRole('alert')).toContainText('Card declined'); }); test.describe('Payment Edge Cases @regression', () => { test('should handle network timeout during payment', async ({ page }) => { await page.goto('/checkout'); await expect(page.getByText('Please try again')).toBeVisible(); }); }); test('should render 3D Secure iframe @slow', async ({ page }) => { test.slow(); await page.goto('/checkout/3ds'); await expect(page.getByText('Verified')).toBeVisible(); }); test('should apply loyalty points discount', async ({ page }) => { test.skip(process.env.CI !== 'true', 'Only run in CI — needs loyalty service'); await page.goto('/checkout'); }); test('should handle Apple Pay on Safari', async ({ page, browserName }) => { test.skip(browserName !== 'webkit', 'Apple Pay only works in Safari'); await page.goto('/checkout'); }); test('should display PayPal button', async ({ page }) => { test.fixme(); // Known broken — tracked in JIRA-1234 await page.goto('/checkout'); await expect(page.getByRole('button', { name: 'PayPal' })).toBeVisible(); }); test('should fail when submitting empty card form', async ({ page }) => { test.fail(); await page.goto('/checkout'); await page.getByRole('button', { name: 'Pay now' }).click(); await expect(page.getByRole('alert')).toBeVisible(); }); ``` **Annotation cheat sheet:** | Annotation | Effect | When to use | |---|---|---| | `test.skip()` | Skips the test entirely | Feature not available in this env/browser | | `test.skip(condition, reason)` | Conditional skip | Browser-specific or env-specific tests | | `test.fixme()` | Skips and marks as "needs fix" | Known bug, not yet fixed | | `test.slow()` | Triples the timeout | Tests with inherently slow workflows | | `test.fail()` | Expects the test to fail; fails if it passes | Documenting a known bug with a regression guard | | `test.info().annotations` | Add custom annotations | Custom metadata for reports | --- ### Pattern 5: Test Filtering **Use when**: Running a subset of tests from the CLI or CI. **Avoid when**: Never — know these commands. ```bash # By tag (in test title) npx playwright test --grep @smoke npx playwright test --grep @regression npx playwright test --grep-invert @slow # everything except @slow # By file npx playwright test tests/auth/ npx playwright test tests/checkout/payment.spec.ts # By test name npx playwright test --grep "should login" # By project (from playwright.config) npx playwright test --project=chromium npx playwright test --project=mobile-safari # Combine filters npx playwright test --grep @smoke --project=chromium npx playwright test tests/checkout/ --grep @smoke # By line number — run a single test npx playwright test tests/auth/login.spec.ts:15 ``` **Using tags with `test.describe.configure()`:** **TypeScript:** ```typescript // playwright.config.ts import { defineConfig } from '@playwright/test'; export default defineConfig({ projects: [ { name: 'smoke', testMatch: '**/*.spec.ts', grep: /@smoke/, }, { name: 'regression', testMatch: '**/*.spec.ts', grep: /@regression/, }, { name: 'all', testMatch: '**/*.spec.ts', grepInvert: /@slow/, }, ], }); ``` **JavaScript:** ```javascript // playwright.config.js const { defineConfig } = require('@playwright/test'); module.exports = defineConfig({ projects: [ { name: 'smoke', testMatch: '**/*.spec.js', grep: /@smoke/, }, { name: 'regression', testMatch: '**/*.spec.js', grep: /@regression/, }, { name: 'all', testMatch: '**/*.spec.js', grepInvert: /@slow/, }, ], }); ``` --- ### Pattern 6: Parallel vs Serial Execution **Use when**: Deciding how tests run relative to each other. **Avoid when**: You have < 5 tests (parallelism doesn't matter). **Default: always parallel.** Set `fullyParallel: true` in config. **TypeScript:** ```typescript // playwright.config.ts import { defineConfig } from '@playwright/test'; export default defineConfig({ fullyParallel: true, // All tests run in parallel by default workers: process.env.CI ? 1 : undefined, // Use all cores locally, 1 in CI (or adjust) }); ``` **JavaScript:** ```javascript // playwright.config.js const { defineConfig } = require('@playwright/test'); module.exports = defineConfig({ fullyParallel: true, workers: process.env.CI ? 1 : undefined, }); ``` **Serial execution — use only when tests share external state you cannot isolate:** **TypeScript:** ```typescript // tests/onboarding/wizard.spec.ts import { test, expect } from '@playwright/test'; // ONLY use serial when steps truly depend on prior state that cannot be set up independently test.describe.serial('Onboarding Wizard', () => { test('step 1: user enters company name', async ({ page }) => { await page.goto('/onboarding'); await page.getByLabel('Company name').fill('Acme Corp'); await page.getByRole('button', { name: 'Next' }).click(); await expect(page).toHaveURL('/onboarding/step-2'); }); test('step 2: user selects plan', async ({ page }) => { await page.goto('/onboarding/step-2'); await page.getByRole('radio', { name: 'Pro plan' }).check(); await page.getByRole('button', { name: 'Next' }).click(); await expect(page).toHaveURL('/onboarding/step-3'); }); test('step 3: user confirms and completes', async ({ page }) => { await page.goto('/onboarding/step-3'); await page.getByRole('button', { name: 'Complete setup' }).click(); await expect(page.getByText('Welcome to Acme Corp')).toBeVisible(); }); }); ``` **JavaScript:** ```javascript // tests/onboarding/wizard.spec.js const { test, expect } = require('@playwright/test'); test.describe.serial('Onboarding Wizard', () => { test('step 1: user enters company name', async ({ page }) => { await page.goto('/onboarding'); await page.getByLabel('Company name').fill('Acme Corp'); await page.getByRole('button', { name: 'Next' }).click(); await expect(page).toHaveURL('/onboarding/step-2'); }); test('step 2: user selects plan', async ({ page }) => { await page.goto('/onboarding/step-2'); await page.getByRole('radio', { name: 'Pro plan' }).check(); await page.getByRole('button', { name: 'Next' }).click(); await expect(page).toHaveURL('/onboarding/step-3'); }); test('step 3: user confirms and completes', async ({ page }) => { await page.goto('/onboarding/step-3'); await page.getByRole('button', { name: 'Complete setup' }).click(); await expect(page.getByText('Welcome to Acme Corp')).toBeVisible(); }); }); ``` **Better alternative: test the full flow in one test with `test.step()`:** **TypeScript:** ```typescript // tests/onboarding/wizard.spec.ts import { test, expect } from '@playwright/test'; test('user completes full onboarding wizard', async ({ page }) => { await test.step('enter company name', async () => { await page.goto('/onboarding'); await page.getByLabel('Company name').fill('Acme Corp'); await page.getByRole('button', { name: 'Next' }).click(); await expect(page).toHaveURL('/onboarding/step-2'); }); await test.step('select plan', async () => { await page.getByRole('radio', { name: 'Pro plan' }).check(); await page.getByRole('button', { name: 'Next' }).click(); await expect(page).toHaveURL('/onboarding/step-3'); }); await test.step('confirm and complete', async () => { await page.getByRole('button', { name: 'Complete setup' }).click(); await expect(page.getByText('Welcome to Acme Corp')).toBeVisible(); }); }); ``` **JavaScript:** ```javascript // tests/onboarding/wizard.spec.js const { test, expect } = require('@playwright/test'); test('user completes full onboarding wizard', async ({ page }) => { await test.step('enter company name', async () => { await page.goto('/onboarding'); await page.getByLabel('Company name').fill('Acme Corp'); await page.getByRole('button', { name: 'Next' }).click(); await expect(page).toHaveURL('/onboarding/step-2'); }); await test.step('select plan', async () => { await page.getByRole('radio', { name: 'Pro plan' }).check(); await page.getByRole('button', { name: 'Next' }).click(); await expect(page).toHaveURL('/onboarding/step-3'); }); await test.step('confirm and complete', async () => { await page.getByRole('button', { name: 'Complete setup' }).click(); await expect(page.getByText('Welcome to Acme Corp')).toBeVisible(); }); }); ``` --- ### Pattern 7: Monorepo Testing **Use when**: A single repository contains multiple apps or packages that each need E2E tests. **Avoid when**: Single app repo. **TypeScript:** ```typescript // playwright.config.ts (root of monorepo) import { defineConfig } from '@playwright/test'; export default defineConfig({ projects: [ { name: 'web-app', testDir: './apps/web/tests', use: { baseURL: 'http://localhost:3000', }, }, { name: 'admin-panel', testDir: './apps/admin/tests', use: { baseURL: 'http://localhost:3001', }, }, { name: 'marketing-site', testDir: './apps/marketing/tests', use: { baseURL: 'http://localhost:4000', }, }, ], }); ``` **JavaScript:** ```javascript // playwright.config.js (root of monorepo) const { defineConfig } = require('@playwright/test'); module.exports = defineConfig({ projects: [ { name: 'web-app', testDir: './apps/web/tests', use: { baseURL: 'http://localhost:3000', }, }, { name: 'admin-panel', testDir: './apps/admin/tests', use: { baseURL: 'http://localhost:3001', }, }, { name: 'marketing-site', testDir: './apps/marketing/tests', use: { baseURL: 'http://localhost:4000', }, }, ], }); ``` **Monorepo directory layout:** ``` monorepo/ ├── apps/ │ ├── web/ │ │ ├── src/ │ │ └── tests/ │ │ ├── auth/ │ │ │ └── login.spec.ts │ │ └── dashboard/ │ │ └── widgets.spec.ts │ ├── admin/ │ │ ├── src/ │ │ └── tests/ │ │ └── user-management.spec.ts │ └── marketing/ │ ├── src/ │ └── tests/ │ └── landing-page.spec.ts ├── packages/ │ └── shared-fixtures/ │ ├── auth.fixture.ts │ └── index.ts └── playwright.config.ts ``` Run only one app's tests: ```bash npx playwright test --project=web-app npx playwright test --project=admin-panel ``` ## Decision Guide ``` How many tests do you have? │ ├── < 20 tests (small) │ ├── Flat structure: all .spec.ts files in tests/ │ ├── No subdirectories needed │ ├── Tags optional — just run everything │ └── fullyParallel: true, single config project │ ├── 20–200 tests (medium) │ ├── Feature subdirectories: tests/auth/, tests/checkout/ │ ├── Use @smoke tag for critical-path subset (< 15 min) │ ├── Shared fixtures in tests/fixtures/ │ ├── Page objects in tests/page-objects/ (if you use them) │ └── Consider separate projects for different browsers │ └── 200+ tests (large) ├── Top-level split: tests/e2e/, tests/api/, tests/visual/ ├── Feature subdirectories under each ├── @smoke, @regression, @slow tags — multiple CI pipelines ├── Sharding in CI for faster runs ├── Project-based filtering: smoke project, full project └── Shared fixtures as a package in monorepo ``` ``` Should I use test.describe.serial()? │ ├── Can each test set up its own state via API/fixture? │ └── YES → Do NOT use serial. Use independent parallel tests. │ ├── Is this a multi-step wizard where each step changes server state │ that cannot be reset between tests? │ └── Write it as ONE test with test.step() instead. │ └── Is this genuinely stateful (e.g., database migration sequence)? └── Use serial as last resort. Add a comment explaining why. ``` ## Anti-Patterns ### One giant test file ```typescript // BAD: tests/all-tests.spec.ts with 80 tests and 2000+ lines test.describe('Everything', () => { test('login works', async ({ page }) => { /* ... */ }); test('signup works', async ({ page }) => { /* ... */ }); test('cart works', async ({ page }) => { /* ... */ }); // ... 77 more tests }); ``` **Fix:** Split by feature — one file per feature area, 5–15 tests per file. --- ### Meaningless test names ```typescript // BAD test('test1', async ({ page }) => { /* ... */ }); test('test2', async ({ page }) => { /* ... */ }); test('payment test', async ({ page }) => { /* ... */ }); test('it works', async ({ page }) => { /* ... */ }); ``` **Fix:** Describe behavior. When a test fails, the name should tell you what broke. ```typescript // GOOD test('should reject expired credit card', async ({ page }) => { /* ... */ }); test('user can update shipping address during checkout', async ({ page }) => { /* ... */ }); ``` --- ### Deep describe nesting ```typescript // BAD: 3+ levels deep — hard to read, hard to find tests in reports test.describe('Auth', () => { test.describe('Login', () => { test.describe('With MFA', () => { test.describe('SMS', () => { test('should send code', async ({ page }) => { /* ... */ }); }); }); }); }); ``` **Fix:** Max 2 levels. Split deeper nesting into separate files. ```typescript // GOOD: tests/auth/login-mfa-sms.spec.ts test.describe('Login with SMS MFA', () => { test('should send verification code', async ({ page }) => { /* ... */ }); test('should reject invalid code', async ({ page }) => { /* ... */ }); }); ``` --- ### Using `test.describe.serial()` as the default ```typescript // BAD: serial for no reason — kills parallelism, creates hidden dependencies test.describe.serial('User Profile', () => { test('should display profile page', async ({ page }) => { /* ... */ }); test('should update display name', async ({ page }) => { /* ... */ }); test('should upload avatar', async ({ page }) => { /* ... */ }); }); ``` **Fix:** Each test should be independent. Use `beforeEach` or fixtures to set up state. ```typescript // GOOD test.describe('User Profile', () => { test.beforeEach(async ({ page }) => { await page.goto('/profile'); }); test('should display profile page', async ({ page }) => { /* ... */ }); test('should update display name', async ({ page }) => { /* ... */ }); test('should upload avatar', async ({ page }) => { /* ... */ }); }); ``` --- ### Relying on test execution order ```typescript // BAD: test 2 depends on test 1 creating data test('should create a product', async ({ page }) => { // creates "Widget X" in the database }); test('should edit the product', async ({ page }) => { // assumes "Widget X" exists — breaks if run alone or in parallel }); ``` **Fix:** Each test creates its own data via API calls or fixtures. ```typescript // GOOD test('should edit a product', async ({ page, request }) => { // Set up test data independently const response = await request.post('/api/products', { data: { name: 'Widget X', price: 9.99 }, }); const product = await response.json(); await page.goto(`/products/${product.id}/edit`); await page.getByLabel('Name').fill('Widget X Updated'); await page.getByRole('button', { name: 'Save' }).click(); await expect(page.getByText('Widget X Updated')).toBeVisible(); }); ``` ## Troubleshooting | Problem | Cause | Fix | |---|---|---| | Tests pass alone but fail together | Shared state between tests (cookies, DB rows, global variables) | Isolate each test — use fixtures for setup/teardown | | `--grep @smoke` matches nothing | Tag not in test title string | Verify the tag appears literally in `test('... @smoke', ...)` | | Serial tests cascade-fail | One test fails, all subsequent tests skip | Rewrite as independent tests or use `test.step()` in a single test | | Tests run slower than expected | `fullyParallel` not set | Add `fullyParallel: true` in `playwright.config` | | Wrong tests run in CI | `testMatch` or `testDir` misconfigured | Check `playwright.config` — print resolved config with `npx playwright test --list` | | Monorepo tests pick up wrong files | Default `testDir` is `.` | Set explicit `testDir` per project | ## Related - [core/configuration.md](foundations/configuration.md) — `testMatch`, `testDir`, `fullyParallel`, `workers` - [core/fixtures-and-hooks.md](foundations/fixtures-and-hooks.md) — shared setup via fixtures instead of `beforeAll` - [core/test-architecture.md](decisions/test-architecture.md) — when to write E2E vs API vs component tests - [ci/parallel-and-sharding.md](infrastructure/parallel-and-sharding.md) — CI sharding for large suites - [ci/projects-and-dependencies.md](infrastructure/projects-and-dependencies.md) — multi-project config