Skip to main content
Version: 1.x

Control Discovery via Playwright CLI

The Playwright CLI provides direct, low-level access to SAP UI5 controls through the Praman bridge (window.__praman_bridge). This guide covers every discovery pattern you need — from looking up a single control by ID to polling for asynchronously rendered controls.

All examples use run-code, which executes an async page => { ... } function in the browser context. Two critical rules apply to every run-code call:

  1. console.log() is invisible — output is silently swallowed. Always use return.
  2. snapshot requires --filename — always pass --filename=snap.yml to get a compact file reference (~200 tokens) instead of inlined YAML.

Prerequisites

  • Playwright CLI session running against a live SAP system
  • Praman bridge injected via initScript (see CLI Iframe Guide for injection details)
  • SAP UI5 application fully loaded in the browser

Bridge Readiness Check

Before running any discovery command, verify the bridge is initialized and the UI5 runtime is available.

Quick status check

playwright-cli run-code "async page => {
return {
bridgeExists: typeof window.__praman_bridge !== 'undefined',
bridgeReady: !!(window.__praman_bridge && window.__praman_bridge.ready),
ui5Version: typeof sap !== 'undefined' ? sap.ui.version : 'UI5 not loaded'
};
}"

Expected output when everything is healthy:

{
"bridgeExists": true,
"bridgeReady": true,
"ui5Version": "1.120.4"
}

Wait for readiness (with timeout)

On slow SAP systems, UI5 can take minutes to fully initialize. Use waitForFunction to block until the bridge signals readiness:

playwright-cli run-code "async page => {
await page.waitForFunction(
'!!(window.__praman_bridge && window.__praman_bridge.ready)',
{ timeout: 300000 }
);
return 'Bridge ready: UI5 v' + await page.evaluate(() => sap.ui.version);
}"

The 300-second timeout accounts for cold-start scenarios on SAP S/4HANA Cloud and BTP systems.

Single Control Discovery by ID

Look up a single control using its stable DOM ID via bridge.getById(). This calls sap.ui.core.ElementRegistry.get() live — it reflects the current DOM state, not a cached snapshot.

playwright-cli run-code "async page => {
return await page.evaluate((id) => {
const b = window.__praman_bridge;
if (!b?.ready) return { error: 'Bridge not ready' };
const ctrl = b.getById(id);
if (!ctrl) return { error: 'Control not found', id };
return {
id: ctrl.getId(),
type: ctrl.getMetadata().getName(),
visible: ctrl.getVisible?.() ?? null,
text: ctrl.getText?.() ?? null,
value: ctrl.getValue?.() ?? null
};
}, 'myControlId');
}"

When to use: You know the exact control ID from the DOM, from a previous inventory scan, or from the UI5 view XML source.

Expected output:

{
"id": "myControlId",
"type": "sap.m.Button",
"visible": true,
"text": "Save",
"value": null
}

Bulk Control Enumeration

Full page inventory

Discover all UI5 controls currently rendered on the page by scanning [data-sap-ui] attributes:

playwright-cli run-code "async page => {
return await page.evaluate(() => {
const b = window.__praman_bridge;
if (!b?.ready) return { error: 'Bridge not ready' };
const controls = [];
document.querySelectorAll('[data-sap-ui]').forEach(el => {
const ctrl = b.getById(el.getAttribute('data-sap-ui'));
if (ctrl) {
controls.push({
id: ctrl.getId(),
type: ctrl.getMetadata().getName(),
visible: ctrl.getVisible?.() ?? null,
text: ctrl.getText?.() ?? null,
value: ctrl.getValue?.() ?? null
});
}
});
return { count: controls.length, controls: controls.slice(0, 50) };
});
}"

Results are capped at 50 to keep output manageable. Adjust .slice(0, 50) as needed. Hidden controls are included — check the visible field to filter.

Filter by control type

Narrow the inventory to controls matching a specific UI5 type:

playwright-cli run-code "async page => {
return await page.evaluate((typeName) => {
const b = window.__praman_bridge;
if (!b?.ready) return { error: 'Bridge not ready' };
const controls = [];
document.querySelectorAll('[data-sap-ui]').forEach(el => {
const ctrl = b.getById(el.getAttribute('data-sap-ui'));
if (ctrl && ctrl.getMetadata().getName().includes(typeName)) {
controls.push({
id: ctrl.getId(),
type: ctrl.getMetadata().getName(),
visible: ctrl.getVisible?.() ?? null,
text: ctrl.getText?.() ?? null
});
}
});
return { count: controls.length, controls: controls.slice(0, 30) };
}, 'Button');
}"

