# Projects and Dependencies > **When to use**: Running tests across multiple browsers, devices, or environments from a single config. Projects let you define different test configurations that can depend on each other, share setup work, and run selectively. ## Quick Reference ```bash # Run all projects npx playwright test # Run a specific project npx playwright test --project=chromium npx playwright test --project="Mobile Safari" # Run multiple projects npx playwright test --project=chromium --project=firefox # List all projects and their tests npx playwright test --list # Skip dependencies (e.g., skip setup during debugging) npx playwright test --project=chromium --no-deps ``` ## Patterns ### Pattern 1: Multi-Browser Testing **Use when**: Ensuring your application works across Chromium, Firefox, and WebKit. **Avoid when**: Early development -- start with Chromium only and add browsers later. ```ts // playwright.config.ts import { defineConfig, devices } from "@playwright/test" export default defineConfig({ testDir: "./tests", fullyParallel: true, projects: [ { name: "chromium", use: { ...devices["Desktop Chrome"] }, }, { name: "firefox", use: { ...devices["Desktop Firefox"] }, }, { name: "webkit", use: { ...devices["Desktop Safari"] }, }, ], }) ``` **Selective browser testing in CI (fast on PRs, thorough on main):** ```yaml # .github/workflows/ci.yml jobs: test-pr: if: github.event_name == 'pull_request' steps: # Chromium only on PRs for fast feedback - run: npx playwright test --project=chromium test-main: if: github.ref == 'refs/heads/main' steps: # All browsers on main branch - run: npx playwright test ``` ### Pattern 2: Desktop and Mobile Projects **Use when**: Testing responsive layouts, touch interactions, or mobile-specific behavior. **Avoid when**: Your application is desktop-only. ```ts // playwright.config.ts import { defineConfig, devices } from "@playwright/test" export default defineConfig({ testDir: "./tests", fullyParallel: true, projects: [ // Desktop browsers { name: "Desktop Chrome", use: { ...devices["Desktop Chrome"] }, }, { name: "Desktop Firefox", use: { ...devices["Desktop Firefox"] }, }, { name: "Desktop Safari", use: { ...devices["Desktop Safari"] }, }, // Mobile devices { name: "Mobile Chrome", use: { ...devices["Pixel 7"] }, }, { name: "Mobile Safari", use: { ...devices["iPhone 14"] }, }, // Tablet { name: "iPad", use: { ...devices["iPad Pro 11"] }, }, ], }) ``` **Run only mobile or only desktop:** ```bash npx playwright test --project="Mobile Chrome" --project="Mobile Safari" npx playwright test --project="Desktop Chrome" --project="Desktop Firefox" ``` ### Pattern 3: Setup Project with Dependencies **Use when**: Tests need shared state (authentication, seeded data) that should be created once before all test projects run. **Avoid when**: Tests are fully independent with no shared setup phase. ```ts // playwright.config.ts import { defineConfig, devices } from "@playwright/test" export default defineConfig({ testDir: "./tests", projects: [ // Setup runs first -- no dependencies, so it runs immediately { name: "setup", testMatch: /global\.setup\.ts/, }, // Browser projects depend on setup { name: "chromium", use: { ...devices["Desktop Chrome"], storageState: "playwright/.auth/user.json", }, dependencies: ["setup"], }, { name: "firefox", use: { ...devices["Desktop Firefox"], storageState: "playwright/.auth/user.json", }, dependencies: ["setup"], }, ], }) ``` ```ts // tests/global.setup.ts import { test as setup, expect } from "@playwright/test" const authFile = "playwright/.auth/user.json" setup("authenticate", async ({ page }) => { await page.goto("/login") await page.getByLabel("Email").fill("user@example.com") await page.getByLabel("Password").fill(process.env.TEST_PASSWORD!) await page.getByRole("button", { name: "Sign in" }).click() await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible() await page.context().storageState({ path: authFile }) }) ``` **How dependencies work:** 1. Playwright identifies projects with no dependencies and runs them first. 2. Once a dependency project completes successfully, dependent projects start. 3. If a dependency project fails, all dependent projects are skipped. ### Pattern 4: Multiple Auth Roles **Use when**: Tests need different user roles (admin, editor, viewer) with separate auth states. **Avoid when**: All tests use the same user -- use a single setup project. ```ts // playwright.config.ts import { defineConfig, devices } from "@playwright/test" export default defineConfig({ testDir: "./tests", projects: [ // Auth setup for each role { name: "auth-admin", testMatch: /auth\.setup\.ts/, use: { userRole: "admin", storageStatePath: "playwright/.auth/admin.json", }, }, { name: "auth-editor", testMatch: /auth\.setup\.ts/, use: { userRole: "editor", storageStatePath: "playwright/.auth/editor.json", }, }, // Admin tests { name: "admin-tests", testDir: "./tests/admin", use: { ...devices["Desktop Chrome"], storageState: "playwright/.auth/admin.json", }, dependencies: ["auth-admin"], }, // Editor tests { name: "editor-tests", testDir: "./tests/editor", use: { ...devices["Desktop Chrome"], storageState: "playwright/.auth/editor.json", }, dependencies: ["auth-editor"], }, // Unauthenticated tests (no dependencies, no storageState) { name: "public-tests", testDir: "./tests/public", use: { ...devices["Desktop Chrome"] }, }, ], }) ``` ```ts // tests/auth.setup.ts import { test as setup, expect } from "@playwright/test" const credentials: Record = { admin: { email: "admin@example.com", password: process.env.ADMIN_PASSWORD! }, editor: { email: "editor@example.com", password: process.env.EDITOR_PASSWORD! }, } setup("authenticate", async ({ page }, testInfo) => { const role = testInfo.project.use.userRole as string const authFile = testInfo.project.use.storageStatePath as string const { email, password } = credentials[role] await page.goto("/login") await page.getByLabel("Email").fill(email) await page.getByLabel("Password").fill(password) await page.getByRole("button", { name: "Sign in" }).click() await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible() await page.context().storageState({ path: authFile }) }) ``` ### Pattern 5: Environment-Specific Projects **Use when**: Running the same tests against different environments (dev, staging, production) from one config. **Avoid when**: You only test one environment. ```ts // playwright.config.ts import { defineConfig, devices } from "@playwright/test" const ENV = process.env.TEST_ENV || "local" const envConfig: Record = { local: { baseURL: "http://localhost:3000", retries: 0 }, staging: { baseURL: "https://staging.example.com", retries: 2 }, production: { baseURL: "https://www.example.com", retries: 2 }, } const env = envConfig[ENV] export default defineConfig({ testDir: "./tests", retries: env.retries, use: { baseURL: env.baseURL, }, projects: [ { name: "chromium", use: { ...devices["Desktop Chrome"] }, }, // Only run smoke tests in production ...(ENV === "production" ? [ { name: "smoke", testMatch: "**/*smoke*.spec.ts", use: { ...devices["Desktop Chrome"] }, grep: /@smoke/, }, ] : []), ], }) ``` ```bash # Run against different environments TEST_ENV=local npx playwright test TEST_ENV=staging npx playwright test TEST_ENV=production npx playwright test --project=smoke ``` ### Pattern 6: Project with Custom `testDir` and `testMatch` **Use when**: Different projects need different test directories or file patterns. **Avoid when**: All projects run the same tests. ```ts // playwright.config.ts import { defineConfig, devices } from "@playwright/test" export default defineConfig({ projects: [ // E2E tests -- run in all browsers { name: "e2e-chromium", testDir: "./tests/e2e", testMatch: "**/*.spec.ts", use: { ...devices["Desktop Chrome"] }, }, { name: "e2e-firefox", testDir: "./tests/e2e", testMatch: "**/*.spec.ts", use: { ...devices["Desktop Firefox"] }, }, // API tests -- no browser needed, run once { name: "api", testDir: "./tests/api", testMatch: "**/*.spec.ts", use: { baseURL: "https://api.example.com", }, }, // Visual regression -- Chromium only { name: "visual", testDir: "./tests/visual", testMatch: "**/*.spec.ts", use: { ...devices["Desktop Chrome"], // Lock viewport for consistent screenshots viewport: { width: 1280, height: 720 }, }, }, ], }) ``` ```bash # Run only API tests npx playwright test --project=api # Run only visual tests npx playwright test --project=visual # Run all E2E tests npx playwright test --project=e2e-chromium --project=e2e-firefox ``` ### Pattern 7: Teardown Projects **Use when**: You need browser-based cleanup after tests complete (e.g., deleting test data via the UI). **Avoid when**: Cleanup can be done via API in `globalTeardown` or fixtures (the usual case). ```ts // playwright.config.ts import { defineConfig, devices } from "@playwright/test" export default defineConfig({ projects: [ { name: "setup", testMatch: /global\.setup\.ts/, teardown: "teardown", // link to teardown project }, { name: "teardown", testMatch: /global\.teardown\.ts/, }, { name: "chromium", use: { ...devices["Desktop Chrome"], storageState: "playwright/.auth/user.json", }, dependencies: ["setup"], }, ], }) ``` ```ts // tests/global.teardown.ts import { test as teardown } from "@playwright/test" teardown("clean up test data", async ({ request }) => { await request.post("/api/test/cleanup", { headers: { Authorization: `Bearer ${process.env.ADMIN_API_KEY}` }, }) }) ``` **Execution order:** 1. `setup` project runs 2. `chromium` project runs (depends on setup) 3. `teardown` project runs (linked via setup's `teardown` field) ### Pattern 8: Grep and GrepInvert for Project Filtering **Use when**: Different projects should run different subsets of tests based on tags. **Avoid when**: All projects run all tests. ```ts // playwright.config.ts import { defineConfig, devices } from "@playwright/test" export default defineConfig({ testDir: "./tests", projects: [ // Smoke tests -- only @smoke tagged tests, Chromium only { name: "smoke", use: { ...devices["Desktop Chrome"] }, grep: /@smoke/, }, // Full regression -- everything except @slow, all browsers { name: "regression-chromium", use: { ...devices["Desktop Chrome"] }, grepInvert: /@slow/, }, { name: "regression-firefox", use: { ...devices["Desktop Firefox"] }, grepInvert: /@slow/, }, // Slow tests -- only @slow, Chromium only, higher timeout { name: "slow", use: { ...devices["Desktop Chrome"] }, grep: /@slow/, timeout: 120_000, }, ], }) ``` ```bash # CI: run smoke on PRs npx playwright test --project=smoke # CI: run full regression on main npx playwright test --project=regression-chromium --project=regression-firefox # CI: run slow tests nightly npx playwright test --project=slow ``` ## Decision Guide | Scenario | Number of Projects | Configuration | | --------------------------------------- | ----------------------------------- | ------------------------------- | | Getting started | 1 (chromium) | Single project, no dependencies | | Cross-browser testing | 3 (chromium, firefox, webkit) | One per browser engine | | Responsive design | 5-6 (desktop + mobile + tablet) | Use `devices` presets | | Authenticated tests | 1 setup + N browser projects | Setup with `dependencies` | | Multiple auth roles | N setup + N test projects | One setup project per role | | Different test types (E2E, API, visual) | One per type | Custom `testDir` per project | | Environment targeting | Same projects, different `baseURL` | Use `TEST_ENV` env var | | Smoke vs regression suites | Projects with `grep` / `grepInvert` | Tag-based filtering | | Feature | `dependencies` | `teardown` | `globalSetup` | | --------------------------- | ------------------------------------- | --------------------------------------- | ------------------------------------ | | Has browser context | Yes | Yes | No | | Runs when | Before dependent projects | After linked setup project's dependents | Before all projects | | Use for | Auth state, data seeding with browser | Browser-based cleanup | Non-browser setup (DB, health check) | | Skips dependents on failure | Yes | N/A | Yes (entire suite fails) | ## Anti-Patterns | Anti-Pattern | Problem | Do This Instead | | ------------------------------------------------------- | --------------------------------------------- | ------------------------------------------------------------- | | All browsers on every PR | 3x CI time for marginal benefit | Chromium on PRs; all browsers on main | | `dependencies` on projects that don't need shared state | Forced serial execution; slower | Only use dependencies when projects truly need shared state | | Duplicating config across projects | Hard to maintain; settings drift | Use shared `use` at the top level; override per project | | No `--project` filtering in CI | All projects always run; no control | Use `--project` to run subsets based on context | | Setup project modifies database without teardown | Dirty state for next run | Always pair setup with teardown or make setup idempotent | | Many projects with overlapping test directories | Same test runs multiple times unintentionally | Set explicit `testDir` and `testMatch` per project | | Not using `--no-deps` for debugging | Setup runs every time, even for one test | Use `--no-deps` to skip dependencies during focused debugging | ## Troubleshooting ### "No tests found" when running specific project **Cause**: The project's `testDir` or `testMatch` doesn't match any files. **Fix**: List tests for the project to see what matches: ```bash npx playwright test --project=chromium --list ``` Check that `testDir` and `testMatch` are correct: ```ts { name: 'chromium', testDir: './tests', // must contain test files testMatch: '**/*.spec.ts', // must match your file naming } ``` ### Setup project runs every time, even for a single test **Cause**: The test's project has `dependencies: ['setup']`, so setup always runs first. **Fix**: Use `--no-deps` to skip dependencies during development: ```bash npx playwright test --project=chromium --no-deps tests/specific-test.spec.ts ``` ### `storageState` file not found **Cause**: Setup project failed or didn't create the file. Or the path in `use.storageState` doesn't match the path in the setup project. **Fix**: Verify paths match exactly: ```ts // In setup test: await page.context().storageState({ path: "playwright/.auth/user.json" }) // In project config: use: { storageState: "playwright/.auth/user.json" } // must match exactly ``` Add `.auth` to `.gitignore`: ```bash echo "playwright/.auth/" >> .gitignore ``` ### Dependent projects run even when setup fails **Cause**: This should not happen -- Playwright skips dependent projects when a dependency fails. If it seems like they run, check that `dependencies` is spelled correctly. **Fix**: Verify the dependency name matches exactly: ```ts // Setup project name { name: 'setup', ... } // Dependency reference (must match exactly, case-sensitive) { dependencies: ['setup'] } // correct { dependencies: ['Setup'] } // WRONG -- case mismatch ``` ### Tests from multiple projects interfere with each other **Cause**: Projects share a database or external state. Since projects run in parallel by default, they can collide. **Fix**: Either: 1. Use per-project isolated data (different test users, different database schemas) 2. Run projects sequentially by adding artificial dependencies: ```ts projects: [ { name: 'chromium', use: { ...devices['Desktop Chrome'] } }, { name: 'firefox', use: { ...devices['Desktop Firefox'] }, dependencies: ['chromium'], // forces serial execution }, ], ``` ### Custom `use` properties cause TypeScript errors **Cause**: Playwright's `use` type doesn't include your custom properties. **Fix**: Extend the type or use `as any`: ```ts // Option 1: Declare custom properties declare module '@playwright/test' { interface PlaywrightTestOptions { userRole: string; storageStatePath: string; } } // Option 2: Quick fix (less type-safe) { name: 'auth-admin', use: { userRole: 'admin', storageStatePath: 'playwright/.auth/admin.json', } as any, } ``` ## Related - [core/configuration.md](../core/configuration.md) -- base config, `projects`, `use` settings - [ci/global-setup-teardown.md](global-setup-teardown.md) -- `globalSetup` vs setup projects - [core/fixtures-and-hooks.md](../core/fixtures-and-hooks.md) -- option fixtures configured per project - [core/authentication.md](../core/authentication.md) -- auth state via setup projects - [ci/parallel-and-sharding.md](parallel-and-sharding.md) -- sharding across projects - [ci/ci-github-actions.md](ci-github-actions.md) -- running specific projects in CI