Skip to main content

Fixture Composition

Praman uses Playwright's mergeTests() to compose fixture modules into a single test object. This page explains the composition pattern, how to customize it, and why it exists.

How mergeTests() Works​

Playwright's mergeTests() combines multiple test.extend() definitions into one test object. Each fixture module defines its own fixtures independently, and mergeTests() produces a unified test function that includes all of them.

import { mergeTests } from '@playwright/test';
import { coreTest } from './core-fixtures.js';
import { authTest } from './auth-fixtures.js';
import { navTest } from './nav-fixtures.js';

export const test = mergeTests(coreTest, authTest, navTest);

When you import from playwright-praman, this merge has already been done for you:

import { test, expect } from 'playwright-praman';

test('all fixtures available', async ({ ui5, sapAuth, ui5Navigation }) => {
// Everything is available in a single destructure
});

The Fixture Module Chain​

Praman merges 12 fixture modules in a specific order:

coreTest          → ui5, pramanConfig, pramanLogger, rootLogger, tracer
stabilityTest → ui5Stability, requestInterceptor (auto)
selectorTest → selectorRegistration (auto)
matcherTest → matcherRegistration (auto)
compatTest → playwrightCompat (auto)
navTest → ui5Navigation, btpWorkZone
authTest → sapAuth
moduleTest → ui5.table, ui5.dialog, ui5.date, ui5.odata
feTest → fe (listReport, objectPage, table, list)
aiTest → pramanAI
intentTest → intent (procurement, sales, finance, manufacturing, masterData)
shellFooterTest → ui5Shell, ui5Footer
flpLocksTest → flpLocks
flpSettingsTest → flpSettings
testDataTest → testData

The order matters: later modules can depend on fixtures defined by earlier modules. For example, navTest depends on ui5 from coreTest, and authTest depends on pramanConfig.

Why mergeTests() Over a Monolithic File​

A single giant test.extend() call with all fixtures creates several problems:

  1. Circular dependencies — fixtures that reference each other in one block cause TypeScript compilation errors
  2. Bloated imports — every test file imports every dependency, even if unused
  3. Testing isolation — unit-testing fixture logic requires loading the entire fixture tree
  4. Readability — a 500-line fixture definition is unmaintainable

With mergeTests(), each module is independently testable, tree-shakeable, and can be composed in any combination.

Tree-Shaking: Use Only What You Need​

If your tests only need core UI5 operations and authentication, skip the full import:

import { mergeTests } from '@playwright/test';
import { coreTest, authTest } from 'playwright-praman';

const test = mergeTests(coreTest, authTest);

test('lightweight test', async ({ ui5, sapAuth }) => {
// Only core + auth fixtures are loaded
// AI, intents, FE helpers are not initialized
});

This reduces worker initialization time and avoids loading optional dependencies (e.g., LLM SDKs) that are not installed.

Adding Custom Fixtures​

Extend the merged test object with your own fixtures using test.extend():

import { test as base, expect } from 'playwright-praman';

interface MyFixtures {
adminUser: { username: string; password: string };
appUrl: string;
}

const test = base.extend<MyFixtures>({
adminUser: async ({}, use) => {
await use({
username: process.env.ADMIN_USER ?? 'admin',
password: process.env.ADMIN_PASS ?? 'secret',
});
},

appUrl: async ({ pramanConfig }, use) => {
const baseUrl = pramanConfig.auth?.baseUrl ?? 'http://localhost:8080';
await use(`${baseUrl}/sap/bc/ui5_ui5/ui2/ushell/shells/abap/FioriLaunchpad.html`);
},
});

export { test, expect };

Composing Across Test Files​

For large test suites, create domain-specific test objects:

// fixtures/procurement-test.ts
import { test as base } from 'playwright-praman';

export const test = base.extend({
poDefaults: async ({}, use) => {
await use({ plant: '1000', purchOrg: '1000', companyCode: '1000' });
},
});

// fixtures/finance-test.ts
import { test as base } from 'playwright-praman';

export const test = base.extend({
fiscalYear: async ({}, use) => {
await use(new Date().getFullYear().toString());
},
});
// tests/procurement/create-po.spec.ts
import { test } from '../../fixtures/procurement-test.js';

test('create PO with defaults', async ({ ui5, intent, poDefaults }) => {
await intent.procurement.createPurchaseOrder({
vendor: '100001',
material: 'MAT-001',
quantity: 10,
plant: poDefaults.plant,
});
});

Fixture Scopes​

Praman fixtures use two scopes:

  • Worker-scoped — created once per Playwright worker process. Shared across all tests in that worker. Used for expensive initialization: pramanConfig, rootLogger, tracer.
  • Test-scoped — created fresh for each test. Used for stateful objects: ui5, sapAuth, flpLocks, testData.

Worker-scoped fixtures cannot depend on test-scoped fixtures. Test-scoped fixtures can depend on both.

Auto-Fixtures​

Five fixtures are marked with { auto: 'on' } or { auto: true } and fire without being requested in the test signature:

// These run automatically for every test:
// - playwrightCompat (worker) — version compatibility checks
// - selectorRegistration (worker) — registers ui5= selector engine
// - matcherRegistration (worker) — registers 10 custom matchers
// - requestInterceptor (test) — blocks WalkMe/analytics scripts
// - ui5Stability (test) — auto-waits for UI5 stability after navigation

You never destructure these — they just work.