Table of contents

What is the best way to organize and structure Playwright test suites?

Organizing and structuring Playwright test suites effectively is crucial for maintaining scalable, readable, and maintainable test automation projects. A well-organized test suite reduces maintenance overhead, improves test reliability, and makes it easier for teams to collaborate on test development.

Recommended Project Structure

Here's a comprehensive folder structure that follows industry best practices:

tests/
├── config/
│   ├── playwright.config.ts
│   ├── test-data/
│   │   ├── users.json
│   │   └── api-endpoints.json
│   └── environments/
│       ├── dev.config.ts
│       ├── staging.config.ts
│       └── prod.config.ts
├── fixtures/
│   ├── auth.fixture.ts
│   ├── api.fixture.ts
│   └── database.fixture.ts
├── page-objects/
│   ├── base/
│   │   └── BasePage.ts
│   ├── auth/
│   │   ├── LoginPage.ts
│   │   └── SignupPage.ts
│   └── dashboard/
│       ├── DashboardPage.ts
│       └── ProfilePage.ts
├── utils/
│   ├── helpers.ts
│   ├── constants.ts
│   └── data-generators.ts
├── tests/
│   ├── e2e/
│   │   ├── auth/
│   │   │   ├── login.spec.ts
│   │   │   └── signup.spec.ts
│   │   └── dashboard/
│   │       └── user-profile.spec.ts
│   ├── api/
│   │   ├── users.spec.ts
│   │   └── products.spec.ts
│   └── visual/
│       └── homepage.spec.ts
└── reports/
    └── test-results/

Configuration Management

Main Configuration File

Create a centralized playwright.config.ts file to manage global settings:

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: [
    ['html'],
    ['json', { outputFile: 'reports/test-results.json' }],
    ['junit', { outputFile: 'reports/junit.xml' }]
  ],
  use: {
    baseURL: process.env.BASE_URL || 'http://localhost:3000',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
    video: 'retain-on-failure',
  },
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },
  ],
});

Environment-Specific Configurations

Create separate configuration files for different environments:

// config/environments/staging.config.ts
import { defineConfig } from '@playwright/test';
import baseConfig from '../playwright.config';

export default defineConfig({
  ...baseConfig,
  use: {
    ...baseConfig.use,
    baseURL: 'https://staging.example.com',
  },
  retries: 3,
  workers: 2,
});

Page Object Model Implementation

Base Page Class

Create a base page class to share common functionality:

// page-objects/base/BasePage.ts
import { Page, Locator } from '@playwright/test';

export class BasePage {
  readonly page: Page;
  readonly loadingSpinner: Locator;
  readonly errorMessage: Locator;

  constructor(page: Page) {
    this.page = page;
    this.loadingSpinner = page.locator('[data-testid="loading-spinner"]');
    this.errorMessage = page.locator('[data-testid="error-message"]');
  }

  async waitForPageLoad(): Promise<void> {
    await this.page.waitForLoadState('networkidle');
    await this.loadingSpinner.waitFor({ state: 'hidden' });
  }

  async getErrorMessage(): Promise<string> {
    await this.errorMessage.waitFor({ state: 'visible' });
    return await this.errorMessage.textContent() || '';
  }
}

Feature-Specific Page Objects

Organize page objects by feature or application section:

// page-objects/auth/LoginPage.ts
import { Page, Locator } from '@playwright/test';
import { BasePage } from '../base/BasePage';

export class LoginPage extends BasePage {
  readonly emailInput: Locator;
  readonly passwordInput: Locator;
  readonly loginButton: Locator;
  readonly forgotPasswordLink: Locator;

  constructor(page: Page) {
    super(page);
    this.emailInput = page.locator('[data-testid="email-input"]');
    this.passwordInput = page.locator('[data-testid="password-input"]');
    this.loginButton = page.locator('[data-testid="login-button"]');
    this.forgotPasswordLink = page.locator('[data-testid="forgot-password"]');
  }

  async login(email: string, password: string): Promise<void> {
    await this.emailInput.fill(email);
    await this.passwordInput.fill(password);
    await this.loginButton.click();
    await this.waitForPageLoad();
  }

  async isLoginFormVisible(): Promise<boolean> {
    return await this.emailInput.isVisible() && 
           await this.passwordInput.isVisible() && 
           await this.loginButton.isVisible();
  }
}

Test Organization Patterns

Grouping by Feature

Organize tests by application features rather than technical layers:

// tests/e2e/auth/login.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../../../page-objects/auth/LoginPage';
import { DashboardPage } from '../../../page-objects/dashboard/DashboardPage';

test.describe('User Authentication', () => {
  test.beforeEach(async ({ page }) => {
    await page.goto('/login');
  });

  test('should login with valid credentials', async ({ page }) => {
    const loginPage = new LoginPage(page);
    const dashboardPage = new DashboardPage(page);

    await loginPage.login('user@example.com', 'password123');

    await expect(dashboardPage.welcomeMessage).toBeVisible();
    await expect(page).toHaveURL(/dashboard/);
  });

  test('should display error for invalid credentials', async ({ page }) => {
    const loginPage = new LoginPage(page);

    await loginPage.login('invalid@example.com', 'wrongpassword');

    const errorMessage = await loginPage.getErrorMessage();
    expect(errorMessage).toContain('Invalid credentials');
  });
});

Test Data Management

Create reusable test data and utilities:

