Skip to content

Commit

Permalink
fix: safeguard devtools protocol stale web element ref (#524)
Browse files Browse the repository at this point in the history
  • Loading branch information
Siolto committed Aug 31, 2023
1 parent 65d1250 commit ece986f
Show file tree
Hide file tree
Showing 11 changed files with 81 additions and 7 deletions.
1 change: 1 addition & 0 deletions .github/workflows/wdi5-tests_core.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ on:
pull_request:
branches:
- main
- main.bak
paths:
# relevant
- "client-side-js/**"
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/wdi5-tests_fe-app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ on:
pull_request:
branches:
- main
- main.bak
paths:
# relevant
- "client-side-js/**"
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/wdi5-tests_js-app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
pull_request:
branches:
- main
- main.bak
paths:
# relevant
- "client-side-js/**"
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/wdi5-tests_ts-app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
pull_request:
branches:
- main
- main.bak
paths:
# relevant
- "client-side-js/**"
Expand Down
38 changes: 35 additions & 3 deletions client-side-js/executeControlMethod.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
async function clientSide_executeControlMethod(webElement, methodName, browserInstance, args) {
const { Logger } = require("../dist/lib/Logger.js")
const logger = Logger.getInstance()
/**
* Execute method on the ui5 control through the browser. Here the real magic happens :)
* @param {Object} webElement representation of a webElement in node depending on the protocol
* @param {String} methodName function name to be called on the ui5 control
* @param {WebdriverIO.Browser} browserInstance
* @param {Object} args proxied arguments to UI5 control method at runtime
*/
async function executeControlMethod(webElement, methodName, browserInstance, args) {
return await browserInstance.executeAsync(
(webElement, methodName, args, done) => {
window.wdi5.waitForUI5(
Expand All @@ -18,7 +27,6 @@ async function clientSide_executeControlMethod(webElement, methodName, browserIn
// expect the method call delivers non-primitive results (like getId())
// but delivers a complex/structured type
// -> currenlty, only getAggregation(...) is supported

// read classname eg. sap.m.ComboBox
controlType = oControl.getMetadata()._sClassName

Expand Down Expand Up @@ -60,7 +68,6 @@ async function clientSide_executeControlMethod(webElement, methodName, browserIn
const uuid = window.wdi5.saveObject(result)

// FIXME: extract, collapse and remove cylic in 1 step

// extract the methods first
const aProtoFunctions = window.wdi5.retrieveControlMethods(result, true)

Expand Down Expand Up @@ -115,6 +122,31 @@ async function clientSide_executeControlMethod(webElement, methodName, browserIn
args
)
}
/**
* Execute method on the ui5 control through the browser. If element "is stale" we first retrieve it
* from the browser again
* @param {Object} webElement representation of a webElement in node depending on the protocol
* @param {String} methodName function name to be called on the ui5 control
* @param {WebdriverIO.Browser} browserInstance
* @param {Object} args proxied arguments to UI5 control method at runtime
* @param {WDI5Control} wdi5Control wdi5 representation of the ui5 control
*/
async function clientSide_executeControlMethod(webElement, methodName, browserInstance, args, wdi5Control) {
let result
try {
result = await executeControlMethod(webElement, methodName, browserInstance, args)
} catch (err) {
if (err.message?.includes("is stale")) {
logger.debug(`webElement ${JSON.stringify(webElement)} stale, trying to renew reference...`)
let renewedWebElement = await wdi5Control.renewWebElementReference()
result = await executeControlMethod(renewedWebElement, methodName, browserInstance, args)
logger.debug(`successfully renewed reference: ${JSON.stringify(renewedWebElement)}`)
} else {
throw err
}
}
return result
}

module.exports = {
clientSide_executeControlMethod
Expand Down
28 changes: 28 additions & 0 deletions examples/ui5-ts-app/test/e2e/protocol/devtools-stale.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { wdi5Selector } from "wdio-ui5-service/dist/types/wdi5.types"
import Button from "sap/ui/webc/main/Button"

describe("Devtools: ", async () => {
it("safeguard 'stale' element handling", async () => {
const buttonWDI5 = await getButtonOnPage1()

// mock a stale element
// @ts-expect-error stub getVisible
buttonWDI5.getVisible = async function () {
return await this._executeControlMethod("getVisible", {
"element-6066-11e4-a52e-4f735466cecf": "stale"
})
}

expect(await buttonWDI5.getVisible()).toBe(true)
})

async function getButtonOnPage1() {
const wdi5Button: wdi5Selector = {
// forceSelect: true,
selector: {
id: "__component0---Main--NavFwdButton"
}
}
return await browser.asControl(wdi5Button)
}
})
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import { setDefaultResultOrder } from "node:dns"
export const config: wdi5Config = {
baseUrl: "https://wdi5-sample-app.cfapps.eu20.hana.ondemand.com/no-auth/",
services: ["ui5"] /* no drivers, so wdio is falling back to devtools + puppeteer*/,
specs: ["test/e2e/Protocol.test.ts"],
specs: ["test/e2e/protocol/*.test.ts"],
wdi5: {
logLevel: "verbose"
},
capabilities: [
{
browserName: "chromium",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import { wdi5Config } from "wdio-ui5-service/dist/types/wdi5.types"
export const config: wdi5Config = {
baseUrl: "https://wdi5-sample-app.cfapps.eu20.hana.ondemand.com/no-auth/",
services: ["chromedriver", "ui5"],
specs: ["test/e2e/Protocol.test.ts"],
specs: ["test/e2e/protocol/*.test.ts"],
exclude: ["test/e2e/protocol/devtools-stale.test.ts"],
capabilities: [
{
browserName: "chrome",
Expand Down
3 changes: 2 additions & 1 deletion examples/ui5-ts-app/wdio-ui5.conf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ export const config: wdi5Config = {
"./test/e2e/multiremote.test.ts",
"./test/e2e/BasicMultiRemoteAuthentication.test.ts",
"./test/e2e/Authentication.test.ts",
"./test/e2e/ui5-late.test.ts"
"./test/e2e/ui5-late.test.ts",
"./test/e2e/protocol/*.test.ts"
],

maxInstances: 10,
Expand Down
7 changes: 6 additions & 1 deletion src/lib/wdi5-control.ts
Original file line number Diff line number Diff line change
Expand Up @@ -514,7 +514,9 @@ export class WDI5Control {
webElement,
methodName,
this._browserInstance,
args
args,
// to safeguard "stale" elements in the devtools protocol we pass the whole wdi5 object
this
)) as clientSide_ui5Response

// create logging
Expand Down Expand Up @@ -629,6 +631,9 @@ export class WDI5Control {
throw Error("control could not be found")
}
}
async renewWebElementReference() {
return await this._renewWebElementReference()
}

/**
* retrieve a DOM element via UI5 locator
Expand Down

0 comments on commit ece986f

Please sign in to comment.