Skip to content

Commit

Permalink
feat: getObject (#366)
Browse files Browse the repository at this point in the history
API alignment for UI5 managed objects between browser- and Node.js-scope,
yet excluding fluent api support

Co-authored-by: dominik.feininger <[email protected]>
Co-authored-by: Volker Buzek <[email protected]>
  • Loading branch information
3 people committed Nov 14, 2022
1 parent 4e6be0c commit 2bca472
Show file tree
Hide file tree
Showing 11 changed files with 344 additions and 5 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/npm-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ jobs:
# and generating changelog
# and tagging the release properly
# and auto-incrementing the release number
- run: npm run release -- --prerelease pre --release-as major
- run: npm run release -- --prerelease pre
# push to self aka main from within gh action with "release-state"
# doesn't trigger this workflow again
# b/c of missing personal access token here
Expand Down
25 changes: 23 additions & 2 deletions client-side-js/executeControlMethod.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,14 @@ async function clientSide_executeControlMethod(webElement, methodName, browserIn
!(result instanceof sap.ui.core.Control) &&
!(result instanceof sap.ui.core.Item)
) {
// save before manipulate
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)

// flatten the prototype so we have all funcs available
const collapsed = window.wdi5.collapseObject(result)
// exclude cyclic references
Expand All @@ -59,10 +67,23 @@ async function clientSide_executeControlMethod(webElement, methodName, browserIn
)
done({
status: 0,
result: collapsedAndNonCyclic,
returnType: "result",
object: collapsedAndNonCyclic,
returnType: "object",
aProtoFunctions: aProtoFunctions,
uuid: uuid,
nonCircularResultObject: collapsedAndNonCyclic
})
} else if (
typeof result === "object" &&
result !== null &&
// wdi5 returns a wdi5 control if the UI5 api return its control
// allows method chaining
!(result instanceof sap.ui.base.Object)
) {
done({
status: 2,
returnType: "unknown"
})
} else {
// we got ourselves a regular UI5 control
// check that we're not working against ourselves :)
Expand Down
49 changes: 49 additions & 0 deletions client-side-js/executeObjectMethod.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
async function clientSide_executeObjectMethod(uuid, methodName, args) {
return await browser.executeAsync(
(uuid, methodName, args, done) => {
window.wdi5.waitForUI5(
window.wdi5.waitForUI5Options,
() => {
// DOM to UI5
const oObject = window.wdi5.objectMap[uuid]

// execute the function
// TODO: if (methodName === "getName") { debugger }
let result = oObject[methodName].apply(oObject, args)

// result mus be a primitive
if (window.wdi5.isPrimitive(result)) {
// getter
done({ status: 0, result: result, returnType: "result" })
} else {
// create new object
const uuid = window.wdi5.saveObject(result)
const aProtoFunctions = window.wdi5.retrieveControlMethods(result, true)

result = window.wdi5.collapseObject(result)

const collapsedAndNonCyclic = JSON.parse(
JSON.stringify(result, window.wdi5.getCircularReplacer())
)

done({
status: 0,
object: collapsedAndNonCyclic,
uuid: uuid,
returnType: "object",
aProtoFunctions: aProtoFunctions
})
}
},
window.wdi5.errorHandling.bind(this, done)
)
},
uuid,
methodName,
args
)
}

module.exports = {
clientSide_executeObjectMethod
}
46 changes: 46 additions & 0 deletions client-side-js/getObject.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
async function clientSide_getObject(uuid) {
return await browser.executeAsync((uuid, done) => {
const waitForUI5Options = Object.assign({}, window.wdi5.waitForUI5Options)

window.wdi5.waitForUI5(
waitForUI5Options,
() => {
window.wdi5.Log.info("[browser wdi5] locating object " + uuid)

let object = window.wdi5.objectMap[uuid]
if (!object) {
const errorMessage = `[browser wdi5] ERR: no object with uuid: ${uuid} found`
window.wdi5.Log.error(errorMessage)
done({ status: 1, messsage: errorMessage })
}

let className = ""
if (object && object.getMetadata) {
className = object.getMetadata()._sClassName
}
window.wdi5.Log.info(`[browser wdi5] object with uuid: ${uuid} located!`)

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

const aProtoFunctions = window.wdi5.retrieveControlMethods(object, true)

object = window.wdi5.collapseObject(object)

const collapsedAndNonCyclic = JSON.parse(JSON.stringify(object, window.wdi5.getCircularReplacer()))

done({
status: 0,
uuid: uuid,
aProtoFunctions: aProtoFunctions,
className: className,
object: collapsedAndNonCyclic
})
},
window.wdi5.errorHandling.bind(this, done)
)
}, uuid)
}

module.exports = {
clientSide_getObject
}
14 changes: 14 additions & 0 deletions client-side-js/injectUI5.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,23 @@ async function clientSide_injectUI5(config, waitForUI5Timeout, browserInstance)
waitForUI5Options: {
timeout: waitForUI5Timeout,
interval: 400
},
objectMap: {
// GUID: {}
}
}

