Skip to content

Commit

Permalink
feat: get all controls of a specific type (#212)
Browse files Browse the repository at this point in the history
* wip($$): getControls

* feat($$): add getControls/ asControls

* feat($$): add getControls/ asControls

* refactor: allControls

* feat(preformance): change to waitForUi5 with callbacks

* refactor: allControls

* refactor: pr review

Co-authored-by: dominik.feininger <[email protected]>
  • Loading branch information
2 people authored and vobu committed Apr 8, 2022
1 parent 99dd250 commit f4e8082
Show file tree
Hide file tree
Showing 8 changed files with 234 additions and 8 deletions.
45 changes: 45 additions & 0 deletions client-side-js/allControls.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
async function clientSide_allControls(controlSelector) {
controlSelector = await Promise.resolve(controlSelector) // to plug into fluent async api
return await browser.executeAsync((controlSelector, done) => {
const errorHandling = (error) => {
window.wdi5.Log.error("[browser wdi5] ERR: ", error)
done(["error", error.toString()])
}

const waitForUI5Options = Object.assign({}, window.wdi5.waitForUI5Options)
if (controlSelector.timeout) {
waitForUI5Options.timeout = controlSelector.timeout
}

window.wdi5.waitForUI5(
waitForUI5Options,
() => {
window.wdi5.Log.info("[browser wdi5] locating " + JSON.stringify(controlSelector))
controlSelector.selector = window.wdi5.createMatcher(controlSelector.selector)
window.bridge
.findAllDOMElementsByControlSelector(controlSelector)
.then((domElements) => {
// window.wdi5.Log.info('[browser wdi5] control located! - Message: ' + JSON.stringify(domElement));
// ui5 control
let returnElements = []
domElements.forEach((domElement) => {
const ui5Control = window.wdi5.getUI5CtlForWebObj(domElement)
const id = ui5Control.getId()
window.wdi5.Log.info(`[browser wdi5] control with id: ${id} located!`)
const aProtoFunctions = window.wdi5.retrieveControlMethods(ui5Control)
// @type [String, String?, String, "Array of Strings"]
returnElements.push({ domElement: domElement, id: id, aProtoFunctions: aProtoFunctions })
})

done(["success", returnElements])
})
.catch(errorHandling)
},
errorHandling
)
}, controlSelector)
}

module.exports = {
clientSide_allControls
}
3 changes: 2 additions & 1 deletion client-side-js/injectUI5.js
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,8 @@ async function clientSide_injectUI5(config, waitForUI5Timeout) {

// filter not working methods
// and those with a specific api from wdi5/wdio-ui5-service
const aFilterFunctions = ["$", "getAggregation", "constructor", "fireEvent"]
// prevent overwriting wdi5-control's own init method
const aFilterFunctions = ["$", "getAggregation", "constructor", "fireEvent", "init"]

if (aFilterFunctions.includes(item)) {
return false
Expand Down
11 changes: 11 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@ With `wdi5` being a service to WebdriverIO, it provides a superset of `wdio`'s f

At the same time, the `wdi5`-api can be mixed with `wdio`'s api during tests at will - there is no restriction to use either or. See below for many examples, denoting which api is used were.

## Control Retrieval

### asControl

- `findDOMElementByControlSelector`

### allControls

- selector caching
- `findAllDOMElementsByControlSelector`

## Control selectors

The entry point to retrieve a control is always awaiting the `async` function `browser.asControl(oSelector)`.
Expand Down
1 change: 0 additions & 1 deletion examples/ui5-js-app/wdio-webserver.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ const _config = {
},
specs: [join("webapp", "test", "e2e", "**/*.test.js")],
exclude: [join("webapp", "test", "e2e", "ui5-late.test.js")],
logLevel: "error",
bail: 0,
baseUrl: "http://localhost:8888"
}
Expand Down
45 changes: 45 additions & 0 deletions examples/ui5-js-app/webapp/test/e2e/allControls.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
const Main = require("./pageObjects/Main")

const selector = {
wdio_ui5_key: "allButtons",
selector: {
controlType: "sap.m.Button",
viewName: "test.Sample.view.Main"
}
}

describe("ui5 basic, get all buttons", () => {
before(async () => {
await Main.open()
})

it("check number of buttons", async () => {
const buttons = await browser.allControls(selector)
// 7 buttons in view and the panel expand button => 8
expect(buttons.length).toEqual(8)
})

it("no force select", async () => {
const buttons = await browser.allControls(selector)
expect(await buttons[0].getText()).toEqual("to Other view")
})

it("with force select", async () => {
const selectorWForce = selector
selectorWForce.forceSelect = true

const buttons = await browser.allControls(selectorWForce)
expect(await buttons[0].getText()).toEqual("to Other view")
})

it("reuse the cached wdi5 controls", async () => {
const buttons = await browser.allControls(selector)
expect(await buttons[0].getText()).toEqual("to Other view")
})

it("test webelement", async () => {
const buttons = await browser.allControls(selector)
const webButton = await buttons[0].getWebElement()
expect(webButton).toBeTruthy()
})
})
88 changes: 87 additions & 1 deletion src/lib/wdi5-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { clientSide_getSelectorForElement } from "../../client-side-js/getSelect
import { clientSide__checkForUI5Ready } from "../../client-side-js/_checkForUI5Ready"
import { clientSide_getUI5Version } from "../../client-side-js/getUI5Version"
import { clientSide__navTo } from "../../client-side-js/_navTo"
import { clientSide_allControls } from "../../client-side-js/allControls"

import { Logger as _Logger } from "./Logger"
const Logger = _Logger.getInstance()
Expand Down Expand Up @@ -193,14 +194,33 @@ export async function addWdi5Commands() {
if (!browser._controls?.[internalKey] || wdi5Selector.forceSelect /* always retrieve control */) {
Logger.info(`creating internal control with id ${internalKey}`)
wdi5Selector.wdio_ui5_key = internalKey
const wdi5Control = await new WDI5Control().init(wdi5Selector, wdi5Selector.forceSelect)
const wdi5Control = await new WDI5Control({}).init(wdi5Selector, wdi5Selector.forceSelect)
browser._controls[internalKey] = wdi5Control
} else {
Logger.info(`reusing internal control with id ${internalKey}`)
}
return browser._controls[internalKey]
})

// no fluent API -> no private method
browser.addCommand("allControls", async (wdi5Selector: wdi5Selector) => {
if (!_verifySelector(wdi5Selector)) {
return "ERROR: Specified selector is not valid -> abort"
}

const internalKey = wdi5Selector.wdio_ui5_key || _createWdioUI5KeyFromSelector(wdi5Selector)

if (!browser._controls?.[internalKey] || wdi5Selector.forceSelect /* always retrieve control */) {
wdi5Selector.wdio_ui5_key = internalKey
Logger.info(`creating internal controls with id ${internalKey}`)
browser._controls[internalKey] = await _allControls(wdi5Selector)
return browser._controls[internalKey]
} else {
Logger.info(`reusing internal control with id ${internalKey}`)
}
return browser._controls[internalKey]
})

/**
* Find the best control selector for a DOM element. A selector uniquely represents a single element.
* The 'best' selector is the one with which it is most likely to uniquely identify a control with the least possible inspection of the control tree.
Expand Down Expand Up @@ -299,6 +319,57 @@ export async function addWdi5Commands() {
})
}

/**
* retrieve a DOM element via UI5 locator
* @param {sap.ui.test.RecordReplay.ControlSelector} controlSelector
* @return {[WebdriverIO.Element | String, [aProtoFunctions]]} UI5 control or error message, array of function names of this control
*/
async function _allControls(controlSelector = this._controlSelector) {
// check whether we have a "by id regex" locator request
if (controlSelector.selector.id && typeof controlSelector.selector.id === "object") {
// make it a string for serializing into browser-scope and
// further processing there
controlSelector.selector.id = controlSelector.selector.id.toString()
}

if (
typeof controlSelector.selector.properties?.text === "object" &&
controlSelector.selector.properties?.text instanceof RegExp
) {
// make it a string for serializing into browser-scope and
// further processing there
controlSelector.selector.properties.text = controlSelector.selector.properties.text.toString()
}

// pre retrive control information
const response = await clientSide_allControls(controlSelector)
_writeResultLog(response, "allControls()")

if (response[0] === "success") {
const retrievedElements = response[1]
const resultWDi5Elements = []

// domElement: domElement, id: id, aProtoFunctions
for await (const cControl of retrievedElements) {
const oOptions = {
controlSelector: controlSelector,
wdio_ui5_key: controlSelector.wdio_ui5_key,
forceSelect: controlSelector.forceSelect,
generatedUI5Methods: cControl.aProtoFunctions,
webdriverRepresentation: null,
webElement: cControl.domElement,
domId: cControl.id
}

resultWDi5Elements.push(new WDI5Control(oOptions))
}

return resultWDi5Elements
} else {
return "[WDI5] Error: fetch multiple elements failed: " + response[1]
}
}

/**
* can be called to make sure before you access any eg. DOM Node the ui5 framework is done loading
* @returns {Boolean} if the UI5 page is fully loaded and ready to interact.
Expand Down Expand Up @@ -384,3 +455,18 @@ async function _navTo(sComponentId, sName, oParameters, oComponentTargetInfo, bR
return result
}
}

/**
* create log based on the status of result[0]
* @param {Array} result
* @param {*} functionName
*/
function _writeResultLog(result, functionName) {
if (result[0] === "error") {
Logger.error(`call of ${functionName} failed because of: ${result[1]}`)
} else if (result[0] === "success") {
Logger.success(`call of function ${functionName} returned: ${JSON.stringify(result[1])}`)
} else {
Logger.warn(`Unknown status: ${functionName} returned: ${JSON.stringify(result[1])}`)
}
}
48 changes: 43 additions & 5 deletions src/lib/wdi5-control.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,36 @@ export class WDI5Control {
_generatedUI5Methods: [] | string = null
_initialisation = false
_forceSelect = false
_domId: string

constructor(oOptions) {
const {
controlSelector,
wdio_ui5_key,
forceSelect,
generatedUI5Methods,
webdriverRepresentation,
webElement,
domId
} = oOptions

this._controlSelector = controlSelector
this._wdio_ui5_key = wdio_ui5_key
this._forceSelect = forceSelect
this._generatedUI5Methods = generatedUI5Methods
this._webElement = webElement
this._webdriverRepresentation = webdriverRepresentation
this._domId = domId

this.attachControlBridge(this._generatedUI5Methods as Array<string>)

// set the succesful init param
this._initialisation = true

constructor() {
return this
}

async init(controlSelector, forceSelect) {
async init(controlSelector = this._controlSelector, forceSelect = this._forceSelect) {
this._controlSelector = controlSelector
this._wdio_ui5_key = controlSelector.wdio_ui5_key
this._forceSelect = forceSelect
Expand All @@ -44,7 +68,7 @@ export class WDI5Control {

// dynamic function bridge
this._generatedUI5Methods = controlResult[1]
await this.attachControlBridge(this._generatedUI5Methods as Array<string>)
this.attachControlBridge(this._generatedUI5Methods as Array<string>)

// set the succesful init param
this._initialisation = true
Expand All @@ -64,6 +88,10 @@ export class WDI5Control {
* @return the webdriver Element
*/
async getWebElement() {
if (!this._webdriverRepresentation) {
// to enable transition from wdi5 to wdio api in allControls
await this.renewWebElement()
}
//// TODO: check this "fix"
//// why is the renew necessary here?
//// it causes hiccup with the fluent async api as the transition from node-scope
Expand All @@ -79,6 +107,16 @@ export class WDI5Control {
}
}

/**
*
* @param id
* @returns
*/
async renewWebElement(id: string = this._domId) {
this._webdriverRepresentation = await $(`//*[@id="${id}"]`)
return this._webdriverRepresentation
}

/**
* bridge to UI5 control api "getAggregation"
* @param name name of the aggregation
Expand Down Expand Up @@ -207,11 +245,11 @@ export class WDI5Control {
*
* @param sReplFunctionNames
*/
private async attachControlBridge(sReplFunctionNames: Array<string>) {
private attachControlBridge(sReplFunctionNames: Array<string>) {
// check the validity of param
if (sReplFunctionNames) {
sReplFunctionNames.forEach(async (sMethodName) => {
this[sMethodName] = await this.executeControlMethod.bind(this, sMethodName, this._webElement)
this[sMethodName] = this.executeControlMethod.bind(this, sMethodName, this._webElement)
})
} else {
Logger.warn(`${this._wdio_ui5_key} has no sReplFunctionNames`)
Expand Down
1 change: 1 addition & 0 deletions src/types/browser-commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ declare global {
namespace WebdriverIO {
interface Browser {
asControl: (arg: wdi5Selector) => Promise<any>
allControls: (arg: wdi5Selector) => Promise<any>
screenshot: (arg: string) => Promise<any>
goTo: (arg: string | object) => Promise<any>
/**
Expand Down

0 comments on commit f4e8082

Please sign in to comment.