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.
๐ 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:
| Layer | Folder | Responsibility | Knows about |
|---|---|---|---|
| Tests | tests/ | What to verify, written as user flows | Fixtures |
| Fixtures | src/fixtures/ | Create and inject page objects, API clients, data | Pages, API, data |
| Pages and components | src/pages/ | Locators and user actions for one page or widget | Playwright Page |
| API clients | src/api/ | HTTP calls for setup, cleanup and API tests | Playwright APIRequestContext |
| Data, types, utils | src/data/, src/types/, src/utils/ | Static data, interfaces, pure helpers | Nothing above |
| Config | config/, playwright.config.ts | Environments, browsers, timeouts, reporters | Environment variables |
Rules that keep the framework clean:
- Tests never contain raw selectors; they call page-object methods.
- Page objects never contain assertions about business rules; tests do (simple “is the page loaded” checks are fine).
- Utilities are pure functions with no Playwright dependency.
- 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
| Item | Convention | Example |
|---|---|---|
| UI test files | feature.spec.ts | checkout.spec.ts |
| API test files | feature.api.spec.ts | users.api.spec.ts |
| Setup files | name.setup.ts | auth.setup.ts |
| Page objects | PascalCase + Page | CheckoutPage.ts |
| Components | PascalCase + Component | HeaderComponent.ts |
| Test titles | Behaviour, not steps | 'guest can apply a discount code' |
| Locator properties | Noun describing the element | submitButton, cartBadge |
| Action methods | Verb describing the user intent | login(), addToCart() |
๐งน Code Quality Tooling
- ESLint with
typescript-eslintandeslint-plugin-playwright(rules such asno-floating-promisesandplaywright/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/dataor is generated per test. - One command runs everything locally the same way CI does.
- CI uploads the HTML report and traces on failure.
๐ Assignment
- Create the folder structure above for Sauce Demo (you can leave some folders empty).
- Move your login and cart tests into
tests/ui/and createLoginPage,InventoryPageandBasePageinsrc/pages/. - Create
src/fixtures/index.tsthat injects both page objects, and update the tests to use it. - Add path aliases
@pages/*,@fixturesand@data/*and replace the relative imports. - Add
config/environments.tsand select the base URL withTEST_ENV. - Install ESLint with the Playwright plugin and fix every warning.
- Bonus: add an
auth.setup.tssetup project that logs in once and savesstorageState.
โ 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
- ๐ญ Design Patterns: Factory, Builder, Strategy
- ๐ญ SOLID Principles with Playwright
- ๐ญ Real-World Enterprise Framework
- ๐ญ Playwright Tutorial with TypeScript
๐ฌ Watch the full Playwright tutorial series on YouTube!
Subscribe to SqaToolsByDY • Practice on sqatools.in