Playwright Framework Folder Structure: Best Practices with TypeScript

A good folder structure is the difference between a framework that scales and one that collapses after 200 tests. In this lesson you will build a proven Playwright + TypeScript framework layout, understand each layer, and apply naming and quality rules used in real teams.

Advertisement

๐Ÿ“– What You’ll Learn: why structure matters, a recommended folder layout, the framework layers and their rules, example page objects, data, fixtures and tests, tsconfig path aliases, environment configuration, naming conventions, code quality tooling and a framework checklist.


๐Ÿ—๏ธ Why Folder Structure Matters

A Playwright project with 20 tests can live in one folder. A project with 2,000 tests, five contributors and three environments cannot. A clear structure gives you:

  • Findability: anyone can guess where a page object, a fixture or a data file lives.
  • Reuse: login, API clients and helpers are written once.
  • Safe change: a UI change is fixed in one page object, not in 40 tests.
  • Easy onboarding: new team members follow existing patterns.

This lesson presents a proven structure for Playwright + TypeScript frameworks, explains every folder, and shows how the layers connect.


๐Ÿ“ Recommended Folder Structure

playwright-framework/
โ”œโ”€โ”€ .github/workflows/
โ”‚   โ””โ”€โ”€ playwright.yml          # CI pipeline
โ”œโ”€โ”€ config/
โ”‚   โ”œโ”€โ”€ env.ts                  # reads and validates environment variables
โ”‚   โ””โ”€โ”€ environments.ts         # URLs per environment (dev, qa, staging)
โ”œโ”€โ”€ src/
โ”‚   โ”œโ”€โ”€ pages/                  # Page Object Model
โ”‚   โ”‚   โ”œโ”€โ”€ BasePage.ts
โ”‚   โ”‚   โ”œโ”€โ”€ LoginPage.ts
โ”‚   โ”‚   โ”œโ”€โ”€ InventoryPage.ts
โ”‚   โ”‚   โ””โ”€โ”€ components/
โ”‚   โ”‚       โ””โ”€โ”€ HeaderComponent.ts
โ”‚   โ”œโ”€โ”€ api/                    # API clients for setup and API tests
โ”‚   โ”‚   โ”œโ”€โ”€ ApiClient.ts
โ”‚   โ”‚   โ””โ”€โ”€ UsersApi.ts
โ”‚   โ”œโ”€โ”€ fixtures/
โ”‚   โ”‚   โ””โ”€โ”€ index.ts            # custom test with pages, API clients, test data
โ”‚   โ”œโ”€โ”€ data/                   # static test data
โ”‚   โ”‚   โ”œโ”€โ”€ users.ts
โ”‚   โ”‚   โ””โ”€โ”€ products.json
โ”‚   โ”œโ”€โ”€ utils/                  # pure helpers (dates, random data, formatting)
โ”‚   โ”‚   โ””โ”€โ”€ random.ts
โ”‚   โ””โ”€โ”€ types/                  # shared interfaces
โ”‚       โ””โ”€โ”€ user.ts
โ”œโ”€โ”€ tests/
โ”‚   โ”œโ”€โ”€ auth.setup.ts           # logs in once, saves storageState
โ”‚   โ”œโ”€โ”€ ui/
โ”‚   โ”‚   โ”œโ”€โ”€ login.spec.ts
โ”‚   โ”‚   โ””โ”€โ”€ cart.spec.ts
โ”‚   โ”œโ”€โ”€ api/
โ”‚   โ”‚   โ””โ”€โ”€ users.api.spec.ts
โ”‚   โ””โ”€โ”€ visual/
โ”‚       โ””โ”€โ”€ home.visual.spec.ts
โ”œโ”€โ”€ playwright/.auth/           # saved sessions (git-ignored)
โ”œโ”€โ”€ .env.example                # documented variables, no secrets
โ”œโ”€โ”€ .gitignore
โ”œโ”€โ”€ eslint.config.mjs
โ”œโ”€โ”€ package.json
โ”œโ”€โ”€ playwright.config.ts
โ””โ”€โ”€ tsconfig.json

