Skip to content

Commit

Permalink
feat: add exec to ui5 controls to boost performance for data retrieva…
Browse files Browse the repository at this point in the history
…l from many ui5 child controls (#456)

* add evalOnControl to ui5 controls to boost performance for data retrieval from many ui5 controls

* also test object return type for evalOnControl function

* Update evalOnControl.test.js

* feat: add runtime measure

* fix(core): run npm audit fix --force

* rename evalOnControl to exec

* revert changes to package json and lock files

* exec: accept arguments & make arrow function work

* update chromedriver in package lock

* exec: propagate browserside errors to be logged in console

* Add documentation for exec

---------

Co-authored-by: Philipp Thiele <[email protected]>
  • Loading branch information
philippthiele and philith authored May 9, 2023
1 parent 16d6984 commit 93116d4
Show file tree
Hide file tree
Showing 6 changed files with 748 additions and 3,144 deletions.
5 changes: 5 additions & 0 deletions client-side-js/executeControlMethod.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ async function clientSide_executeControlMethod(webElement, methodName, browserIn
result: `called focus() on wdi5 representation of a ${metadata.getElementName()}`,
returnType: "element"
})
} else if (methodName === "exec" && result && result.status > 0) {
done({
status: result.status,
message: result.message
})
} else if (result === undefined || result === null) {
done({
status: 1,
Expand Down
13 changes: 12 additions & 1 deletion client-side-js/injectUI5.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ async function clientSide_injectUI5(config, waitForUI5Timeout, browserInstance)
// until it is only available in secure contexts.
// See https://github.com/WICG/uuid/issues/23
const uuid = ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c =>
( c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))).toString(16) )
(c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))).toString(16))
window.wdi5.objectMap[uuid] = object
return uuid
}
Expand Down Expand Up @@ -96,6 +96,17 @@ async function clientSide_injectUI5(config, waitForUI5Timeout, browserInstance)
done(true)
})

// make exec function available on all ui5 controls, so more complex evaluations can be done on browser side for better performance
sap.ui.require(["sap/ui/core/Control"], (Control) => {
Control.prototype.exec = function (funcToEval, ...args) {
try {
return new Function('return ' + funcToEval).apply(this).apply(this, args)
} catch (error) {
return { status: 1, message: error.toString() }
}
}
})