Common type filters:

Filter StringMatches
'Button'sap.m.Button, sap.m.ToggleButton, sap.m.MenuButton
'Input'sap.m.Input, sap.m.MaskInput
'Table'sap.m.Table, sap.ui.table.Table, sap.ui.table.AnalyticalTable
'Select'sap.m.Select, sap.m.MultiComboBox
'SmartField'sap.ui.comp.smartfield.SmartField
'DatePicker'sap.m.DatePicker, sap.m.DateRangePicker

For exact matches, replace .includes(typeName) with ctrl.getMetadata().getName() === 'sap.m.Button'.

Filter by ID prefix

SAP-generated IDs often share a common prefix per view. Use this to scope discovery to a specific view or section:

playwright-cli run-code "async page => {
return await page.evaluate((prefix) => {
const b = window.__praman_bridge;
if (!b?.ready) return { error: 'Bridge not ready' };
const controls = [];
document.querySelectorAll('[data-sap-ui]').forEach(el => {
const id = el.getAttribute('data-sap-ui');
if (id && id.startsWith(prefix)) {
const ctrl = b.getById(id);
if (ctrl) {
controls.push({
id: ctrl.getId(),
type: ctrl.getMetadata().getName(),
visible: ctrl.getVisible?.() ?? null,
text: ctrl.getText?.() ?? null
});
}
}
});
return { count: controls.length, controls: controls.slice(0, 30) };
}, 'app--PODetail--');
}"

Control Property Inspection

After discovering a control, inspect its runtime properties. UI5 controls expose getter methods like getValue(), getText(), getVisible(), and getEnabled().

Common property getters

playwright-cli run-code "async page => {
return await page.evaluate((id) => {
const b = window.__praman_bridge;
if (!b?.ready) return { error: 'Bridge not ready' };
const ctrl = b.getById(id);
if (!ctrl) return { error: 'Control not found', id };
return {
id: ctrl.getId(),
type: ctrl.getMetadata().getName(),
text: ctrl.getText?.() ?? null,
value: ctrl.getValue?.() ?? null,
visible: ctrl.getVisible?.() ?? null,
enabled: ctrl.getEnabled?.() ?? null,
editable: ctrl.getEditable?.() ?? null,
selected: ctrl.getSelected?.() ?? null,
selectedKey: ctrl.getSelectedKey?.() ?? null,
title: ctrl.getTitle?.() ?? null,
placeholder: ctrl.getPlaceholder?.() ?? null
};
}, 'myInputField');
}"

Filter visible controls by property value

Find all visible controls that match specific property criteria:

playwright-cli run-code "async page => {
return await page.evaluate(() => {
const b = window.__praman_bridge;
if (!b?.ready) return { error: 'Bridge not ready' };
const controls = [];
document.querySelectorAll('[data-sap-ui]').forEach(el => {
const ctrl = b.getById(el.getAttribute('data-sap-ui'));
if (!ctrl) return;
const text = ctrl.getText?.() ?? '';
const value = ctrl.getValue?.() ?? '';
const visible = ctrl.getVisible?.();
if (visible && (text.includes('Save') || value.includes('MAT-'))) {
controls.push({
id: ctrl.getId(),
type: ctrl.getMetadata().getName(),
text, value
});
}
});
return controls;
});
}"

Tips:

  • Use .includes() for partial matches (e.g., localized text fragments).
  • Combine type + property filters for precision: check getName().includes('Button') AND getText() === 'Save'.
  • Filter by getEnabled?.() to find only actionable controls.

Dialog Control Discovery

Dialogs and popovers render in the #sap-ui-static container, which sits outside the main app view tree. Standard [data-sap-ui] scans on document will find them, but if you need to specifically target dialog controls, scan #sap-ui-static directly.

Discover all dialog controls

