Organizing a large Playwright suite: a step-by-step guide
As your test suite grows beyond a few dozen tests, maintaining order becomes as important as writing the tests themselves. A well-organized Playwright suite can scale to hundreds of tests without becoming unmaintainable, but this requires intentional structure from the start. In this guide, we'll walk through proven patterns that teams across e-commerce, SaaS, and travel platforms use to keep their test suites lean, discoverable, and easy to own.
Why structure matters at scale
Without intentional organization, test suites become hard to navigate. Developers can't find related tests, CI pipelines run everything when only a subset is relevant, and ownership becomes unclear. A structured approach solves these problems by creating visual hierarchies, enabling selective execution, and assigning clear responsibility.
We've seen teams go from running all 500 tests on every commit to running 30 smoke tests in 2 minutes, then scheduling regression suites during off-peak hours. That kind of efficiency only happens with deliberate structure.
Folders by feature — your first organizing principle
The most intuitive way to organize Playwright tests is by feature area. This mirrors how your application is structured and how your teams think about ownership.
A typical structure looks like this:
tests/
├── checkout/
│ ├── cart-addition.spec.ts
│ ├── payment-methods.spec.ts
│ ├── shipping-address.spec.ts
│ └── order-confirmation.spec.ts
├── account/
│ ├── login.spec.ts
│ ├── registration.spec.ts
│ ├── profile-update.spec.ts
│ └── password-reset.spec.ts
├── admin/
│ ├── dashboard.spec.ts
│ ├── user-management.spec.ts
│ ├── reporting.spec.ts
│ └── settings.spec.ts
├── search/
│ ├── filters.spec.ts
│ ├── sorting.spec.ts
│ └── pagination.spec.ts
└── shared/
├── fixtures.ts
├── page-objects/
└── helpers.tsEach folder represents a major feature area where related workflows live together. This makes it immediately obvious where to add a new test or where to look when something breaks. When a checkout bug is reported, you know exactly where those tests are.
Keep folder depth shallow—two or three levels is usually enough. If you go deeper, navigation becomes tedious and paths become verbose in CI logs.
Shared fixtures — centralize auth and page objects
Every test needs certain things: authenticated users, clean browser state, page objects for interacting with common UI elements. Rather than recreating these in each test file, use a centralized fixtures file.
Here's a practical example:
// tests/shared/fixtures.ts
import { test as base, expect } from '@playwright/test';
import { LoginPage } from './page-objects/login';
import { AccountPage } from './page-objects/account';
import { AdminDashboard } from './page-objects/admin-dashboard';
type TestFixtures = {
authenticatedUser: { email: string; password: string };
loginPage: LoginPage;
accountPage: AccountPage;
adminDashboard: AdminDashboard;
};
export const test = base.extend({
authenticatedUser: async ({}, use) => {
const testUser = {
email: `test-${Date.now()}@example.com`,
password: 'SecurePass123!',
};
// Optionally create the user via API before tests run
await use(testUser);
},
loginPage: async ({ page }, use) => {
const loginPage = new LoginPage(page);
await use(loginPage);
},
accountPage: async ({ page }, use) => {
const accountPage = new AccountPage(page);
await use(accountPage);
},
adminDashboard: async ({ page, authenticatedUser }, use) => {
const adminDash = new AdminDashboard(page);
// Pre-authenticate for admin tests
await adminDash.loginAsAdmin(authenticatedUser.email, authenticatedUser.password);
await use(adminDash);
},
});
export { expect };Then in your test files, you use this custom test instead of Playwright's base test:
// tests/account/profile-update.spec.ts
import { test, expect } from '../shared/fixtures';
test('user updates their profile name', async ({ accountPage, authenticatedUser }) => {
await accountPage.goto();
await accountPage.updateName('Jane Doe');
await expect(accountPage.nameDisplay).toContainText('Jane Doe');
});This approach keeps setup logic out of test files and makes tests read like specifications. If auth logic changes, you update it in one place. Page objects live in `tests/shared/page-objects/` alongside helper functions in `tests/shared/helpers.ts`.
Tags — selective execution with @smoke and @regression
Not every test needs to run on every trigger. Smoke tests verify critical paths quickly; regression tests catch edge cases. Tags let you label tests and run subsets without editing configuration files.
Add tags to your test declarations:
// tests/checkout/order-confirmation.spec.ts
import { test, expect } from '../shared/fixtures';
test('@smoke - user completes checkout', async ({ page, authenticatedUser }) => {
// Critical path test
await page.goto('/products');
// ... add to cart, checkout flow
});
test('@regression - user applies promotional code', async ({ page, authenticatedUser }) => {
// Edge case test
// ... test promo logic
});
test('@regression - user changes shipping after adding items', async ({ page, authenticatedUser }) => {
// Another edge case
});In your Playwright config, define these tags:
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
use: {
baseURL: 'http://localhost:3000',
},
projects: [
{
name: 'smoke',
use: { ...devices['chromium'] },
},
{
name: 'regression',
use: { ...devices['chromium'] },
},
],
});Run smoke tests in CI on every commit:
npx playwright test --grep @smokeRun full regression suite on schedule or before release:
npx playwright test --grep @regressionYou can also exclude tags:
npx playwright test --grep-invert @slowThis pattern gives you flexibility without duplicating test code. A single test can have multiple tags: `@smoke @critical @auth`.
Ownership — CODEOWNERS for test folders
As test suites grow, clarity about who maintains which tests becomes critical. A CODEOWNERS file (already familiar from PR reviews) extends naturally to tests.
Create or update your `.github/CODEOWNERS` file:
# Core team owns checkout
tests/checkout/ @core-team
# Account team owns authentication and user management
tests/account/ @account-team
# Admin features owned by ops
tests/admin/ @ops-team
# Search owned by discovery team
tests/search/ @discovery-team
# Shared fixtures reviewed by QA lead
tests/shared/ @qa-leadWhen tests in the checkout folder fail, GitHub automatically requests review from the core team. When someone proposes changes to shared fixtures, the QA lead reviews. This prevents orphaned tests and ensures the right expertise is applied.
Pair CODEOWNERS with a clear convention: tests live as close as possible to the feature they test. If you maintain a `checkout/` module, you own `tests/checkout/`. This alignment makes ownership obvious.
Practical structure in action
Let's see how these pieces fit together. Your CI pipeline might look like:
- Every PR: run
--grep @smoke(fast feedback, 3-5 minutes) - After merge: run full
--grep @regressionovernight (thorough, no time pressure) - Before release: run everything, all projects, all browsers (final gate)
Each run produces a clear report. When smoke tests fail, the core team knows to look at checkout. When regression fails, the discovery team checks search. Ownership is explicit.
In your test files, the structure guides developers:
// This file lives in tests/checkout/cart-addition.spec.ts
// It belongs to @core-team (from CODEOWNERS)
// It runs on every commit (@smoke)
// It uses shared fixtures for clean setup
import { test, expect } from '../shared/fixtures';
test('@smoke - user adds product to cart', async ({ page, loginPage }) => {
// Your test
});Every piece of information (owner, frequency, feature area) is encoded in structure and metadata, not comments.
Common mistakes
Mistake 1: Nesting folders too deeply. Avoid `tests/features/checkout/flows/happy-path/complete-order/`. Three levels max. Developers waste time navigating.
Mistake 2: Mixing feature tests with utilities. Keep page objects and helpers in `shared/`. Feature folders hold only `.spec.ts` files. This keeps the intent clear and directories lean.
Mistake 3: Tags without discoverability. Document your tag convention (what does @critical mean vs @smoke?) in a README or wiki. Inconsistent tagging defeats the purpose.
Mistake 4: CODEOWNERS without enforcement. Set branch protection to require CODEOWNERS approval for test changes. Otherwise, the file becomes a formality.
Mistake 5: Forgetting to isolate fixtures. If your authenticatedUser fixture makes network calls, tests become flaky and slow. Use API setup or test data factories when possible; avoid UI login in fixtures unless necessary.
Key takeaways
- Organize by feature area (checkout/, account/, admin/) to match your codebase and team structure.
- Centralize shared logic—auth, page objects, helpers—in a single fixtures file to avoid duplication.
- Use tags (@smoke, @regression) to control test execution frequency and keep CI pipelines fast.
- Assign ownership via CODEOWNERS to make accountability explicit and prevent orphaned tests.
- Keep folder depth shallow and filenames descriptive so tests are easy to locate and understand.
- Document tag meanings and folder conventions so new team members can contribute confidently.






Comments
Loading comments…