In the previous chapters, you learned how to perform actions, make assertions, and use Playwright’s test runner to automate real-world scenarios. Now it’s time to level up your test automation skills by learning how to organize your Playwright project like a professional.
A well-structured project is easier to maintain, scale, and debug. Whether you’re working alone or as part of a QA or development team, good organization saves countless hours and makes your automation framework more robust and reliable.
Why structure matters
When you first start writing Playwright tests, it’s tempting to keep everything in one file. That’s fine for small demos but as soon as your project grows beyond a few tests, you’ll quickly run into problems:
- Repeated code (like login steps or setup logic).
- Confusing folder layouts.
- Difficulty finding and fixing broken tests.
- Hard-to-share test data or fixtures.
A clear folder structure ensures readability, reusability, and scalability.
Think of your Playwright project like a house, if the foundation (structure) is solid, everything else fits neatly on top.
Recommended folder structure
Here’s a real-world folder structure used in most professional Playwright automation projects:
playwright-project/
├── tests/                # All test files go here
│   ├── login.spec.js
│   ├── dashboard.spec.js
│   └── cart.spec.js
│
├── pages/                # Page Object Model (POM) classes
│   ├── LoginPage.js
│   ├── DashboardPage.js
│   └── CartPage.js
│
├── fixtures/             # Shared setup and teardown logic
│   └── authFixture.js
│
├── utils/                # Helper functions and reusable code
│   ├── apiHelpers.js
│   └── dataHelpers.js
│
├── config/               # Configuration and environment setup
│   ├── playwright.config.js
│   └── env.js
│
├── reports/              # HTML reports, screenshots, and traces
│
├── test-data/            # JSON or CSV files for input data
│   └── users.json
│
├── package.json
└── README.md
Let’s go through each folder and its purpose.
1. tests/ – Where your test files live
All test files go inside this folder. Each file represents a test suite (like login tests, checkout tests, etc.).
Example file: tests/login.spec.js
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
test.describe('Login tests', () => {
  test('should login successfully with valid credentials', async ({ page }) => {
    const loginPage = new LoginPage(page);
    await loginPage.goto();
    await loginPage.login('user1', 'password123');
    await expect(page).toHaveURL(/dashboard/);
  });
});
Each test focuses on verifying one key functionality.
Naming tip: Use
.spec.jsor.test.jsat the end of test file names. Playwright automatically looks for these.
2. pages/ – Page Object Model (POM)
The Page Object Model is a design pattern that organizes web pages as separate classes, each with its own locators and actions. It helps you write cleaner, reusable, and maintainable tests.
Example: pages/LoginPage.js
export class LoginPage {
  constructor(page) {
    this.page = page;
    this.usernameInput = page.locator('#user-name');
    this.passwordInput = page.locator('#password');
    this.loginButton = page.locator('#login-button');
  }
  async goto() {
    await this.page.goto('https://www.saucedemo.com/');
  }
  async login(username, password) {
    await this.usernameInput.fill(username);
    await this.passwordInput.fill(password);
    await this.loginButton.click();
  }
}
Now your tests become clean and readable:
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('user1', 'password123');
Best practice: One class per page. Keep locators private inside the class and expose only meaningful actions.
3. fixtures/ – Shared setup and teardown
Fixtures are reusable setups for tests, such as logging in before tests or cleaning up after.
Example: fixtures/authFixture.js
import { test as base } from '@playwright/test';
export const test = base.extend({
  authenticatedPage: async ({ page }, use) => {
    await page.goto('https://www.saucedemo.com/');
    await page.fill('#user-name', 'standard_user');
    await page.fill('#password', 'secret_sauce');
    await page.click('#login-button');
    await use(page);
  },
});
Usage in a test:
import { test, expect } from '../fixtures/authFixture';
test('dashboard should show after login', async ({ authenticatedPage }) => {
  await expect(authenticatedPage).toHaveURL(/inventory/);
});
Fixtures help eliminate repeated login or navigation code in every test.
4. utils/ – Helper functions and tools
This folder is for reusable utilities like API helpers, data generators, or file readers.
Example: utils/apiHelpers.js
export async function createUser(apiContext, userData) {
  const response = await apiContext.post('/api/users', { data: userData });
  return response.json();
}
Example: utils/dataHelpers.js
export function randomEmail() {
  return `user_${Date.now()}@test.com`;
}
These helpers make your tests clean and modular.
5. config/ – Environment and Playwright settings
This folder contains environment-specific configuration files.
Example: config/playwright.config.js
import { defineConfig } from '@playwright/test';
export default defineConfig({
  testDir: './tests',
  timeout: 30000,
  retries: 1,
  use: {
    browserName: 'chromium',
    headless: true,
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
  },
  reporter: [['html', { outputFolder: 'reports' }]],
});
Example: config/env.js
export const ENV = {
  baseURL: 'https://www.saucedemo.com/',
  username: 'standard_user',
  password: 'secret_sauce',
};
You can import these in tests or page objects:
import { ENV } from '../config/env';
await page.goto(ENV.baseURL);
6. test-data/ – Store input data
Keep your data separate from your test logic. This makes it easy to update or extend tests.
Example: test-data/users.json
[
  {
    "username": "standard_user",
    "password": "secret_sauce"
  },
  {
    "username": "locked_out_user",
    "password": "secret_sauce"
  }
]
You can load it like this:
import users from '../test-data/users.json';
await loginPage.login(users[0].username, users[0].password);
7. reports/ – Test results and traces
Playwright automatically generates HTML reports and trace files if configured. Keep them in a separate folder for easy cleanup and sharing.
npx playwright test --reporter=html
To open the report:
npx playwright show-report
Naming conventions
Good naming keeps things consistent and predictable.
| Type | Example | Convention | 
|---|---|---|
| Test files | login.spec.js | Use lowercase with .spec.jssuffix | 
| Page classes | LoginPage.js | PascalCase for classes | 
| Fixtures | authFixture.js | camelCase for custom fixtures | 
| Helper files | apiHelpers.js | Descriptive names for purpose | 
| Test titles | 'should login successfully' | Describe behavior, not implementation | 
Tip: File and class names should describe what the test or module does, not how it does it.
Organizing tests by feature
When your project grows, it’s wise to organize tests by features or modules.
Example:
/tests
├── auth/
│   ├── login.spec.js
│   ├── logout.spec.js
│
├── cart/
│   ├── addItem.spec.js
│   ├── removeItem.spec.js
│
├── profile/
│   ├── updateProfile.spec.js
│   ├── changePassword.spec.js
This mirrors your application structure and helps teams work independently on different features.
Example: putting it all together
Here’s a simple working example of an organized Playwright project:
pages/LoginPage.js
export class LoginPage {
  constructor(page) {
    this.page = page;
    this.username = page.locator('#user-name');
    this.password = page.locator('#password');
    this.loginBtn = page.locator('#login-button');
  }
  async open() {
    await this.page.goto('https://www.saucedemo.com/');
  }
  async login(user, pass) {
    await this.username.fill(user);
    await this.password.fill(pass);
    await this.loginBtn.click();
  }
}
tests/login.spec.js
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages/LoginPage';
test('User can log in successfully', async ({ page }) => {
  const loginPage = new LoginPage(page);
  await loginPage.open();
  await loginPage.login('standard_user', 'secret_sauce');
  await expect(page).toHaveURL(/inventory/);
});
That’s a clean, maintainable setup. If the login page changes, you only need to update the LoginPage.js file, not every test.
Exercise
Task 1: Create a new project structure using the layout above.
Task 2: Move your login test and page class into separate folders.
Task 3: Create a utility function in utils/ that generates a random username.
Bonus: Add a fixture that logs in automatically before each test.
Cheat Sheet
| Folder | Purpose | 
| tests/ | Holds all your .spec.jstest files | 
| pages/ | Stores Page Object Model classes | 
| fixtures/ | Shared setup and teardown logic | 
| utils/ | Helper functions or API utilities | 
| config/ | Configuration and environment files | 
| test-data/ | Static input data (JSON, CSV) | 
| reports/ | Output reports, screenshots, and traces | 
Summary
In this chapter, you learned how to:
- Structure a Playwright project for scalability and clarity.
- Use the Page Object Model for maintainable tests.
- Separate fixtures, utilities, and configuration files.
- Follow consistent naming and organization patterns.
Good structure turns your automation code into a long-term asset, not just a temporary script. You’re now ready to start writing clean, modular, and professional Playwright tests.
 
