diff --git a/docs/usage.md b/docs/usage.md index 5eda1361..2554e7e1 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 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)_. 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..9b43e1eb 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-bypass cache because searchOpenDialogs is present + it("should bypass cache 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 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() + 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..0e37ea3b 100644 --- a/src/lib/wdi5-bridge.ts +++ b/src/lib/wdi5-bridge.ts @@ -265,10 +265,23 @@ export async function _addWdi5Commands(browserInstance: WebdriverIO.Browser) { return "ERROR: Specified selector is not valid -> abort" } + // 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 @@ -311,8 +324,20 @@ export async function _addWdi5Commands(browserInstance: WebdriverIO.Browser) { } const internalKey = wdi5Selector.wdio_ui5_key || _createWdioUI5KeyFromSelector(wdi5Selector) + + // 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 875d190d..6264c435 100644 --- a/src/lib/wdi5-control.ts +++ b/src/lib/wdi5-control.ts @@ -93,6 +93,7 @@ export class WDI5Control { this._controlSelector = controlSelector this._wdio_ui5_key = controlSelector?.wdio_ui5_key this._forceSelect = forceSelect + this._logging = this._controlSelector?.logging ?? true const controlResult = await this._getControl()