๐Ÿ’ก Tip: Keep framework code (src/) separate from tests (tests/). Tests should read like user stories; all the “how” lives in src/.


๐Ÿงฑ The Layers and How They Connect

Each layer only talks to the layer below it:

LayerFolderResponsibilityKnows about
Teststests/What to verify, written as user flowsFixtures
Fixturessrc/fixtures/Create and inject page objects, API clients, dataPages, API, data
Pages and componentssrc/pages/Locators and user actions for one page or widgetPlaywright Page
API clientssrc/api/HTTP calls for setup, cleanup and API testsPlaywright APIRequestContext
Data, types, utilssrc/data/, src/types/, src/utils/Static data, interfaces, pure helpersNothing above
Configconfig/, playwright.config.tsEnvironments, browsers, timeouts, reportersEnvironment variables

Rules that keep the framework clean:

  1. Tests never contain raw selectors; they call page-object methods.
  2. Page objects never contain assertions about business rules; tests do (simple “is the page loaded” checks are fine).
  3. Utilities are pure functions with no Playwright dependency.
  4. Secrets come only from environment variables.

๐Ÿ“„ Example Files

src/pages/BasePage.ts

import { Page } from '@playwright/test';

export abstract class BasePage {
  constructor(protected readonly page: Page) {}
  protected abstract readonly path: string;

  async open(): Promise<void> {
    await this.page.goto(this.path);
  }
}

src/pages/LoginPage.ts

import { Page, Locator } from '@playwright/test';
import { BasePage } from './BasePage';

export class LoginPage extends BasePage {
  protected readonly path = '/';
  readonly username: Locator;
  readonly password: Locator;
  readonly submit: Locator;
  readonly error: Locator;

  constructor(page: Page) {
    super(page);
    this.username = page.getByPlaceholder('Username');
    this.password = page.getByPlaceholder('Password');
    this.submit = page.getByRole('button', { name: 'Login' });
    this.error = page.locator('[data-test="error"]');
  }

  async login(user: { username: string; password: string }): Promise<void> {
    await this.username.fill(user.username);
    await this.password.fill(user.password);
    await this.submit.click();
  }
}

src/types/user.ts and src/data/users.ts

// src/types/user.ts
export interface User { username: string; password: string }

// src/data/users.ts
import { User } from '../types/user';

export const users = {
  standard: { username: 'standard_user', password: process.env.SAUCE_PASSWORD ?? 'secret_sauce' },
  locked:   { username: 'locked_out_user', password: process.env.SAUCE_PASSWORD ?? 'secret_sauce' },
} satisfies Record<string, User>;

src/fixtures/index.ts

import { test as base } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
import { InventoryPage } from '../pages/InventoryPage';

type Fixtures = {
  loginPage: LoginPage;
  inventoryPage: InventoryPage;
};

export const test = base.extend<Fixtures>({
  loginPage: async ({ page }, use) => { await use(new LoginPage(page)); },
  inventoryPage: async ({ page }, use) => { await use(new InventoryPage(page)); },
});
export { expect } from '@playwright/test';

tests/ui/login.spec.ts

import { test, expect } from '../../src/fixtures';
import { users } from '../../src/data/users';

test.describe('Login', { tag: '@smoke' }, () => {
  test('standard user reaches the products page', async ({ loginPage, page }) => {
    await loginPage.open();
    await loginPage.login(users.standard);
    await expect(page).toHaveURL(/inventory/);
  });

  test('locked user sees an error', async ({ loginPage }) => {
    await loginPage.open();
    await loginPage.login(users.locked);
    await expect(loginPage.error).toContainText('locked out');
  });
});

The test is short, readable and free of selectors. Learn each building block in depth: Page Object Model, custom fixtures, API with POM.


๐Ÿ”— Cleaner Imports with Path Aliases

Long relative paths like ../../src/pages/LoginPage are hard to read. Define aliases in tsconfig.json; Playwright resolves them automatically:

