Skip to main content

Behavioral Equivalence (BEHAV-EQ)

Praman v1.0 is a ground-up rewrite. Behavioral equivalence testing ensures it produces the same observable results as the reference implementation (wdi5) for core SAP UI5 operations, while documenting intentional divergences.

Golden Master Methodology

Golden master testing (also called characterization testing or approval testing) captures the output of the reference implementation and compares it against the new implementation.

┌──────────────────────┐    ┌──────────────────────┐
│ wdi5 (reference) │ │ Praman (new) │
│ │ │ │
│ Run scenario S │ │ Run scenario S │
│ Capture output O_r │ │ Capture output O_n │
│ │ │ │
└──────────┬───────────┘ └──────────┬────────────┘
│ │
▼ ▼
┌─────────────────────────────────────┐
│ Compare O_r vs O_n │
│ ● Match → behavioral equivalence │
│ ● Diff → classify as bug or │
│ intentional divergence │
└─────────────────────────────────────┘

What Is Compared

For each scenario, the comparison covers:

AspectHow Compared
Control discoverySame control found (by ID, type, properties)
Property valuesgetValue(), getText(), getProperty() match
Event side effectsSame UI5 events fired (liveChange, change, press)
OData model stateSame model data after interaction
Error behaviorSame error for invalid selectors / missing controls
Timing (qualitative)Both complete within reasonable time (not exact match)

What Is NOT Compared

  • Exact timing (Praman may be faster or slower)
  • Internal implementation details (how the bridge communicates)
  • Log output format (Praman uses pino structured logging, wdi5 uses console)
  • Error message text (Praman uses structured errors with codes, wdi5 uses plain strings)

The 8 Parity Scenarios

These scenarios cover the core operations that most SAP UI5 test suites rely on.

Scenario 1: Control Discovery by ID

Find a control using its stable ID.

// wdi5 reference
const control = await browser.asControl({ selector: { id: 'container-app---main--saveBtn' } });

// Praman equivalent
const control = await ui5.control({ id: 'saveBtn' });

Comparison: Both return a proxy wrapping the same DOM element. Verified by comparing control.getId() output.

Known divergence: Praman accepts short IDs (saveBtn) and resolves the full ID internally. wdi5 requires the full generated ID including view prefix. This is an intentional improvement.

Scenario 2: Control Discovery by Type + Properties

Find a control using its UI5 type and property values.

// wdi5 reference
const input = await browser.asControl({
selector: {
controlType: 'sap.m.Input',
properties: { placeholder: 'Enter vendor' },
viewName: 'myApp.view.Main',
},
});

// Praman equivalent
const input = await ui5.control({
controlType: 'sap.m.Input',
properties: { placeholder: 'Enter vendor' },
viewName: 'myApp.view.Main',
});

Comparison: Both discover the same control. Verified by comparing getId() and getMetadata().getName().

Known divergence: None -- selectors are semantically identical.

Scenario 3: Property Read (getValue, getText)

Read a property from a discovered control.

// wdi5 reference
const value = await input.getValue();
const text = await button.getText();

// Praman equivalent
const value = await input.getValue();
const text = await button.getText();

Comparison: Return values are strictly equal (===). Tested with string, number, boolean, and null properties.

Known divergence: None -- both call the same UI5 control API methods.

Scenario 4: Input Fill (enterText + Events)

Fill a value into an input control with proper event firing.

// wdi5 reference
await input.enterText('100001');

// Praman equivalent
await ui5.fill({ id: 'vendorInput' }, '100001');

Comparison: After filling, both produce the same:

  • getValue() returns '100001'
  • liveChange event was fired (verified by model binding update)
  • change event was fired (verified by validation trigger)

Known divergence: Praman fires events in the order liveChange then change, matching SAP's documented event sequence. wdi5's event ordering depends on the WebDriverIO interaction mode. This is an intentional alignment with SAP documentation.

Scenario 5: Button Press (firePress / click)

Trigger a press action on a button control.

// wdi5 reference
await button.press();

// Praman equivalent
await ui5.press({ id: 'submitBtn' });

Comparison: Both trigger the button's press event, which fires any attached handlers. Verified by checking the UI state change caused by the handler (e.g., dialog opens, navigation occurs).

Known divergence: Praman uses a three-tier strategy (UI5 firePress -> fireTap -> DOM click). wdi5 uses WebDriverIO's click action. Both produce the same observable outcome, but through different mechanisms. If firePress is not available on a control, Praman falls back to DOM click; wdi5 always uses DOM click.

Scenario 6: Table Row Count and Cell Text

Read table dimensions and cell contents.

// wdi5 reference
const rows = await table.getAggregation('items');
const cellText = await rows[0].getCells()[2].getText();

// Praman equivalent
await expect(table).toHaveUI5RowCount(5);
await expect(table).toHaveUI5CellText(0, 2, 'Expected Value');

Comparison: Same row count and cell text values.

Known divergence: Praman provides dedicated matchers (toHaveUI5RowCount, toHaveUI5CellText) that are more ergonomic than manual aggregation traversal. The underlying data is identical, but the API surface differs intentionally for readability.

Scenario 7: Select Dropdown Item

Select an item in a sap.m.Select or sap.m.ComboBox.

// wdi5 reference
await select.open();
const items = await select.getAggregation('items');
await items[2].press();

