diff --git a/client-side-js/_checkForUI5Ready.js b/client-side-js/_checkForUI5Ready.js index d554840b..3213d47a 100644 --- a/client-side-js/_checkForUI5Ready.js +++ b/client-side-js/_checkForUI5Ready.js @@ -1,5 +1,5 @@ -async function clientSide__checkForUI5Ready() { - return await browser.executeAsync((done) => { +async function clientSide__checkForUI5Ready(browserInstance) { + return await browserInstance.executeAsync((done) => { window.bridge .waitForUI5(window.wdi5.waitForUI5Options) .then(() => { diff --git a/client-side-js/_getAggregation.js b/client-side-js/_getAggregation.js index 50aaca71..8c5a3084 100644 --- a/client-side-js/_getAggregation.js +++ b/client-side-js/_getAggregation.js @@ -1,6 +1,6 @@ -async function clientSide_getAggregation(webElement, aggregationName) { +async function clientSide_getAggregation(webElement, aggregationName, browserInstance) { webElement = await Promise.resolve(webElement) // to plug into fluent async api - return await browser.executeAsync( + return await browserInstance.executeAsync( (webElement, aggregationName, done) => { window.bridge.waitForUI5(window.wdi5.waitForUI5Options).then(() => { // DOM to UI5 diff --git a/client-side-js/_navTo.js b/client-side-js/_navTo.js index dcc06c13..3b018c28 100644 --- a/client-side-js/_navTo.js +++ b/client-side-js/_navTo.js @@ -1,5 +1,5 @@ -async function clientSide__navTo(sComponentId, sName, oParameters, oComponentTargetInfo, bReplace) { - return await browser.executeAsync( +async function clientSide__navTo(sComponentId, sName, oParameters, oComponentTargetInfo, bReplace, browserInstance) { + return await browserInstance.executeAsync( (sComponentId, sName, oParameters, oComponentTargetInfo, bReplace, done) => { window.bridge.waitForUI5(window.wdi5.waitForUI5Options).then(() => { window.wdi5.Log.info(`[browser wdi5] navigation to ${sName} triggered`) diff --git a/client-side-js/allControls.js b/client-side-js/allControls.js index da7dd8c4..4c4d39eb 100644 --- a/client-side-js/allControls.js +++ b/client-side-js/allControls.js @@ -1,6 +1,6 @@ -async function clientSide_allControls(controlSelector) { +async function clientSide_allControls(controlSelector, browserInstance) { controlSelector = await Promise.resolve(controlSelector) // to plug into fluent async api - return await browser.executeAsync((controlSelector, done) => { + return await browserInstance.executeAsync((controlSelector, done) => { const waitForUI5Options = Object.assign({}, window.wdi5.waitForUI5Options) if (controlSelector.timeout) { waitForUI5Options.timeout = controlSelector.timeout diff --git a/client-side-js/executeControlMethod.js b/client-side-js/executeControlMethod.js index 780ed0ee..fa645247 100644 --- a/client-side-js/executeControlMethod.js +++ b/client-side-js/executeControlMethod.js @@ -1,5 +1,5 @@ -async function clientSide_executeControlMethod(webElement, methodName, args) { - return await browser.executeAsync( +async function clientSide_executeControlMethod(webElement, methodName, browserInstance, args) { + return await browserInstance.executeAsync( (webElement, methodName, args, done) => { window.wdi5.waitForUI5( window.wdi5.waitForUI5Options, diff --git a/client-side-js/fireEvent.js b/client-side-js/fireEvent.js index 6b6813ec..57162add 100644 --- a/client-side-js/fireEvent.js +++ b/client-side-js/fireEvent.js @@ -1,5 +1,5 @@ -async function clientSide_fireEvent(webElement, eventName, oOptions) { - return await browser.executeAsync( +async function clientSide_fireEvent(webElement, eventName, oOptions, browserInstance) { + return await browserInstance.executeAsync( (webElement, eventName, oOptions, done) => { window.wdi5.waitForUI5( window.wdi5.waitForUI5Options, diff --git a/client-side-js/getControl.js b/client-side-js/getControl.js index 038156ad..4e0760f2 100644 --- a/client-side-js/getControl.js +++ b/client-side-js/getControl.js @@ -1,6 +1,6 @@ -async function clientSide_getControl(controlSelector) { +async function clientSide_getControl(controlSelector, browserInstance) { controlSelector = await Promise.resolve(controlSelector) // to plug into fluent async api - return await browser.executeAsync((controlSelector, done) => { + return await browserInstance.executeAsync((controlSelector, done) => { const waitForUI5Options = Object.assign({}, window.wdi5.waitForUI5Options) if (controlSelector.timeout) { waitForUI5Options.timeout = controlSelector.timeout diff --git a/client-side-js/getSelectorForElement.js b/client-side-js/getSelectorForElement.js index b71ec5c7..0ee72681 100644 --- a/client-side-js/getSelectorForElement.js +++ b/client-side-js/getSelectorForElement.js @@ -1,5 +1,5 @@ -async function clientSide_getSelectorForElement(oOptions) { - return await browser.executeAsync((oOptions, done) => { +async function clientSide_getSelectorForElement(oOptions, browserInstance) { + return await browserInstance.executeAsync((oOptions, done) => { window.wdi5.waitForUI5( window.wdi5.waitForUI5Options, () => { diff --git a/client-side-js/getUI5Version.js b/client-side-js/getUI5Version.js index 3f1a32e9..39594ac4 100644 --- a/client-side-js/getUI5Version.js +++ b/client-side-js/getUI5Version.js @@ -1,8 +1,8 @@ /** * @returns {string} UI5 version number in string form */ -async function clientSide_getUI5Version() { - return await browser.executeAsync((done) => { +async function clientSide_getUI5Version(browserInstance = browser) { + return await browserInstance.executeAsync((done) => { done(sap.ui.version) }) } diff --git a/client-side-js/injectTools.js b/client-side-js/injectTools.js index c9669544..f1c8564c 100644 --- a/client-side-js/injectTools.js +++ b/client-side-js/injectTools.js @@ -1,5 +1,5 @@ -async function clientSide_injectTools() { - return await browser.executeAsync((done) => { +async function clientSide_injectTools(browserInstance) { + return await browserInstance.executeAsync((done) => { // terser'ed compare-versions@4.1.3.js from https://github.com/omichelsen/compare-versions // prettier-ignore !function (r, t) { "function" == typeof define && define.amd ? define([], t) : "object" == typeof exports ? module.exports = t() : window.compareVersions = t() }(this, (function () { var r = /^[v^~<>=]*?(\d+)(?:\.([x*]|\d+)(?:\.([x*]|\d+)(?:\.([x*]|\d+))?(?:-([\da-z\-]+(?:\.[\da-z\-]+)*))?(?:\+[\da-z\-]+(?:\.[\da-z\-]+)*)?)?)?$/i; function t(r) { var t, e, n = r.replace(/^v/, "").replace(/\+.*$/, ""), i = (e = "-", -1 === (t = n).indexOf(e) ? t.length : t.indexOf(e)), o = n.substring(0, i).split("."); return o.push(n.substring(i + 1)), o } function e(r) { var t = parseInt(r, 10); return isNaN(t) ? r : t } function n(t) { if ("string" != typeof t) throw new TypeError("Invalid argument expected string"); var e = t.match(r); if (!e) throw new Error("Invalid argument not valid semver ('" + t + "' received)"); return e.shift(), e } function i(r, t) { var [n, i] = function (r, t) { return typeof r != typeof t ? [String(r), String(t)] : [r, t] }(e(r), e(t)); return n > i ? 1 : n < i ? -1 : 0 } function o(r, i) { [r, i].forEach(n); for (var o = t(r), f = t(i), a = 0; a < Math.max(o.length - 1, f.length - 1); a++) { var u = parseInt(o[a] || 0, 10), p = parseInt(f[a] || 0, 10); if (u > p) return 1; if (p > u) return -1 } var s = o[o.length - 1], d = f[f.length - 1]; if (s && d) { var c = s.split(".").map(e), v = d.split(".").map(e); for (a = 0; a < Math.max(c.length, v.length); a++) { if (void 0 === c[a] || "string" == typeof v[a] && "number" == typeof c[a]) return -1; if (void 0 === v[a] || "string" == typeof c[a] && "number" == typeof v[a]) return 1; if (c[a] > v[a]) return 1; if (v[a] > c[a]) return -1 } } else if (s || d) return s ? -1 : 1; return 0 } var f = [">", ">=", "=", "<", "<="], a = { ">": [1], ">=": [0, 1], "=": [0], "<=": [-1, 0], "<": [-1] }; return o.validate = function (t) { return "string" == typeof t && r.test(t) }, o.compare = function (r, t, e) { !function (r) { if ("string" != typeof r) throw new TypeError("Invalid operator type, expected string but got " + typeof r); if (-1 === f.indexOf(r)) throw new TypeError("Invalid operator, expected one of " + f.join("|")) }(e); var n = o(r, t); return a[e].indexOf(n) > -1 }, o.satisfies = function (r, t) { var e = t.match(/^([<>=~^]+)/), f = e ? e[1] : "="; if ("^" !== f && "~" !== f) return o.compare(r, t, f); var [a, u, p] = n(r), [s, d, c] = n(t); return 0 === i(a, s) && ("^" === f ? function (r, t) { for (var e = 0; e < Math.max(r.length, t.length); e++) { var n = i(r[e] || 0, t[e] || 0); if (0 !== n) return n } return 0 }([u, p], [d, c]) >= 0 : 0 === i(u, d) && i(p, c) >= 0) }, o })); diff --git a/client-side-js/injectUI5.js b/client-side-js/injectUI5.js index 2645f2a7..1105bf65 100644 --- a/client-side-js/injectUI5.js +++ b/client-side-js/injectUI5.js @@ -1,5 +1,5 @@ -async function clientSide_injectUI5(config, waitForUI5Timeout) { - return await browser.executeAsync((waitForUI5Timeout, done) => { +async function clientSide_injectUI5(config, waitForUI5Timeout, browserInstance) { + return await browserInstance.executeAsync((waitForUI5Timeout, done) => { if (window.bridge) { // setup sap testing already done done(true) diff --git a/client-side-js/interactWithControl.js b/client-side-js/interactWithControl.js index 8dd8589f..8d17c2f2 100644 --- a/client-side-js/interactWithControl.js +++ b/client-side-js/interactWithControl.js @@ -1,5 +1,5 @@ -async function clientSide_interactWithControl(oOptions) { - return await browser.executeAsync((oOptions, done) => { +async function clientSide_interactWithControl(oOptions, browserInstance) { + return await browserInstance.executeAsync((oOptions, done) => { window.wdi5.waitForUI5( window.wdi5.waitForUI5Options, () => { diff --git a/client-side-js/testLibrary.js b/client-side-js/testLibrary.js index 08e91911..ded5d0e3 100644 --- a/client-side-js/testLibrary.js +++ b/client-side-js/testLibrary.js @@ -1,5 +1,5 @@ -async function initOPA(pageObjectConfig) { - return await browser.executeAsync((pageObjectConfig, done) => { +async function initOPA(pageObjectConfig, browserInstance) { + return await browserInstance.executeAsync((pageObjectConfig, done) => { window.bridge .waitForUI5(window.wdi5.waitForUI5Options) .then(() => { @@ -29,8 +29,8 @@ async function initOPA(pageObjectConfig) { }) }, pageObjectConfig) } -async function emptyQueue() { - return await browser.executeAsync((done) => { +async function emptyQueue(browserInstance) { + return await browserInstance.executeAsync((done) => { window.bridge .waitForUI5(window.wdi5.waitForUI5Options) .then(() => { @@ -47,8 +47,8 @@ async function emptyQueue() { }) } -async function addToQueue(type, target, aMethods) { - return await browser.executeAsync( +async function addToQueue(type, target, aMethods, browserInstance) { + return await browserInstance.executeAsync( (type, target, aMethods, done) => { window.bridge .waitForUI5(window.wdi5.waitForUI5Options) @@ -88,8 +88,8 @@ async function addToQueue(type, target, aMethods) { ) } -async function loadFELibraries() { - return await browser.executeAsync((done) => { +async function loadFELibraries(browserInstance = browser) { + return await browserInstance.executeAsync((done) => { sap.ui.require( ["sap/fe/test/ListReport", "sap/fe/test/ObjectPage", "sap/fe/test/Shell"], (ListReport, ObjectPage, Shell) => { diff --git a/examples/ui5-js-app/e2e-test-config/wdio-multiremote.conf.js b/examples/ui5-js-app/e2e-test-config/wdio-multiremote.conf.js new file mode 100644 index 00000000..675a1831 --- /dev/null +++ b/examples/ui5-js-app/e2e-test-config/wdio-multiremote.conf.js @@ -0,0 +1,47 @@ +const { baseConfig } = require("./wdio.base.conf") +const { join } = require("path") +const merge = require("deepmerge") + +// avoid multiple chrome sessions +delete baseConfig.capabilities + +const _config = { + wdi5: { + url: "#", + screenshotPath: join("report", "screenshots") + }, + maxInstances: 1, + capabilities: { + one: { + capabilities: { + browserName: "chrome", + acceptInsecureCerts: true, + "goog:chromeOptions": { + args: + 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"] + } + } + }, + two: { + capabilities: { + browserName: "chrome", + acceptInsecureCerts: true, + "goog:chromeOptions": { + args: + 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"] + } + } + } + }, + specs: ["webapp/test/e2e/multiremote.test.js"] +} + +exports.config = merge(baseConfig, _config) diff --git a/examples/ui5-js-app/e2e-test-config/wdio-webserver.conf.js b/examples/ui5-js-app/e2e-test-config/wdio-webserver.conf.js index ba813a63..6a7563ae 100644 --- a/examples/ui5-js-app/e2e-test-config/wdio-webserver.conf.js +++ b/examples/ui5-js-app/e2e-test-config/wdio-webserver.conf.js @@ -7,7 +7,7 @@ const _config = { url: "#" }, specs: [join("webapp", "test", "e2e", "**/*.test.js")], - exclude: [join("webapp", "test", "e2e", "ui5-late.test.js")], + exclude: [join("webapp", "test", "e2e", "ui5-late.test.js"), join("webapp", "test", "e2e", "multiremote.test.js")], baseUrl: "http://localhost:8888" } diff --git a/examples/ui5-js-app/e2e-test-config/wdio.base.conf.js b/examples/ui5-js-app/e2e-test-config/wdio.base.conf.js index f663dded..20208482 100644 --- a/examples/ui5-js-app/e2e-test-config/wdio.base.conf.js +++ b/examples/ui5-js-app/e2e-test-config/wdio.base.conf.js @@ -14,10 +14,10 @@ exports.baseConfig = { "goog:chromeOptions": { args: process.argv.indexOf("--headless") > -1 - ? ["window-size=1440,800", "--headless"] + ? ["window-size=1920,1280", "--headless"] : process.argv.indexOf("--debug") > -1 ? ["window-size=1920,1280", "--auto-open-devtools-for-tabs"] - : ["window-size=1440,800"] + : ["window-size=1920,1280"] } } ], diff --git a/examples/ui5-js-app/package.json b/examples/ui5-js-app/package.json index bad5e8f4..6ef3bcd7 100644 --- a/examples/ui5-js-app/package.json +++ b/examples/ui5-js-app/package.json @@ -14,6 +14,7 @@ "test:lateInject": "wdio run e2e-test-config/wdio-ui5-late.conf.js", "test:ui5tooling": "wdio run e2e-test-config/wdio-ui5tooling.conf.js", "test:webserver": "wdio run e2e-test-config/wdio-webserver.conf.js", + "test:multiremote": "wdio run e2e-test-config/wdio-multiremote.conf.js", "test:multiversion": "node e2e-test-config/wdi5-multiversion.js", "test-selenium": "wdio run e2e-test-config/wdio-selenium-service.conf.js" }, @@ -24,7 +25,7 @@ "@wdio/mocha-framework": "^7.19.7", "@wdio/selenium-standalone-service": "^7.19.5", "@wdio/spec-reporter": "^7.19.7", - "chromedriver": "^101.0.0", + "chromedriver": "^103.0.0", "ui5-middleware-simpleproxy": "^0.8.4", "wdio-chromedriver-service": "^7.3.2", "wdio-ui5-service": "*" diff --git a/examples/ui5-js-app/webapp/test/e2e/multiremote.test.js b/examples/ui5-js-app/webapp/test/e2e/multiremote.test.js new file mode 100644 index 00000000..0df50234 --- /dev/null +++ b/examples/ui5-js-app/webapp/test/e2e/multiremote.test.js @@ -0,0 +1,47 @@ +const Main = require("./pageObjects/Main") +describe("Multi Remote", () => { + before(async () => { + await Main.open() + }) + + it("allows handling ui5 controls in different browsers", async () => { + const button = await browser.two.asControl({ + selector: { + id: "openDialogButton", + viewName: "test.Sample.view.Main" + } + }) + const metadata = button.getControlInfo() + + expect(metadata.id).toEqual("container-Sample---Main--openDialogButton") + expect(metadata.className).toEqual("sap.m.Button") + expect(metadata.key).toEqual("openDialogButtontestSample.view.Main") + + await button.press() + + const dialogSelector = { + selector: { + id: "Dialog-title", + searchOpenDialogs: true + } + } + + const text = await browser.two.asControl(dialogSelector).getText() + expect(text).toEqual("Here we are!") + expect(await browser.one.asControl(dialogSelector).getInitStatus()).toBeFalsy() + }) + it("should return an array of results of both browsers if called directly by browser", async () => { + const button = await browser.asControl({ + selector: { + id: "openDialogButton", + viewName: "test.Sample.view.Main" + } + }) + const buttonOne = button[0] + const buttonTwo = button[1] + expect(buttonOne._browserInstance.sessionId).not.toEqual(buttonTwo._browserInstance.sessionId) + + expect(await buttonOne.getText()).toEqual("open Dialog") + expect(await buttonTwo.getText()).toEqual("open Dialog") + }) +}) diff --git a/package-lock.json b/package-lock.json index e173efab..f552db51 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,7 +29,7 @@ "@wdio/local-runner": "^7.19.7", "@wdio/mocha-framework": "^7.19.7", "@wdio/spec-reporter": "^7.19.7", - "chromedriver": "^101.0.0", + "chromedriver": "^103.0.0", "cross-env": "^7.0.3", "deepmerge": "^4.2.2", "eslint": "^8.16.0", @@ -79,6 +79,28 @@ "wdio-ui5-service": "*" } }, + "examples/fe-app/node_modules/chromedriver": { + "version": "101.0.0", + "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-101.0.0.tgz", + "integrity": "sha512-LkkWxy6KM/0YdJS8qBeg5vfkTZTRamhBfOttb4oic4echDgWvCU1E8QcBbUBOHqZpSrYMyi7WMKmKMhXFUaZ+w==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@testim/chrome-version": "^1.1.2", + "axios": "^0.24.0", + "del": "^6.0.0", + "extract-zip": "^2.0.1", + "https-proxy-agent": "^5.0.0", + "proxy-from-env": "^1.1.0", + "tcp-port-used": "^1.0.1" + }, + "bin": { + "chromedriver": "bin/chromedriver" + }, + "engines": { + "node": ">=10" + } + }, "examples/ui5-js-app": { "name": "ui5-app", "version": "0.8.15-notimportant", @@ -90,7 +112,7 @@ "@wdio/mocha-framework": "^7.19.7", "@wdio/selenium-standalone-service": "^7.19.5", "@wdio/spec-reporter": "^7.19.7", - "chromedriver": "^101.0.0", + "chromedriver": "^103.0.0", "ui5-middleware-simpleproxy": "^0.8.4", "wdio-chromedriver-service": "^7.3.2", "wdio-ui5-service": "*" @@ -6980,6 +7002,12 @@ "node": ">=0.12.0" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, "node_modules/at-least-node": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", @@ -7600,14 +7628,14 @@ } }, "node_modules/chromedriver": { - "version": "101.0.0", - "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-101.0.0.tgz", - "integrity": "sha512-LkkWxy6KM/0YdJS8qBeg5vfkTZTRamhBfOttb4oic4echDgWvCU1E8QcBbUBOHqZpSrYMyi7WMKmKMhXFUaZ+w==", + "version": "103.0.0", + "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-103.0.0.tgz", + "integrity": "sha512-7BHf6HWt0PeOHCzWO8qlnD13sARzr5AKTtG8Csn+czsuAsajwPxdLNtry5GPh8HYFyl+i0M+yg3bT43AGfgU9w==", "dev": true, "hasInstallScript": true, "dependencies": { "@testim/chrome-version": "^1.1.2", - "axios": "^0.24.0", + "axios": "^0.27.2", "del": "^6.0.0", "extract-zip": "^2.0.1", "https-proxy-agent": "^5.0.0", @@ -7621,6 +7649,16 @@ "node": ">=10" } }, + "node_modules/chromedriver/node_modules/axios": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", + "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", + "dev": true, + "dependencies": { + "follow-redirects": "^1.14.9", + "form-data": "^4.0.0" + } + }, "node_modules/clean-stack": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", @@ -7838,6 +7876,18 @@ "text-hex": "1.0.x" } }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/commander/-/commander-9.3.0.tgz", @@ -8674,6 +8724,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", @@ -9908,6 +9967,20 @@ } } }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -22855,6 +22928,12 @@ "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", "dev": true }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, "at-least-node": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", @@ -23326,18 +23405,30 @@ } }, "chromedriver": { - "version": "101.0.0", - "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-101.0.0.tgz", - "integrity": "sha512-LkkWxy6KM/0YdJS8qBeg5vfkTZTRamhBfOttb4oic4echDgWvCU1E8QcBbUBOHqZpSrYMyi7WMKmKMhXFUaZ+w==", + "version": "103.0.0", + "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-103.0.0.tgz", + "integrity": "sha512-7BHf6HWt0PeOHCzWO8qlnD13sARzr5AKTtG8Csn+czsuAsajwPxdLNtry5GPh8HYFyl+i0M+yg3bT43AGfgU9w==", "dev": true, "requires": { "@testim/chrome-version": "^1.1.2", - "axios": "^0.24.0", + "axios": "^0.27.2", "del": "^6.0.0", "extract-zip": "^2.0.1", "https-proxy-agent": "^5.0.0", "proxy-from-env": "^1.1.0", "tcp-port-used": "^1.0.1" + }, + "dependencies": { + "axios": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", + "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", + "dev": true, + "requires": { + "follow-redirects": "^1.14.9", + "form-data": "^4.0.0" + } + } } }, "clean-stack": { @@ -23510,6 +23601,15 @@ "text-hex": "1.0.x" } }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, "commander": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/commander/-/commander-9.3.0.tgz", @@ -24147,6 +24247,12 @@ "slash": "^3.0.0" } }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true + }, "delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", @@ -25114,6 +25220,23 @@ "sqlite3": "^5.0.8", "wdio-chromedriver-service": "^7.3.2", "wdio-ui5-service": "*" + }, + "dependencies": { + "chromedriver": { + "version": "101.0.0", + "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-101.0.0.tgz", + "integrity": "sha512-LkkWxy6KM/0YdJS8qBeg5vfkTZTRamhBfOttb4oic4echDgWvCU1E8QcBbUBOHqZpSrYMyi7WMKmKMhXFUaZ+w==", + "dev": true, + "requires": { + "@testim/chrome-version": "^1.1.2", + "axios": "^0.24.0", + "del": "^6.0.0", + "extract-zip": "^2.0.1", + "https-proxy-agent": "^5.0.0", + "proxy-from-env": "^1.1.0", + "tcp-port-used": "^1.0.1" + } + } } }, "flat": { @@ -25148,6 +25271,17 @@ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz", "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==" }, + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, "forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -29430,7 +29564,7 @@ "@wdio/mocha-framework": "^7.19.7", "@wdio/selenium-standalone-service": "^7.19.5", "@wdio/spec-reporter": "^7.19.7", - "chromedriver": "^101.0.0", + "chromedriver": "^103.0.0", "ui5-middleware-simpleproxy": "^0.8.4", "wdio-chromedriver-service": "^7.3.2", "wdio-ui5-service": "*" @@ -29654,7 +29788,7 @@ "@wdio/local-runner": "^7.19.7", "@wdio/mocha-framework": "^7.19.7", "@wdio/spec-reporter": "^7.19.7", - "chromedriver": "^101.0.0", + "chromedriver": "^103.0.0", "cross-env": "^7.0.3", "deepmerge": "^4.2.2", "eslint": "^8.16.0", @@ -36214,6 +36348,12 @@ "integrity": "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==", "dev": true }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, "at-least-node": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", @@ -36685,18 +36825,30 @@ } }, "chromedriver": { - "version": "101.0.0", - "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-101.0.0.tgz", - "integrity": "sha512-LkkWxy6KM/0YdJS8qBeg5vfkTZTRamhBfOttb4oic4echDgWvCU1E8QcBbUBOHqZpSrYMyi7WMKmKMhXFUaZ+w==", + "version": "103.0.0", + "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-103.0.0.tgz", + "integrity": "sha512-7BHf6HWt0PeOHCzWO8qlnD13sARzr5AKTtG8Csn+czsuAsajwPxdLNtry5GPh8HYFyl+i0M+yg3bT43AGfgU9w==", "dev": true, "requires": { "@testim/chrome-version": "^1.1.2", - "axios": "^0.24.0", + "axios": "^0.27.2", "del": "^6.0.0", "extract-zip": "^2.0.1", "https-proxy-agent": "^5.0.0", "proxy-from-env": "^1.1.0", "tcp-port-used": "^1.0.1" + }, + "dependencies": { + "axios": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", + "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", + "dev": true, + "requires": { + "follow-redirects": "^1.14.9", + "form-data": "^4.0.0" + } + } } }, "clean-stack": { @@ -36869,6 +37021,15 @@ "text-hex": "1.0.x" } }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, "commander": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/commander/-/commander-9.3.0.tgz", @@ -37506,6 +37667,12 @@ "slash": "^3.0.0" } }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true + }, "delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", @@ -38473,6 +38640,23 @@ "sqlite3": "^5.0.8", "wdio-chromedriver-service": "^7.3.2", "wdio-ui5-service": "*" + }, + "dependencies": { + "chromedriver": { + "version": "101.0.0", + "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-101.0.0.tgz", + "integrity": "sha512-LkkWxy6KM/0YdJS8qBeg5vfkTZTRamhBfOttb4oic4echDgWvCU1E8QcBbUBOHqZpSrYMyi7WMKmKMhXFUaZ+w==", + "dev": true, + "requires": { + "@testim/chrome-version": "^1.1.2", + "axios": "^0.24.0", + "del": "^6.0.0", + "extract-zip": "^2.0.1", + "https-proxy-agent": "^5.0.0", + "proxy-from-env": "^1.1.0", + "tcp-port-used": "^1.0.1" + } + } } }, "flat": { @@ -38507,6 +38691,17 @@ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz", "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==" }, + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, "forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -42789,7 +42984,7 @@ "@wdio/mocha-framework": "^7.19.7", "@wdio/selenium-standalone-service": "^7.19.5", "@wdio/spec-reporter": "^7.19.7", - "chromedriver": "^101.0.0", + "chromedriver": "^103.0.0", "ui5-middleware-simpleproxy": "^0.8.4", "wdio-chromedriver-service": "^7.3.2", "wdio-ui5-service": "*" diff --git a/package.json b/package.json index 4ea77db8..82fa6043 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "@wdio/local-runner": "^7.19.7", "@wdio/mocha-framework": "^7.19.7", "@wdio/spec-reporter": "^7.19.7", - "chromedriver": "^101.0.0", + "chromedriver": "^103.0.0", "cross-env": "^7.0.3", "deepmerge": "^4.2.2", "eslint": "^8.16.0", diff --git a/src/lib/wdi5-bridge.ts b/src/lib/wdi5-bridge.ts index 98e9a5b1..504a9db0 100644 --- a/src/lib/wdi5-bridge.ts +++ b/src/lib/wdi5-bridge.ts @@ -5,6 +5,7 @@ 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 { MultiRemoteDriver } from "webdriverio/build/multiremote" import { WDI5Control } from "./wdi5-control" import { WDI5FE } from "./wdi5-fe" import { clientSide_injectTools } from "../../client-side-js/injectTools" @@ -36,56 +37,13 @@ export async function setup(config: wdi5Config) { // jump-start the desired log level Logger.setLogLevel(config.wdi5.logLevel || "error") - // init control cache - if (!browser._controls) { - Logger.info("creating internal control map") - browser._controls = [] - } - - _addWdi5Commands() - - // inspired by and after staring a long time hard at: - // https://stackoverflow.com/questions/51635378/keep-object-chainable-using-async-methods - // https://github.com/Shigma/prochain - // https://github.com/l8js/l8/blob/main/src/core/liquify.js - - // channel the async function browser._asControl (init'ed via browser.addCommand above) through a Proxy - // in order to chain calls of any subsequent UI5 api methods on the retrieved UI5 control: - // await browser.asControl(selector).methodOfUI5control().anotherMethodOfUI5control() - // the way this works is twofold: - // 1. (almost) all UI5 $control's API methods are reinjected from the browser-scope - // into the Node.js scope via async WDI5._executeControlMethod(), which in term actually calls - // the reinjected API method within the browser scope - // 2. the execution of each UI5 $control's API method (via async WDI5._executeControlMethod() => Promise) is then chained - // via the below "then"-ing of the (async WDI5._executeControlMethod() => Promise)-Promises with the help of - // the a Proxy and a recursive `handler` function - if (!browser.asControl) { - browser.asControl = function (ui5ControlSelector) { - const asyncMethods = ["then", "catch", "finally"] - function makeFluent(target) { - const promise = Promise.resolve(target) - const handler = { - get(_, prop) { - return asyncMethods.includes(prop) - ? (...boundArgs) => makeFluent(promise[prop](...boundArgs)) - : makeFluent(promise.then((object) => object[prop])) - }, - apply(_, thisArg, boundArgs) { - return makeFluent( - promise.then((targetFunction) => Reflect.apply(targetFunction, thisArg, boundArgs)) - ) - } - } - // eslint-disable-next-line @typescript-eslint/no-empty-function - return new Proxy(function () {}, handler) - } - // @ts-ignore - return makeFluent(browser._asControl(ui5ControlSelector)) - } - } - - if (!(browser as any).fe) { - ;(browser as any).fe = WDI5FE + if (browser instanceof MultiRemoteDriver) { + ;(browser as MultiRemoteDriver).instances.forEach((name) => { + initBrowser(browser[name]) + }) + initMultiRemoteBrowser() + } else { + initBrowser(browser) } _setupComplete = true @@ -102,21 +60,58 @@ export async function start(config: wdi5Config) { } } -/** - * function library to setup the webdriver to UI5 bridge, it runs alle the initial setup - * make sap/ui/test/RecordReplay accessible via wdio - * attach the sap/ui/test/RecordReplay object to the application context window object as 'bridge' - */ -export async function injectUI5(config: wdi5Config) { - const ui5Version = await browser.getUI5Version() +function initMultiRemoteBrowser() { + ;["asControl", "goTo", "screenshot", "waitForUI5", "getUI5Version", "getSelectorForElement", "allControls"].forEach( + (command) => { + browser.addCommand(command, async (...args) => { + const multiRemoteInstance = browser as unknown as MultiRemoteDriver + const result = [] + multiRemoteInstance.instances.forEach((name) => { + result.push(multiRemoteInstance[name][command].apply(this, args)) + }) + return Promise.all(result) + }) + } + ) +} + +function initBrowser(browserInstance: WebdriverIO.Browser) { + // init control cache + if (!browserInstance._controls) { + Logger.info("creating internal control map") + browserInstance._controls = [] + } + + _addWdi5Commands(browserInstance) + + if (!(browserInstance as any).fe) { + ;(browserInstance as any).fe = WDI5FE + } + + _setupComplete = true +} + +function checkUI5Version(ui5Version: string) { if (semver.lt(ui5Version, "1.60.0")) { // the record replay api is only available since 1.60 Logger.error("The ui5 version of your application is to low. Minimum required UI5 version is 1.60") throw new Error("The ui5 version of your application is to low. Minimum required UI5 version is 1.60") } +} + +/** + * function library to setup the webdriver to UI5 bridge, it runs alle the initial setup + * make sap/ui/test/RecordReplay accessible via wdio + * attach the sap/ui/test/RecordReplay object to the application context window object as 'bridge' + */ +export async function injectUI5(config: wdi5Config, browserInstance) { const waitForUI5Timeout = config.wdi5.waitForUI5Timeout || 15000 - await clientSide_injectTools() // helpers for wdi5 browser scope - const result: boolean = await clientSide_injectUI5(config, waitForUI5Timeout) + let result = true + + const version = await (browserInstance as WebdriverIO.Browser).getUI5Version() + await checkUI5Version(version) + await clientSide_injectTools(browserInstance) // helpers for wdi5 browser scope + result = result && (await clientSide_injectUI5(config, waitForUI5Timeout, browserInstance)) if (result) { // set when call returns @@ -195,8 +190,8 @@ function _verifySelector(wdi5Selector: wdi5Selector) { return false } -async function _addWdi5Commands() { - browser.addCommand("_asControl", async (wdi5Selector: wdi5Selector) => { +export async function _addWdi5Commands(browserInstance: WebdriverIO.Browser) { + browserInstance.addCommand("_asControl", async (wdi5Selector: wdi5Selector) => { if (!_verifySelector(wdi5Selector)) { return "ERROR: Specified selector is not valid -> abort" } @@ -204,41 +199,41 @@ async function _addWdi5Commands() { const internalKey = wdi5Selector.wdio_ui5_key || _createWdioUI5KeyFromSelector(wdi5Selector) // either retrieve and cache a UI5 control // or return a cached version - if (!browser._controls?.[internalKey] || wdi5Selector.forceSelect /* always retrieve control */) { + if (!browserInstance._controls?.[internalKey] || wdi5Selector.forceSelect /* always retrieve control */) { Logger.info(`creating internal control with id ${internalKey}`) wdi5Selector.wdio_ui5_key = internalKey marky_mark("retrieveSingleControl") - const wdi5Control = await new WDI5Control({}).init(wdi5Selector, wdi5Selector.forceSelect) + const wdi5Control = await new WDI5Control({ browserInstance }).init(wdi5Selector, wdi5Selector.forceSelect) const e = marky_stop("retrieveSingleControl") Logger.info(`_asControl() needed ${e.duration} for ${internalKey}`) - browser._controls[internalKey] = wdi5Control + browserInstance._controls[internalKey] = wdi5Control } else { Logger.info(`reusing internal control with id ${internalKey}`) } - return browser._controls[internalKey] + return browserInstance._controls[internalKey] }) // no fluent API -> no private method - browser.addCommand("allControls", async (wdi5Selector: wdi5Selector) => { + browserInstance.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 */) { + if (!browserInstance._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] + browserInstance._controls[internalKey] = await _allControls(wdi5Selector, browserInstance) + return browserInstance._controls[internalKey] } else { Logger.info(`reusing internal control with id ${internalKey}`) } - return browser._controls[internalKey] + return browserInstance._controls[internalKey] }) /** @@ -249,8 +244,8 @@ async function _addWdi5Commands() { * @param {object} oOptions.settings - ui5 settings object * @param {boolean} oOptions.settings.preferViewId */ - browser.addCommand("getSelectorForElement", async (oOptions) => { - const result = (await clientSide_getSelectorForElement(oOptions)) as clientSide_ui5Response + browserInstance.addCommand("getSelectorForElement", async (oOptions) => { + const result = (await clientSide_getSelectorForElement(oOptions, browserInstance)) as clientSide_ui5Response if (result.status === 1) { console.error("ERROR: getSelectorForElement() failed because of: " + result.message) @@ -261,9 +256,9 @@ async function _addWdi5Commands() { } }) - browser.addCommand("getUI5Version", async () => { + browserInstance.addCommand("getUI5Version", async () => { if (!_sapUI5Version) { - const resultVersion = await clientSide_getUI5Version() + const resultVersion = await clientSide_getUI5Version(browserInstance) _sapUI5Version = resultVersion } @@ -273,19 +268,19 @@ async function _addWdi5Commands() { /** * uses the UI5 native waitForUI5 function to wait for all promises to be settled */ - browser.addCommand("waitForUI5", async () => { - return await _waitForUI5() + browserInstance.addCommand("waitForUI5", async () => { + return await _waitForUI5(browserInstance) }) /** * wait for ui5 and take a screenshot */ - browser.addCommand("screenshot", async (fileAppendix) => { - await _waitForUI5() + browserInstance.addCommand("screenshot", async (fileAppendix) => { + await _waitForUI5(browserInstance) await _writeScreenshot(fileAppendix) }) - browser.addCommand("goTo", async (oOptions) => { + browserInstance.addCommand("goTo", async (oOptions) => { // allow for method sig to be both // wdi5()...goTo("#/accounts/create") // wdi5()...goTo({sHash:"#/accounts/create"}) @@ -298,25 +293,25 @@ async function _addWdi5Commands() { const oRoute = oOptions.oRoute if (sHash && sHash.length > 0) { - const url = (browser.config as wdi5Config).wdi5["url"] || (await browser.getUrl()) + const url = (browserInstance.config as wdi5Config).wdi5["url"] || (await browserInstance.getUrl()) // navigate via hash if defined if (url && url.length > 0 && url !== "#") { // prefix url config if is not just a hash (#) - const currentUrl = await browser.getUrl() + const currentUrl = await browserInstance.getUrl() const alreadyNavByHash = currentUrl.includes("#") const navToRoot = url.startsWith("/") if (alreadyNavByHash && !navToRoot) { - await browser.url(`${currentUrl.split("#")[0]}${sHash}`) + await browserInstance.url(`${currentUrl.split("#")[0]}${sHash}`) } else { - await browser.url(`${url}${sHash}`) + await browserInstance.url(`${url}${sHash}`) } } else if (url && url.length > 0 && url === "#") { // route without the double hash - await browser.url(`${sHash}`) + await browserInstance.url(`${sHash}`) } else { // just a fallback - await browser.url(`${sHash}`) + await browserInstance.url(`${sHash}`) } } else if (oRoute && oRoute.sName) { // navigate using the ui5 router @@ -326,12 +321,53 @@ async function _addWdi5Commands() { oRoute.sName, oRoute.oParameters, oRoute.oComponentTargetInfo, - oRoute.bReplace + oRoute.bReplace, + browserInstance ) } else { Logger.error("ERROR: navigating to another page") } }) + + // inspired by and after staring a long time hard at: + // https://stackoverflow.com/questions/51635378/keep-object-chainable-using-async-methods + // https://github.com/Shigma/prochain + // https://github.com/l8js/l8/blob/main/src/core/liquify.js + + // channel the async function browser._asControl (init'ed via browser.addCommand above) through a Proxy + // in order to chain calls of any subsequent UI5 api methods on the retrieved UI5 control: + // await browser.asControl(selector).methodOfUI5control().anotherMethodOfUI5control() + // the way this works is twofold: + // 1. (almost) all UI5 $control's API methods are reinjected from the browser-scope + // into the Node.js scope via async WDI5._executeControlMethod(), which in term actually calls + // the reinjected API method within the browser scope + // 2. the execution of each UI5 $control's API method (via async WDI5._executeControlMethod() => Promise) is then chained + // via the below "then"-ing of the (async WDI5._executeControlMethod() => Promise)-Promises with the help of + // the a Proxy and a recursive `handler` function + if (!browserInstance.asControl) { + browserInstance.asControl = function (ui5ControlSelector) { + const asyncMethods = ["then", "catch", "finally"] + function makeFluent(target) { + const promise = Promise.resolve(target) + const handler = { + get(_, prop) { + return asyncMethods.includes(prop) + ? (...boundArgs) => makeFluent(promise[prop](...boundArgs)) + : makeFluent(promise.then((object) => object[prop])) + }, + apply(_, thisArg, boundArgs) { + return makeFluent( + promise.then((targetFunction) => Reflect.apply(targetFunction, thisArg, boundArgs)) + ) + } + } + // eslint-disable-next-line @typescript-eslint/no-empty-function + return new Proxy(function () {}, handler) + } + // @ts-ignore + return makeFluent(browserInstance._asControl(ui5ControlSelector)) + } + } } /** @@ -339,7 +375,7 @@ async function _addWdi5Commands() { * @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) { +async function _allControls(controlSelector = this._controlSelector, browserInstance = browser) { // 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 @@ -357,7 +393,7 @@ async function _allControls(controlSelector = this._controlSelector) { } // pre retrive control information - const response = (await clientSide_allControls(controlSelector)) as clientSide_ui5Response + const response = await clientSide_allControls(controlSelector, browserInstance) _writeObjectResultLog(response, "allControls()") if (response.status === 0) { @@ -373,9 +409,11 @@ async function _allControls(controlSelector = this._controlSelector) { generatedUI5Methods: cControl.aProtoFunctions, webdriverRepresentation: null, webElement: cControl.domElement, - domId: cControl.id + domId: cControl.id, + browserInstance } + // FIXME: multi remote support by providing browserInstance in constructor resultWDi5Elements.push(new WDI5Control(oOptions)) } @@ -389,13 +427,13 @@ async function _allControls(controlSelector = this._controlSelector) { * 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. */ -async function _waitForUI5() { +async function _waitForUI5(browserInstance) { if (_isInitialized) { // injectUI5 was already called and was successful attached - return await _checkForUI5Ready() + return await _checkForUI5Ready(browserInstance) } else { - if (await injectUI5(_config)) { - return await _checkForUI5Ready() + if (await injectUI5(_config, browserInstance)) { + return await _checkForUI5Ready(browserInstance) } else { return false } @@ -405,12 +443,13 @@ async function _waitForUI5() { /** * check for UI5 via the RecordReplay.waitForUI5 method */ -async function _checkForUI5Ready() { +async function _checkForUI5Ready(browserInstance) { + const ready = false if (_isInitialized) { // can only be executed when RecordReplay is attached - return await clientSide__checkForUI5Ready() + return await clientSide__checkForUI5Ready(browserInstance) } - return false + return ready } /** @@ -455,13 +494,14 @@ function _getDateString() { * @param {Object} oComponentTargetInfo * @param {Boolean} bReplace */ -async function _navTo(sComponentId, sName, oParameters, oComponentTargetInfo, bReplace) { +async function _navTo(sComponentId, sName, oParameters, oComponentTargetInfo, bReplace, browserInstance) { const result = (await clientSide__navTo( sComponentId, sName, oParameters, oComponentTargetInfo, - bReplace + bReplace, + browserInstance )) as clientSide_ui5Response if (result.status === 1) { Logger.error("ERROR: navigation using UI5 router failed because of: " + result.message) diff --git a/src/lib/wdi5-control.ts b/src/lib/wdi5-control.ts index 2ea87128..16c5c72e 100644 --- a/src/lib/wdi5-control.ts +++ b/src/lib/wdi5-control.ts @@ -32,8 +32,10 @@ export class WDI5Control { _generatedWdioMethods: Array _domId: string + _browserInstance: WebdriverIO.Browser constructor(oOptions) { const { + browserInstance, controlSelector, wdio_ui5_key, forceSelect, @@ -47,6 +49,7 @@ export class WDI5Control { this._wdio_ui5_key = wdio_ui5_key this._forceSelect = forceSelect this._generatedUI5Methods = generatedUI5Methods + this._browserInstance = browserInstance this._webElement = webElement this._webdriverRepresentation = webdriverRepresentation this._domId = domId @@ -148,7 +151,7 @@ export class WDI5Control { * @returns */ async renewWebElement(id: string = this._domId) { - this._webdriverRepresentation = await $(`//*[@id="${id}"]`) + this._webdriverRepresentation = await this._browserInstance.$(`//*[@id="${id}"]`) return this._webdriverRepresentation } @@ -211,7 +214,7 @@ export class WDI5Control { * @param {boolean} oOptions.clearTextFirst */ async interactWithControl(oOptions) { - const result = (await clientSide_interactWithControl(oOptions)) as clientSide_ui5Response + const result = (await clientSide_interactWithControl(oOptions, this._browserInstance)) as clientSide_ui5Response this._writeObjectResultLog(result, "interactWithControl()") return result.result @@ -228,7 +231,12 @@ export class WDI5Control { if (oOptions?.eval) { oOptions = "(" + oOptions.eval.toString() + ")" } - const result = (await clientSide_fireEvent(webElement, eventName, oOptions)) as clientSide_ui5Response + const result = (await clientSide_fireEvent( + webElement, + eventName, + oOptions, + this._browserInstance + )) as clientSide_ui5Response this._writeObjectResultLog(result, "fireEvent()") return result.result } @@ -268,7 +276,7 @@ export class WDI5Control { } // get WDI5 control - aResultOfPromises.push(browser.asControl(selector)) + aResultOfPromises.push(this._browserInstance.asControl(selector)) }) return await Promise.all(aResultOfPromises) @@ -298,7 +306,7 @@ export class WDI5Control { } // get WDI5 control - eResult = await browser.asControl(selector) + eResult = await this._browserInstance.asControl(selector) } else { Logger.warn(`${this._wdio_ui5_key} has no aControls`) } @@ -361,7 +369,12 @@ export class WDI5Control { // returns the array of [0: "status", 1: result] // regular browser-time execution of UI5 control method - const result = (await clientSide_executeControlMethod(webElement, methodName, args)) as clientSide_ui5Response + const result = (await clientSide_executeControlMethod( + webElement, + methodName, + this._browserInstance, + args + )) as clientSide_ui5Response // create logging this._writeObjectResultLog(result, methodName) @@ -420,7 +433,11 @@ export class WDI5Control { if (util.types.isProxy(webElement)) { webElement = await Promise.resolve(webElement) } - const result = (await clientSide_getAggregation(webElement, aggregationName)) as clientSide_ui5Response + const result = (await clientSide_getAggregation( + webElement, + aggregationName, + this._browserInstance + )) as clientSide_ui5Response this._writeObjectResultLog(result, "_getAggregation()") @@ -480,7 +497,7 @@ export class WDI5Control { controlSelector.selector.properties.text = controlSelector.selector.properties.text.toString() } - const _result = (await clientSide_getControl(controlSelector)) as clientSide_ui5Response + const _result = (await clientSide_getControl(controlSelector, this._browserInstance)) as clientSide_ui5Response const { status, domElement, id, aProtoFunctions, className } = _result if (status === 0 && id) { diff --git a/src/lib/wdi5-fe.ts b/src/lib/wdi5-fe.ts index fc2c7d07..47fcb1ed 100644 --- a/src/lib/wdi5-fe.ts +++ b/src/lib/wdi5-fe.ts @@ -23,11 +23,11 @@ function createProxy(myObj: any, type: string, methodCalls: any[], pageKeys: str return thisProxy } export class WDI5FE { - constructor(private appConfig: any) {} - static async initialize(appConfig) { - await loadFELibraries() - await initOPA(appConfig) - return new WDI5FE(appConfig) + constructor(private appConfig: any, private browserInstance: any) {} + static async initialize(appConfig, browserInstance = browser) { + await loadFELibraries(browserInstance) + await initOPA(appConfig, browserInstance) + return new WDI5FE(appConfig, browserInstance) } async execute(fnFunction) { @@ -38,13 +38,18 @@ export class WDI5FE { const When = createProxy({}, "When", methodCalls, reservedPages) fnFunction(Given, Then, When) // PrepareQueue for (const methodCall of methodCalls) { - const [type, content] = await addToQueue(methodCall.type, methodCall.target, methodCall.methods) + const [type, content] = await addToQueue( + methodCall.type, + methodCall.target, + methodCall.methods, + this.browserInstance + ) if (type !== "success") { throw content } } // ExecuteTest - const [type, content, feLogs] = await emptyQueue() + const [type, content, feLogs] = await emptyQueue(this.browserInstance) if (type !== "success") { throw content } diff --git a/src/service.ts b/src/service.ts index e852fa9b..5c27d326 100644 --- a/src/service.ts +++ b/src/service.ts @@ -1,4 +1,5 @@ import { Capabilities, Services } from "@wdio/types" +import { MultiRemoteDriver } from "webdriverio/build/multiremote" import { start, injectUI5, setup, checkForUI5Page } from "./lib/wdi5-bridge" import { wdi5Config } from "./types/wdi5.types" @@ -19,7 +20,13 @@ export default class Service implements Services.ServiceInstance { await setup(this._config) Logger.info("setup complete") if (!this._config.wdi5.skipInjectUI5OnStart) { - await injectUI5(this._config) + if (browser instanceof MultiRemoteDriver) { + for (const name of (browser as MultiRemoteDriver).instances) { + await injectUI5(this._config as wdi5Config, browser[name]) + } + } else { + await injectUI5(this._config as wdi5Config, browser) + } } else { Logger.warn("skipped wdi5 injection!") } @@ -30,9 +37,9 @@ export default class Service implements Services.ServiceInstance { * it relays the the wdio configuration (set in the .before() hook to the browser.config parameter by wdio) * to the injectUI5 function of the actual wdi5-bridge */ - async injectUI5() { + async injectUI5(browserInstance = browser) { if (await checkForUI5Page()) { - await injectUI5(browser.config as wdi5Config) + await injectUI5(browserInstance.config as wdi5Config, browserInstance) } else { throw new Error("wdi5: no UI5 page/app present to work on :(") } diff --git a/src/wdi5.ts b/src/wdi5.ts index b44af225..f117ae9d 100644 --- a/src/wdi5.ts +++ b/src/wdi5.ts @@ -4,14 +4,14 @@ export class wdi5 { return Logger.getInstance(sPrefix) } - static async goTo(param: any, oRoute) { + static async goTo(param: any, oRoute, browserInstance: WebdriverIO.Browser = browser) { if (param) { Logger.getInstance().log(`Navigating to: ${JSON.stringify(param)}`) - await browser.goTo(param) + await browserInstance.goTo(param) } else { Logger.getInstance().log(`Navigating to: ${JSON.stringify(oRoute)}`) // ui5 router based navigation - await browser.goTo({ oRoute }) + await browserInstance.goTo({ oRoute }) } } }