playwright-cli run-code "async page => {
return await page.evaluate(() => {
const b = window.__praman_bridge;
const staticArea = document.getElementById('sap-ui-static');
if (!staticArea) return { error: 'No static area found' };
const controls = [];
staticArea.querySelectorAll('[data-sap-ui]').forEach(el => {
const ctrl = b.getById(el.getAttribute('data-sap-ui'));
if (ctrl?.getVisible?.()) controls.push({
id: ctrl.getId(),
type: ctrl.getMetadata().getName(),
text: ctrl.getText?.() ?? ctrl.getTitle?.(),
inDialog: true
});
});
return controls.slice(0, 30);
});
}"

Key facts about #sap-ui-static

  • This is the SAP standard container for popups, dialogs, message boxes, and popovers.
  • Controls found here include dialog buttons (OK, Cancel, Close), dialog titles, and form fields rendered inside dialogs.
  • Always check this area when a button press does not produce visible results in the main view — a dialog may have opened.
  • Use ctrl.getTitle?.() for sap.m.Dialog instances (they use getTitle(), not getText()).

Using searchOpenDialogs: true

When using Praman's fixture API (not raw CLI), the searchOpenDialogs option automatically scans #sap-ui-static:

// In Praman fixture tests — automatically scans dialog area
const okButton = await ui5.control({
controlType: 'sap.m.Button',
properties: { text: 'OK' },
searchOpenDialogs: true,
});

In raw CLI discovery, you replicate this by explicitly targeting #sap-ui-static as shown above.

V2 vs V4 Control Differences

SAP Fiori apps built on OData V2 and OData V4 use different control libraries and patterns. Understanding these differences helps you target the correct controls.

Control library differences

AspectOData V2 (Smart Controls)OData V4 (MDC / Fiori Elements V4)
Tablesap.ui.comp.smarttable.SmartTablesap.ui.mdc.Table
Filter Barsap.ui.comp.smartfilterbar.SmartFilterBarsap.ui.mdc.FilterBar
Fieldsap.ui.comp.smartfield.SmartFieldsap.ui.mdc.Field
Formsap.ui.comp.smartform.SmartFormsap.ui.mdc.Form or sap.uxap.*
Value Helpsap.ui.comp.valuehelpdialog.ValueHelpDialogsap.ui.mdc.ValueHelp
ID patternOften SmartTable- prefixedOften fe::table:: prefixed

Detecting the OData model version

playwright-cli run-code "async page => {
return await page.evaluate(() => {
if (typeof sap === 'undefined') return { error: 'UI5 not loaded' };
const core = sap.ui.getCore();
const component = core.getComponent(
Object.keys(core.mComponents || {})[0]
);
if (!component) return { error: 'No component found' };
const manifest = component.getManifest?.();
const dataSources = manifest?.['sap.app']?.dataSources ?? {};
const models = manifest?.['sap.ui5']?.models ?? {};
const result = {};
for (const [name, config] of Object.entries(models)) {
const ds = dataSources[config.dataSource];
if (ds) {
result[name] = {
type: ds.type,
uri: ds.uri,
odataVersion: ds.settings?.odataVersion ?? 'V2 (default)'
};
}
}
return result;
});
}"

Expected output:

{
"": {
"type": "OData",
"uri": "/sap/opu/odata/sap/API_PURCHASEORDER_PROCESS_SRV/",
"odataVersion": "V2 (default)"
}
}

Discovery strategy by OData version

V2 apps: Look for SmartTable, SmartFilterBar, SmartField controls. Filter by type string 'Smart':

playwright-cli run-code "async page => {
return await page.evaluate(() => {
const b = window.__praman_bridge;
if (!b?.ready) return { error: 'Bridge not ready' };
const controls = [];
document.querySelectorAll('[data-sap-ui]').forEach(el => {
const ctrl = b.getById(el.getAttribute('data-sap-ui'));
if (ctrl && ctrl.getMetadata().getName().includes('Smart')) {
controls.push({
id: ctrl.getId(),
type: ctrl.getMetadata().getName()
});
}
});
return controls.slice(0, 20);
});
}"

V4 apps: Look for sap.ui.mdc.* and sap.fe.* controls. Filter by 'mdc' or 'fe':

playwright-cli run-code "async page => {
return await page.evaluate(() => {
const b = window.__praman_bridge;
if (!b?.ready) return { error: 'Bridge not ready' };
const controls = [];
document.querySelectorAll('[data-sap-ui]').forEach(el => {
const ctrl = b.getById(el.getAttribute('data-sap-ui'));
if (!ctrl) return;
const name = ctrl.getMetadata().getName();
if (name.includes('mdc') || name.includes('.fe.')) {
controls.push({ id: ctrl.getId(), type: name });
}
});
return controls.slice(0, 20);
});
}"