{
  "compilerOptions": {
    "target": "ESNext",
    "module": "commonjs",
    "strict": true,
    "baseUrl": ".",
    "paths": {
      "@pages/*": ["src/pages/*"],
      "@fixtures": ["src/fixtures/index.ts"],
      "@data/*": ["src/data/*"]
    }
  }
}
import { test, expect } from '@fixtures';
import { users } from '@data/users';

๐ŸŒ Environments and Config

Keep environment-specific values out of the code:

// config/environments.ts
export const environments = {
  dev:     'https://dev.example.com',
  qa:      'https://qa.example.com',
  staging: 'https://staging.example.com',
} as const;

export type EnvName = keyof typeof environments;
// playwright.config.ts (excerpt)
import 'dotenv/config';
import { defineConfig } from '@playwright/test';
import { environments, EnvName } from './config/environments';

const env = (process.env.TEST_ENV ?? 'qa') as EnvName;

export default defineConfig({
  use: { baseURL: environments[env] },
});

Run against another environment with TEST_ENV=staging npx playwright test (on PowerShell: $env:TEST_ENV='staging'; npx playwright test). Details in .env files and multiple config files.


๐Ÿ“ Naming Conventions

ItemConventionExample
UI test filesfeature.spec.tscheckout.spec.ts
API test filesfeature.api.spec.tsusers.api.spec.ts
Setup filesname.setup.tsauth.setup.ts
Page objectsPascalCase + PageCheckoutPage.ts
ComponentsPascalCase + ComponentHeaderComponent.ts
Test titlesBehaviour, not steps'guest can apply a discount code'
Locator propertiesNoun describing the elementsubmitButton, cartBadge
Action methodsVerb describing the user intentlogin(), addToCart()

๐Ÿงน Code Quality Tooling

  • ESLint with typescript-eslint and eslint-plugin-playwright (rules such as no-floating-promises and playwright/no-wait-for-timeout).
  • Prettier for consistent formatting.
  • Type check in CI: npx tsc --noEmit (TypeScript for Playwright).
  • Pre-commit hook (Husky + lint-staged) to lint changed files.
  • .env.example listing every variable without real values.

โœ… Framework Checklist

  • Tests contain no selectors and no hard-coded URLs or secrets.
  • Every page and reusable widget has one class.
  • Login runs once per run through a setup project with storageState.
  • Test data is typed and lives in src/data or is generated per test.
  • One command runs everything locally the same way CI does.
  • CI uploads the HTML report and traces on failure.

๐Ÿ“ Assignment

  1. Create the folder structure above for Sauce Demo (you can leave some folders empty).
  2. Move your login and cart tests into tests/ui/ and create LoginPage, InventoryPage and BasePage in src/pages/.
  3. Create src/fixtures/index.ts that injects both page objects, and update the tests to use it.
  4. Add path aliases @pages/*, @fixtures and @data/* and replace the relative imports.
  5. Add config/environments.ts and select the base URL with TEST_ENV.
  6. Install ESLint with the Playwright plugin and fix every warning.
  7. Bonus: add an auth.setup.ts setup project that logs in once and saves storageState.

โ“ FAQ

๐Ÿค” What is the best folder structure for a Playwright framework?

Separate tests (tests/) from framework code (src/ with pages, API clients, fixtures, data, types and utils), keep configuration in config/ and playwright.config.ts, and store sessions and reports in git-ignored folders.

๐Ÿค” Where should page objects go in Playwright?

In a dedicated folder such as src/pages/, with one class per page and a components/ subfolder for reusable widgets.

๐Ÿค” Should Playwright UI tests and API tests be in separate folders?

Yes. Separate folders (and usually separate projects in the config) make it easy to run them independently and give them different settings.

๐Ÿค” Can I use TypeScript path aliases in Playwright?

Yes. Define paths in tsconfig.json; Playwright Test resolves them automatically.

๐Ÿค” How do I switch environments in a Playwright framework?

Store URLs per environment in a config file and choose one with an environment variable such as TEST_ENV, read in playwright.config.ts.


๐ŸŽฏ Next Steps

๐ŸŽฌ Watch the full Playwright tutorial series on YouTube!

Subscribe to SqaToolsByDY • Practice on sqatools.in

Leave a Comment