diff --git a/client-side-js/allControls.cjs b/client-side-js/allControls.cjs index 4c4d39eb..8ca5e7fa 100644 --- a/client-side-js/allControls.cjs +++ b/client-side-js/allControls.cjs @@ -1,6 +1,7 @@ async function clientSide_allControls(controlSelector, browserInstance) { controlSelector = await Promise.resolve(controlSelector) // to plug into fluent async api return await browserInstance.executeAsync((controlSelector, done) => { + //client-side-js/allControls.cjs const waitForUI5Options = Object.assign({}, window.wdi5.waitForUI5Options) if (controlSelector.timeout) { waitForUI5Options.timeout = controlSelector.timeout @@ -21,7 +22,8 @@ async function clientSide_allControls(controlSelector, browserInstance) { 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) + const { functionNames: aProtoFunctions } = + window.wdi5.retrieveControlMethodsAndFlatenObject(ui5Control) // @type [String, String?, String, "Array of Strings"] returnElements.push({ domElement: domElement, id: id, aProtoFunctions: aProtoFunctions }) }) diff --git a/client-side-js/executeControlMethod.cjs b/client-side-js/executeControlMethod.cjs index aa122c48..80edbb09 100644 --- a/client-side-js/executeControlMethod.cjs +++ b/client-side-js/executeControlMethod.cjs @@ -13,6 +13,7 @@ if (global.browser) { async function executeControlMethod(webElement, methodName, browserInstance, args) { return await browserInstance.executeAsync( (webElement, methodName, args, done) => { + //client-side-js/executeControlMethod.cjs window.wdi5.waitForUI5( window.wdi5.waitForUI5Options, () => { @@ -67,21 +68,8 @@ async function executeControlMethod(webElement, methodName, browserInstance, arg !(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 - const collapsedAndNonCyclic = JSON.parse( - JSON.stringify(collapsed, window.wdi5.getCircularReplacer()) - ) - // remove all empty Array elements, inlcuding private keys (starting with "_") - const semanticCleanedElements = window.wdi5.removeEmptyElements(collapsedAndNonCyclic) + const { semanticCleanedElements, uuid, aProtoFunctions, objectNames } = + window.wdi5.prepareObjectForSerialization(result) done({ status: 0, @@ -89,7 +77,8 @@ async function executeControlMethod(webElement, methodName, browserInstance, arg returnType: "object", aProtoFunctions: aProtoFunctions, uuid: uuid, - nonCircularResultObject: semanticCleanedElements + nonCircularResultObject: semanticCleanedElements, + objectNames }) } else if ( typeof result === "object" && diff --git a/client-side-js/executeObjectMethod.cjs b/client-side-js/executeObjectMethod.cjs index 151eb93e..946eaf8d 100644 --- a/client-side-js/executeObjectMethod.cjs +++ b/client-side-js/executeObjectMethod.cjs @@ -1,58 +1,38 @@ async function clientSide_executeObjectMethod(uuid, methodName, args) { return await browser.executeAsync( (uuid, methodName, args, done) => { + //client-side-js/executeObjectMethod.cjs window.wdi5.waitForUI5( window.wdi5.waitForUI5Options, // this callback is denoted "async" even though it is truely not // but what other way to `await` a potentially async UI5 managed object fn in here? async () => { + window.wdi5.Log.info(`[browser wdi5] call function ${methodName} from object ${uuid}`) // DOM to UI5 const oObject = window.wdi5.objectMap[uuid] // execute the function - // TODO: if (methodName === "getName") { debugger } let result - let threw = false - let threwMessage = "" if (oObject[methodName].constructor.name === "AsyncFunction") { - try { - result = await oObject[methodName].apply(oObject, args) - } catch (error) { - threw = true - threwMessage = JSON.stringify(error) - window.wdi5.Log.error(threwMessage) - } + result = await oObject[methodName].apply(oObject, args) } else { result = oObject[methodName].apply(oObject, args) } - - // async message call rejected - if (threw) { - done({ status: 1, message: threwMessage }) - } - // result mus be a primitive - else if (window.wdi5.isPrimitive(result)) { + // result musz 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()) - ) - // remove all empty Array elements, inlcuding private keys (starting with "_") - const semanticCleanedElements = window.wdi5.removeEmptyElements(collapsedAndNonCyclic) + const { semanticCleanedElements, uuid, aProtoFunctions, objectNames } = + window.wdi5.prepareObjectForSerialization(result) done({ status: 0, object: semanticCleanedElements, uuid: uuid, returnType: "object", - aProtoFunctions: aProtoFunctions + aProtoFunctions: aProtoFunctions, + objectNames }) } }, diff --git a/client-side-js/getControl.cjs b/client-side-js/getControl.cjs index 4e0760f2..a72a0a53 100644 --- a/client-side-js/getControl.cjs +++ b/client-side-js/getControl.cjs @@ -17,7 +17,8 @@ async function clientSide_getControl(controlSelector, browserInstance) { const id = ui5Control.getId() const className = ui5Control.getMetadata()._sClassName window.wdi5.Log.info(`[browser wdi5] control with id: ${id} located!`) - const aProtoFunctions = window.wdi5.retrieveControlMethods(ui5Control) + const { functionNames: aProtoFunctions } = + window.wdi5.retrieveControlMethodsAndFlatenObject(ui5Control) done({ status: 0, domElement: domElement, diff --git a/client-side-js/getObject.cjs b/client-side-js/getObject.cjs index 254e9751..a040b502 100644 --- a/client-side-js/getObject.cjs +++ b/client-side-js/getObject.cjs @@ -19,24 +19,16 @@ async function clientSide_getObject(uuid) { 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())) - - // remove all empty Array elements, inlcuding private keys (starting with "_") - const semanticCleanedElements = window.wdi5.removeEmptyElements(collapsedAndNonCyclic) + const { semanticCleanedElements, aProtoFunctions, objectNames } = + window.wdi5.prepareObjectForSerialization(object, true) done({ status: 0, uuid: uuid, aProtoFunctions: aProtoFunctions, className: className, - object: semanticCleanedElements + object: semanticCleanedElements, + objectNames }) }, window.wdi5.errorHandling.bind(this, done) diff --git a/client-side-js/getProperty.cjs b/client-side-js/getProperty.cjs new file mode 100644 index 00000000..c2676d0f --- /dev/null +++ b/client-side-js/getProperty.cjs @@ -0,0 +1,63 @@ +async function clientSide_getProperty(uuid, propertyName) { + return await browser.executeAsync( + (uuid, propertyName, done) => { + const waitForUI5Options = Object.assign({}, window.wdi5.waitForUI5Options) + + window.wdi5.waitForUI5( + waitForUI5Options, + () => { + window.wdi5.Log.info(`[browser wdi5] locating property ${propertyName} from 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, message: errorMessage }) + } + object = object[propertyName] + + if (!object) { + const errorMessage = `[browser wdi5] ERR: property: ${propertyName} not found on object ${uuid}` + window.wdi5.Log.error(errorMessage) + done({ status: 1, message: errorMessage }) + } + + let className = "" + if (object && object.getMetadata) { + className = object.getMetadata()._sClassName + } + window.wdi5.Log.info( + `[browser wdi5] property: ${propertyName} in object with uuid: ${uuid} located!` + ) + if (typeof object !== "object") { + done({ status: 0, result: object }) + return + } + const { + semanticCleanedElements, + aProtoFunctions, + objectNames, + uuid: newUUID + } = window.wdi5.prepareObjectForSerialization(object) + + done({ + status: 0, + uuid: newUUID, + aProtoFunctions: aProtoFunctions, + className: className, + object: semanticCleanedElements, + objectNames, + returnType: "object" + }) + }, + window.wdi5.errorHandling.bind(this, done) + ) + }, + uuid, + propertyName + ) +} + +module.exports = { + clientSide_getProperty +} diff --git a/client-side-js/injectUI5.cjs b/client-side-js/injectUI5.cjs index f20a287c..1a019418 100644 --- a/client-side-js/injectUI5.cjs +++ b/client-side-js/injectUI5.cjs @@ -51,7 +51,7 @@ async function clientSide_injectUI5(config, waitForUI5Timeout, browserInstance) sap.ui.require(["sap/base/Log"], (Log) => { // Logger is loaded -> can be use internally // attach logger to wdi5 to be able to use it globally - window.wdi5.Log = Log + window.wdi5.Log = console window.wdi5.Log.info("[browser wdi5] injected!") }) @@ -72,7 +72,15 @@ async function clientSide_injectUI5(config, waitForUI5Timeout, browserInstance) if (sError) { errorCallback(new Error(sError)) } else { - callback() + if (callback.constructor.name === "AsyncFunction") { + callback().catch(errorCallback) + } else { + try { + callback() + } catch (e) { + errorCallback(e) + } + } } }) } @@ -237,118 +245,75 @@ async function clientSide_injectUI5(config, waitForUI5Timeout, browserInstance) return jQuery(ui5Control).control(0) } - /** - * gets a UI5 controls' methods to proxy from browser- to Node.js-runtime - * - * @param {sap..} control UI5 control - * @returns {String[]} UI5 control's method names - */ - window.wdi5.retrieveControlMethods = (control) => { - // create keys of all parent prototypes - let properties = new Set() - let currentObj = control - do { - Object.getOwnPropertyNames(currentObj).map((item) => properties.add(item)) - } while ((currentObj = Object.getPrototypeOf(currentObj))) - - // filter for: - // @ts-expect-error - TS doesn't know that the keys are strings - let controlMethodsToProxy = [...properties.keys()].filter((item) => { - if (typeof control[item] === "function") { - // function - - // filter private methods - if (item.startsWith("_")) { - return false + window.wdi5.retrieveControlMethodsAndFlatenObject = (contol) => { + let protoChain = [] + let proto = contol + while (proto !== null) { + protoChain.unshift(proto) + proto = Object.getPrototypeOf(proto) + } + let collapsedObj = {} + const functionNames = new Set() + const objectNames = new Set() + for (let i = 0; i < protoChain.length; i++) { + const prop = protoChain[i] + const propertyNames = Object.getOwnPropertyNames(prop) + + for (let j = 0; j < propertyNames.length; j++) { + const propertyName = propertyNames[j] + const value = prop[propertyName] + + if (propertyName.startsWith("_")) { + continue } - - if (item.indexOf("Render") !== -1) { - return false + if (typeof value === "object") { + objectNames.add(propertyName) + continue } + collapsedObj[propertyName] = value + // filter not working methods // and those with a specific api from wdi5/wdio-ui5-service // prevent overwriting wdi5-control's own init method const aFilterFunctions = ["$", "getAggregation", "constructor", "fireEvent", "init"] - - if (aFilterFunctions.includes(item)) { - return false + if ( + typeof value === "function" && + !propertyName.indexOf("Render") !== -1 && + !aFilterFunctions.includes(propertyName) + ) { + functionNames.add(propertyName) } - - // if not already discarded -> should be in the result - return true } - return false - }) - - return controlMethodsToProxy - } - - /** - * flatten all functions and properties on the Prototype directly into the returned object - * @param {object} obj - * @returns {object} all functions and properties of the inheritance chain in a flat structure - */ - window.wdi5.collapseObject = (obj) => { - let protoChain = [] - let proto = obj - while (proto !== null) { - protoChain.unshift(proto) - proto = Object.getPrototypeOf(proto) } - let collapsedObj = {} - protoChain.forEach((prop) => Object.assign(collapsedObj, prop)) - return collapsedObj + return { + collapsedObj, + functionNames: Array.from(functionNames), + objectNames: Array.from(objectNames) + } } - /** - * used as a replacer function in JSON.stringify - * removes circular references in an object - * all credit to https://bobbyhadz.com/blog/javascript-typeerror-converting-circular-structure-to-json - */ - window.wdi5.getCircularReplacer = () => { - const seen = new WeakSet() - return (key, value) => { - if (typeof value === "object" && value !== null) { - if (seen.has(value)) { - return - } - seen.add(value) - } - return value + window.wdi5.prepareObjectForSerialization = (object, skipSave) => { + let uuid + if (!skipSave) { + // save before manipulate + uuid = window.wdi5.saveObject(object) } - } - /** - * removes all empty collection members from an object, - * e.g. empty, null, or undefined array elements - * - * @param {object} obj - * @returns {object} obj without empty collection members - */ - window.wdi5.removeEmptyElements = (obj, i = 0) => { - for (let key in obj) { - if (obj[key] === null || key.startsWith("_")) { - delete obj[key] - } else if (Array.isArray(obj[key])) { - obj[key] = obj[key].filter( - (element) => - element !== null && - element !== undefined && - element !== "" && - Object.keys(element).length > 0 - ) - if (obj[key].length > 0) { - i++ - window.wdi5.removeEmptyElements(obj[key], i) - } - } else if (typeof obj[key] === "object") { - i++ - window.wdi5.removeEmptyElements(obj[key], i) - } + let { + collapsedObj, + functionNames: aProtoFunctions, + objectNames + } = window.wdi5.retrieveControlMethodsAndFlatenObject(object) + + return { + semanticCleanedElements: collapsedObj, + uuid, + aProtoFunctions, + objectNames } - return obj } + /** * if parameter is JS primitive type * returns {boolean} diff --git a/examples/fe-app/wdio.conf.js b/examples/fe-app/wdio.conf.js index 9dd43ac2..95abfd72 100644 --- a/examples/fe-app/wdio.conf.js +++ b/examples/fe-app/wdio.conf.js @@ -23,8 +23,8 @@ exports.config = { process.argv.indexOf("--headless") > -1 ? ["window-size=1440,800", "--headless"] : process.argv.indexOf("--debug") > -1 - ? ["window-size=1920,1280", "--auto-open-devtools-for-tabs"] - : ["window-size=1440,800"] + ? ["window-size=1920,1280", "--auto-open-devtools-for-tabs"] + : ["window-size=1440,800"] } } ], diff --git a/examples/fe-app/webapp/wdi5-test/Journey.test.js b/examples/fe-app/webapp/wdi5-test/Journey.test.js index 4a471548..eea86af5 100644 --- a/examples/fe-app/webapp/wdi5-test/Journey.test.js +++ b/examples/fe-app/webapp/wdi5-test/Journey.test.js @@ -22,6 +22,60 @@ describe("FE basics", () => { }) }) + it("should work with method execution and property selection", async () => { + await FioriElementsFacade.execute((Given, When) => { + Given.onTheMainPage.onFilterBar().iExecuteSearch() + When.onTheMainPage.onTable().iPressRow({ identifier: "inc_0002" }) + }) + const textcontroll = await browser.asControl({ + selector: { + controlType: "sap.m.Text", + viewName: "sap.fe.templates.ObjectPage.ObjectPage", + viewId: "sap.fe.demo.incidents::IncidentsObjectPage", + bindingPath: { + path: "/Incidents(ID=919f1a94-0281-4226-ad3a-9148af4cb5d2,IsActiveEntity=true)", + propertyPath: "identifier" + } + } + }) + const text1 = await browser + .asControl({ + selector: { + controlType: "sap.m.Text", + viewName: "sap.fe.templates.ObjectPage.ObjectPage", + viewId: "sap.fe.demo.incidents::IncidentsObjectPage", + bindingPath: { + path: "/Incidents(ID=919f1a94-0281-4226-ad3a-9148af4cb5d2,IsActiveEntity=true)", + propertyPath: "identifier" + } + } + }) + .getBindingContext() + .getObject().incidentStatus.name + expect(text1).toBe("New") + const bindingContext = await textcontroll.getBindingContext() + + const text2 = (await (await bindingContext.getObject()).incidentStatus).name + expect(text2).toBe("New") + + const bindingContext2 = await browser.asObject(bindingContext.getUUID()) + const text3 = (await (await bindingContext2.getObject()).incidentStatus).name + expect(text3).toBe("New") + + const text4 = await bindingContext2.getProperty("incidentStatus/name") + expect(text4).toBe("New") + + const model = await bindingContext.getModel() + const bindings = await model.aAllBindings + const zero = await bindings[0] + const updateGroup1 = await zero.sUpdateGroupId + + expect(updateGroup1).toBe("$auto") + + const updateGroup2 = await browser.asObject(bindingContext.getUUID()).getModel().aAllBindings[0].sUpdateGroupId + expect(updateGroup2).toBe("$auto") + }) + it("should trigger search on ListReport page", async () => { await FioriElementsFacade.execute((Given, When, Then) => { Given.onTheMainPage.onFilterBar().iExecuteSearch() diff --git a/src/lib/wdi5-bridge.ts b/src/lib/wdi5-bridge.ts index 3eda9371..69ad520e 100644 --- a/src/lib/wdi5-bridge.ts +++ b/src/lib/wdi5-bridge.ts @@ -1,29 +1,34 @@ -import { resolve } from "path" import { writeFile } from "fs/promises" +import { mark as marky_mark, stop as marky_stop } from "marky" import { tmpdir } from "os" +import { resolve } from "path" import * as semver from "semver" -import { mark as marky_mark, stop as marky_stop } from "marky" -import { clientSide_ui5Object, clientSide_ui5Response, wdi5Config, wdi5Selector } from "../types/wdi5.types.js" import { MultiRemoteBrowser } from "webdriverio" -import { WDI5Control } from "./wdi5-control.js" -import { WDI5FE } from "./wdi5-fe.js" -import { clientSide_injectTools } from "../../client-side-js/injectTools.cjs" -import { clientSide_injectUI5 } from "../../client-side-js/injectUI5.cjs" -import { clientSide_injectXHRPatch } from "../../client-side-js/injectXHRPatch.cjs" -import { clientSide_getSelectorForElement } from "../../client-side-js/getSelectorForElement.cjs" import { clientSide__checkForUI5Ready } from "../../client-side-js/_checkForUI5Ready.cjs" -import { clientSide_getObject } from "../../client-side-js/getObject.cjs" -import { clientSide_getUI5Version } from "../../client-side-js/getUI5Version.cjs" import { clientSide__navTo } from "../../client-side-js/_navTo.cjs" import { clientSide_allControls } from "../../client-side-js/allControls.cjs" +import { clientSide_getObject } from "../../client-side-js/getObject.cjs" +import { clientSide_getSelectorForElement } from "../../client-side-js/getSelectorForElement.cjs" +import { clientSide_getUI5Version } from "../../client-side-js/getUI5Version.cjs" +import { clientSide_injectTools } from "../../client-side-js/injectTools.cjs" +import { clientSide_injectUI5 } from "../../client-side-js/injectUI5.cjs" +import { clientSide_injectXHRPatch } from "../../client-side-js/injectXHRPatch.cjs" +import { + BTPAuthenticator as BTPAuthenticatorType, + clientSide_ui5Object, + clientSide_ui5Response, + wdi5Config, + wdi5Selector +} from "../types/wdi5.types.js" import { Logger as _Logger } from "./Logger.js" -import { WDI5Object } from "./wdi5-object.js" import BTPAuthenticator from "./authentication/BTPAuthenticator.js" -import { BTPAuthenticator as BTPAuthenticatorType } from "../types/wdi5.types.js" import BasicAuthenticator from "./authentication/BasicAuthenticator.js" import CustomAuthenticator from "./authentication/CustomAuthenticator.js" import Office365Authenticator from "./authentication/Office365Authenticator.js" +import { WDI5Control } from "./wdi5-control.js" +import { WDI5FE } from "./wdi5-fe.js" +import { WDI5Object } from "./wdi5-object.js" const Logger = _Logger.getInstance() @@ -272,12 +277,12 @@ export async function _addWdi5Commands(browserInstance: WebdriverIO.Browser) { return browserInstance._controls[internalKey] }) - browser.addCommand("asObject", async (_uuid: string) => { + browser.addCommand("_asObject", async (_uuid: string) => { const _result = (await clientSide_getObject(_uuid)) as clientSide_ui5Object - const { uuid, status, aProtoFunctions, className, object } = _result + const { uuid, status, aProtoFunctions, className, object, objectNames } = _result if (status === 0) { // create new wdi5-Object - const wdiObject = new WDI5Object(uuid, aProtoFunctions, object) + const wdiObject = new WDI5Object(uuid, aProtoFunctions, object, objectNames) return wdiObject } _writeObjectResultLog(_result, "asObject()") @@ -474,6 +479,63 @@ export async function _addWdi5Commands(browserInstance: WebdriverIO.Browser) { return makeFluent(browserInstance._asControl(ui5ControlSelector)) } } + + if (!browserInstance.asObject) { + browserInstance.asObject = function (uuid) { + const asyncMethods = ["then", "catch", "finally"] + const functionQueue = [] + // we need to do the same operation as in the 'init' of 'wdi5-control.ts' + const logging = true + function makeFluent(target) { + const promise = Promise.resolve(target) + const handler = { + get(_, prop) { + functionQueue.push(prop) + return asyncMethods.includes(prop) + ? (...boundArgs) => makeFluent(promise[prop](...boundArgs)) + : makeFluent( + promise.then((object) => { + // when object is undefined the previous function call failed + try { + return object[prop] + } catch (error) { + // different node versions return a different `error.message` so we use our own message + if (logging) { + Logger.error(`Cannot read property '${prop}' in the execution queue!`) + } + } + }) + ) + }, + apply(_, thisArg, boundArgs) { + return makeFluent( + // When "targetFunction" is empty we can assume that there are errors in the execution queue + promise.then((targetFunction) => { + if (targetFunction) { + return Reflect.apply(targetFunction, thisArg, boundArgs) + } else { + // a functionQueue without a 'then' can be ignored + // as the original error was already logged + if (functionQueue.includes("then") && logging) { + functionQueue.splice(functionQueue.indexOf("then")) + Logger.error( + `One of the calls in the queue "${functionQueue.join( + "()." + )}()" previously failed!` + ) + } + } + }) + ) + } + } + // eslint-disable-next-line @typescript-eslint/no-empty-function + return new Proxy(function () {}, handler) + } + // @ts-ignore + return makeFluent(browserInstance._asObject(uuid)) + } + } } /** @@ -628,7 +690,9 @@ function _writeObjectResultLog(response: clientSide_ui5Response, functionName: s Logger.error(`call of ${functionName} failed because of: ${response.message}`) } else if (response.status === 0) { Logger.success( - `call of function ${functionName} returned: ${JSON.stringify(response.id ? response.id : response.result)}` + `call of function ${functionName} returned: ${JSON.stringify( + response.id ? response.id : response.result ? response.result : "an object" + )}` ) } else { Logger.warn(`Unknown status: ${functionName} returned: ${JSON.stringify(response.message)}`) diff --git a/src/lib/wdi5-control.ts b/src/lib/wdi5-control.ts index a56a2057..1e6d0193 100644 --- a/src/lib/wdi5-control.ts +++ b/src/lib/wdi5-control.ts @@ -2,15 +2,15 @@ import * as util from "util" export const ELEMENT_KEY = "element-6066-11e4-a52e-4f735466cecf" // TODO: import { ELEMENT_KEY } from "webdriverio/build/constants.js" // patch in webdriverio repo? -import { clientSide_getControl } from "../../client-side-js/getControl.cjs" -import { clientSide_interactWithControl } from "../../client-side-js/interactWithControl.cjs" -import { clientSide_executeControlMethod } from "../../client-side-js/executeControlMethod.cjs" import { clientSide_getAggregation } from "../../client-side-js/_getAggregation.cjs" +import { clientSide_executeControlMethod } from "../../client-side-js/executeControlMethod.cjs" import { clientSide_fireEvent } from "../../client-side-js/fireEvent.cjs" +import { clientSide_getControl } from "../../client-side-js/getControl.cjs" +import { clientSide_interactWithControl } from "../../client-side-js/interactWithControl.cjs" import { clientSide_ui5Response, wdi5ControlMetadata, wdi5Selector } from "../types/wdi5.types.js" import { Logger as _Logger } from "./Logger.js" -import { wdioApi } from "./wdioApi.js" import { WDI5Object } from "./wdi5-object.js" +import { wdioApi } from "./wdioApi.js" const Logger = _Logger.getInstance() /** @@ -535,7 +535,7 @@ export class WDI5Control { return result.nonCircularResultObject ? result.nonCircularResultObject : result.result case "object": // enhance with uuid - return new WDI5Object(result.uuid, result.aProtoFunctions, result.object) + return new WDI5Object(result.uuid, result.aProtoFunctions, result.object, result.objectNames) case "empty": if (this._logging) { Logger.warn("No data found in property or aggregation") @@ -736,7 +736,7 @@ export class WDI5Control { } else if (response.status === 0) { Logger.success( `call of function ${functionName} returned: ${JSON.stringify( - response.id ? response.id : response.result + response.id ? response.id : response.result ? response.result : "an object" )}` ) } else { diff --git a/src/lib/wdi5-object.ts b/src/lib/wdi5-object.ts index 38c3063e..d3ecf50e 100644 --- a/src/lib/wdi5-object.ts +++ b/src/lib/wdi5-object.ts @@ -1,4 +1,5 @@ import { clientSide_executeObjectMethod } from "../../client-side-js/executeObjectMethod.cjs" +import { clientSide_getProperty } from "../../client-side-js/getProperty.cjs" import { clientSide_ui5Response } from "../types/wdi5.types.js" import { Logger as _Logger } from "./Logger.js" @@ -9,9 +10,10 @@ const Logger = _Logger.getInstance() export class WDI5Object { private _uuid: any private _aProtoFunctions: [] + private _propertyNames: [] private _baseObject: null - constructor(uuid, aProtoFunctions, object) { + constructor(uuid, aProtoFunctions, object, propertyNames) { this._uuid = uuid if (aProtoFunctions) { @@ -27,6 +29,10 @@ export class WDI5Object { } else { Logger.warn(`[WANING] creating object: ${uuid} without properties`) } + if (propertyNames) { + this._propertyNames = propertyNames + this._attachGetterForObjects(this._propertyNames) + } } public getUUID() { @@ -39,6 +45,33 @@ export class WDI5Object { } } + private _attachGetterForObjects(propertyNames: any) { + propertyNames.forEach(async (propertyName) => { + // eslint-disable-next-line @typescript-eslint/no-this-alias + const that = this + Object.defineProperty(this, propertyName, { + async get() { + return await that._getProperty.call(that, propertyName, this._uuid) + } + }) + }) + } + + private async _getProperty(propertyName: string, uuid: string) { + // call browser scope + // regular browser-time execution of UI5 object method + const result = (await clientSide_getProperty(uuid, propertyName)) as clientSide_ui5Response + + // create logging + this._writeObjectResultLog(result, propertyName) + + if (result.returnType === "object") { + return new WDI5Object(result.uuid, result.aProtoFunctions, result.object, result.objectNames) + } else { + return result.result + } + } + private async _excuteObjectMethod(methodName: string, uuid: string, ...args) { // call browser scope // regular browser-time execution of UI5 object method @@ -48,7 +81,7 @@ export class WDI5Object { this._writeObjectResultLog(result, methodName) if (result.returnType === "object") { - return new WDI5Object(result.uuid, result.aProtoFunctions, result.object) + return new WDI5Object(result.uuid, result.aProtoFunctions, result.object, result.objectNames) } else { return result.result } @@ -72,7 +105,7 @@ export class WDI5Object { } else if (response.status === 0) { Logger.success( `call of function ${functionName} returned: ${JSON.stringify( - response.id ? response.id : response.result + response.id ? response.id : response.result ? response.result : "an object" )}` ) } else { diff --git a/src/types/browser-commands.ts b/src/types/browser-commands.ts index 879ed416..639b97b6 100644 --- a/src/types/browser-commands.ts +++ b/src/types/browser-commands.ts @@ -1,6 +1,7 @@ import Control from "sap/ui/core/Control" -import { wdi5Config, wdi5Selector } from "./wdi5.types.js" import { WDI5Control } from "../lib/wdi5-control.js" +import { WDI5Object } from "../lib/wdi5-object.js" +import { wdi5Config, wdi5Selector } from "./wdi5.types.js" /** * wdi5 control cache aka @@ -16,6 +17,7 @@ declare global { export interface Browser { config: wdi5Config asControl: (arg: wdi5Selector) => Promise + asObject: (arg: wdi5Selector) => Promise allControls: (arg: wdi5Selector) => Promise<(WDI5Control & T)[]> screenshot: (arg: string) => Promise goTo: (arg: string | object) => Promise diff --git a/src/types/wdi5.types.ts b/src/types/wdi5.types.ts index 1a03a9d1..8d1a86e7 100644 --- a/src/types/wdi5.types.ts +++ b/src/types/wdi5.types.ts @@ -1,6 +1,5 @@ import Log from "sap/base/Log" -import RecordReplay from "sap/ui/test/RecordReplay" -import { ControlSelector } from "sap/ui/test/RecordReplay" +import RecordReplay, { ControlSelector } from "sap/ui/test/RecordReplay" import { WDI5Object } from "../lib/wdi5-object.js" // // copypasta from @@ -233,6 +232,7 @@ export interface clientSide_ui5Response { nonCircularResultObject?: any uuid?: string // unique sap.ui.base.Object id object: WDI5Object + objectNames?: [] } export interface clientSide_ui5Object { @@ -241,6 +241,7 @@ export interface clientSide_ui5Object { aProtoFunctions?: [] className?: string object: WDI5Object + objectNames?: [] } /**