A well-organized project structure is the difference between a test suite that scales and one that becomes a mess after 20 tests. You do not want to be the person who puts all 50 test files in one folder with names like test1.spec.ts, test2.spec.ts.
e2e/
├── playwright.config.ts # Playwright configuration
├── package.json # Dependencies
├── .env.test # Local test credentials (gitignored)
├── .env.test.example # Template with dummy values (committed)
│
├── fixtures/
│ └── test-fixtures.ts # Custom Playwright fixtures
│
├── pages/ # Page Object Model classes
│ ├── base-page.ts # Shared methods (waitForLoad, screenshot)
│ ├── login-page.ts
│ ├── product-list-page.ts
│ ├── product-detail-page.ts
│ ├── cart-page.ts
│ ├── checkout-page.ts
│ └── order-history-page.ts
│
├── specs/ # Test files organized by feature
│ ├── auth/
│ │ ├── login.spec.ts
│ │ └── logout.spec.ts
│ ├── products/
│ │ ├── search.spec.ts
│ │ └── product-detail.spec.ts
│ ├── cart/
│ │ └── cart-management.spec.ts
│ ├── checkout/
│ │ ├── checkout-flow.spec.ts
│ │ └── checkout-validation.spec.ts
│ ├── orders/
│ │ └── order-history.spec.ts
│ └── visual/
│ └── visual-regression.spec.ts
│
├── test-data/
│ ├── users.ts # Test user credentials
│ ├── products.ts # Expected product data
│ └── addresses.ts # Shipping addresses for checkout
│
├── utils/
│ ├── api-helpers.ts # Direct API calls for test setup/teardown
│ └── test-helpers.ts # Shared utility functions
│
└── .github/
└── workflows/
└── playwright.yml # CI pipelineEvery page object shares common behavior -- waiting for the page to load, taking a screenshot, checking for error banners. A base page class avoids duplicating these methods in every page object.
import { type Page, type Locator, expect } from '@playwright/test';
export abstract class BasePage {
readonly page: Page;
readonly errorBanner: Locator;
readonly loadingSpinner: Locator;
constructor(page: Page) {
this.page = page;
this.errorBanner = page.locator('[data-testid="error-banner"]');
this.loadingSpinner = page.locator('[data-testid="loading-spinner"]');
}
async waitForPageLoad() {
await this.loadingSpinner.waitFor({ state: 'hidden', timeout: 10_000 });
}
async expectNoErrors() {
await expect(this.errorBanner).not.toBeVisible();
}
async takeScreenshot(name: string) {
return this.page.screenshot({ path: `screenshots/${name}.png`, fullPage: true });
}
}export const testUsers = {
standard: {
email: process.env.TEST_USER_EMAIL || 'testuser@example.com',
password: process.env.TEST_USER_PASSWORD || 'Test@123',
name: 'Test User',
},
admin: {
email: process.env.ADMIN_EMAIL || 'admin@example.com',
password: process.env.ADMIN_PASSWORD || 'Admin@123',
name: 'Admin User',
},
invalid: {
email: 'nonexistent@example.com',
password: 'WrongPassword',
name: 'Invalid User',
},
} as const;
export const shippingAddresses = {
valid: {
name: 'John Doe',
address: '123 Test Street',
city: 'Test City',
state: 'TS',
zip: '12345',
phone: '9876543210',
},
incomplete: {
name: 'Jane Doe',
address: '',
city: '',
state: '',
zip: '',
phone: '',
},
} as const;Use process.env with fallback values for test data. In CI, credentials come from secrets. Locally, developers use the fallback defaults or their own .env.test file. This way tests work in both environments without any config changes.
Q: How do you organize a large Playwright test suite for maintainability?
A: I use a feature-based structure: pages/ for Page Object classes, specs/ organized by feature area (auth, cart, checkout -- not by page name), fixtures/ for custom Playwright fixtures, test-data/ for centralized test data, and utils/ for API helpers. I use a BasePage class for shared page methods (wait for load, check errors). Test data is centralized with environment variable support for CI. This structure scales because adding a new feature means adding one new spec folder and possibly one new page object -- existing files are untouched.
Key Point: Organize by feature (not by page), use a base page class, centralize test data with env var support, and separate fixtures from specs. This structure scales from 10 tests to 500.
Key Point: Organize tests by feature area, use BasePage for shared methods, centralize test data, and separate concerns into pages/specs/fixtures/utils