Build test helpers as pure functions first, then wrap them in framework-specific fixtures. Compose capabilities using `mergeTests` (Playwright) or layered commands (Cypress) instead of inheritance. Each fixture should solve one isolated concern (auth, API, logs, network).
## Rationale
Traditional Page Object Models create tight coupling through inheritance chains (`BasePage → LoginPage → AdminPage`). When base classes change, all descendants break. Pure functions with fixture wrappers provide:
- **Testability**: Pure functions run in unit tests without framework overhead
- **Composability**: Mix capabilities freely via `mergeTests`, no inheritance constraints
- **Reusability**: Export fixtures via package subpaths for cross-project sharing
- **Maintainability**: One concern per fixture = clear responsibility boundaries
## Pattern Examples
### Example 1: Pure Function → Fixture Pattern
**Context**: When building any test helper, always start with a pure function that accepts all dependencies explicitly. Then wrap it in a Playwright fixture or Cypress command.
**Implementation**:
```typescript
// playwright/support/helpers/api-request.ts
// Step 1: Pure function (ALWAYS FIRST!)
type ApiRequestParams = {
request: APIRequestContext;
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
url: string;
data?: unknown;
headers?: Record<string, string>;
};
export async function apiRequest({
request,
method,
url,
data,
headers = {}
}: ApiRequestParams) {
const response = await request.fetch(url, {
method,
data,
headers: {
'Content-Type': 'application/json',
...headers
}
});
if (!response.ok()) {
throw new Error(`API request failed: ${response.status()} ${await response.text()}`);
### Example 2: Composable Fixture System with mergeTests
**Context**: When building comprehensive test capabilities, compose multiple focused fixtures instead of creating monolithic helper classes. Each fixture provides one capability.
**Implementation**:
```typescript
// playwright/support/fixtures/merged-fixtures.ts
import { test as base, mergeTests } from '@playwright/test';
import { test as apiRequestFixture } from './api-request-fixture';
import { test as networkFixture } from './network-fixture';
import { test as authFixture } from './auth-fixture';
import { test as logFixture } from './log-fixture';
// Compose all fixtures for comprehensive capabilities
export const test = mergeTests(base, apiRequestFixture, networkFixture, authFixture, logFixture);
export { expect } from '@playwright/test';
// Example usage in tests:
// import { test, expect } from './support/fixtures/merged-fixtures';
-`mergeTests` combines fixtures without inheritance
- Each fixture has single responsibility (network, auth, logs)
- Tests import merged fixture and access all capabilities
- No coupling between fixtures—add/remove freely
### Example 3: Framework-Agnostic HTTP Helper
**Context**: When building HTTP helpers, keep them framework-agnostic. Accept all params explicitly so they work in unit tests, Playwright, Cypress, or any context.