Playwright Projects TypeScript Guide
Day 29 of the Playwright + TypeScript tutorial series. Playwright projects TypeScript is the feature I use when one suite has to answer many questions: does it work in Chromium, WebKit, Firefox, mobile Safari-like viewports, logged-in flows, smoke mode, and setup-driven environments? A clean projects setup keeps that matrix explicit instead of hiding it inside npm scripts and random environment variables.
If your Playwright suite is still one big default project, this tutorial gives you a practical structure you can copy. I will show browser projects, setup dependencies, mobile projects, API setup, grep filters, CI commands, screenshots you should expect, and the mistakes that make projects painfully slow.
Table of Contents
- What Are Playwright Projects?
- A Practical Project Layout
- Build a Browser Matrix Without Noise
- Use Setup Dependencies for Login and Data
- Mobile, API, and Smoke Projects
- Playwright Projects TypeScript in CI
- Debugging and Common Pitfalls
- Key Takeaways
- FAQ
Contents
What Are Playwright Projects?
Playwright calls a project a logical group of tests that run with the same configuration. The official Playwright projects documentation explains the common use case clearly: run the same tests across different browsers and devices by configuring projects in playwright.config.ts.
That sounds small, but it changes how you design your suite. Instead of writing separate commands for Chrome, Firefox, mobile, staging, production smoke, and logged-in tests, you define named projects once and run them by name when needed.
Why projects matter after the first 50 tests
A beginner suite can survive with one config. A team suite cannot. Once you have login state, seeded data, feature flags, API checks, and release gates, one default project becomes a dumping ground.
I see three problems in messy suites:
- Developers do not know which command is safe before a pull request.
- QA engineers rerun the full suite when a focused project would be enough.
- CI time grows because every browser runs every test, even tests that only need one browser.
The adoption context
This is not a niche feature anymore. At the time of research for this tutorial, the Microsoft Playwright GitHub repository showed about 93,126 stars through the GitHub repository API, and the npm download API reported 181,316,212 last-month downloads for @playwright/test. The latest npm registry response showed version 1.61.1 for @playwright/test.
I cite those numbers for one reason: teams are building serious automation on Playwright now. Serious automation needs serious configuration hygiene.
A Practical Project Layout
The first decision is naming. Names should tell a human exactly what a project does. I avoid names like project1, full, or test. They look harmless on day one and become tribal knowledge by month three.
Folder structure I recommend
Here is a simple structure for a Playwright projects TypeScript setup:
tests/
e2e/
checkout.spec.ts
profile.spec.ts
api/
health.spec.ts
orders-api.spec.ts
setup/
auth.setup.ts
data.setup.ts
smoke/
login-smoke.spec.ts
playwright.config.ts
.env.example
The important part is not the exact folder name. The important part is that setup tests, smoke tests, API tests, and E2E tests are easy to target from the config.
Start with a typed config
Use TypeScript types directly in the config. It gives you autocomplete and catches mistakes in properties like testMatch, dependencies, and use.
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
timeout: 30_000,
expect: { timeout: 5_000 },
fullyParallel: true,
retries: process.env.CI ? 2 : 0,
reporter: process.env.CI
? [['html'], ['github']]
: [['list'], ['html', { open: 'never' }]],
use: {
baseURL: process.env.BASE_URL ?? 'https://example.com',
trace: 'retain-on-failure',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
projects: [
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
dependencies: ['setup'],
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
dependencies: ['setup'],
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
dependencies: ['setup'],
},
],
});
Screenshot description: after running npx playwright test --list, you should see the same E2E test expanded under chromium, firefox, and webkit, plus the setup files under the setup project. This is the first sanity check.
Build a Browser Matrix Without Noise
The browser matrix is the most common reason teams use projects. But the trap is obvious: three browsers can turn a 10-minute suite into a 30-minute suite if you run everything everywhere.
Run critical flows across browsers
I usually split tests into two groups. Critical user journeys run across browsers. Deep business-rule tests run on Chromium unless there is a browser-specific risk.
projects: [
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
{
name: 'chromium-critical',
testMatch: /.*\.critical\.spec\.ts/,
use: { ...devices['Desktop Chrome'] },
dependencies: ['setup'],
},
{
name: 'firefox-critical',
testMatch: /.*\.critical\.spec\.ts/,
use: { ...devices['Desktop Firefox'] },
dependencies: ['setup'],
},
{
name: 'webkit-critical',
testMatch: /.*\.critical\.spec\.ts/,
use: { ...devices['Desktop Safari'] },
dependencies: ['setup'],
},
{
name: 'chromium-regression',
testIgnore: /.*\.critical\.spec\.ts/,
use: { ...devices['Desktop Chrome'] },
dependencies: ['setup'],
},
]
This gives you cross-browser confidence without multiplying every single assertion. It also makes your CI report easier to read because the project name already explains intent.
Use command line targeting
Playwright lets you run a project by name:
npx playwright test --project=chromium-critical
npx playwright test --project=webkit-critical
npx playwright test tests/e2e/checkout.critical.spec.ts --project=chromium-critical
Teach the team these commands. A good project design is wasted if everyone still runs npx playwright test blindly for every change.
Connect projects with annotations
Projects become even stronger when paired with tags and annotations. I covered skip, fixme, fail, slow, and grep habits in the Playwright annotations TypeScript guide. The short rule is simple: use project selection for environment and browser differences, and use annotations for test intent.
Use Setup Dependencies for Login and Data
The cleanest project setup I use has a setup project that signs in once, creates storage state, and prepares predictable data. Playwright documents authenticated state in the authentication guide, including the pattern of saving state to a file and loading it in tests.
Create authentication state once
Here is a setup test that logs in and writes storage state:
// tests/setup/auth.setup.ts
import { test as setup, expect } from '@playwright/test';
const authFile = 'playwright/.auth/user.json';
setup('authenticate test user', async ({ page }) => {
await page.goto('/login');
await page.getByLabel('Email').fill(process.env.TEST_USER_EMAIL!);
await page.getByLabel('Password').fill(process.env.TEST_USER_PASSWORD!);
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page.getByRole('heading', { name: /dashboard/i })).toBeVisible();
await page.context().storageState({ path: authFile });
});
Then attach that state to dependent projects:
const authFile = 'playwright/.auth/user.json';
export default defineConfig({
projects: [
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
{
name: 'chromium-authenticated',
use: {
...devices['Desktop Chrome'],
storageState: authFile,
},
dependencies: ['setup'],
testMatch: /.*\.auth\.spec\.ts/,
},
],
});
Do not put secrets in the repo
Never commit the generated auth JSON file. It can contain cookies and tokens. Add this to .gitignore:
playwright/.auth/
.env
If your team is still debating how to load environment variables safely, read the ScrollTest guide on secrets management in Playwright with dotenv. Projects and secrets are connected because project configs often decide which account, URL, and storage state to use.
Use API setup for test data
Setup projects are not only for login. You can seed data through APIs before UI tests run.
// tests/setup/data.setup.ts
import { test as setup, request, expect } from '@playwright/test';
setup('seed checkout product', async () => {
const api = await request.newContext({
baseURL: process.env.API_URL,
extraHTTPHeaders: {
Authorization: `Bearer ${process.env.TEST_ADMIN_TOKEN}`,
},
});
const response = await api.post('/test-data/products', {
data: { sku: 'PW-TS-29', price: 4999, stock: 10 },
});
expect(response.ok()).toBeTruthy();
await api.dispose();
});
Screenshot description: in the HTML report, setup tests appear as their own project. If setup fails, dependent projects are skipped or blocked. That is good. It tells you the suite did not fail because checkout is broken. It failed because the environment was not ready.
Mobile, API, and Smoke Projects
Playwright projects TypeScript configuration is not only a browser matrix. You can model different test purposes. This is where projects become a release strategy, not just a config trick.
Add mobile emulation without copying tests
Use Playwright devices for mobile-like coverage:
{
name: 'mobile-chrome-critical',
testMatch: /.*\.critical\.spec\.ts/,
use: { ...devices['Pixel 7'] },
dependencies: ['setup'],
},
{
name: 'mobile-safari-critical',
testMatch: /.*\.critical\.spec\.ts/,
use: { ...devices['iPhone 15'] },
dependencies: ['setup'],
}
This does not replace real-device testing for every risk. It does catch layout, viewport, and responsive flow issues early. For most product teams in India, this is a practical middle ground before investing in a device lab.
Create a fast API project
Keep API tests in a separate project when they do not need a browser. They run faster and give earlier feedback.
{
name: 'api-contracts',
testDir: './tests/api',
use: {
baseURL: process.env.API_URL ?? 'https://api.example.com',
},
}
If the API project fails, I want that failure before spending 15 minutes on UI flows. This is especially useful in CI where one broken backend dependency can waste the whole test budget.
Create a smoke project for pull requests
A smoke project should answer one question: is this build safe enough for a deeper run?
{
name: 'pr-smoke',
testDir: './tests/smoke',
timeout: 20_000,
retries: process.env.CI ? 1 : 0,
use: { ...devices['Desktop Chrome'] },
dependencies: ['setup'],
}
Keep it small. Five to fifteen tests are often enough for a pull request gate. Put login, home page, one business-critical path, and one API health check there. Do not let the smoke project become a hidden regression suite.
Playwright Projects TypeScript in CI
Playwright has dedicated CI documentation, and projects make that CI setup more intentional. The mistake is running all projects on every event. That burns minutes and trains the team to ignore failures.
Use different commands for different triggers
I like this split:
- Pull request: run
api-contractsandpr-smoke. - Main branch merge: run Chromium regression and critical browser projects.
- Nightly: run every browser, mobile critical flows, and visual checks.
- Release candidate: run the full suite with retries and trace retention.
name: playwright
on:
pull_request:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci
- run: npx playwright install --with-deps
- name: PR smoke
if: github.event_name == 'pull_request'
run: npx playwright test --project=api-contracts --project=pr-smoke
- name: Main regression
if: github.event_name == 'push'
run: npx playwright test --project=chromium-regression --project=chromium-critical --project=firefox-critical
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
path: playwright-report/
Sharding and projects
If your suite is large, combine projects with sharding. I covered CI sharding earlier in the series, and the same rule applies here: shard expensive projects, not setup projects. The setup project should run once per worker context unless your pipeline is designed to isolate each shard completely.
For visual projects, connect this with the Playwright visual regression testing tutorial. Visual baselines become noisy when every browser and viewport writes snapshots without naming discipline.
Debugging and Common Pitfalls
Most project bugs are not Playwright bugs. They are design bugs in the config.
Pitfall 1: Too many projects
Do not create a project for every tiny variation. A matrix like browser × role × locale × viewport × feature flag can explode quickly. Use projects for stable dimensions. Use fixtures and test data for scenario-level variation. If you need a refresher on fixture composition, read merging multiple fixtures in Playwright with mergeTests.
Pitfall 2: Setup project runs tests by accident
A setup project should match only setup files. Keep a strict testMatch. If your setup project runs normal specs, your reports become confusing and your state creation may run repeatedly.
{ name: 'setup', testMatch: /.*\.setup\.ts/ } // good
{ name: 'setup' } // risky because it can match everything
Pitfall 3: Browser-specific expectations everywhere
If a test has a different expectation in each browser, ask whether it belongs in the shared cross-browser project. Sometimes the right fix is one browser-specific spec. Sometimes the right fix is improving product consistency.
Pitfall 4: Auth state expires silently
If login state expires in the middle of a run, tests fail far away from the real cause. Add a small assertion at the start of authenticated tests or create a fixture that verifies the logged-in marker. Do not let every page object assume the session is valid.
import { test as base, expect } from '@playwright/test';
export const test = base.extend({
verifiedPage: async ({ page }, use) => {
await page.goto('/dashboard');
await expect(page.getByTestId('user-menu')).toBeVisible();
await use(page);
},
});
Pitfall 5: Project names are not visible in failure messages
Name projects like they will appear in Slack at 2 AM. webkit-critical is useful. safari-1 is vague. mobile-safari-critical tells the owner exactly where to start.
Review Checklist Before You Merge
Before I approve a Playwright projects TypeScript config, I run a small review checklist. This catches most suite design problems before they become CI debt.
Check the project list output
Run the list command and read it like a new team member would read it:
npx playwright test --list
npx playwright test --project=pr-smoke --list
npx playwright test --project=chromium-regression --list
If the output surprises you, the config is not ready. The setup project should only list setup files. Smoke should only list smoke tests. Browser critical projects should not accidentally include API-only specs. This boring check saves hours later.
Check runtime budgets
Every project should have a rough runtime budget. I like simple numbers: PR smoke under 5 minutes, API contracts under 3 minutes, main branch regression under 20 minutes, nightly full suite whatever the team can tolerate. The exact number depends on your product, but the budget must exist.
Without budgets, teams keep adding tests until the pipeline becomes unusable. Once that happens, people bypass tests, mark failures as flaky, or merge with red builds. The problem looks technical, but the root cause is usually no ownership of runtime.
Check ownership
Each project needs an owner. API contract failures usually belong near backend owners. Visual failures may need frontend review. Smoke failures should block the release owner immediately. Put this mapping in your repository README so the CI report leads to action, not confusion.
pr-smoke: pull request owner fixes or reverts.api-contracts: backend or platform owner checks the contract.webkit-critical: frontend owner validates browser behavior.setup: QA platform owner checks accounts, secrets, and data.
Screenshot description: the ideal CI job page has project names that match this ownership model. A failed setup job should not look like a failed checkout test. A failed mobile-safari-critical job should not be buried inside a generic regression job.
Key Takeaways
Playwright projects TypeScript gives your automation suite a clear operating model. It helps you decide what runs on pull requests, what runs after merges, what runs nightly, and what needs setup state before it starts.
- Use projects for browser, device, setup, API, and release-gate differences.
- Keep setup projects strict with
testMatchanddependencies. - Do not run every test in every browser by default.
- Use smoke projects for pull requests and full projects for nightly or release runs.
- Name projects like a human will debug them under pressure.
If I had to give one rule, it would be this: project design should reduce decisions for the team. A junior QA engineer should know which command to run without asking the senior SDET on Slack.
FAQ
Should every Playwright test run in every browser?
No. Run critical flows across browsers. Run deeper business-rule regression on one primary browser unless you have a real cross-browser risk. This keeps the suite useful and fast.
Should setup be a global setup file or a setup project?
For small suites, global setup can work. For team suites, I prefer setup projects because they appear in the report, support dependencies, and make failures easier to understand.
Can I use projects for staging and production?
Yes, but be careful. I usually prefer separate environment variables for URLs and accounts, then projects for browser and test purpose. If staging and production need different safety rules, separate projects can make sense.
How many projects are too many?
If engineers cannot explain when each project runs, you have too many. Start with setup, chromium regression, critical cross-browser projects, API contracts, and PR smoke. Add more only when a release risk demands it.
What should I teach beginners first?
Teach the idea of a project as a named test configuration. Then show --project, browser devices, and setup dependencies. Beginners understand projects faster when they see the CLI output and HTML report.
