From 06b23e1c7750edc3dc00e1438ee6fed003c24acd Mon Sep 17 00:00:00 2001 From: Daniel Nanovski Date: Wed, 8 Apr 2026 14:05:35 +0300 Subject: [PATCH 1/3] fix: auto enable forceSelect when searchOpenDialogs is used This is tacling a UI5 specific in combination with WDI5 caching when enforceWebDriverClassic is used. UI5 destroys the dialog DOM when the dialog is closed and re-creates the DOM when the dialog is opened again. During this transition WDI5 internal caching holds on to the old DOM and thus resulting in a stale reference error. The fix is protocol agnostic for future proofing and only auto enables the forceSelect if it is not explicitly set by the test. Relevant to https://github.com/ui5-community/wdi5/issues/741 --- docs/usage.md | 2 ++ examples/wdio-classic/ui5.test.js | 37 +++++++++++++++++++++++++++++++ src/lib/wdi5-bridge.ts | 15 +++++++++++++ src/lib/wdi5-control.ts | 7 ++++++ 4 files changed, 61 insertions(+) diff --git a/docs/usage.md b/docs/usage.md index 5eda1361..d5edf199 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -210,6 +210,8 @@ The `forceSelect` (default: `false`) property can be set to `true` to force `wdi The `forceSelect` option also updates the `wdio` control reference each time a method is executed on a `wdi5` control. +?> When `searchOpenDialogs` is set in the selector and `forceSelect` is not explicitly provided, `wdi5` will automatically enable `forceSelect` under the hood. This is because UI5 dialogs destroy and recreate their DOM on close/open, which would otherwise cause stale element references. + The `timeout` option (default based on the global configuration `waitForUI5Timeout` [setting](wdio-ui5-service/README.md#installation)) controls the maximum waiting time while checking for UI5 availability _(meaning no pending requests / promises / timeouts)_. The `logging` (default: `true`) property can be set to `false` to disable the log for this specific selector. This can be useful when you want to assert, that specific controls should not be visible on the UI to decrease the amount of pointless error messages. diff --git a/examples/wdio-classic/ui5.test.js b/examples/wdio-classic/ui5.test.js index 375a7bba..eadc7a91 100644 --- a/examples/wdio-classic/ui5.test.js +++ b/examples/wdio-classic/ui5.test.js @@ -44,6 +44,43 @@ describe("ui5 basic", () => { expect(isOpen).toBeFalsy() }) + // NOTE: no forceSelect set — wdi5 should auto-enable it because searchOpenDialogs is present + it("should auto-enable forceSelect for searchOpenDialogs on dialog reopen", async () => { + const filterButtonSelector = { + selector: { + id: "container-orderbrowser---master--filterButton", + viewName: "sap.ui.demo.orderbrowser.view.Master" + } + } + const dialogOkButtonSelector = { + selector: { + id: "container-orderbrowser---master--viewSettingsDialog-acceptbutton", + searchOpenDialogs: true, + interaction: { + idSuffix: "BDI-content" + } + } + } + + // first open + await browser.asControl(filterButtonSelector).press() + const dialogButton = await browser.asControl(dialogOkButtonSelector) + expect(dialogButton.isInitialized()).toBeTruthy() + expect(await dialogButton.isActive()).toBeTruthy() + + // close the dialog + await dialogButton.press() + + // reopen — without auto forceSelect, this would fail due to stale cached reference + await browser.asControl(filterButtonSelector).press() + const dialogButton2 = await browser.asControl(dialogOkButtonSelector) + expect(dialogButton2.isInitialized()).toBeTruthy() + expect(await dialogButton2.isActive()).toBeTruthy() + + // close again + await dialogButton2.press() + }) + it("wdi5 should search and return no results", async () => { const selector1 = { selector: { diff --git a/src/lib/wdi5-bridge.ts b/src/lib/wdi5-bridge.ts index 5185d59f..33d7d912 100644 --- a/src/lib/wdi5-bridge.ts +++ b/src/lib/wdi5-bridge.ts @@ -265,6 +265,13 @@ export async function _addWdi5Commands(browserInstance: WebdriverIO.Browser) { return "ERROR: Specified selector is not valid -> abort" } + // Auto-enable forceSelect when searchOpenDialogs is set. We only do that when this is not explicitly set by the + // user. Dialogs destroy/recreate DOM on close/open, causing stale element references. + if (wdi5Selector.selector?.searchOpenDialogs && wdi5Selector.forceSelect === undefined) { + wdi5Selector.forceSelect = true + Logger.info(`auto-enabled forceSelect for selector (searchOpenDialogs is set)`) + } + const internalKey = wdi5Selector.wdio_ui5_key || _createWdioUI5KeyFromSelector(wdi5Selector) // either retrieve and cache a UI5 control // or return a cached version @@ -311,6 +318,14 @@ export async function _addWdi5Commands(browserInstance: WebdriverIO.Browser) { } const internalKey = wdi5Selector.wdio_ui5_key || _createWdioUI5KeyFromSelector(wdi5Selector) + + // Auto-enable forceSelect when searchOpenDialogs is set. We only do that when this is not explicitly set by the + // user. Dialogs destroy/recreate DOM on close/open, causing stale element references. + if (wdi5Selector.selector?.searchOpenDialogs && wdi5Selector.forceSelect === undefined) { + wdi5Selector.forceSelect = true + Logger.info(`auto-enabled forceSelect for allControls selector (searchOpenDialogs is set)`) + } + // REVISIT all elements receive the same! internal key if (!browserInstance._controls?.[internalKey] || wdi5Selector.forceSelect /* always retrieve control */) { wdi5Selector.wdio_ui5_key = internalKey diff --git a/src/lib/wdi5-control.ts b/src/lib/wdi5-control.ts index 875d190d..a20c6528 100644 --- a/src/lib/wdi5-control.ts +++ b/src/lib/wdi5-control.ts @@ -93,6 +93,13 @@ export class WDI5Control { this._controlSelector = controlSelector this._wdio_ui5_key = controlSelector?.wdio_ui5_key this._forceSelect = forceSelect + + // Auto-enable forceSelect when searchOpenDialogs is set. We only do that when this is not explicitly set by the + // user. Dialogs destroy/recreate DOM on close/open, causing stale element references. + if (forceSelect === undefined && this._controlSelector?.selector?.searchOpenDialogs) { + this._forceSelect = true + } + this._logging = this._controlSelector?.logging ?? true const controlResult = await this._getControl() From 93df9524f8bf8bfc7d138c81707f0e9c709d87b2 Mon Sep 17 00:00:00 2001 From: Daniel Nanovski Date: Mon, 4 May 2026 15:05:31 +0300 Subject: [PATCH 2/3] fix: use cache bypass instead of forceSelect for searchOpenDialogs Separate cache-bypass from per-method re-retrieval for searchOpenDialogs selectors. Previously, auto-enabling forceSelect caused every method call to re-retrieve the element, timing out when the dialog was closed. Now only the control cache is bypassed, while the control instance retains normal behavior. --- docs/usage.md | 2 +- src/lib/wdi5-bridge.ts | 34 ++++++++++++++++++++++------------ src/lib/wdi5-control.ts | 6 ------ 3 files changed, 23 insertions(+), 19 deletions(-) diff --git a/docs/usage.md b/docs/usage.md index d5edf199..2554e7e1 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -210,7 +210,7 @@ The `forceSelect` (default: `false`) property can be set to `true` to force `wdi The `forceSelect` option also updates the `wdio` control reference each time a method is executed on a `wdi5` control. -?> When `searchOpenDialogs` is set in the selector and `forceSelect` is not explicitly provided, `wdi5` will automatically enable `forceSelect` under the hood. This is because UI5 dialogs destroy and recreate their DOM on close/open, which would otherwise cause stale element references. +?> When `searchOpenDialogs` is set in the selector and `forceSelect` is not explicitly provided, `wdi5` will automatically bypass the internal control cache for that selector. This ensures a fresh control lookup each time `browser.asControl()` is called, which is necessary because UI5 dialogs destroy and recreate their DOM on close/open. Note that this does not enable the per-method re-retrieval behavior of explicit `forceSelect: true`. The `timeout` option (default based on the global configuration `waitForUI5Timeout` [setting](wdio-ui5-service/README.md#installation)) controls the maximum waiting time while checking for UI5 availability _(meaning no pending requests / promises / timeouts)_. diff --git a/src/lib/wdi5-bridge.ts b/src/lib/wdi5-bridge.ts index 33d7d912..0e37ea3b 100644 --- a/src/lib/wdi5-bridge.ts +++ b/src/lib/wdi5-bridge.ts @@ -265,17 +265,23 @@ export async function _addWdi5Commands(browserInstance: WebdriverIO.Browser) { return "ERROR: Specified selector is not valid -> abort" } - // Auto-enable forceSelect when searchOpenDialogs is set. We only do that when this is not explicitly set by the - // user. Dialogs destroy/recreate DOM on close/open, causing stale element references. - if (wdi5Selector.selector?.searchOpenDialogs && wdi5Selector.forceSelect === undefined) { - wdi5Selector.forceSelect = true - Logger.info(`auto-enabled forceSelect for selector (searchOpenDialogs is set)`) + // When searchOpenDialogs is set and forceSelect is not explicitly provided by the user, + // bypass the control cache (so each asControl() call fetches fresh from browser), + // but do NOT propagate forceSelect to the control instance (which would cause per-method + // re-retrieval and timeout when the dialog is closed and the element no longer exists). + const skipCache = !!(wdi5Selector.selector?.searchOpenDialogs && wdi5Selector.forceSelect === undefined) + if (skipCache) { + Logger.info(`bypassing cache for selector (searchOpenDialogs is set)`) } const internalKey = wdi5Selector.wdio_ui5_key || _createWdioUI5KeyFromSelector(wdi5Selector) // either retrieve and cache a UI5 control // or return a cached version - if (!browserInstance._controls?.[internalKey] || wdi5Selector.forceSelect /* always retrieve control */) { + if ( + !browserInstance._controls?.[internalKey] || + wdi5Selector.forceSelect || + skipCache /* always retrieve control */ + ) { Logger.info(`creating internal control with id ${internalKey}`) wdi5Selector.wdio_ui5_key = internalKey @@ -319,15 +325,19 @@ export async function _addWdi5Commands(browserInstance: WebdriverIO.Browser) { const internalKey = wdi5Selector.wdio_ui5_key || _createWdioUI5KeyFromSelector(wdi5Selector) - // Auto-enable forceSelect when searchOpenDialogs is set. We only do that when this is not explicitly set by the - // user. Dialogs destroy/recreate DOM on close/open, causing stale element references. - if (wdi5Selector.selector?.searchOpenDialogs && wdi5Selector.forceSelect === undefined) { - wdi5Selector.forceSelect = true - Logger.info(`auto-enabled forceSelect for allControls selector (searchOpenDialogs is set)`) + // When searchOpenDialogs is set and forceSelect is not explicitly provided by the user, + // bypass the control cache but do NOT propagate forceSelect to control instances. + const skipCache = !!(wdi5Selector.selector?.searchOpenDialogs && wdi5Selector.forceSelect === undefined) + if (skipCache) { + Logger.info(`bypassing cache for allControls selector (searchOpenDialogs is set)`) } // REVISIT all elements receive the same! internal key - if (!browserInstance._controls?.[internalKey] || wdi5Selector.forceSelect /* always retrieve control */) { + if ( + !browserInstance._controls?.[internalKey] || + wdi5Selector.forceSelect || + skipCache /* always retrieve control */ + ) { wdi5Selector.wdio_ui5_key = internalKey Logger.info(`creating internal controls with id ${internalKey}`) browserInstance._controls[internalKey] = await _allControls(wdi5Selector, browserInstance) diff --git a/src/lib/wdi5-control.ts b/src/lib/wdi5-control.ts index a20c6528..6264c435 100644 --- a/src/lib/wdi5-control.ts +++ b/src/lib/wdi5-control.ts @@ -94,12 +94,6 @@ export class WDI5Control { this._wdio_ui5_key = controlSelector?.wdio_ui5_key this._forceSelect = forceSelect - // Auto-enable forceSelect when searchOpenDialogs is set. We only do that when this is not explicitly set by the - // user. Dialogs destroy/recreate DOM on close/open, causing stale element references. - if (forceSelect === undefined && this._controlSelector?.selector?.searchOpenDialogs) { - this._forceSelect = true - } - this._logging = this._controlSelector?.logging ?? true const controlResult = await this._getControl() From bdece83673890e5945eca744a9b6e25f2e45446f Mon Sep 17 00:00:00 2001 From: Daniel Nanovski Date: Mon, 4 May 2026 15:23:11 +0300 Subject: [PATCH 3/3] fix: update test wording to reflect cache bypass approach --- examples/wdio-classic/ui5.test.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/wdio-classic/ui5.test.js b/examples/wdio-classic/ui5.test.js index eadc7a91..9b43e1eb 100644 --- a/examples/wdio-classic/ui5.test.js +++ b/examples/wdio-classic/ui5.test.js @@ -44,8 +44,8 @@ describe("ui5 basic", () => { expect(isOpen).toBeFalsy() }) - // NOTE: no forceSelect set — wdi5 should auto-enable it because searchOpenDialogs is present - it("should auto-enable forceSelect for searchOpenDialogs on dialog reopen", async () => { + // NOTE: no forceSelect set — wdi5 should auto-bypass cache because searchOpenDialogs is present + it("should bypass cache for searchOpenDialogs on dialog reopen", async () => { const filterButtonSelector = { selector: { id: "container-orderbrowser---master--filterButton", @@ -71,7 +71,7 @@ describe("ui5 basic", () => { // close the dialog await dialogButton.press() - // reopen — without auto forceSelect, this would fail due to stale cached reference + // reopen — without auto cache bypass, this would fail due to stale cached reference await browser.asControl(filterButtonSelector).press() const dialogButton2 = await browser.asControl(dialogOkButtonSelector) expect(dialogButton2.isInitialized()).toBeTruthy()