Skip to content
ExponaBLOG
PLAYWRIGHT & BROWSER AUTOMATION · DAY 044 / 100

Organizing a large Playwright suite: a step-by-step guide

Structure that scales to hundreds of tests.

Expona Admin8 min read

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:

typescript
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.ts

Each 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:

typescript
// 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:

typescript
// 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:

typescript
// 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:

typescript
// 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:

typescript
npx playwright test --grep @smoke

Run full regression suite on schedule or before release:

typescript
npx playwright test --grep @regression

You can also exclude tags:

typescript
npx playwright test --grep-invert @slow

This 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:

typescript
# 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-lead

When 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:

  1. Every PR: run --grep @smoke (fast feedback, 3-5 minutes)
  2. After merge: run full --grep @regression overnight (thorough, no time pressure)
  3. 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:

typescript
// 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.
---
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles