Skip to main content
Version: 1.x

Custom Matchers

In this guide
  • Assert UI5 control state with 10 purpose-built matchers
  • Understand why DOM-based assertions break on i18n changes and async updates
  • Use auto-retrying matchers that poll the UI5 runtime until timeout
  • Extend assertions with expect.poll() for custom UI5 checks
  • Compare Praman matchers with Playwright built-in matchers

Standard Playwright assertions like expect(text).toBe('Active') break when your SAP app runs in a different locale, because the displayed text changes with i18n translations. DOM-based checks like expect(locator).toBeEnabled() can also produce false positives when a control appears enabled in the DOM but is disabled in the UI5 model during a pending OData operation.

Praman extends Playwright's expect() with 10 UI5-specific matchers that query control state directly from the UI5 runtime. These matchers check the UI5 model/control state, not the DOM — which is the source of truth in SAP applications.

Key Difference From Playwright Matchers

Standard Playwright matchers inspect DOM attributes:

// Playwright: checks DOM attribute
await expect(page.locator('#saveBtn')).toBeEnabled();

Praman matchers query the UI5 control API:

// Praman: checks UI5 control state via sap.ui.getCore().byId()
await expect(page).toBeUI5Enabled('saveBtn');

Why does this matter? A control can appear enabled in the DOM but be disabled in the UI5 model (e.g., during a pending OData operation). The UI5 state is the source of truth.

Important: Matchers Receive controlId, Not Locator

Unlike Playwright's built-in matchers that operate on Locator objects, Praman matchers receive a control ID string as their first argument and are called on page:

// Correct — pass string controlId to page expectation
await expect(page).toHaveUI5Text('headerTitle', 'Purchase Order');

// Wrong — these are NOT locator-based matchers
// await expect(locator).toHaveUI5Text('Purchase Order'); // Does not work

All 10 Matchers

toHaveUI5Text

Asserts the text property of a control matches the expected value. Supports both exact string and RegExp matching.

await expect(page).toHaveUI5Text('headerTitle', 'Purchase Order Details');
await expect(page).toHaveUI5Text('headerTitle', /Purchase Order/);

Error message preview:

Expected: "Purchase Order Details"
Received: "Sales Order Details"
Control: headerTitle (sap.m.Title)

toBeUI5Visible

Asserts the control's visible property is true.

await expect(page).toBeUI5Visible('saveBtn');

Negation:

await expect(page).not.toBeUI5Visible('hiddenPanel');

toBeUI5Enabled

Asserts the control's enabled property is true.

await expect(page).toBeUI5Enabled('submitBtn');

Negation:

await expect(page).not.toBeUI5Enabled('disabledInput');

toHaveUI5Property

Asserts any named property of a control matches the expected value.

await expect(page).toHaveUI5Property('vendorInput', 'value', '100001');
await expect(page).toHaveUI5Property('saveBtn', 'type', 'Emphasized');
await expect(page).toHaveUI5Property('statusIcon', 'src', /accept/);

toHaveUI5ValueState

Asserts the control's valueState property matches one of the UI5 value states.

await expect(page).toHaveUI5ValueState('vendorInput', 'Error');
await expect(page).toHaveUI5ValueState('amountField', 'Success');
await expect(page).toHaveUI5ValueState('noteInput', 'None');

Valid value states: 'None', 'Success', 'Warning', 'Error', 'Information'.

toHaveUI5Binding

Asserts the control has an OData binding at the specified path.

await expect(page).toHaveUI5Binding('vendorField', '/PurchaseOrder/Vendor');
await expect(page).toHaveUI5Binding('priceField', '/PO/NetAmount');

toBeUI5ControlType

Asserts the control's fully qualified type name.

await expect(page).toBeUI5ControlType('saveBtn', 'sap.m.Button');
await expect(page).toBeUI5ControlType('mainTable', 'sap.ui.table.Table');

toHaveUI5RowCount

Asserts the number of rows in a table control.

await expect(page).toHaveUI5RowCount('poTable', 5);
await expect(page).toHaveUI5RowCount('emptyTable', 0);

toHaveUI5CellText

Asserts the text content of a specific table cell by row and column index (both zero-based).

await expect(page).toHaveUI5CellText('poTable', 0, 0, '4500000001');
await expect(page).toHaveUI5CellText('poTable', 0, 2, 'Active');
await expect(page).toHaveUI5CellText('poTable', 1, 3, /pending/i);

toHaveUI5SelectedRows

Asserts which row indices are currently selected in a table.

await expect(page).toHaveUI5SelectedRows('poTable', [0, 2]);
await expect(page).toHaveUI5SelectedRows('poTable', []); // No selection

Auto-Retry Semantics

All Praman matchers are registered as async custom matchers. They query the UI5 runtime via page.evaluate() on each poll iteration. Praman auto-retries failed assertions until the configured timeout expires.

SettingValueOverride
Poll interval100 msNot configurable (internal)
Default timeout5,000 msPass { timeout } option per assertion

Timeout Priority

Praman matchers resolve their timeout using this priority (highest wins):

  1. Explicit option — a { timeout } object passed as the last matcher argument
  2. Playwright expect timeout — configured via expect.configure({ timeout }) or use: { expect: { timeout } } in playwright.config.ts
  3. Default — 5000 ms
