Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

optional enhancement: you can validate error messages on the console in the Node.js scope with the help of sinon.
See

const sandbox = sinon.createSandbox()

//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 () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

small typo

Suggested change
it("execute function browserside on button to get its text with fluent sync api, basic return type", async () => {
it("execute function browserside on button to get its text with fluent async 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