QA How-To
Playwright projects config: Examples and Best Practices
Explore Playwright projects config examples for browsers, devices, environments, roles, test tiers, visual checks, and scalable CI with TypeScript in 2026.
21 min read | 2,482 words
TL;DR
The most useful Playwright projects config examples cover browser engines, mobile devices, deployments, test tiers, authentication roles, and specialized checks. Keep common values global, make project differences explicit, and verify the resulting matrix with `npx playwright test --list`.
Key Takeaways
- Choose one project dimension at a time so a failed profile has an obvious meaning.
- Use device descriptors as complete starting points, then add explicit overrides after the spread.
- Separate production-safe smoke selection from broad staging regression coverage.
- Model login setup as a dependency when traces, fixtures, and report visibility matter.
- Create typed custom options when projects must parameterize application behavior beyond browser settings.
- Keep visual baselines isolated by project name whenever rendering is expected to differ.
- Review the collected test list and CI cost whenever a project is added.
Good Playwright projects config examples do more than show three browser names. They demonstrate how to express test risk clearly: which target is running, which tests it selects, what state it uses, and why the profile exists.
This cookbook provides production-oriented TypeScript patterns for browser coverage, mobile emulation, environments, authentication, custom parameters, visual testing, and CI. Each example is designed to be adapted, not copied without considering data isolation, runtime cost, and deployment safety.
TL;DR
| Scenario | Recommended project pattern | Main caution |
|---|---|---|
| Cross-browser regression | One project per browser engine | Avoid running low-value tests on every engine |
| Responsive coverage | Device descriptor projects | Emulation is not physical-device coverage |
| Staging and production | Different baseURL plus different selection |
Never run destructive regression in production |
| Authenticated roles | Setup dependencies and separate storage state | Isolate server-side test data too |
| Smoke and regression | Tags, grep, or file patterns |
Audit accidental overlap |
| Visual testing | Dedicated project and project-aware snapshots | Review baselines per rendering target |
| CI fan-out | Stable project names selected by jobs | Preserve dependencies and merge reports |
A strong default is one Chromium project for fast feedback, Firefox and WebKit projects for cross-engine risk, and narrow specialty projects only where configuration changes expected behavior.
1. Read the Core Playwright Projects Config Examples Syntax
Every project is an object inside projects. The name is its stable identity. use contains browser-context and test options. Runner controls such as testMatch, grep, retries, timeout, workers, dependencies, and metadata sit beside use, not inside it.
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
timeout: 30_000,
expect: { timeout: 5_000 },
use: {
baseURL: 'https://playwright.dev',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
metadata: { tier: 'default', owner: 'web-qa' },
},
],
});
This example is intentionally small. It has a single project but establishes the inheritance pattern used by every larger configuration. The test directory, timeout, assertion timeout, base URL, trace policy, and screenshot policy are shared. The project says only what makes this execution distinct.
Add a runnable test:
// tests/home.spec.ts
import { test, expect } from '@playwright/test';
test('opens the documentation site', async ({ page }, testInfo) => {
await page.goto('/');
await expect(page).toHaveTitle(/Playwright/);
expect(testInfo.project.name).toBe('chromium');
});
Use npx playwright test --list to confirm collection, then npx playwright test --project=chromium. Avoid testing project names in normal product assertions as shown in the diagnostic line. Most tests should be configuration-agnostic. The line exists here only to demonstrate where resolved project identity is available.
2. Example: Three Browser Engines With Shared Defaults
The classic browser matrix targets Chromium, Firefox, and WebKit. Use Playwright's desktop descriptors so each entry receives an appropriate browser name and desktop context defaults. A shared baseURL keeps tests portable, while trace provides failure evidence consistently.
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
fullyParallel: true,
forbidOnly: Boolean(process.env.CI),
retries: process.env.CI ? 1 : 0,
use: {
baseURL: 'https://example.com',
trace: 'on-first-retry',
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
],
});
fullyParallel allows tests within files to run concurrently as well as files, so enable it only when tests and test data are isolated. The browser matrix does not create isolation in the system under test. Three projects can still update the same user, order, or feature flag at once.
Use this matrix when engine differences matter to customers. Do not assume desktop Safari means an installed Safari executable. Playwright's WebKit build provides the WebKit engine and excellent cross-engine signal, but release validation may still require the supported real-browser and operating-system combinations in your product policy.
Run the whole matrix with npx playwright test, or select two profiles by repeating --project. For debugging, one project plus one file is the fastest useful scope: npx playwright test tests/checkout.spec.ts --project=webkit --headed. If the suite is new, read Playwright projects config fundamentals before adding environment and role dimensions.
3. Example: Mobile, Tablet, and Branded Browser Profiles
Device descriptors bundle several settings that should remain coherent. They can include viewport, user agent, browser engine, touch support, device scale factor, and mobile behavior. Spread a descriptor first, then place intentional overrides afterward.
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
projects: [
{
name: 'pixel-5',
use: {
...devices['Pixel 5'],
locale: 'en-US',
},
},
{
name: 'iphone-12',
use: {
...devices['iPhone 12'],
locale: 'en-US',
},
},
{
name: 'tablet',
use: { ...devices['iPad (gen 7)'] },
},
{
name: 'google-chrome',
use: { ...devices['Desktop Chrome'], channel: 'chrome' },
},
{
name: 'microsoft-edge',
use: { ...devices['Desktop Edge'], channel: 'msedge' },
},
],
});
Branded channels depend on the corresponding browser being available. A CI image that contains Playwright's bundled Chromium does not automatically contain Google Chrome or Microsoft Edge. Make installation part of the runner contract, and fail clearly when a channel is absent.
Mobile emulation is ideal for responsive layout, touch-oriented web interactions, mobile user-agent branches, and viewport-dependent navigation. It cannot reproduce every physical input, hardware, browser chrome, memory condition, or operating-system integration. Name the project after the emulated profile rather than claiming real-device coverage.
Avoid adding every descriptor in the registry. Pick representative viewports based on supported customer traffic and layout breakpoints. A focused phone, tablet, and desktop set typically provides more actionable signal than many similar profiles. See Playwright geolocation emulation when location is the dimension under test.
4. Example: Staging and Production-Safe Environments
Environment projects should change both destination and safety policy. Staging can host broad regression with retries and test-data creation. Production should normally select tagged, read-only checks, disable retries for mutating workflows, and use separate credentials with minimal privileges.
import { defineConfig, devices } from '@playwright/test';
const stagingURL = process.env.STAGING_URL ?? 'https://staging.example.com';
const productionURL = process.env.PRODUCTION_URL ?? 'https://example.com';
export default defineConfig({
use: { ...devices['Desktop Chrome'] },
projects: [
{
name: 'staging-regression',
use: { baseURL: stagingURL },
grepInvert: /@production-only/,
retries: 1,
},
{
name: 'production-smoke',
use: { baseURL: productionURL },
grep: /@production-safe/,
retries: 0,
workers: 1,
metadata: { environment: 'production', safety: 'read-only' },
},
],
});
The code uses regular expressions containing no punctuation that requires special JSON escaping. In real tests, declare structured tags: test('health journey', { tag: ['@smoke', '@production-safe'] }, async ({ page }) => { ... }). The production project then collects only approved tests. A code review rule should protect that tag because a configuration filter is only as safe as its classification.
Validate required environment variables in CI rather than silently directing staging tests to an unintended fallback. Defaults are convenient for a tutorial and local sandbox, but a production pipeline should reject a missing target. Do not print secrets while logging resolved configuration.
Environment projects are appropriate when the application behavior and test logic are comparable. If production uses a radically different tenant model or security boundary, a separate safe suite may communicate intent better than extensive conditional logic.
5. Example: Smoke, Regression, and Visual Test Selection
Test-tier projects turn collection rules into named commands. Tags work well when a test belongs to a behavioral tier. File patterns work well when a specialty requires different baseline ownership, such as screenshots. Use one consistent scheme rather than mixing title suffixes, folders, and tags without a convention.
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
use: { ...devices['Desktop Chrome'] },
projects: [
{
name: 'smoke',
grep: /@smoke/,
retries: 0,
},
{
name: 'regression',
grepInvert: /@manual-only/,
retries: 1,
},
{
name: 'visual',
testMatch: '**/*.visual.spec.ts',
snapshotPathTemplate: '__screenshots__/{projectName}/{testFilePath}/{arg}{ext}',
},
],
});
A smoke-tagged test also matches regression unless excluded. That overlap is often desirable: smoke tests are part of the full regression pack. It becomes a problem only if a pipeline runs both projects and interprets the repeated execution as additional coverage. Decide whether tier projects are alternatives or cumulative layers.
Snapshot identity deserves special care. Different browsers, viewports, platforms, and locales can render legitimate differences. Including {projectName} prevents one project's expected image from replacing another's. Keep project names stable, store only reviewed baselines, and pin the rendering environment used to update them.
For API-only tests, consider a distinct project with testMatch: '**/*.api.spec.ts' and no browser-specific device descriptor. Playwright Test can use its request fixture without opening a page. Separating the scope can reduce unnecessary browser matrix executions while keeping reporting in the same runner.
6. Example: Authentication Setup, Roles, and Teardown
Project dependencies express a directed execution graph. A setup project authenticates and writes storage state. Role projects depend on it and load the appropriate file. A teardown can clean server-side test data after dependent projects finish.
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
projects: [
{
name: 'auth-setup',
testMatch: '**/*.auth.setup.ts',
teardown: 'data-cleanup',
},
{
name: 'data-cleanup',
testMatch: '**/*.cleanup.setup.ts',
},
{
name: 'admin-chromium',
use: {
...devices['Desktop Chrome'],
storageState: 'playwright/.auth/admin.json',
},
dependencies: ['auth-setup'],
grep: /@admin/,
},
{
name: 'member-chromium',
use: {
...devices['Desktop Chrome'],
storageState: 'playwright/.auth/member.json',
},
dependencies: ['auth-setup'],
grepInvert: /@admin/,
},
],
});
// tests/session.auth.setup.ts
import { test as setup, expect } from '@playwright/test';
setup('create member storage state', async ({ page }) => {
await page.goto('https://example.com/login');
await page.getByLabel('Email').fill(process.env.MEMBER_EMAIL ?? 'member@example.com');
await page.getByLabel('Password').fill(process.env.MEMBER_PASSWORD ?? 'change-me');
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page).toHaveURL(/dashboard/);
await page.context().storageState({ path: 'playwright/.auth/member.json' });
});
The second snippet is structurally runnable but its selectors and URL must match the application. Do not commit generated authentication files. Storage state can contain cookies and local storage tokens, so add playwright/.auth to .gitignore and protect any CI artifact. Create one setup test per role if independent reporting is useful.
Dependencies do not share a live page or browser context. They sequence projects and pass durable state through files or the target system. If tests change the same member account concurrently, create per-worker accounts or constrain that project intentionally.
7. Example: Typed Custom Project Parameters
Projects can parameterize application-specific behavior through custom fixtures. This is cleaner than reading testInfo.project.name throughout the suite. Extend the base test type with an option fixture, provide a default, then set that option in each project.
// fixtures.ts
import { test as base } from '@playwright/test';
type AppOptions = {
featureVariant: 'control' | 'candidate';
accountRegion: 'us' | 'eu';
};
export const test = base.extend<AppOptions>({
featureVariant: ['control', { option: true }],
accountRegion: ['us', { option: true }],
});
export { expect, defineConfig } from '@playwright/test';
// playwright.config.ts
import { defineConfig } from './fixtures';
export default defineConfig({
projects: [
{
name: 'control-us',
use: { featureVariant: 'control', accountRegion: 'us' },
},
{
name: 'candidate-eu',
use: { featureVariant: 'candidate', accountRegion: 'eu' },
},
],
});
// tests/variant.spec.ts
import { test, expect } from '../fixtures';
test('shows the configured experience', async ({ page, featureVariant }) => {
await page.goto('https://example.com');
expect(['control', 'candidate']).toContain(featureVariant);
});
Importing defineConfig from the extended fixture module gives TypeScript knowledge of custom options. The fixture is available directly to tests, so names remain reporting identifiers rather than hidden application inputs.
Use this pattern for stable test dimensions such as a seeded tenant class, backend mode, or feature variant. Do not create dozens of parameters that reproduce a combinatorial test-data system in configuration. If a value changes per case, use normal data-driven tests. If it changes the whole execution environment, a project option is a good fit.
8. Example: Specialized Accessibility and Locale Profiles
Some risks need different context settings but not a new browser engine. Locale, timezone, color scheme, reduced motion, permissions, and geolocation can be project dimensions when many tests should run under the same condition. Keep the selection narrow if only a few behaviors depend on that condition.
import { defineConfig, devices } from '@playwright/test';
const desktop = devices['Desktop Chrome'];
export default defineConfig({
projects: [
{
name: 'default-ui',
use: { ...desktop, locale: 'en-US', timezoneId: 'America/New_York' },
},
{
name: 'dark-reduced-motion',
testMatch: '**/*.appearance.spec.ts',
use: {
...desktop,
colorScheme: 'dark',
reducedMotion: 'reduce',
},
},
{
name: 'french-ui',
testMatch: '**/*.localization.spec.ts',
use: { ...desktop, locale: 'fr-FR', timezoneId: 'Europe/Paris' },
},
],
});
A locale project should assert user-visible localization behavior, not merely that navigator.language changed. Cover translated labels, formats, text expansion, sorting assumptions, and locale-dependent routes where relevant. A reduced-motion project should assert the accessible outcome and stable interaction, not animation implementation details.
Accessibility scanning can also have a dedicated project selected by a tag or file pattern, but the project alone does not perform an audit. It only creates a consistent profile in which your chosen assertions or scanner run. Combine automated checks with keyboard, screen-reader, focus, semantics, and human review appropriate to the product.
9. Example: CI Jobs, Shards, and Project Reports
CI can select a stable project name from a matrix. This keeps playwright.config.ts authoritative and lets the workflow decide scheduling. The illustrative GitHub Actions fragment below runs three projects independently. Adapt installation and caching to the repository.
strategy:
fail-fast: false
matrix:
project: [chromium, firefox, webkit]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npx playwright install --with-deps
- run: npx playwright test --project=${{ matrix.project }}
env:
CI: 'true'
Independent jobs repeat dependency projects because each job has its own filesystem and process boundary. That is often safer than trying to share authentication. If you do share state, treat it as a secret-bearing artifact, use short retention, and ensure the environment accepts the reused session.
For a very large Chromium suite, add sharding inside the Chromium job. Shards split the selected tests, while projects define the environment. Keep those concepts separate in names and reporting. Use blob reports when your pipeline merges distributed Playwright results, and preserve trace, screenshot, and video attachments according to failure policy.
Pin runner images and browser installation behavior. Browser projects are only comparable when the underlying dependencies are reproducible. Docker-based Playwright execution is useful when hosted and local Linux environments drift.
10. Review Playwright Projects Config Examples Before Adoption
Before adding a pattern, answer four questions: What risk does the project cover? Which tests does it collect? What data and credentials does it require? Where and how often will it run? If an entry has no specific answers, it probably adds maintenance without enough signal.
Run these checks during review:
npx playwright test --list
npx playwright test --list --project=production-smoke
npx playwright test --project=chromium tests/home.spec.ts
npx playwright test --project=webkit --headed --debug tests/checkout.spec.ts
The first two reveal scope and overlap. The targeted runs prove that the profile can start, resolve browser binaries, reach its base URL, and produce artifacts. CI should also reject committed test.only with forbidOnly and expose a clear report even when setup fails.
Check object spread order, duplicated names, stale dependency references, unsafe production tags, snapshots without a target dimension, and dynamic values created while the config loads. Verify that any project-level worker cap has an explained external constraint. Review credentials and storage-state paths for accidental source control exposure.
Finally, compare the matrix with the support policy. A project named mobile-safari is emulated Mobile Safari parameters on WebKit, not evidence from a physical iPhone. Precise naming keeps reports credible to engineers, interviewers, and release stakeholders.
Interview Questions and Answers
Q: Why use projects instead of separate Playwright config files?
Projects centralize shared policy and make variations selectable by name. Separate files can drift and complicate reporting. I use separate configs only when workflows have genuinely different ownership or runner behavior that a single config would make less clear.
Q: How would you represent browsers and environments without creating too many projects?
I map projects to risk rather than generate every combination. For example, I run full staging regression in Chromium, critical journeys in Firefox and WebKit, and a read-only Chromium smoke profile in production. This preserves important coverage while avoiding a large Cartesian product.
Q: How do custom project options reach a test?
Define an option fixture with base.extend, provide its default using { option: true }, and import defineConfig from the extended fixture module. Each project supplies the typed value under use. Tests receive the option as a normal fixture.
Q: What should a setup project produce?
It should produce durable state needed by dependents, such as an authenticated storage-state file or seeded server data. It does not pass a live page between projects. Sensitive files must be ignored by version control and protected in CI.
Q: How do you verify that a smoke project selects the right tests?
I run npx playwright test --list --project=smoke and inspect the collected cases. I also use a reviewable tag convention and a small guard test or launch check when production safety depends on the classification.
Q: What can go wrong with device descriptor spreads?
Later object properties win. If the descriptor is spread after custom values, it can replace the intended viewport, browser, or context settings. I spread the descriptor first and place explicit overrides after it.
Q: When should a project use workers: 1?
Only when the project depends on a truly exclusive or rate-limited resource and parallel isolation cannot be provided. It can prevent simultaneous use of a shared tenant, but it should not become a blanket cure for poor test-data design.
Q: How should visual projects store baselines?
The snapshot path should include dimensions that legitimately change rendering, commonly {projectName} and sometimes platform. Baselines should be generated in a pinned environment and reviewed per target. Updating every image without investigating differences defeats the assertion.
Common Mistakes
- Copying an example matrix without mapping its projects to the product's supported browsers and risks.
- Combining browser, role, locale, environment, and test tier in every possible way.
- Using a production base URL with the same unrestricted regression selection used for staging.
- Committing
playwright/.authfiles or publishing them as long-lived CI artifacts. - Assuming setup dependencies share browser objects with dependent projects.
- Reversing device spread order and silently losing custom context options.
- Using project names as application logic instead of typed option fixtures.
- Allowing smoke and regression projects to overlap without understanding pipeline duplication.
- Sharing one screenshot baseline across targets that render differently.
- Adding
workers: 1before fixing test data that could be isolated.
Conclusion
The best Playwright projects config examples are small patterns with explicit intent. Use browser projects for engine risk, device projects for representative responsive contexts, environment projects for deployment policy, dependencies for observable setup, and typed fixtures for application-specific parameters.
Start with the smallest example that covers the immediate need. List its collected tests, run it independently, verify artifacts and data isolation, then decide where it belongs in CI. That discipline produces a project matrix that remains understandable as the suite and team grow.
Interview Questions and Answers
Give a practical Playwright browser projects example.
I define Chromium, Firefox, and WebKit entries using the corresponding desktop device descriptors. Shared base URL, trace, and timeout policy remain at the top level. CI selects Chromium for fast feedback and schedules broader engine coverage based on release risk.
How would you design staging and production projects?
Each gets its own `use.baseURL`, metadata, and test selection. Staging can run broad regression, while production selects only approved read-only tests and usually has zero retries for mutating actions. Credentials come from the runtime environment and have minimal privilege.
What is the value of a setup dependency over hidden global setup?
A setup project is normal test execution, so reporters show its steps and traces can capture failures. It can use fixtures and participate in a teardown graph. This makes authentication and data preparation easier to diagnose.
How do you avoid a project configuration explosion?
I identify the risk each dimension covers and use representative combinations instead of a full Cartesian product. High-value journeys receive broader coverage, while lower-risk tests run in a default profile. I remove entries whose failures no longer influence decisions.
How do you configure a custom application variant per project?
I declare a typed option fixture and import `defineConfig` from the extended fixture module. Projects set the option under `use`, and tests receive it directly as a fixture. This keeps configuration typed and avoids parsing project names.
What would you inspect in a Playwright projects config review?
I check stable unique names, inheritance, device spread order, collection rules, dependency references, worker caps, artifact identity, production safety, and secret handling. I also run `--list` to compare intended and actual test scope.
How are projects and shards different?
A project defines the execution configuration and eligible tests. A shard divides the selected executions across workers or CI jobs. Large suites often select one project and then shard it, while preserving the same project identity in merged reports.
Can mobile emulation prove real mobile-browser compatibility?
It provides strong signal for viewport, touch, user-agent, and browser-engine behavior, but it is not a physical device. Hardware, operating-system integration, resource pressure, and real browser chrome may differ. I label the profile as emulation and supplement it according to product risk.
Frequently Asked Questions
What are the best Playwright projects config examples for beginners?
Start with Chromium, Firefox, and WebKit projects that share one base URL and artifact policy. Once that works, add a single mobile or smoke profile only when the product needs that coverage.
Can Playwright projects select different test files?
Yes. Each project can set `testMatch` and `testIgnore`, or filter titles and tags with `grep` and `grepInvert`. Use `--list --project=<name>` to verify the resolved set.
How do I configure Playwright for Chrome and Edge?
Create projects with desktop device descriptors and set `channel: 'chrome'` or `channel: 'msedge'`. Ensure those branded browsers are installed on every runner that executes the projects.
Can one Playwright project depend on another?
Yes. Add the prerequisite project name to `dependencies`. The dependent project starts only after all required dependency tests pass, unless dependencies are explicitly bypassed with `--no-deps`.
How do I pass custom values from a project to tests?
Define typed option fixtures with `test.extend`, then set those option values in each project's `use` object. Tests consume them as fixtures, which is safer than branching on the project name.
Should smoke tests be a separate Playwright project?
A smoke project is useful when it has a distinct schedule, retry policy, environment, or reporting purpose. If smoke is merely a CLI tag selection under the same profile, an additional project may be unnecessary.
How many Playwright projects should a suite have?
There is no universal number. Keep only projects that cover a named risk or operational workflow, and avoid multiplying every possible browser, device, role, and environment combination.
Related Guides
- Playwright drag and drop: Examples and Best Practices
- Playwright mock date and time: Examples and Best Practices
- Playwright popup and new tab handling: Examples and Best Practices
- Playwright tag and grep filters: Examples and Best Practices
- Playwright webServer config: Examples and Best Practices
- Playwright APIRequestContext: Examples and Best Practices