Chapter 6 – Organizing Tests and Code: Folders, Files, and Naming

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.

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.js or .test.js at 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.

TypeExampleConvention
Test fileslogin.spec.jsUse lowercase with .spec.js suffix
Page classesLoginPage.jsPascalCase for classes
FixturesauthFixture.jscamelCase for custom fixtures
Helper filesapiHelpers.jsDescriptive 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

FolderPurpose
tests/Holds all your .spec.js test 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.