# 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);
```