// 1. Explicit: always wins
await expect(page).toHaveUI5Text('statusField', 'Approved', { timeout: 15_000 });

// 2. Playwright config: honored automatically
// playwright.config.ts: use: { expect: { timeout: 10_000 } }
await expect(page).toHaveUI5Text('statusField', 'Approved'); // uses 10s

// 3. Default: 5000ms when nothing else is configured
await expect(page).toHaveUI5Text('statusField', 'Approved'); // uses 5s

This means users who configure expect.configure({ timeout: 10_000 }) or set expect: { timeout: 10_000 } in their Playwright config will see Praman matchers honor that timeout without any additional configuration.

Using expect.poll() With UI5 Methods

For custom assertions beyond the 10 built-in matchers, use expect.poll() with ui5.* methods:

// Poll until the control value matches
await expect
.poll(
async () => {
const input = await ui5.control({ id: 'vendorInput' });
return input.getProperty('value');
},
{ timeout: 10_000 },
)
.toBe('100001');

// Poll for table row count
await expect
.poll(async () => ui5.table.getRowCount('poTable'), { timeout: 15_000 })
.toBeGreaterThan(0);

Comparison With Playwright Built-In Matchers

Matcher TypeSource of TruthAuto-RetryNeeds Locator
Playwright toBeEnabled()DOM disabled attributeYesYes (Locator)
Praman toBeUI5Enabled()UI5 getEnabled() APIYesNo (string controlId)
Playwright toHaveText()DOM textContentYesYes (Locator)
Praman toHaveUI5Text()UI5 getText() APIYesNo (string controlId)
Playwright toBeVisible()DOM intersection/displayYesYes (Locator)
Praman toBeUI5Visible()UI5 getVisible() APIYesNo (string controlId)

Use Praman matchers when you need to check the UI5 model state. Use Playwright matchers when you need to verify DOM-level rendering (e.g., CSS visibility, element position).

Complete Example

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

test('validate purchase order form', async ({ ui5, page }) => {
// Check form state
await expect(page).toBeUI5Visible('vendorInput');
await expect(page).toBeUI5Enabled('vendorInput');

// Fill and validate
await ui5.fill({ id: 'vendorInput' }, 'INVALID');
await expect(page).toHaveUI5ValueState('vendorInput', 'Error');

await ui5.fill({ id: 'vendorInput' }, '100001');
await expect(page).toHaveUI5ValueState('vendorInput', 'None');
await expect(page).toHaveUI5Text('vendorName', 'Acme Corp');

// Check binding
await expect(page).toHaveUI5Binding('vendorInput', '/PurchaseOrder/Vendor');

// Verify table
await expect(page).toHaveUI5RowCount('itemTable', 3);
await expect(page).toHaveUI5CellText('itemTable', 0, 0, 'MAT-001');
await expect(page).toHaveUI5CellText('itemTable', 0, 1, '10');

// Check button states
await expect(page).toBeUI5Enabled('saveBtn');
await expect(page).not.toBeUI5Enabled('deleteBtn');
await expect(page).toBeUI5ControlType('saveBtn', 'sap.m.Button');
});
Common mistake

Do not pass a Locator to Praman matchers. Unlike Playwright's built-in matchers, Praman matchers receive a control ID string and are called on page:

// Wrong — Praman matchers do not accept Locator objects
const locator = page.locator('ui5=sap.m.Button#saveBtn');
await expect(locator).toBeUI5Enabled(); // Does not work

// Correct — pass the control ID string to page
await expect(page).toBeUI5Enabled('saveBtn');

Registration

Matchers are auto-registered via the matcherRegistration worker-scoped auto-fixture. No setup code is needed — just import { expect } from playwright-praman and the matchers are available.

import { test, expect } from 'playwright-praman';
// All 10 matchers are ready to use

FAQ

Do Praman matchers auto-retry like Playwright matchers?

Yes. All 10 matchers are registered as async custom matchers. They query the UI5 runtime via page.evaluate() on each poll iteration, retrying until the timeout expires. The timeout is resolved in this order: explicit { timeout } option > Playwright's expect.configure({ timeout })

5000 ms default. You can increase the timeout per assertion: await expect(page).toHaveUI5Text('field', 'value', { timeout: 15_000 }).

Can I use Playwright built-in matchers alongside Praman matchers?

Yes. Use Praman matchers when checking UI5 model/control state (e.g., toBeUI5Enabled, toHaveUI5Text). Use Playwright matchers when checking DOM-level rendering (e.g., toBeVisible for CSS visibility, toHaveURL for page URL, toHaveTitle for page title). Both work in the same test.

How do I assert a property that has no built-in matcher?

Use toHaveUI5Property for any named property, or expect.poll() for fully custom checks:

// Named property
await expect(page).toHaveUI5Property('myInput', 'placeholder', 'Enter value');

// Custom poll
await expect
.poll(async () => {
const ctrl = await ui5.control({ id: 'myInput' });
return ctrl.getProperty('valueStateText');
})
.toBe('Required field');
Next steps