OData Model Detection

Beyond the manifest-based approach above, you can detect OData models at runtime by inspecting the component's model bindings:

playwright-cli run-code "async page => {
return await page.evaluate(() => {
if (typeof sap === 'undefined') return { error: 'UI5 not loaded' };
const models = {};
const core = sap.ui.getCore();
const allModels = core.oModels || {};
for (const [name, model] of Object.entries(allModels)) {
models[name] = {
className: model.getMetadata().getName(),
serviceUrl: model.sServiceUrl ?? model.getServiceUrl?.() ?? null,
isV4: model.getMetadata().getName().includes('v4')
};
}
return models;
});
}"

This shows all registered OData models, their service URLs, and whether they are V2 or V4 instances.

Dynamic Control Polling

Controls that render asynchronously — after OData responses, lazy-loaded fragments, or navigation transitions — require polling. This is the standard pattern:

playwright-cli run-code "async page => {
const start = Date.now();
while (Date.now() - start < 10000) {
const found = await page.evaluate((id) => {
const b = window.__praman_bridge;
if (!b?.ready) return null;
const ctrl = b.getById(id);
if (!ctrl) return null;
return {
id: ctrl.getId(),
type: ctrl.getMetadata().getName(),
visible: ctrl.getVisible?.()
};
}, 'confirmButton');
if (found?.visible) return found;
await new Promise(r => setTimeout(r, 250));
}
return { error: 'Control not found within 10s timeout' };
}"

When to use polling

  • After navigation to a new view
  • After triggering an OData create/update that opens a detail page
  • After pressing a button that opens a dialog or popover
  • After applying a filter that reloads table data

Parameters to adjust

ParameterDefaultGuidance
Total timeout10000msIncrease for slow OData calls (e.g., 30000)
Polling interval250msDo not go below 100ms to avoid excessive CPU load
Target ID'confirmButton'Replace with your target control ID

Polling for a control by type + text

When you do not know the exact ID, poll by type and text instead:

playwright-cli run-code "async page => {
const start = Date.now();
while (Date.now() - start < 15000) {
const found = await page.evaluate((targetText) => {
const b = window.__praman_bridge;
if (!b?.ready) return null;
let match = null;
document.querySelectorAll('[data-sap-ui]').forEach(el => {
const ctrl = b.getById(el.getAttribute('data-sap-ui'));
if (!ctrl) return;
if (
ctrl.getMetadata().getName().includes('Button') &&
ctrl.getText?.() === targetText &&
ctrl.getVisible?.()
) {
match = {
id: ctrl.getId(),
type: ctrl.getMetadata().getName(),
text: ctrl.getText()
};
}
});
return match;
}, 'Confirm');
if (found) return found;
await new Promise(r => setTimeout(r, 250));
}
return { error: 'Button with text \"Confirm\" not found within 15s' };
}"

Complete Working Examples

Example 1: Discover all buttons on a Purchase Order page

playwright-cli run-code "async page => {
await page.waitForFunction(
'!!(window.__praman_bridge && window.__praman_bridge.ready)',
{ timeout: 60000 }
);
return await page.evaluate(() => {
const b = window.__praman_bridge;
const buttons = [];
document.querySelectorAll('[data-sap-ui]').forEach(el => {
const ctrl = b.getById(el.getAttribute('data-sap-ui'));
if (ctrl && ctrl.getMetadata().getName().includes('Button') && ctrl.getVisible?.()) {
buttons.push({
id: ctrl.getId(),
type: ctrl.getMetadata().getName(),
text: ctrl.getText?.() ?? '(no text)',
enabled: ctrl.getEnabled?.() ?? null
});
}
});
return { count: buttons.length, buttons };
});
}"

Expected output:

{
"count": 8,
"buttons": [
{ "id": "app--PODetail--editBtn", "type": "sap.m.Button", "text": "Edit", "enabled": true },
{ "id": "app--PODetail--deleteBtn", "type": "sap.m.Button", "text": "Delete", "enabled": true },
{ "id": "app--PODetail--copyBtn", "type": "sap.m.Button", "text": "Copy", "enabled": false },
{ "id": "app--PODetail--postBtn", "type": "sap.m.Button", "text": "Post", "enabled": true }
]
}

