Custom Matchers
- 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 Type | Source of Truth | Auto-Retry | Needs Locator |
|---|---|---|---|
Playwright toBeEnabled() | DOM disabled attribute | Yes | Yes (Locator) |
Praman toBeUI5Enabled() | UI5 getEnabled() API | Yes | No (string controlId) |
Playwright toHaveText() | DOM textContent | Yes | Yes (Locator) |
Praman toHaveUI5Text() | UI5 getText() API | Yes | No (string controlId) |
Playwright toBeVisible() | DOM intersection/display | Yes | Yes (Locator) |
Praman toBeUI5Visible() | UI5 getVisible() API | Yes | No (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');
});
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');
- Control Interactions → — Click, fill, and read controls before asserting state
- Gold Standard Test Pattern → — See matchers used in a complete reference test
- Selector Reference → — Find the right control to assert against