Skip to main content
Version: 1.x

Custom Control Extension

SAP projects frequently use custom UI5 controls that extend standard controls or create entirely new ones. This guide explains how to interact with custom controls through Praman when they are not part of the built-in type system.

Finding Custom Controls by Type

Use the controlType selector to target custom controls by their fully qualified UI5 class name:

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

test('interact with custom rating control', async ({ ui5 }) => {
const rating = await ui5.control({
controlType: 'com.myapp.controls.StarRating',
properties: { value: 4 },
});

await expect(rating).toBeDefined();
});

The controlType selector searches the UI5 control tree by class name. It works for any control registered with sap.ui.define, including third-party and project-specific controls.

Combining controlType with Properties

When multiple instances of a custom control exist on the page, narrow the match with properties:

await ui5.control({
controlType: 'com.myapp.controls.StatusBadge',
properties: {
status: 'Critical',
category: 'Inventory',
},
});

Properties are matched against the control's current property values using the UI5 getProperty() API. Property names must match the control's metadata definition exactly.

Accessing Inner Controls of Composites

Many custom controls are composites that wrap standard UI5 controls internally. For example, a SmartField wraps an Input, ComboBox, or DatePicker depending on the OData metadata.

The SmartField Pattern

SmartField is a common source of confusion. Its getControlType() returns sap.ui.comp.smartfield.SmartField, not the inner control type:

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

test('interact with SmartField inner control', async ({ ui5 }) => {
await test.step('Set value on SmartField', async () => {
// Target the SmartField directly -- Praman handles the inner control
await ui5.fill({ id: 'vendorSmartField' }, 'ACME Corp');
await ui5.waitForUI5();
});

await test.step('Read value from SmartField', async () => {
const value = await ui5.getValue({ id: 'vendorSmartField' });
expect(value).toBe('ACME Corp');
});
});

Accessing the Inner Control Directly

If you need to interact with the inner control specifically (e.g., to check its type or invoke a method not exposed by the wrapper):

test('access SmartField inner control type', async ({ ui5, page }) => {
const innerType = await page.evaluate(() => {
const sf = sap.ui.getCore().byId('vendorSmartField');
const inner = sf?.getInnerControls?.()[0];
return inner?.getMetadata().getName();
});

expect(innerType).toBe('sap.m.Input');
});

Extending ControlProxy for Custom Methods

When a custom control has domain-specific methods you call frequently, wrap the interactions in test helper functions:

// test-helpers/custom-controls.ts
import type { Page } from '@playwright/test';

export async function setStarRating(page: Page, controlId: string, stars: number) {
await page.evaluate(
({ id, value }) => {
const control = sap.ui.getCore().byId(id);
control?.setProperty('value', value);
control?.fireEvent('change', { value });
},
{ id: controlId, value: stars },
);
}

Use it in tests:

import { test, expect } from 'playwright-praman';
import { setStarRating } from './test-helpers/custom-controls';

test('rate a product', async ({ ui5, page }) => {
await setStarRating(page, 'productRating', 5);
await ui5.waitForUI5();
await expect(ui5.control({ id: 'ratingDisplay' })).toHaveText('5 / 5');
});

Handling Controls with Custom Renderers

Some custom controls override the standard renderer and produce non-standard DOM. When Praman's standard selectors cannot find a visible element, fall back to properties-based selectors:

test('interact with custom-rendered control', async ({ ui5 }) => {
// Instead of relying on DOM structure, use UI5 control tree
await ui5.click({
controlType: 'com.myapp.controls.CustomButton',
properties: { text: 'Submit Order' },
});
});

This approach works because Praman queries the UI5 control tree via the bridge, not the DOM. The control's rendered output does not affect selector resolution.

Common Pitfalls

  • Assuming inner control type: A SmartField on an Edm.String property wraps sap.m.Input, but on an Edm.DateTime it wraps sap.m.DatePicker. Always verify against the live system.
  • Using DOM selectors for custom controls: Custom renderers produce unpredictable DOM. Always prefer controlType + properties selectors over CSS selectors.
  • Missing fireEvent after setValue: If your custom control listens for change or liveChange events, calling setProperty() alone may not trigger downstream logic. Use fireEvent() explicitly.
  • Namespace typos: Custom control class names are case-sensitive. com.myapp.Controls.StatusBadge (capital C) is different from com.myapp.controls.StatusBadge.