|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382 |
- # Fixtures Composition with mergeTests
-
- ## Principle
-
- Combine multiple Playwright fixtures using `mergeTests` to create a unified test object with all capabilities. Build composable test infrastructure by merging playwright-utils fixtures with custom project fixtures.
-
- ## Rationale
-
- Using fixtures from multiple sources requires combining them:
-
- - Importing from multiple fixture files is verbose
- - Name conflicts between fixtures
- - Duplicate fixture definitions
- - No clear single test object
-
- Playwright's `mergeTests` provides:
-
- - **Single test object**: All fixtures in one import
- - **Conflict resolution**: Handles name collisions automatically
- - **Composition pattern**: Mix utilities, custom fixtures, third-party fixtures
- - **Type safety**: Full TypeScript support for merged fixtures
- - **Maintainability**: One place to manage all fixtures
-
- ## Pattern Examples
-
- ### Example 1: Basic Fixture Merging
-
- **Context**: Combine multiple playwright-utils fixtures into single test object.
-
- **Implementation**:
-
- ```typescript
- // playwright/support/merged-fixtures.ts
- import { mergeTests } from '@playwright/test';
- import { test as apiRequestFixture } from '@seontechnologies/playwright-utils/api-request/fixtures';
- import { test as authFixture } from '@seontechnologies/playwright-utils/auth-session/fixtures';
- import { test as recurseFixture } from '@seontechnologies/playwright-utils/recurse/fixtures';
-
- // Merge all fixtures
- export const test = mergeTests(apiRequestFixture, authFixture, recurseFixture);
-
- export { expect } from '@playwright/test';
- ```
-
- ```typescript
- // In your tests - import from merged fixtures
- import { test, expect } from '../support/merged-fixtures';
-
- test('all utilities available', async ({
- apiRequest, // From api-request fixture
- authToken, // From auth fixture
- recurse, // From recurse fixture
- }) => {
- // All fixtures available in single test signature
- const { body } = await apiRequest({
- method: 'GET',
- path: '/api/protected',
- headers: { Authorization: `Bearer ${authToken}` },
- });
-
- await recurse(
- () => apiRequest({ method: 'GET', path: `/status/${body.id}` }),
- (res) => res.body.ready === true,
- );
- });
- ```
-
- **Key Points**:
-
- - Create one `merged-fixtures.ts` per project
- - Import test object from merged fixtures in all test files
- - All utilities available without multiple imports
- - Type-safe access to all fixtures
-
- ### Example 2: Combining with Custom Fixtures
-
- **Context**: Add project-specific fixtures alongside playwright-utils.
-
- **Implementation**:
-
- ```typescript
- // playwright/support/custom-fixtures.ts - Your project fixtures
- import { test as base } from '@playwright/test';
- import { createUser } from './factories/user-factory';
- import { seedDatabase } from './helpers/db-seeder';
-
- export const test = base.extend({
- // Custom fixture 1: Auto-seeded user
- testUser: async ({ request }, use) => {
- const user = await createUser({ role: 'admin' });
- await seedDatabase('users', [user]);
- await use(user);
- // Cleanup happens automatically
- },
-
- // Custom fixture 2: Database helpers
- db: async ({}, use) => {
- await use({
- seed: seedDatabase,
- clear: () => seedDatabase.truncate(),
- });
- },
- });
-
- // playwright/support/merged-fixtures.ts - Combine everything
- import { mergeTests } from '@playwright/test';
- import { test as apiRequestFixture } from '@seontechnologies/playwright-utils/api-request/fixtures';
- import { test as authFixture } from '@seontechnologies/playwright-utils/auth-session/fixtures';
- import { test as customFixtures } from './custom-fixtures';
-
- export const test = mergeTests(
- apiRequestFixture,
- authFixture,
- customFixtures, // Your project fixtures
- );
-
- export { expect } from '@playwright/test';
- ```
-
- ```typescript
- // In tests - all fixtures available
- import { test, expect } from '../support/merged-fixtures';
-
- test('using mixed fixtures', async ({
- apiRequest, // playwright-utils
- authToken, // playwright-utils
- testUser, // custom
- db, // custom
- }) => {
- // Use playwright-utils
- const { body } = await apiRequest({
- method: 'GET',
- path: `/api/users/${testUser.id}`,
- headers: { Authorization: `Bearer ${authToken}` },
- });
-
- // Use custom fixture
- await db.clear();
- });
- ```
-
- **Key Points**:
-
- - Custom fixtures extend `base` test
- - Merge custom with playwright-utils fixtures
- - All available in one test signature
- - Maintainable separation of concerns
-
- ### Example 3: Full Utility Suite Integration
-
- **Context**: Production setup with all core playwright-utils and custom fixtures.
-
- **Implementation**:
-
- ```typescript
- // playwright/support/merged-fixtures.ts
- import { mergeTests } from '@playwright/test';
-
- // Playwright utils fixtures
- import { test as apiRequestFixture } from '@seontechnologies/playwright-utils/api-request/fixtures';
- import { test as authFixture } from '@seontechnologies/playwright-utils/auth-session/fixtures';
- import { test as interceptFixture } from '@seontechnologies/playwright-utils/intercept-network-call/fixtures';
- import { test as recurseFixture } from '@seontechnologies/playwright-utils/recurse/fixtures';
- import { test as networkRecorderFixture } from '@seontechnologies/playwright-utils/network-recorder/fixtures';
-
- // Custom project fixtures
- import { test as customFixtures } from './custom-fixtures';
-
- // Merge everything
- export const test = mergeTests(apiRequestFixture, authFixture, interceptFixture, recurseFixture, networkRecorderFixture, customFixtures);
-
- export { expect } from '@playwright/test';
- ```
-
- ```typescript
- // In tests
- import { test, expect } from '../support/merged-fixtures';
-
- test('full integration', async ({
- page,
- context,
- apiRequest,
- authToken,
- interceptNetworkCall,
- recurse,
- networkRecorder,
- testUser, // custom
- }) => {
- // All utilities + custom fixtures available
- await networkRecorder.setup(context);
-
- const usersCall = interceptNetworkCall({ url: '**/api/users' });
-
- await page.goto('/users');
- const { responseJson } = await usersCall;
-
- expect(responseJson).toContainEqual(expect.objectContaining({ id: testUser.id }));
- });
- ```
-
- **Key Points**:
-
- - One merged-fixtures.ts for entire project
- - Combine all playwright-utils you use
- - Add custom project fixtures
- - Single import in all test files
-
- ### Example 4: Fixture Override Pattern
-
- **Context**: Override default options for specific test files or describes.
-
- **Implementation**:
-
- ```typescript
- import { test, expect } from '../support/merged-fixtures';
-
- // Override auth options for entire file
- test.use({
- authOptions: {
- userIdentifier: 'admin',
- environment: 'staging',
- },
- });
-
- test('uses admin on staging', async ({ authToken }) => {
- // Token is for admin user on staging environment
- });
-
- // Override for specific describe block
- test.describe('manager tests', () => {
- test.use({
- authOptions: {
- userIdentifier: 'manager',
- },
- });
-
- test('manager can access reports', async ({ page }) => {
- // Uses manager token
- await page.goto('/reports');
- });
- });
- ```
-
- **Key Points**:
-
- - `test.use()` overrides fixture options
- - Can override at file or describe level
- - Options merge with defaults
- - Type-safe overrides
-
- ### Example 5: Avoiding Fixture Conflicts
-
- **Context**: Handle name collisions when merging fixtures with same names.
-
- **Implementation**:
-
- ```typescript
- // If two fixtures have same name, last one wins
- import { test as fixture1 } from './fixture1'; // has 'user' fixture
- import { test as fixture2 } from './fixture2'; // also has 'user' fixture
-
- const test = mergeTests(fixture1, fixture2);
- // fixture2's 'user' overrides fixture1's 'user'
-
- // Better: Rename fixtures before merging
- import { test as base } from '@playwright/test';
- import { test as fixture1 } from './fixture1';
-
- const fixture1Renamed = base.extend({
- user1: fixture1._extend.user, // Rename to avoid conflict
- });
-
- const test = mergeTests(fixture1Renamed, fixture2);
- // Now both 'user1' and 'user' available
-
- // Best: Design fixtures without conflicts
- // - Prefix custom fixtures: 'myAppUser', 'myAppDb'
- // - Playwright-utils uses descriptive names: 'apiRequest', 'authToken'
- ```
-
- **Key Points**:
-
- - Last fixture wins in conflicts
- - Rename fixtures to avoid collisions
- - Design fixtures with unique names
- - Playwright-utils uses descriptive names (no conflicts)
-
- ## Recommended Project Structure
-
- ```
- playwright/
- ├── support/
- │ ├── merged-fixtures.ts # ⭐ Single test object for project
- │ ├── custom-fixtures.ts # Your project-specific fixtures
- │ ├── auth/
- │ │ ├── auth-fixture.ts # Auth wrapper (if needed)
- │ │ └── custom-auth-provider.ts
- │ ├── fixtures/
- │ │ ├── user-fixture.ts
- │ │ ├── db-fixture.ts
- │ │ └── api-fixture.ts
- │ └── utils/
- │ └── factories/
- └── tests/
- ├── api/
- │ └── users.spec.ts # import { test } from '../../support/merged-fixtures'
- ├── e2e/
- │ └── login.spec.ts # import { test } from '../../support/merged-fixtures'
- └── component/
- └── button.spec.ts # import { test } from '../../support/merged-fixtures'
- ```
-
- ## Benefits of Fixture Composition
-
- **Compared to direct imports:**
-
- ```typescript
- // ❌ Without mergeTests (verbose)
- import { test as base } from '@playwright/test';
- import { apiRequest } from '@seontechnologies/playwright-utils/api-request';
- import { getAuthToken } from './auth';
- import { createUser } from './factories';
-
- test('verbose', async ({ request }) => {
- const token = await getAuthToken();
- const user = await createUser();
- const response = await apiRequest({ request, method: 'GET', path: '/api/users' });
- // Manual wiring everywhere
- });
-
- // ✅ With mergeTests (clean)
- import { test } from '../support/merged-fixtures';
-
- test('clean', async ({ apiRequest, authToken, testUser }) => {
- const { body } = await apiRequest({ method: 'GET', path: '/api/users' });
- // All fixtures auto-wired
- });
- ```
-
- **Reduction:** ~10 lines per test → ~2 lines
-
- ## Related Fragments
-
- - `overview.md` - Installation and design principles
- - `api-request.md`, `auth-session.md`, `recurse.md` - Utilities to merge
- - `network-recorder.md`, `intercept-network-call.md`, `log.md` - Additional utilities
-
- ## Anti-Patterns
-
- **❌ Importing test from multiple fixture files:**
-
- ```typescript
- import { test } from '@seontechnologies/playwright-utils/api-request/fixtures';
- // Also need auth...
- import { test as authTest } from '@seontechnologies/playwright-utils/auth-session/fixtures';
- // Name conflict! Which test to use?
- ```
-
- **✅ Use merged fixtures:**
-
- ```typescript
- import { test } from '../support/merged-fixtures';
- // All utilities available, no conflicts
- ```
-
- **❌ Merging too many fixtures (kitchen sink):**
-
- ```typescript
- // Merging 20+ fixtures makes test signature huge
- const test = mergeTests(...20 different fixtures)
-
- test('my test', async ({ fixture1, fixture2, ..., fixture20 }) => {
- // Cognitive overload
- })
- ```
-
- **✅ Merge only what you actually use:**
-
- ```typescript
- // Merge the 4-6 fixtures your project actually needs
- const test = mergeTests(apiRequestFixture, authFixture, recurseFixture, customFixtures);
- ```
|