Building a SOLID Playwright Framework: The Architecture Guide for Maintainable Test Suites
One class, one job. That is the principle that separates a Playwright framework that scales from one that becomes a maintenance nightmare. Here is how to apply SOLID design principles to test automation.
Contents
The Problem: Monolithic Page Objects
// BAD: One class doing everything
class DashboardPage {
// 500 lines of methods covering every dashboard interaction
// Navbar methods mixed with chart methods mixed with sidebar methods
// Impossible to reuse, impossible to maintain
}
The Solution: Component-Based Architecture
// GOOD: Each component has one responsibility
class Navbar {
constructor(private page: Page) {}
async navigateTo(section: string) { /* ... */ }
async search(query: string) { /* ... */ }
}
class DataChart {
constructor(private page: Page) {}
async selectDateRange(start: string, end: string) { /* ... */ }
async exportToCsv() { /* ... */ }
}
class Sidebar {
constructor(private page: Page) {}
async selectFilter(filter: string) { /* ... */ }
async collapse() { /* ... */ }
}
// Dashboard composes components
class DashboardPage {
readonly navbar: Navbar;
readonly chart: DataChart;
readonly sidebar: Sidebar;
constructor(private page: Page) {
this.navbar = new Navbar(page);
this.chart = new DataChart(page);
this.sidebar = new Sidebar(page);
}
}
Folder Structure
tests/
+-- pages/ # Full page classes (compose components)
| +-- DashboardPage.ts
| +-- LoginPage.ts
+-- components/ # Reusable UI components
| +-- Navbar.ts
| +-- DataTable.ts
| +-- Modal.ts
+-- fixtures/ # Custom test fixtures
| +-- auth.fixture.ts
| +-- data.fixture.ts
+-- utils/ # Helpers (API client, data generators)
+-- data/ # Test data files
+-- specs/ # Test files
+-- playwright.config.ts
Custom Fixtures for DRY Setup
// fixtures/base.fixture.ts
import { test as base } from '@playwright/test';
import { DashboardPage } from '../pages/DashboardPage';
import { ApiClient } from '../utils/ApiClient';
export const test = base.extend<{
dashboard: DashboardPage;
api: ApiClient;
}>({
dashboard: async ({ page }, use) => {
const dashboard = new DashboardPage(page);
await use(dashboard);
},
api: async ({ request }, use) => {
const api = new ApiClient(request);
await use(api);
},
});
// Usage in tests
test('chart shows correct data', async ({ dashboard, api }) => {
await api.seedData({ users: 100 });
await dashboard.chart.selectDateRange('2026-01-01', '2026-03-31');
// assertions...
});