Example 2: Inspect a SmartTable and its column count

playwright-cli run-code "async page => {
return await page.evaluate(() => {
const b = window.__praman_bridge;
if (!b?.ready) return { error: 'Bridge not ready' };
const controls = [];
document.querySelectorAll('[data-sap-ui]').forEach(el => {
const ctrl = b.getById(el.getAttribute('data-sap-ui'));
if (!ctrl) return;
const name = ctrl.getMetadata().getName();
if (name.includes('Table')) {
controls.push({
id: ctrl.getId(),
type: name,
visible: ctrl.getVisible?.() ?? null,
rowCount: ctrl.getItems?.()?.length ?? ctrl.getRows?.()?.length ?? null
});
}
});
return controls;
});
}"

Example 3: Full discovery pipeline (readiness + inventory + snapshot)

Run these three commands in sequence for a complete picture of a page:

Step 1 — Verify bridge:

playwright-cli run-code "async page => {
await page.waitForFunction(
'!!(window.__praman_bridge && window.__praman_bridge.ready)',
{ timeout: 60000 }
);
return 'Bridge ready';
}"

Step 2 — Inventory all visible controls:

playwright-cli run-code "async page => {
return await page.evaluate(() => {
const b = window.__praman_bridge;
const summary = {};
document.querySelectorAll('[data-sap-ui]').forEach(el => {
const ctrl = b.getById(el.getAttribute('data-sap-ui'));
if (ctrl?.getVisible?.()) {
const type = ctrl.getMetadata().getName();
summary[type] = (summary[type] || 0) + 1;
}
});
return { totalTypes: Object.keys(summary).length, summary };
});
}"

Step 3 — Save a page snapshot for agent consumption:

playwright-cli snapshot --filename=snap.yml

The snapshot file (~200 tokens) provides a structural reference that agents can consume without reading the full DOM.

Pre-Built Discovery Scripts

Praman ships 4 parameter-free scripts in node_modules/playwright-praman/dist/scripts/ for the most common operations. These avoid composing inline run-code for standard tasks:

ScriptPurposeReturns
discover-all.jsAll controls with IDs, types, methods (max 100){ total, showing, controls: [...] }
wait-for-ui5.jsPoll for UI5 stability (30s timeout, 500ms poll){ stable: boolean, elapsed: number }
bridge-status.jsBridge readiness diagnostics{ ready, ui5Version, bridgeVersion, controlCount, ... }
dialog-controls.jsControls inside open dialogs, grouped by dialog{ dialogs: [...], totalControls: number }

Usage

# Discover all controls (includes methods via bridge.utils.retrieveControlMethods)
playwright-cli -s=sap run-code "$(cat node_modules/playwright-praman/dist/scripts/discover-all.js)"

# Wait for UI5 stability before running other discovery
playwright-cli -s=sap run-code "$(cat node_modules/playwright-praman/dist/scripts/wait-for-ui5.js)"

# Check bridge status
playwright-cli -s=sap run-code "$(cat node_modules/playwright-praman/dist/scripts/bridge-status.js)"

# Find controls in open dialogs
playwright-cli -s=sap run-code "$(cat node_modules/playwright-praman/dist/scripts/dialog-controls.js)"

These scripts use the async page => { ... } contract required by run-code. They are shell-safe (no ", backtick, or $ in the script content) and return only JSON-serializable values.

For parameterized operations (inspecting a specific control, filtering by type), use the inline run-code patterns shown above — pre-built scripts cannot accept parameters because run-code only exposes page in scope.

Troubleshooting

SymptomCauseSolution
{ error: 'Bridge not ready' }UI5 still loadingUse waitForFunction with a longer timeout
{ error: 'Control not found', id }ID does not existRun a page inventory to find the correct ID
Empty results from inventoryPage is not a UI5 appCheck typeof sap !== 'undefined' first
Dialog controls not foundNot scanning #sap-ui-staticUse the dialog discovery pattern
Controls appear but visible: falseControls are rendered but hiddenFilter by ctrl.getVisible?.() in your query
Polling times outControl renders later than expectedIncrease the timeout or check if a different event triggers the render