// utils/data-generators.ts
export const TestData = {
  users: {
    validUser: {
      email: 'test@example.com',
      password: 'SecurePassword123!',
      firstName: 'John',
      lastName: 'Doe'
    },
    invalidUser: {
      email: 'invalid@example.com',
      password: 'wrongpassword'
    }
  },

  generateRandomUser: () => ({
    email: `test${Date.now()}@example.com`,
    password: 'TempPassword123!',
    firstName: 'Test',
    lastName: 'User'
  })
};

Fixtures and Setup

Custom Fixtures

Create reusable fixtures for common setup tasks:

// fixtures/auth.fixture.ts
import { test as base } from '@playwright/test';
import { LoginPage } from '../page-objects/auth/LoginPage';
import { TestData } from '../utils/data-generators';

export const test = base.extend<{
  authenticatedPage: any;
  loginPage: LoginPage;
}>({
  authenticatedPage: async ({ page }, use) => {
    const loginPage = new LoginPage(page);
    await page.goto('/login');
    await loginPage.login(TestData.users.validUser.email, TestData.users.validUser.password);
    await use(page);
  },

  loginPage: async ({ page }, use) => {
    const loginPage = new LoginPage(page);
    await use(loginPage);
  }
});

Database and API Fixtures

For comprehensive testing, create fixtures for database and API setup:

// fixtures/database.fixture.ts
import { test as base } from '@playwright/test';
import { DatabaseHelper } from '../utils/database-helper';

export const test = base.extend<{
  cleanDatabase: void;
}>({
  cleanDatabase: [async ({}, use) => {
    // Setup: Clean database before test
    await DatabaseHelper.cleanTestData();
    await use();
    // Teardown: Clean database after test
    await DatabaseHelper.cleanTestData();
  }, { auto: true }]
});

Advanced Organization Strategies

Test Categories and Tags

Use test tags to organize and filter tests:

test.describe('E2E Tests', () => {
  test('critical user flow @smoke @critical', async ({ page }) => {
    // Critical path test
  });

  test('edge case scenario @regression', async ({ page }) => {
    // Edge case test
  });

  test('performance validation @performance', async ({ page }) => {
    // Performance test
  });
});

Parallel Test Execution

Structure tests to run efficiently in parallel:

// playwright.config.ts
export default defineConfig({
  fullyParallel: true,
  workers: process.env.CI ? 4 : 2,
  projects: [
    {
      name: 'setup',
      testMatch: /.*\.setup\.ts/,
    },
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
      dependencies: ['setup'],
    },
  ],
});

Running Tests with Different Configurations

Command Line Examples

Execute tests with different configurations:

# Run all tests
npx playwright test

# Run tests in specific browser
npx playwright test --project=chromium

# Run tests with tags
npx playwright test --grep "@smoke"

# Run tests in specific environment
npx playwright test --config=config/environments/staging.config.ts

# Run tests in headed mode for debugging
npx playwright test --headed

# Run tests with UI mode
npx playwright test --ui

CI/CD Integration

Configure your CI/CD pipeline to run tests efficiently:

# .github/workflows/playwright.yml
name: Playwright Tests
on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        project: [chromium, firefox, webkit]
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '18'
      - name: Install dependencies
        run: npm ci
      - name: Install Playwright
        run: npx playwright install --with-deps
      - name: Run tests
        run: npx playwright test --project=${{ matrix.project }}
      - name: Upload test results
        if: failure()
        uses: actions/upload-artifact@v3
        with:
          name: playwright-report-${{ matrix.project }}
          path: playwright-report/

Best Practices Summary

  1. Separation of Concerns: Keep page objects, test data, and test logic separate
  2. Reusability: Create fixtures and utilities for common operations
  3. Maintainability: Use descriptive naming and consistent patterns
  4. Scalability: Structure tests to handle growth in team size and test volume
  5. Environment Management: Support multiple test environments with separate configurations
  6. Reporting: Implement comprehensive reporting for test results and failures

When implementing web scraping solutions, similar organizational principles apply. Just as handling dynamic content that loads after page navigation requires careful consideration of timing and element states, organizing your test automation requires thoughtful planning of structure and dependencies.

For teams working with both Playwright and other automation tools, understanding different browser contexts and when to use them becomes crucial for creating isolated, reliable test environments.

This structured approach ensures your Playwright test suites remain maintainable, scalable, and reliable as your application and team grow. The key is to establish these patterns early and consistently apply them across your entire test automation project.

Try WebScraping.AI for Your Web Scraping Needs

Looking for a powerful web scraping solution? WebScraping.AI provides an LLM-powered API that combines Chromium JavaScript rendering with rotating proxies for reliable data extraction.

Key Features:

  • AI-powered extraction: Ask questions about web pages or extract structured data fields
  • JavaScript rendering: Full Chromium browser support for dynamic content
  • Rotating proxies: Datacenter and residential proxies from multiple countries
  • Easy integration: Simple REST API with SDKs for Python, Ruby, PHP, and more
  • Reliable & scalable: Built for developers who need consistent results

Getting Started:

Get page content with AI analysis:

curl "https://api.webscraping.ai/ai/question?url=https://example.com&question=What is the main topic?&api_key=YOUR_API_KEY"

Extract structured data:

curl "https://api.webscraping.ai/ai/fields?url=https://example.com&fields[title]=Page title&fields[price]=Product price&api_key=YOUR_API_KEY"

Try in request builder

Related Questions

Get Started Now

WebScraping.AI provides rotating proxies, Chromium rendering and built-in HTML parser for web scraping
Icon