// make sure the resources are required
// TODO: "sap/ui/test/matchers/Sibling",
sap.ui.require(
Expand Down
57 changes: 43 additions & 14 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,20 +215,20 @@ These are the supported selectors from [sap.ui.test.RecordReplay.ControlSelector

<!-- prettier-ignore-start -->

| selector | supported in `wdi5` |
| ------------: | ------------------- |
| `ancestor` | &check; |
| `bindingPath` | &check; |
| `controlType` | &check; |
| `descendant` | &check; |
| `I18NText` | &check; |
| `id` | &check; |
|`interactable` | &check; |
| `labelFor` | &check; |
| `properties` | &check; |
| `RegEx` | &check; |
| `sibling` | &check; |
| `viewName` | &check; |
| selector | supported in `wdi5` |
| -------------: | ------------------- |
| `ancestor` | &check; |
| `bindingPath` | &check; |
| `controlType` | &check; |
| `descendant` | &check; |
| `I18NText` | &check; |
| `id` | &check; |
| `interactable` | &check; |
| `labelFor` | &check; |
| `properties` | &check; |
| `RegEx` | &check; |
| `sibling` | &check; |
| `viewName` | &check; |

<!-- prettier-ignore-end -->

Expand Down Expand Up @@ -399,6 +399,35 @@ await button.press()

Under the hoode, this first retrieves the UI5 control, then feeds it to [WebdriverIO's `click()` method](https://webdriver.io/docs/api/element/click).

### `exec`
You can execute a given function, optionally with arguments, on any UI5 control and return an arbitrary result of a basic type, or even an object or array. This is for example helpful to boost performance when verifying many entries in a single table, since there is only one round trip to the browser to return the data.

The `this` keyword will refer to the UI5 control you execute the `exec` function on.

Regular functions are accepted as well as arrow functions.
```javascript
const button = await browser.asControl(buttonSelector)
let buttonText = await button.exec(function () { //regular function
return this.getText()
})
buttonText = await button.exec(() => this.getText()) //inline arrow function
buttonText = await button.exec(() => { return this.getText() }) //arrow function

//passing arguments is possible, example for using it to verify on browser side and returning only a boolean value
const textIsEqualToArguments = await button.exec((textHardcodedArg, textVariableArg) => {
return this.getText() === textHardcodedArg && this.getText() === textVariableArg
}, "open Dialog", expectedText)

//example what could be done with a list
const listData = await browser.asControl(listSelector).exec(function () {
return {
listTitle: this.getHeaderText(),
listEntries: this.getItems().map((item) => item.getTitle())
}
})

```

### fluent async api

`wdi5` supports `async` method chaining. This means you can directly call a `UI5` control's methods after retrieveing it via `browser.asControl(selector)`:
Expand Down
181 changes: 181 additions & 0 deletions examples/ui5-js-app/webapp/test/e2e/exec.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
const Main = require("./pageObjects/Main")
const Other = require("./pageObjects/Other")
const marky = require("marky")
const { wdi5 } = require("wdio-ui5-service")

describe("ui5 eval on control", () => {
before(async () => {
await Main.open()
})

it("should have the right title", async () => {
const title = await browser.getTitle()
expect(title).toEqual("Sample UI5 Application")
})


it("should be able to propagate a browserside error", async () => {
//Log Output during this test should be 3 times: [wdi5] call of exec failed because of: TypeError: this.getTex is not a function
//Can't be reasonably verified programatically, only that returned result should be null
const button = await browser.asControl({
selector: {
id: "openDialogButton",
viewName: "test.Sample.view.Main"
}
})

//regular function
const resultRegularFunction = await button.exec(function () {
return this.getTex()
})
expect(resultRegularFunction).toBeNull()

//arrow functions
const resultArrowFunction1 = await button.exec(() => this.getTex())
expect(resultArrowFunction1).toBeNull()
const resultArrowFunction2 = await button.exec(() => { return this.getTex() })
expect(resultArrowFunction2).toBeNull()
})

it("execute function browserside on button to get its text, basic return type", async () => {
const button = await browser.asControl({
selector: {
id: "openDialogButton",
viewName: "test.Sample.view.Main"
}
})

const regularBtnText = await button.getText()
//regular function
const buttonText = await button.exec(function () {
return this.getText()
})
expect(buttonText).toEqual("open Dialog")
expect(buttonText).toEqual(regularBtnText)

//arrow functions
const buttonTextArrow1 = await button.exec(() => this.getText())
expect(buttonTextArrow1).toEqual("open Dialog")
expect(buttonTextArrow1).toEqual(regularBtnText)
const buttonTextArrow2 = await button.exec(() => { return this.getText() })
expect(buttonTextArrow2).toEqual("open Dialog")
expect(buttonTextArrow2).toEqual(regularBtnText)
})

it("execute function browserside on button to get its text with fluent sync api, basic return type", async () => {
const buttonText = await browser.asControl({
selector: {
id: "openDialogButton",
viewName: "test.Sample.view.Main"
}
}).exec(function () {
return this.getText()
})
expect(buttonText).toEqual("open Dialog")
})

it("execute function browserside on button and compare text there, boolean return type", async () => {
const button = await browser.asControl({
selector: {
id: "openDialogButton",
viewName: "test.Sample.view.Main"
}
})

const regularBtnText = await button.getText()
//regular function
const textIsEqual = await button.exec(function (dialogTextHardcoded, dialogTextFromUI) {
return this.getText() === dialogTextHardcoded && this.getText() === dialogTextFromUI
}, "open Dialog", regularBtnText)
expect(textIsEqual).toEqual(true)

//arrow functions
const textIsEqualArrow1 = await button.exec((dialogTextHardcoded, dialogTextFromUI) => this.getText() === dialogTextHardcoded && this.getText() === dialogTextFromUI, "open Dialog", regularBtnText)
expect(textIsEqualArrow1).toEqual(true)
const textIsEqualArrow2 = await button.exec((dialogTextHardcoded, dialogTextFromUI) => {
return this.getText() === dialogTextHardcoded && this.getText() === dialogTextFromUI
}, "open Dialog", regularBtnText)
expect(textIsEqualArrow2).toEqual(true)
})

it("nav to other view and get people list names, array return type", async () => {
// click webcomponent button to trigger navigation
const navButton = await browser.asControl({
selector: {
id: "NavFwdButton",
viewName: "test.Sample.view.Main"
}
})
await navButton.press()

const listSelector = {
selector: {
id: "PeopleList",
viewName: "test.Sample.view.Other",
interaction: "root"
}
}
const list = await browser.asControl(listSelector)

/**
* need to set
* wdi5: {logLevel: "verbose"}
* in config.js
*/

// *********
// new approach -> takes ~4.3sec
marky.mark("execForListItemTitles")
const peopleListNames = await list.exec(function () {
return this.getItems().map((item) => item.getTitle())
})
wdi5.getLogger().info(marky.stop("execForListItemTitles"))
// *********

Other.allNames.forEach((name) => {
expect(peopleListNames).toContain(name)
})

// *********
// UI5 API straight forward approach -> takes ~8.1sec
marky.mark("regularGetAllItemTitles")
const regularPeopleListNames = await Promise.all(
// prettier-ignore
(await list.getItems()).map(async (e) => {
return await e.getTitle()
})
)
wdi5.getLogger().info(marky.stop("regularGetAllItemTitles"))
// *********

Other.allNames.forEach((name) => {
expect(regularPeopleListNames).toContain(name)
})

// compare results
regularPeopleListNames.forEach((name) => {
expect(peopleListNames).toContain(name)
})
})

it("get people list title and people names, object return type", async () => {
const listSelector = {
selector: {
id: "PeopleList",
viewName: "test.Sample.view.Other",
interaction: "root"
}
}
const peopleListData = await browser.asControl(listSelector).exec(function () {
return {
tableTitle: this.getHeaderText(),
peopleListNames: this.getItems().map((item) => item.getTitle())
}
})

expect(peopleListData.tableTitle).toEqual("...bites the dust!")
Other.allNames.forEach((name) => {
expect(peopleListData.peopleListNames).toContain(name)
})
})
})
Loading

0 comments on commit 93116d4

Please sign in to comment.