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 Mechanism

All Praman matchers are registered as async custom matchers. They query the UI5 runtime via page.evaluate() on each poll iteration. Playwright's expect() auto-retries failed assertions until the configured timeout (default 5 seconds).

This means you do not need manual retry loops:

// This auto-retries for up to 5 seconds:
await expect(page).toHaveUI5Text('statusField', 'Approved');

For longer waits, increase the timeout:

await expect(page).toHaveUI5Text('statusField', 'Approved', { timeout: 15_000 });

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, and Playwright's expect() auto-retries until the configured timeout (default 5 seconds). 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