// Praman equivalent
await ui5.select({ id: 'countrySelect' }, 'US');

Comparison: After selection, getSelectedKey() returns the same value in both.

Known divergence: Praman accepts the key or display text directly, without requiring manual open/press steps. This is an intentional API simplification. The underlying setSelectedKey() / fireChange() calls are equivalent.

Scenario 8: Error on Missing Control

Attempt to find a control that does not exist.

// wdi5 reference
const control = await browser.asControl({
selector: { id: 'nonExistentControl' },
});
// wdi5: returns a falsy value or throws after timeout

// Praman equivalent
const control = await ui5.control({ id: 'nonExistentControl' });
// Praman: throws ControlError with code ERR_CONTROL_NOT_FOUND

Comparison: Both fail to find the control within the configured timeout.

Known divergence (intentional): Praman throws a structured ControlError with error code ERR_CONTROL_NOT_FOUND, retryable: true, and suggestions[] array. wdi5 returns a falsy proxy or throws a generic error. Praman's structured errors are an intentional improvement for debuggability.

Running Parity Tests

Parity tests live in a dedicated directory and run both implementations against the same SAP system.

tests/
parity/
scenarios/
01-discovery-by-id.ts
02-discovery-by-type.ts
03-property-read.ts
04-input-fill.ts
05-button-press.ts
06-table-operations.ts
07-select-dropdown.ts
08-missing-control.ts
golden-masters/
01-discovery-by-id.json
02-discovery-by-type.json
...
compare.ts

Generating Golden Masters (wdi5)

// tests/parity/generate-golden-master.ts
import { writeFileSync } from 'node:fs';
import { join } from 'node:path';

interface GoldenMaster {
scenario: string;
timestamp: string;
results: Record<string, unknown>;
}

function saveGoldenMaster(scenario: string, results: Record<string, unknown>): void {
const master: GoldenMaster = {
scenario,
timestamp: new Date().toISOString(),
results,
};

writeFileSync(
join(__dirname, 'golden-masters', `${scenario}.json`),
JSON.stringify(master, null, 2),
);
}

// Example: capture discovery result
const control = await browser.asControl({
selector: { id: 'container-app---main--saveBtn' },
});

saveGoldenMaster('01-discovery-by-id', {
controlId: await control.getId(),
controlType: await control.getMetadata().getName(),
text: await control.getText(),
enabled: await control.getEnabled(),
});

Comparing Against Golden Masters (Praman)

// tests/parity/scenarios/01-discovery-by-id.ts
import { test, expect } from 'playwright-praman';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';

interface GoldenMaster {
scenario: string;
results: {
controlId: string;
controlType: string;
text: string;
enabled: boolean;
};
}

test('parity: discovery by ID matches golden master', async ({ ui5 }) => {
const golden: GoldenMaster = JSON.parse(
readFileSync(join(__dirname, '../golden-masters/01-discovery-by-id.json'), 'utf-8'),
);

const control = await ui5.control({ id: 'saveBtn' });

await test.step('control type matches', async () => {
const controlType = await control.getMetadata().getName();
expect(controlType).toBe(golden.results.controlType);
});

await test.step('text matches', async () => {
const text = await control.getText();
expect(text).toBe(golden.results.text);
});

await test.step('enabled state matches', async () => {
const enabled = await control.getEnabled();
expect(enabled).toBe(golden.results.enabled);
});

// controlId may differ in prefix -- check suffix only (intentional divergence)
await test.step('control ID resolves to same element', async () => {
const id = await control.getId();
expect(id).toContain('saveBtn');
});
});

Divergence Classification

Every difference between Praman and wdi5 output must be classified:

ClassificationAction Required
BugFix in Praman to match wdi5 behavior
Intentional improvementDocument in this page with rationale
Behavioral equivalentSame observable outcome via different mechanism

Documented Intentional Divergences

ScenarioPraman Behaviorwdi5 BehaviorRationale
Short ID resolutionAccepts saveBtn, resolves full IDRequires full generated IDBetter DX, less brittle
Event orderingliveChange then change (SAP spec)Varies by interaction modeAligns with SAP documentation
Press mechanismThree-tier: firePress -> fireTap -> DOMDOM click via WebDriverIOMore reliable for UI5 controls
Error structureControlError with code + suggestionsGeneric error or falsy returnBetter debuggability
Table assertionsDedicated matchers (toHaveUI5RowCount)Manual aggregation traversalErgonomic API for common operations
Select APIui5.select(selector, value) one-linerManual open -> find item -> pressSimplified API, same result

How to Add a New Divergence

When a parity test reveals a difference:

  1. Determine if it is a bug or intentional divergence
  2. If intentional, add a row to the divergence table above with rationale
  3. If a bug, file an issue and fix Praman to match wdi5's observable behavior
  4. Update the golden master if wdi5's behavior was also wrong (rare -- wdi5 is the reference)

Running the Full Parity Suite

# Generate golden masters (requires wdi5 + WebDriverIO setup)
npx wdio run wdio.conf.ts --spec tests/parity/generate-golden-master.ts

# Run Praman parity tests against golden masters
npx playwright test tests/parity/scenarios/

# Compare results
npx tsx tests/parity/compare.ts

The compare script exits with a non-zero code if any undocumented divergence is found. Documented divergences (listed in divergences.json) are excluded from the comparison.