/**
*
* @param {sap.ui.base.Object} object
* @returns uuid
*/
window.wdi5.saveObject = (object) => {
const uuid = crypto.randomUUID()
window.wdi5.objectMap[uuid] = object
return uuid
}

// load UI5 logger
sap.ui.require(["sap/base/Log"], (Log) => {
// Logger is loaded -> can be use internally
Expand Down
8 changes: 7 additions & 1 deletion docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ At the same time, the `wdi5`-api can be mixed with `wdio`'s api during tests at

### Location

The files containing tests should reside in `$ui5-app/webapp/test/` and be named `*.test.(j|t)s`.
The files containing tests should reside in `$ui5-app/webapp/test/` and be named `*.test.(j|t)s`.
Yet both test file directory and naming pattern can be specified [via WebdriverIO's `specs`](https://webdriver.io/docs/options#specs) in [`wdio.conf.(j|t)s`](/configuration#wdi5).

### Test suites
Expand Down Expand Up @@ -123,6 +123,12 @@ const $button0 = await buttons[0].getWebElement()
const $button = await browser.asControl(singleSelector).getWebElement()
```

### asObject

(insert proper docs here :) )
(original:
Every call of an UI5 method, which is under the hood execued via `executeControlMehtod`, which retuns an object aka. as type `clientSide_ui5Response` contains an uuid as reference to the object saved in the browser at `window.wdi5.objectMap`. This object can be used later again by calling `browser.asObject(uuid)`.)

## Control selectors

The entry point to retrieve a control is always awaiting the `async` function `browser.asControl(oSelector)`.
Expand Down
88 changes: 88 additions & 0 deletions examples/ui5-js-app/webapp/test/e2e/object.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
const Main = require("./pageObjects/Main")
const marky = require("marky")
const { wdi5 } = require("wdio-ui5-service")
const Other = require("./pageObjects/Other")

const titleSelector = { selector: { id: "container-Sample---Main--Title::NoAction.h1" } }

const buttonSelector = {
wdio_ui5_key: "allButtons",
selector: {
controlType: "sap.m.Button",
viewName: "test.Sample.view.Main",
properties: {
text: new RegExp(/.*ialog.*/gm)
}
}
}

describe("ui5 object tests", () => {
before(async () => {
await Main.open()
})

it("check getBinding returns a proper object", async () => {
const title = await browser.asControl(titleSelector)
const bindingInfo = await title.getBinding("text")
// bindingInfo is an object and it's oValue property can be accessed
const response = bindingInfo.oValue
expect(response).toEqual("UI5 demo")
})

it("check getBinding returns a wdi5 object with functions", async () => {
const title = await browser.asControl(titleSelector)
const bindingInfo = await title.getBinding("text")
// bindingInfo is an object and it's oValue property can be accessed
const response = await bindingInfo.getValue()
expect(response).toEqual("UI5 demo")

const bindingInfoMetadata = await bindingInfo.getMetadata()
const bindingTypeName = await bindingInfoMetadata.getName()
expect(bindingTypeName).toEqual("sap.ui.model.resource.ResourcePropertyBinding")

// new uuid interface
const fullBindingInfo = await browser.asObject(bindingInfo.getUUID())
const bindingInfoMetadata_new = await fullBindingInfo.getMetadata()
const bindingTypeName_new = await bindingInfoMetadata_new.getName()
expect(bindingTypeName_new).toEqual("sap.ui.model.resource.ResourcePropertyBinding")
})

it("check new object implementation", async () => {
const input = await browser.asControl({
selector: {
id: "mainUserInput",
viewName: "test.Sample.view.Main"
}
})
// new object interface
const binding = await input.getBinding("value")
const path = await binding.getPath()
expect(path).toEqual("/Customers('TRAIH')/ContactName")
})

it("getModel and Property", async () => {
const mainView = await browser.asControl({
selector: {
id: "container-Sample---Main"
}
})
// new object interface
const northwaveModel = await mainView.getModel()
const customerName = await northwaveModel.getProperty("/Customers('TRAIH')/ContactName")
expect(customerName).toEqual("Helvetius Nagy")
})

it("getModel via BindingContext and Object", async () => {
// test equivalent of
// sap.ui.getCore().byId("container-Sample---Other--PeopleList").getItems()[0].getBindingContext().getObject().FirstName

await wdi5.goTo({ sHash: "#/Other" })

const table = await Other.getList(true)
const firstItem = await table.getItems(0)
const itemContext = await firstItem.getBindingContext()
const myObject = await itemContext.getObject()

expect(myObject.FirstName).toEqual("Nancy")
})
})
17 changes: 16 additions & 1 deletion src/lib/wdi5-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,20 @@ import { tmpdir } from "os"
import * as semver from "semver"
import { mark as marky_mark, stop as marky_stop } from "marky"

import { clientSide_ui5Response, wdi5Config, wdi5Selector } from "../types/wdi5.types"
import { clientSide_ui5Object, clientSide_ui5Response, wdi5Config, wdi5Selector } from "../types/wdi5.types"
import { MultiRemoteDriver } from "webdriverio/build/multiremote"
import { WDI5Control } from "./wdi5-control"
import { WDI5FE } from "./wdi5-fe"
import { clientSide_injectTools } from "../../client-side-js/injectTools"
import { clientSide_injectUI5 } from "../../client-side-js/injectUI5"
import { clientSide_getSelectorForElement } from "../../client-side-js/getSelectorForElement"
import { clientSide__checkForUI5Ready } from "../../client-side-js/_checkForUI5Ready"
import { clientSide_getObject } from "../../client-side-js/getObject"
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"
import { WDI5Object } from "./wdi5-object"
import BTPAuthenticator from "./authentication/BTPAuthenticator"
import BasicAuthenticator from "./authentication/BasicAuthenticator"
import CustomAuthenticator from "./authentication/CustomAuthenticator"
Expand Down Expand Up @@ -240,6 +242,19 @@ export async function _addWdi5Commands(browserInstance: WebdriverIO.Browser) {
return browserInstance._controls[internalKey]
})

browser.addCommand("asObject", async (_uuid: string) => {
const _result = (await clientSide_getObject(_uuid)) as clientSide_ui5Object
const { uuid, status, aProtoFunctions, className, object } = _result
if (status === 0) {
// create new WDI5-Object
const wdiOjject = new WDI5Object(uuid, aProtoFunctions, object)
return wdiOjject
}
_writeObjectResultLog(_result, "asObject()")

return { status: status, aProtoFunctions: aProtoFunctions, className: className, uuid: uuid }
})

// no fluent API -> no private method
browserInstance.addCommand("allControls", async (wdi5Selector: wdi5Selector) => {
if (!_verifySelector(wdi5Selector)) {
Expand Down
7 changes: 7 additions & 0 deletions src/lib/wdi5-control.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { clientSide_fireEvent } from "../../client-side-js/fireEvent"
import { clientSide_ui5Response, wdi5ControlMetadata, wdi5Selector } from "../types/wdi5.types"
import { Logger as _Logger } from "./Logger"
import { wdioApi } from "./wdioApi"
import { WDI5Object } from "./wdi5-object"

const Logger = _Logger.getInstance()

Expand Down Expand Up @@ -520,6 +521,9 @@ export class WDI5Control {
return this
case "result":
return result.nonCircularResultObject ? result.nonCircularResultObject : result.result
case "object":
// enhance with uuid
return new WDI5Object(result.uuid, result.aProtoFunctions, result.object)
case "empty":
if (this._logging) {
Logger.warn("No data found in property or aggregation")
Expand All @@ -546,6 +550,9 @@ export class WDI5Control {
// return wdio elements
return result.result
}
case "unknown":
Logger.warn(`${methodName} returned unknown status`)
return null
case "none":
return null
default:
Expand Down
Loading

0 comments on commit 2bca472

Please sign in to comment.