From 90fb2bb1da01106bf28526b94dacb5842bb15542 Mon Sep 17 00:00:00 2001 From: Volker Buzek Date: Mon, 28 Aug 2023 11:55:27 +0200 Subject: [PATCH 01/17] feat: friendly take-over from #447 original PR is #447 --- examples/ui5-ts-app/test/e2e/Input.test.ts | 36 ++++++----- .../test/e2e/workzone/journey.test.ts | 59 +++++++++++++++++++ .../e2e/workzone/wdio-ui5-workzone.conf.ts | 53 +++++++++++++++++ package.json | 2 + src/lib/authentication/BTPAuthenticator.ts | 20 +++++++ src/lib/wdi5-bridge.ts | 10 +++- src/lib/wdi5-fe.ts | 44 +++++++++++++- src/service.ts | 39 +++++++++++- src/types/wdi5.types.ts | 15 +++++ src/wdi5.ts | 12 ++++ 10 files changed, 268 insertions(+), 22 deletions(-) create mode 100644 examples/ui5-ts-app/test/e2e/workzone/journey.test.ts create mode 100644 examples/ui5-ts-app/test/e2e/workzone/wdio-ui5-workzone.conf.ts diff --git a/examples/ui5-ts-app/test/e2e/Input.test.ts b/examples/ui5-ts-app/test/e2e/Input.test.ts index 47239c17..eae03a25 100644 --- a/examples/ui5-ts-app/test/e2e/Input.test.ts +++ b/examples/ui5-ts-app/test/e2e/Input.test.ts @@ -1,28 +1,32 @@ import Input from "sap/m/Input" import { wdi5Selector } from "wdio-ui5-service" +const inputSelector: wdi5Selector = { + selector: { + id: "mainUserInput", + viewName: "test.Sample.tsapp.view.Main" + } +} + describe("Input", async () => { it("should read name from username field", async () => { - const inputText: wdi5Selector = { - selector: { - id: "mainUserInput", - viewName: "test.Sample.tsapp.view.Main" - } - } - const input = await browser.asControl(inputText) + const input = await browser.asControl(inputSelector) const value = await input.getValue() expect(value).toEqual("Helvetius Nagy") }) it("should check if the field is writeable", async () => { - const inputText: wdi5Selector = { - selector: { - id: "mainUserInput", - viewName: "test.Sample.tsapp.view.Main" - } - } - await (browser.asControl(inputText) as unknown as Input).setValue("Smith Smithersson") - const input = await (browser.asControl(inputText) as unknown as Input).getValue() - expect(input).toEqual("Smith Smithersson") + const newValue = "Smith Smithersson" + await browser.asControl(inputSelector).setValue(newValue) + const input = await browser.asControl(inputSelector).getValue() + expect(input).toEqual(newValue) + }) + + it("should retrieve the webcomponent's bound path via a managed object", async () => { + const control = await browser.asControl(inputSelector) + const bindingInfo = await control.getBindingInfo("value") + // @ts-ignore + const parts = await bindingInfo.parts + expect(parts[0].path).toEqual("/Customers('TRAIH')/ContactName") }) }) diff --git a/examples/ui5-ts-app/test/e2e/workzone/journey.test.ts b/examples/ui5-ts-app/test/e2e/workzone/journey.test.ts new file mode 100644 index 00000000..2371f167 --- /dev/null +++ b/examples/ui5-ts-app/test/e2e/workzone/journey.test.ts @@ -0,0 +1,59 @@ +describe("drive in Work Zone", () => { + let FioriElementsFacade + before(async () => { + FioriElementsFacade = await browser.fe.initialize({ + onTheMainPage: { + ListReport: { + appId: "sap.fe.cap.travel", + componentId: "TravelList", + entitySet: "Travel" + } + }, + onTheDetailPage: { + ObjectPage: { + appId: "sap.fe.cap.travel", + componentId: "TravelObjectPage", + entitySet: "Travel" + } + }, + onTheItemPage: { + ObjectPage: { + appId: "sap.fe.cap.travel", + componentId: "BookingObjectPage", + entitySet: "Booking" + } + }, + onTheShell: { + Shell: {} + } + }) + }) + + it("should see the List Report page", async () => { + await FioriElementsFacade.execute((Given, When, Then) => { + Then.onTheMainPage.iSeeThisPage() + }) + }) + + it("should see the Object Pages load and then returns to list", async () => { + await FioriElementsFacade.execute((Given, When, Then) => { + When.onTheMainPage.onTable().iPressRow(1) + Then.onTheDetailPage.iSeeThisPage() + + When.onTheDetailPage.onTable({ property: "to_Booking" }).iPressRow({ BookingID: "1" }) + Then.onTheItemPage.iSeeThisPage() + + // When.onTheShell.iNavigateBack() // beh, b/c wrong iframe + }) + + // await FioriElementsFacade.onTheShell.iNavigateBack() + // await FioriElementsFacade.onTheShell.iNavigateBack() + + // we want: + await wdi5.wz.iNavigateBack() + + await FioriElementsFacade.execute((Given, When, Then) => { + Then.onTheMainPage.iSeeThisPage() + }) + }) +}) diff --git a/examples/ui5-ts-app/test/e2e/workzone/wdio-ui5-workzone.conf.ts b/examples/ui5-ts-app/test/e2e/workzone/wdio-ui5-workzone.conf.ts new file mode 100644 index 00000000..3a4b470b --- /dev/null +++ b/examples/ui5-ts-app/test/e2e/workzone/wdio-ui5-workzone.conf.ts @@ -0,0 +1,53 @@ +import { wdi5Config } from "wdio-ui5-service" + +process.env.wdi5_username = process.env.wdi5_wz_username +process.env.wdi5_password = process.env.wdi5_wz_password + +export const config: wdi5Config = { + wdi5: { + btpWorkZoneEnablement: true, + logLevel: "verbose" + }, + baseUrl: "https://cswdev.launchpad.cfapps.eu10.hana.ondemand.com/site/csw#travel-process", + + services: ["chromedriver", "ui5"], + + specs: ["./test/e2e/workzone/*.test.ts"], + exclude: ["./test/e2e/*.test.ts"], + + maxInstances: 10, + capabilities: [ + { + "wdi5:authentication": { + provider: "BTP", + disableBiometricAuthentication: true, + idpDomain: "aqywyhweh.accounts.ondemand.com" + }, + maxInstances: 2, + browserName: "chrome", + "goog:chromeOptions": { + args: + process.argv.indexOf("--headless") > -1 + ? ["--headless"] + : process.argv.indexOf("--debug") > -1 + ? ["window-size=1440,800", "--auto-open-devtools-for-tabs"] + : ["window-size=1440,800"] + }, + acceptInsecureCerts: true + } + ], + logLevel: "error", + bail: 0, + + waitforTimeout: 10000, + connectionRetryTimeout: process.argv.indexOf("--debug") > -1 ? 1200000 : 120000, + connectionRetryCount: 3, + + reporters: ["spec"], + + framework: "mocha", + mochaOpts: { + ui: "bdd", + timeout: process.argv.indexOf("--debug") > -1 ? 600000 : 60000 + } +} diff --git a/package.json b/package.json index 0e2bc18f..9322fef0 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,8 @@ "test:auth": "npm run test:authentication:bstack -w examples/ui5-ts-app", "//test:protocol": "runs the same test with webdriver and devtools protocol", "test:protocol": "npm run test:protocol -w examples/ui5-ts-app", + "//test:wz": "specific tests for SAP Build Workzone, standard edition", + "test:wz": "wdio run test/e2e/workzone/wdio-ui5-workzone.conf.ts", "test": "cross-env TS_NODE_PROJECT=\"test/tsconfig.json\" mocha", "prepare": "husky install", "release": "standard-version", diff --git a/src/lib/authentication/BTPAuthenticator.ts b/src/lib/authentication/BTPAuthenticator.ts index 0785fe7b..f50efe26 100644 --- a/src/lib/authentication/BTPAuthenticator.ts +++ b/src/lib/authentication/BTPAuthenticator.ts @@ -2,11 +2,31 @@ import { BTPAuthenticator as BTPAuthenticatorType } from "../../types/wdi5.types import Authenticator from "./Authenticator.js" class BTPAuthenticator extends Authenticator { + private disableBiometricAuth: boolean + private idpDomain: string + constructor(options: BTPAuthenticatorType, browserInstanceName) { super(browserInstanceName) this.usernameSelector = options.usernameSelector ?? "#j_username" this.passwordSelector = options.passwordSelector ?? "#j_password" this.submitSelector = options.submitSelector ?? "#logOnFormSubmit" + this.disableBiometricAuth = options.disableBiometricAuthentication ?? false + if (this.disableBiometricAuth) { + if (!options.idpDomain) { + throw new Error( + "idpDomain is required when disableBiometricAuthentication is set to `true` your wdi5.conf.(j|t)s" + ) + } + this.idpDomain = options.idpDomain + } + } + + async disableBiometricAuthentication() { + await this.browserInstance.setCookies({ + name: "skipPasswordlessAuthnDeviceConfig", + value: "true", + domain: this.idpDomain + }) } async login() { diff --git a/src/lib/wdi5-bridge.ts b/src/lib/wdi5-bridge.ts index 5ee9aff2..d2faa9d6 100644 --- a/src/lib/wdi5-bridge.ts +++ b/src/lib/wdi5-bridge.ts @@ -19,6 +19,7 @@ import { clientSide_allControls } from "../../client-side-js/allControls.cjs" 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" import BasicAuthenticator from "./authentication/BasicAuthenticator.js" import CustomAuthenticator from "./authentication/CustomAuthenticator.js" import Office365Authenticator from "./authentication/Office365Authenticator.js" @@ -116,6 +117,9 @@ export async function injectUI5(config: wdi5Config, browserInstance) { const waitForUI5Timeout = config.wdi5?.waitForUI5Timeout || 15000 let result = true + // unify timeouts across Node.js- and browser-scope + await (browserInstance as WebdriverIO.Browser).setTimeout({ script: waitForUI5Timeout }) + const version = await (browserInstance as WebdriverIO.Browser).getUI5Version() await checkUI5Version(version) await clientSide_injectTools(browserInstance) // helpers for wdi5 browser scope @@ -155,7 +159,11 @@ export async function checkForUI5Page(browserInstance) { export async function authenticate(options, browserInstanceName?) { switch (options.provider) { case "BTP": - await new BTPAuthenticator(options, browserInstanceName).login() + const btp = new BTPAuthenticator(options, browserInstanceName) + if ((options as BTPAuthenticatorType).disableBiometricAuthentication) { + await btp.disableBiometricAuthentication() + } + await btp.login() break case "BasicAuth": await new BasicAuthenticator(browserInstanceName).login() diff --git a/src/lib/wdi5-fe.ts b/src/lib/wdi5-fe.ts index abd590f7..1e84d98b 100644 --- a/src/lib/wdi5-fe.ts +++ b/src/lib/wdi5-fe.ts @@ -23,14 +23,52 @@ function createProxy(myObj: any, type: string, methodCalls: any[], pageKeys: str return thisProxy } export class WDI5FE { + onTheShell: any + constructor( private appConfig: any, - private browserInstance: any - ) {} + private browserInstance: any, + private shell?: any + ) { + this.onTheShell = { + iNavigateBack: async () => { + await this.toShell() + // eslint-disable-next-line @typescript-eslint/no-unused-vars + await this.shell.execute((Given, When, Then) => { + When.onTheShell.iNavigateBack() + }) + await this.toApp() + } + } + } + + async toShell() { + await browser.switchToParentFrame() + } + + async toApp() { + await browser.switchToFrame(0) + } + static async initialize(appConfig, browserInstance = browser) { + // first magic wand wave -> app context await loadFELibraries(browserInstance) await initOPA(appConfig, browserInstance) - return new WDI5FE(appConfig, browserInstance) + + // second magic wand wave -> shell context + await browser.switchToParentFrame() + const shellConfig = { + onTheShell: { + Shell: {} + } + } + const shell = new WDI5FE(shellConfig, browserInstance) + await loadFELibraries(browserInstance) + await initOPA(shellConfig, browserInstance) + + // back to app + await browser.switchToFrame(0) + return new WDI5FE(appConfig, browserInstance, shell) } async execute(fnFunction) { diff --git a/src/service.ts b/src/service.ts index 603632ea..9b0ac75e 100644 --- a/src/service.ts +++ b/src/service.ts @@ -45,19 +45,54 @@ export default class Service implements Services.ServiceInstance { } else { Logger.warn("skipped wdi5 injection!") } + if (this._config.wdi5.btpWorkZoneEnablement) { + await this.enableBTPWorkZoneStdEdition(browser[name]) + } } } else { if (this._capabilities["wdi5:authentication"]) { await authenticate(this._capabilities["wdi5:authentication"]) } - if (!this._config.wdi5.skipInjectUI5OnStart) { + if (!this._config.wdi5.skipInjectUI5OnStart && !this._config.wdi5.btpWorkZoneEnablement) { await this.injectUI5(browser) - } else { + } + + if (this._config.wdi5.skipInjectUI5OnStart) { Logger.warn("skipped wdi5 injection!") + } else if (this._config.wdi5.btpWorkZoneEnablement) { + Logger.info("delegating wdi5 injection to Work Zone enablement...") + await this.enableBTPWorkZoneStdEdition(browser) + } else { + await this.injectUI5(browser) } } } + /** + * waits until btp's wz std ed iframe containing the target app is available, + * switches the browser context into the iframe + * and eventually injects the wdi5 into the target app + */ + async enableBTPWorkZoneStdEdition(browser) { + await $("iframe").waitForExist() //> wz only has a single iframe (who's id is also not updated upon subsequent app navigation) + + await browser.switchToFrame(null) + if (this._config.wdi5.skipInjectUI5OnStart) { + Logger.warn("also skipped wdi5 injection in WorkZone std ed's shell!") + } else { + await this.injectUI5(browser) + Logger.debug("injected wdi5 into the WorkZone std ed's shell!") + } + + await browser.switchToFrame(0) + if (this._config.wdi5.skipInjectUI5OnStart) { + Logger.warn("also skipped wdi5 injection in application iframe!") + } else { + await this.injectUI5(browser) + Logger.debug("injected wdi5 into the WorkZone std ed's iframe containing the target app!") + } + } + /** * this is a helper function to late-inject ui5 at test-time * it relays the the wdio configuration (set in the .before() hook to the browser.options parameter by wdio) diff --git a/src/types/wdi5.types.ts b/src/types/wdi5.types.ts index 6ee1db2f..fb136378 100644 --- a/src/types/wdi5.types.ts +++ b/src/types/wdi5.types.ts @@ -48,6 +48,11 @@ export interface wdi5Config extends WebdriverIO.Config { * maximum waiting time while checking for UI5 (control) availability */ waitForUI5Timeout?: number + /** + * indicated whether wdi5 operates on an app + * in a SAP Build Workzone Standard Edition (fka Launchpad) environment + */ + btpWorkZoneEnablement?: boolean } capabilities: wdi5Capabilites[] | wdi5MultiRemoteCapability } @@ -67,6 +72,16 @@ export type BTPAuthenticator = { usernameSelector?: string passwordSelector?: string submitSelector?: string + /** + * set this, when IAS is in use as custom IdP + * IAS provides this biometric login option which wdi5 as of now does not support for authentication + */ + disableBiometricAuthentication?: boolean + /** + * set this when `disableBiometricAuthentication` is set to `true` + * the domain of the custom IdP, e.g. "your-IAS-tenant.accounts.ondemand.com" + */ + idpDomain?: string } export type BasicAuthAuthenticator = { diff --git a/src/wdi5.ts b/src/wdi5.ts index 21417c5a..7ef93b5b 100644 --- a/src/wdi5.ts +++ b/src/wdi5.ts @@ -11,6 +11,18 @@ export class wdi5 { return Logger.getInstance(sPrefix) } + static wz = new Proxy(this, { + get(target, prop, receiver) { + browser.switchToParentFrame() + + // eslint-disable-next-line prefer-rest-params + console.log("GET", ...arguments) + Reflect.get(odatav4Lib, prop, receiver) + + browser.switchToFrame(0) + } + }) + /** * expose the current authentication status * From 0b71de2bde6184b64e81f26a5605b5b46913a948 Mon Sep 17 00:00:00 2001 From: Volker Buzek Date: Mon, 28 Aug 2023 11:59:30 +0200 Subject: [PATCH 02/17] wip: wz property on wdi5 object --- .../test/e2e/workzone/journey.test.ts | 6 +++--- src/wdi5.ts | 19 ++++++++++--------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/examples/ui5-ts-app/test/e2e/workzone/journey.test.ts b/examples/ui5-ts-app/test/e2e/workzone/journey.test.ts index 2371f167..48ccc057 100644 --- a/examples/ui5-ts-app/test/e2e/workzone/journey.test.ts +++ b/examples/ui5-ts-app/test/e2e/workzone/journey.test.ts @@ -46,11 +46,11 @@ describe("drive in Work Zone", () => { // When.onTheShell.iNavigateBack() // beh, b/c wrong iframe }) - // await FioriElementsFacade.onTheShell.iNavigateBack() - // await FioriElementsFacade.onTheShell.iNavigateBack() + await FioriElementsFacade.onTheShell.iNavigateBack() + await FioriElementsFacade.onTheShell.iNavigateBack() // we want: - await wdi5.wz.iNavigateBack() + // await wdi5.wz.iNavigateBack() await FioriElementsFacade.execute((Given, When, Then) => { Then.onTheMainPage.iSeeThisPage() diff --git a/src/wdi5.ts b/src/wdi5.ts index 7ef93b5b..849f8e9d 100644 --- a/src/wdi5.ts +++ b/src/wdi5.ts @@ -11,17 +11,18 @@ export class wdi5 { return Logger.getInstance(sPrefix) } - static wz = new Proxy(this, { - get(target, prop, receiver) { - browser.switchToParentFrame() + //// not yet + // static wz = new Proxy(this, { + // get(target, prop, receiver) { + // browser.switchToParentFrame() - // eslint-disable-next-line prefer-rest-params - console.log("GET", ...arguments) - Reflect.get(odatav4Lib, prop, receiver) + // // eslint-disable-next-line prefer-rest-params + // console.log("GET", ...arguments) + // Reflect.get(odatav4Lib, prop, receiver) - browser.switchToFrame(0) - } - }) + // browser.switchToFrame(0) + // } + // }) /** * expose the current authentication status From 22831a8f15f770b3df13aee7b2542b2820603838 Mon Sep 17 00:00:00 2001 From: Volker Buzek Date: Mon, 28 Aug 2023 17:51:04 +0200 Subject: [PATCH 03/17] test: fix workzone tests --- examples/ui5-ts-app/package.json | 1 + .../e2e/workzone/wdio-ui5-workzone.conf.ts | 7 +- package-lock.json | 156 +----------------- package.json | 2 +- 4 files changed, 7 insertions(+), 159 deletions(-) diff --git a/examples/ui5-ts-app/package.json b/examples/ui5-ts-app/package.json index a67a4071..13921328 100644 --- a/examples/ui5-ts-app/package.json +++ b/examples/ui5-ts-app/package.json @@ -15,6 +15,7 @@ "//test-h:authentication": "run-s \"authentication:* -- --headless\"", "test:authentication:bstack": "BROWSERSTACK=true run-s authentication:*", "test:lateInject": "wdio run wdio-ui5-late.conf.ts", + "test:wz": "wdio run test/e2e/workzone/wdio-ui5-workzone.conf.ts", "authentication:btp": "wdio run test/e2e/authentication/wdio-btp-authentication.conf.ts", "authentication:basic-auth": "wdio run test/e2e/authentication/wdio-basic-auth-authentication.conf.ts", "authentication:custom": "wdio run test/e2e/authentication/wdio-custom-authentication.conf.ts", diff --git a/examples/ui5-ts-app/test/e2e/workzone/wdio-ui5-workzone.conf.ts b/examples/ui5-ts-app/test/e2e/workzone/wdio-ui5-workzone.conf.ts index 3a4b470b..d7260910 100644 --- a/examples/ui5-ts-app/test/e2e/workzone/wdio-ui5-workzone.conf.ts +++ b/examples/ui5-ts-app/test/e2e/workzone/wdio-ui5-workzone.conf.ts @@ -1,4 +1,5 @@ import { wdi5Config } from "wdio-ui5-service" +import { resolve } from "path" process.env.wdi5_username = process.env.wdi5_wz_username process.env.wdi5_password = process.env.wdi5_wz_password @@ -10,10 +11,10 @@ export const config: wdi5Config = { }, baseUrl: "https://cswdev.launchpad.cfapps.eu10.hana.ondemand.com/site/csw#travel-process", - services: ["chromedriver", "ui5"], + services: ["ui5"], - specs: ["./test/e2e/workzone/*.test.ts"], - exclude: ["./test/e2e/*.test.ts"], + specs: [resolve("./test/e2e/workzone/*.test.ts")], + exclude: [resolve("./test/e2e/*.test.ts")], maxInstances: 10, capabilities: [ diff --git a/package-lock.json b/package-lock.json index 7f62a0a9..c629c160 100644 --- a/package-lock.json +++ b/package-lock.json @@ -74,8 +74,6 @@ "@wdio/local-runner": "^8", "@wdio/mocha-framework": "^8", "@wdio/spec-reporter": "^8", - "chromedriver": "latest", - "wdio-chromedriver-service": "^8", "wdio-ui5-service": "*" } }, @@ -84,13 +82,12 @@ "version": "0.0.1", "devDependencies": { "@sap-ux/ui5-middleware-fe-mockserver": "latest", + "@ui5/cli": "^3", "@wdio/cli": "^8", "@wdio/local-runner": "^8", "@wdio/mocha-framework": "^8", "@wdio/spec-reporter": "^8", - "chromedriver": "latest", "detect-libc": "^2.0.1", - "wdio-chromedriver-service": "^8", "wdio-ui5-service": "*" } }, @@ -4064,12 +4061,6 @@ "node": ">=10.17" } }, - "node_modules/@testim/chrome-version": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@testim/chrome-version/-/chrome-version-1.1.3.tgz", - "integrity": "sha512-g697J3WxV/Zytemz8aTuKjTGYtta9+02kva3C1xc7KXB8GdbfE1akGJIsZLyY/FSh2QrnE+fiB7vmWU3XNcb6A==", - "dev": true - }, "node_modules/@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", @@ -13350,52 +13341,6 @@ "node": ">=12.13.0" } }, - "node_modules/chromedriver": { - "version": "116.0.0", - "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-116.0.0.tgz", - "integrity": "sha512-/TQaRn+RUAYnVqy5Vx8VtU8DvtWosU8QLM2u7BoNM5h55PRQPXF/onHAehEi8Sj/CehdKqH50NFdiumQAUr0DQ==", - "dev": true, - "hasInstallScript": true, - "dependencies": { - "@testim/chrome-version": "^1.1.3", - "axios": "^1.4.0", - "compare-versions": "^6.0.0", - "extract-zip": "^2.0.1", - "https-proxy-agent": "^5.0.1", - "proxy-from-env": "^1.1.0", - "tcp-port-used": "^1.0.1" - }, - "bin": { - "chromedriver": "bin/chromedriver" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/chromedriver/node_modules/axios": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz", - "integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==", - "dev": true, - "dependencies": { - "follow-redirects": "^1.15.0", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, - "node_modules/chromedriver/node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", - "dev": true, - "dependencies": { - "agent-base": "6", - "debug": "4" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/chromium-bidi": { "version": "0.4.16", "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.4.16.tgz", @@ -13660,12 +13605,6 @@ "dot-prop": "^5.1.0" } }, - "node_modules/compare-versions": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-6.1.0.tgz", - "integrity": "sha512-LNZQXhqUvqUTotpZ00qLSaify3b4VFD588aRr8MKFw4CMUr98ytzCW5wDH5qx/DEY5kCDXcbcRuCqL0szEf2tg==", - "dev": true - }, "node_modules/compress-commons": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.1.tgz", @@ -19209,15 +19148,6 @@ "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.8.tgz", "integrity": "sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==" }, - "node_modules/ip-regex": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.3.0.tgz", - "integrity": "sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/is-array-buffer": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", @@ -19666,12 +19596,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-url": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", - "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==", - "dev": true - }, "node_modules/is-utf8": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", @@ -19706,20 +19630,6 @@ "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==", "dev": true }, - "node_modules/is2": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/is2/-/is2-2.0.9.tgz", - "integrity": "sha512-rZkHeBn9Zzq52sd9IUIV3a5mfwBY+o2HePMh0wkGBM4z4qjvy2GwVxQ6nNXSfw6MmVP6gf1QIlWjiOavhM3x5g==", - "dev": true, - "dependencies": { - "deep-is": "^0.1.3", - "ip-regex": "^4.1.0", - "is-url": "^1.2.4" - }, - "engines": { - "node": ">=v0.10.0" - } - }, "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -26032,39 +25942,6 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, - "node_modules/tcp-port-used": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/tcp-port-used/-/tcp-port-used-1.0.2.tgz", - "integrity": "sha512-l7ar8lLUD3XS1V2lfoJlCBaeoaWo/2xfYt81hM7VlvR4RrMVFqfmzfhLVk40hAb368uitje5gPtBRL1m/DGvLA==", - "dev": true, - "dependencies": { - "debug": "4.3.1", - "is2": "^2.0.6" - } - }, - "node_modules/tcp-port-used/node_modules/debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/tcp-port-used/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, "node_modules/temp-fs": { "version": "0.9.9", "resolved": "https://registry.npmjs.org/temp-fs/-/temp-fs-0.9.9.tgz", @@ -27179,37 +27056,6 @@ "defaults": "^1.0.3" } }, - "node_modules/wdio-chromedriver-service": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/wdio-chromedriver-service/-/wdio-chromedriver-service-8.1.1.tgz", - "integrity": "sha512-pN3GiOkTIMnalfq4PJAHdX95pDp1orHnTY8W1fIbd6ok81ba97UjerTgS7lUDRUh1p0MAm35Ww0uc0/9wzB7SA==", - "dev": true, - "dependencies": { - "@wdio/logger": "^8.1.0", - "fs-extra": "^11.1.0", - "split2": "^4.1.0", - "tcp-port-used": "^1.0.2" - }, - "engines": { - "node": "^16.13 || >=18" - }, - "peerDependencies": { - "@wdio/types": "^7.0.0 || ^8.0.0-alpha.219", - "chromedriver": "*", - "webdriverio": "^7.0.0 || ^8.0.0-alpha.219" - }, - "peerDependenciesMeta": { - "@wdio/types": { - "optional": true - }, - "chromedriver": { - "optional": true - }, - "webdriverio": { - "optional": false - } - } - }, "node_modules/wdio-ui5-service": { "resolved": "", "link": true diff --git a/package.json b/package.json index 9322fef0..eac74322 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "//test:protocol": "runs the same test with webdriver and devtools protocol", "test:protocol": "npm run test:protocol -w examples/ui5-ts-app", "//test:wz": "specific tests for SAP Build Workzone, standard edition", - "test:wz": "wdio run test/e2e/workzone/wdio-ui5-workzone.conf.ts", + "test:wz": "npm run test:wz --workspace=examples/ui5-ts-app -- --headless", "test": "cross-env TS_NODE_PROJECT=\"test/tsconfig.json\" mocha", "prepare": "husky install", "release": "standard-version", From d1e1bc3f6716d523f6d9662e9f0eec4da98cae66 Mon Sep 17 00:00:00 2001 From: Volker Buzek Date: Tue, 29 Aug 2023 11:08:53 +0200 Subject: [PATCH 04/17] fix: wdi5 injection sequence --- src/service.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/service.ts b/src/service.ts index 9b0ac75e..48505e76 100644 --- a/src/service.ts +++ b/src/service.ts @@ -40,22 +40,20 @@ export default class Service implements Services.ServiceInstance { if (this._capabilities[name].capabilities["wdi5:authentication"]) { await authenticate(this._capabilities[name].capabilities["wdi5:authentication"], name) } - if (!this._config.wdi5.skipInjectUI5OnStart) { - await this.injectUI5(browser[name]) - } else { + + if (this._config.wdi5.skipInjectUI5OnStart) { Logger.warn("skipped wdi5 injection!") - } - if (this._config.wdi5.btpWorkZoneEnablement) { + } else if (this._config.wdi5.btpWorkZoneEnablement) { + Logger.info("delegating wdi5 injection to Work Zone enablement...") await this.enableBTPWorkZoneStdEdition(browser[name]) + } else { + await this.injectUI5(browser[name]) } } } else { if (this._capabilities["wdi5:authentication"]) { await authenticate(this._capabilities["wdi5:authentication"]) } - if (!this._config.wdi5.skipInjectUI5OnStart && !this._config.wdi5.btpWorkZoneEnablement) { - await this.injectUI5(browser) - } if (this._config.wdi5.skipInjectUI5OnStart) { Logger.warn("skipped wdi5 injection!") From 6939fe0cf0c6c82abc5b4ddd4742261e16c8b69a Mon Sep 17 00:00:00 2001 From: Volker Buzek Date: Tue, 29 Aug 2023 11:09:26 +0200 Subject: [PATCH 05/17] fix(wz): inject v4 test lib into right frames --- src/lib/wdi5-fe.ts | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/src/lib/wdi5-fe.ts b/src/lib/wdi5-fe.ts index 1e84d98b..39f7847c 100644 --- a/src/lib/wdi5-fe.ts +++ b/src/lib/wdi5-fe.ts @@ -1,3 +1,4 @@ +import { Element } from "webdriverio" import { initOPA, addToQueue, emptyQueue, loadFELibraries } from "../../client-side-js/testLibrary.cjs" import { Logger as _Logger } from "./Logger.js" const Logger = _Logger.getInstance() @@ -56,18 +57,28 @@ export class WDI5FE { await initOPA(appConfig, browserInstance) // second magic wand wave -> shell context - await browser.switchToParentFrame() - const shellConfig = { - onTheShell: { - Shell: {} + // yet only wave the wand when there's an iframe somewhere, + // indicating BTP WorkZone territory + await browserInstance.switchToParentFrame() + // @ts-ignore + const iframe: Element = await browserInstance.findElement("css selector", "iframe") + let shell + if (!iframe.error) { + const shellConfig = { + onTheShell: { + Shell: {} + } } - } - const shell = new WDI5FE(shellConfig, browserInstance) - await loadFELibraries(browserInstance) - await initOPA(shellConfig, browserInstance) + shell = new WDI5FE(shellConfig, browserInstance) + await loadFELibraries(browserInstance) + await initOPA(shellConfig, browserInstance) - // back to app - await browser.switchToFrame(0) + // back to app + await browserInstance.switchToFrame(0) + } else { + // revert back to app context + await browserInstance.switchToFrame(null) + } return new WDI5FE(appConfig, browserInstance, shell) } From e4e9351d427d66d9515079b4ef1b281da6b1aa3b Mon Sep 17 00:00:00 2001 From: Volker Buzek Date: Tue, 29 Aug 2023 11:47:30 +0200 Subject: [PATCH 06/17] refactor(wz): split tests in testlib and regular --- .github/workflows/wdi5-tests_fe-app.yml | 4 ++++ .github/workflows/wdi5-tests_ts-app.yml | 4 ++++ package.json | 3 ++- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/wdi5-tests_fe-app.yml b/.github/workflows/wdi5-tests_fe-app.yml index 24eada80..4c7925b0 100644 --- a/.github/workflows/wdi5-tests_fe-app.yml +++ b/.github/workflows/wdi5-tests_fe-app.yml @@ -63,3 +63,7 @@ jobs: # explicit headless mode! - name: test fe-app run: npm run test:fe-app + + # this runs against the deployed CAP SFLIGHT sample app in BTP WorkZone + - name: test lib support for workzone + run: npm run test:wz:testlib diff --git a/.github/workflows/wdi5-tests_ts-app.yml b/.github/workflows/wdi5-tests_ts-app.yml index 6fef62df..577d9ef1 100644 --- a/.github/workflows/wdi5-tests_ts-app.yml +++ b/.github/workflows/wdi5-tests_ts-app.yml @@ -68,3 +68,7 @@ jobs: # this runs against the deployed wdi5 TS sample app - name: test devtools support run: npm run test:protocol + + # this runs against the deployed CAP SFLIGHT sample app in BTP WorkZone + - name: test lib support for workzone + run: npm run test:wz:regular diff --git a/package.json b/package.json index eac74322..f0fc7802 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,8 @@ "//test:protocol": "runs the same test with webdriver and devtools protocol", "test:protocol": "npm run test:protocol -w examples/ui5-ts-app", "//test:wz": "specific tests for SAP Build Workzone, standard edition", - "test:wz": "npm run test:wz --workspace=examples/ui5-ts-app -- --headless", + "test:wz:testlib": "npm run test:wz --workspace=examples/ui5-ts-app -- --headless --spec test/e2e/workzone/testlib-journey.test.ts", + "test:wz:regular": "npm run test:wz --workspace=examples/ui5-ts-app -- --headless --spec test/e2e/workzone/regular-journey.test.ts", "test": "cross-env TS_NODE_PROJECT=\"test/tsconfig.json\" mocha", "prepare": "husky install", "release": "standard-version", From 67c4c14478442eb32f278aa58c737d7964ccdc6d Mon Sep 17 00:00:00 2001 From: Volker Buzek Date: Tue, 29 Aug 2023 11:47:51 +0200 Subject: [PATCH 07/17] ci(ts-app): don't run workzone tests --- examples/ui5-ts-app/wdio-ui5.conf.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/ui5-ts-app/wdio-ui5.conf.ts b/examples/ui5-ts-app/wdio-ui5.conf.ts index 9b31bc22..d93d6f68 100644 --- a/examples/ui5-ts-app/wdio-ui5.conf.ts +++ b/examples/ui5-ts-app/wdio-ui5.conf.ts @@ -16,7 +16,8 @@ export const config: wdi5Config = { "./test/e2e/multiremote.test.ts", "./test/e2e/BasicMultiRemoteAuthentication.test.ts", "./test/e2e/Authentication.test.ts", - "./test/e2e/ui5-late.test.ts" + "./test/e2e/ui5-late.test.ts", + "./test/e2e/workzone/**/*.test.ts" ], maxInstances: 10, From a898188e20d2d142f2ae3c1290ef9966e9e99baf Mon Sep 17 00:00:00 2001 From: Volker Buzek Date: Tue, 29 Aug 2023 11:48:12 +0200 Subject: [PATCH 08/17] refactor: split workzone-related test suites --- .../ui5-ts-app/test/e2e/workzone/regular-journey.test.ts | 5 +++++ .../workzone/{journey.test.ts => testlib-journey.test.ts} | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 examples/ui5-ts-app/test/e2e/workzone/regular-journey.test.ts rename examples/ui5-ts-app/test/e2e/workzone/{journey.test.ts => testlib-journey.test.ts} (96%) diff --git a/examples/ui5-ts-app/test/e2e/workzone/regular-journey.test.ts b/examples/ui5-ts-app/test/e2e/workzone/regular-journey.test.ts new file mode 100644 index 00000000..bec01da7 --- /dev/null +++ b/examples/ui5-ts-app/test/e2e/workzone/regular-journey.test.ts @@ -0,0 +1,5 @@ +describe.skip("drive in Work Zone with standard wdi5/wdio APIs", () => { + it("should see the List Report page", async () => {}) + + it("should see the Object Pages load and then returns to list", async () => {}) +}) diff --git a/examples/ui5-ts-app/test/e2e/workzone/journey.test.ts b/examples/ui5-ts-app/test/e2e/workzone/testlib-journey.test.ts similarity index 96% rename from examples/ui5-ts-app/test/e2e/workzone/journey.test.ts rename to examples/ui5-ts-app/test/e2e/workzone/testlib-journey.test.ts index 48ccc057..66ad01c0 100644 --- a/examples/ui5-ts-app/test/e2e/workzone/journey.test.ts +++ b/examples/ui5-ts-app/test/e2e/workzone/testlib-journey.test.ts @@ -1,4 +1,4 @@ -describe("drive in Work Zone", () => { +describe("drive in Work Zone with testlib support", () => { let FioriElementsFacade before(async () => { FioriElementsFacade = await browser.fe.initialize({ From bc1ef7b2a5ccca77eae368508cb928490088905d Mon Sep 17 00:00:00 2001 From: Volker Buzek Date: Tue, 29 Aug 2023 12:00:34 +0200 Subject: [PATCH 09/17] refactor: move wz tests to auth scope so we make the necessary secrets available external PRs also --- .github/workflows/wdi5-tests_auth.yml | 9 +++++++++ .github/workflows/wdi5-tests_fe-app.yml | 4 ---- .github/workflows/wdi5-tests_ts-app.yml | 4 ---- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/.github/workflows/wdi5-tests_auth.yml b/.github/workflows/wdi5-tests_auth.yml index df8f7f25..e6bd38ac 100644 --- a/.github/workflows/wdi5-tests_auth.yml +++ b/.github/workflows/wdi5-tests_auth.yml @@ -22,6 +22,8 @@ env: wdi5_password: ${{secrets.BTP_PASSWORD}} wdi5_one_password: ${{secrets.BTP_PASSWORD}} wdi5_two_password: ${{secrets.BTP_PASSWORD}} + wdi5_wz_username: ${{secrets.WZ_USER}} + wdi5_wz_password: ${{secrets.WZ_PASSWORD}} BROWSERSTACK_USERNAME: ${{secrets.BROWSERSTACK_USERNAME}} BROWSERSTACK_ACCESS_KEY: ${{secrets.BROWSERSTACK_ACCESS_KEY}} SAUCE_USERNAME: ${{secrets.SAUCE_USERNAME}} @@ -67,3 +69,10 @@ jobs: - name: (browserstack) btp/sap cloud id, basic auth, office 365, custom auth run: BROWSERSTACK=true npm run test:auth + + # these two run against the deployed CAP SFLIGHT sample app in BTP WorkZone + - name: test lib support for workzone + run: npm run test:wz:testlib + + - name: regular support for workzone + run: npm run test:wz:regular diff --git a/.github/workflows/wdi5-tests_fe-app.yml b/.github/workflows/wdi5-tests_fe-app.yml index 4c7925b0..24eada80 100644 --- a/.github/workflows/wdi5-tests_fe-app.yml +++ b/.github/workflows/wdi5-tests_fe-app.yml @@ -63,7 +63,3 @@ jobs: # explicit headless mode! - name: test fe-app run: npm run test:fe-app - - # this runs against the deployed CAP SFLIGHT sample app in BTP WorkZone - - name: test lib support for workzone - run: npm run test:wz:testlib diff --git a/.github/workflows/wdi5-tests_ts-app.yml b/.github/workflows/wdi5-tests_ts-app.yml index 577d9ef1..6fef62df 100644 --- a/.github/workflows/wdi5-tests_ts-app.yml +++ b/.github/workflows/wdi5-tests_ts-app.yml @@ -68,7 +68,3 @@ jobs: # this runs against the deployed wdi5 TS sample app - name: test devtools support run: npm run test:protocol - - # this runs against the deployed CAP SFLIGHT sample app in BTP WorkZone - - name: test lib support for workzone - run: npm run test:wz:regular From 8cad2a353484edacf7816954601c9ead999777b8 Mon Sep 17 00:00:00 2001 From: Volker Buzek Date: Tue, 29 Aug 2023 13:44:28 +0200 Subject: [PATCH 10/17] docs: describe IAS capability --- docs/authentication.md | 70 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git a/docs/authentication.md b/docs/authentication.md index c2a9f744..7748c628 100644 --- a/docs/authentication.md +++ b/docs/authentication.md @@ -3,13 +3,14 @@ `wdi5` currently support these authentication mechanisms and/or providers: - [SAP Cloud IdP (default BTP Identity Provider)](#sap-cloud-idp-default-btp-identity-provider) +- [SAP Cloud Identity Services - Identity Authentication (IAS)](#sap-cloud-identity-services-identity-authentication) - [Office 365](#office-365) - [custom IdP](#custom-idp) - [Basic Authentication](#basic-authentication) Generally speaking, the authentication behavior mimicks that of a regular user session: first, the `baseUrl` (from the `wdio.conf.(j|t)s`-file) is opened in the configured browser. Then, the redirect to the Authentication provider is awaited and [the credentials](#credentials) are supplied. -BTP-, Office365- and custom IdP all supply credentials as a user would, meaning they're literally typed into the respective input fields on each login screen. +BTP-, IAS-, Office365- and custom IdP all supply credentials as a user would, meaning they're literally typed into the respective input fields on each login screen. Basic Authentication prepends username and password in encoded form to the URL, resulting in an `HTTP` `GET` in the form of `https://username:encoded-pwd@your-deployed-UI5.app`. !> Multi-Factor Authentication is not supported as it's nearly impossible to manage any media break (e.g. browser ↔ mobile) in authentication flows out of the box @@ -122,6 +123,73 @@ capabilities: { The `BTP` authenticator will automatically detect whether the login process is a two-step- (first username needs to be supplied, the password) or a single-step (both username and password are supplied on one screen) sequence. +### SAP Cloud Identity Services - Identity Authentication + +?> only available in `wdi5` >= 2 + +Using the 'Identity Authentication Service (IAS) Authenticator' in `wdi5` is a subset of the [above BTP Authentication](#sap-cloud-idp-default-btp-identity-provider). +It takes the same configuration options, plus `disableBiometricAuth` (default: `true`, which you want in almost all cases) and `idpDomain`. The latter is necessary to satisfy cookie conditions in the remote-controlled browser. +Set `idpDomain` to the _domain-only_ part of your IAS tenant URL, e.g. `weiruhg.accounts.ondemand.com`, _omitting_ the protocol prefix (`https://`). + +!> If `disableBiometricAuth` is set to `true`, `idpDomain` must be set as well! + + + +#### **single browser** + +```js +baseUrl: "https://your-deployed-ui5-on-btp.app", +capabilities: { + // browserName: "..." + "wdi5:authentication": { + provider: "BTP", //> mandatory + usernameSelector: "#j_username", //> optional; default: "#j_username" + passwordSelector: "#j_password", //> optional; default: "#j_password" + submitSelector: "#logOnFormSubmit", //> optional; default: "#logOnFormSubmit" + disableBiometricAuth: true, //> optional; default: true + idpDomain: "weiruhg.accounts.ondemand.com", //> mandatory if disableBiometricAuth = true, otherwise optional; no default + } +} +``` + +#### **multiremote** + +```js +baseUrl: "https://your-deployed-ui5-on-btp.app", +capabilities: { + // "one" is the literal reference to a browser instance + one: { + capabilities: { + // browserName: "..." + "wdi5:authentication": { + provider: "BTP", //> mandatory + usernameSelector: "#j_username", //> optional; default: "#j_username" + passwordSelector: "#j_password", //> optional; default: "#j_password" + submitSelector: "#logOnFormSubmit", //> optional; default: "#logOnFormSubmit" + disableBiometricAuth: true, //> optional; default: true + idpDomain: "weiruhg.accounts.ondemand.com", //> mandatory if disableBiometricAuth = true, otherwise optional; no default + } + } + }, + // "two" is the literal reference to a browser instance + two: { + capabilities: { + // browserName: "..." + "wdi5:authentication": { + provider: "BTP", //> mandatory + usernameSelector: "#j_username", //> optional; default: "#j_username" + passwordSelector: "#j_password", //> optional; default: "#j_password" + submitSelector: "#logOnFormSubmit", //> optional; default: "#logOnFormSubmit" + disableBiometricAuth: true, //> optional; default: true + idpDomain: "weiruhg.accounts.ondemand.com", //> mandatory if disableBiometricAuth = true, otherwise optional; no default + } + } + } +} +``` + + + ### Office 365 From be71863796374d31c4b56bdf31927fb689094065 Mon Sep 17 00:00:00 2001 From: Volker Buzek Date: Tue, 29 Aug 2023 14:10:18 +0200 Subject: [PATCH 11/17] docs: workzone enablement + testlib usage [skip ci] --- docs/configuration.md | 8 ++++- docs/fe-testlib.md | 79 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index a89aeef9..0a0f0f7d 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -23,7 +23,8 @@ exports.config = { screenshotsDisabled: false, // [optional] {boolean}, default: false; if set to true, screenshots won't be taken and not written to file system logLevel: "verbose", // [optional] error | verbose | silent, default: "error" skipInjectUI5OnStart: false, // [optional] {boolean}, default: false; true when UI5 is not on the start page, you need to later call .injectUI5() manually - waitForUI5Timeout: 15000 // [optional] {number}, default: 15000; maximum waiting time in milliseconds while checking for UI5 availability + waitForUI5Timeout: 15000, // [optional] {number}, default: 15000; maximum waiting time in milliseconds while checking for UI5 availability + btpWorkZoneEnablement: false // [optional] {boolean}, default: false; whether to instruct wdi5 to inject itself in both the SAP Build Workzone, standard edition, shell and app } // ... } @@ -125,6 +126,11 @@ Number in milliseconds (default: `15000`) to wait for UI5-related operations wit ?> Setting this timeout to 30 seconds or higher requires the [session script timeout](https://webdriver.io/docs/timeouts/#session-script-timeout) to be increased as well. +### `btpWorkZoneEnablement` + +Boolean setting to trigger injecting `wdi5` into both the shell and the app when used with the SAP Build Workzone, standard edition. +Recommended complement is to also [configure IAS Authentication](authentication?id=sap-cloud-identity-services-identity-authentication): as SAP Build requires its own Identity Provider (most likely provided by using an IAS tenant), you'll have to configure authentication against that as well in `wdi5`. + ## `package.json` Not required, but as a convention, put a `test` or `wdi5` script into your UI5.app's `package.json` to start `wdi5/wdio`. diff --git a/docs/fe-testlib.md b/docs/fe-testlib.md index 1cd517f5..1f1f3bd6 100644 --- a/docs/fe-testlib.md +++ b/docs/fe-testlib.md @@ -157,6 +157,85 @@ it("I enter custom data", async () => { }) ``` +## Using the test library with SAP Build Workzone, standard edition + +?> only available in `wdi5` >= 2 + +The SAP Build Workzone, standard edition, runs the Fiori shell and the Fiori Elements app in separate `iframe`s. For operating on both shell and app, `wdi5` injects itself in both as well. This can be triggered by configuring `wdi5` with `btpWorkZoneEnablement` set to `true`: + +```js +// typescript syntax sample +export const config: wdi5Config = { + wdi5: { + btpWorkZoneEnablement: true, + logLevel: "verbose" + }, + // ... additional config +} +``` + +Most likely, a [`wdi5` authentication configuration for IAS](authentication?id=sap-cloud-identity-services-identity-authentication) is also needed to authenticate against the IAS tenant the SAP Build Workzone, standard edition, is running with. + +Then, point the `baseUrl` in your `wdio.conf.(j|t)s` against _the app URL_ in Workzone, e.g. `https://your.launchpad.cfapps.eu10.hana.ondemand.com/site/ymmv#travel-process`: + +```js +export const config: wdi5Config = { + wdi5: { + btpWorkZoneEnablement: true, + logLevel: "verbose" + }, + baseUrl: "https://your.launchpad.cfapps.eu10.hana.ondemand.com/site/ymmv#travel-process", + // ... additional config +} +``` + +?> It is important to use the URL pointing to the app under test, as this is assumed by `wdi5` to be the start of its injection process + +After making `wdi5` aware of the Workzone setting, now inject the testlibrary as usual in the `before` hook of the test suite: + +```js +// sample page obects from the CAP SFLIGHT app +describe("drive in Work Zone with testlib support", () => { + let FioriElementsFacade + before(async () => { + FioriElementsFacade = await browser.fe.initialize({ + onTheMainPage: { + ListReport: { + appId: "sap.fe.cap.travel", + componentId: "TravelList", + entitySet: "Travel" + } + }, + onTheDetailPage: { + ObjectPage: { + appId: "sap.fe.cap.travel", + componentId: "TravelObjectPage", + entitySet: "Travel" + } + }, + onTheItemPage: { + ObjectPage: { + appId: "sap.fe.cap.travel", + componentId: "BookingObjectPage", + entitySet: "Booking" + } + }, + onTheShell: { + Shell: {} + } + }) + }) + + it("...", async () => { + await FioriElementsFacade.execute((Given, When, Then) => { + Then.onTheMainPage.iSeeThisPage() + }) + }) + // further its +``` + +See the files in [`wdi5`'s git repo at `/examples/ui5-ts-app/test/e2e/workzone/**/*`](https://github.com/ui5-community/wdi5/tree/main/examples/ui5-ts-app/test/e2e/workzone) for a sample config and test! + ## Troubleshooting ### Enable verbose mode for test library output From 26cd6f070d201565f86ead0d468e28c503b599b0 Mon Sep 17 00:00:00 2001 From: Volker Buzek Date: Tue, 29 Aug 2023 14:20:06 +0200 Subject: [PATCH 12/17] chore: wording --- examples/ui5-ts-app/test/e2e/workzone/testlib-journey.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/ui5-ts-app/test/e2e/workzone/testlib-journey.test.ts b/examples/ui5-ts-app/test/e2e/workzone/testlib-journey.test.ts index 66ad01c0..628a916f 100644 --- a/examples/ui5-ts-app/test/e2e/workzone/testlib-journey.test.ts +++ b/examples/ui5-ts-app/test/e2e/workzone/testlib-journey.test.ts @@ -49,8 +49,8 @@ describe("drive in Work Zone with testlib support", () => { await FioriElementsFacade.onTheShell.iNavigateBack() await FioriElementsFacade.onTheShell.iNavigateBack() - // we want: - // await wdi5.wz.iNavigateBack() + // REVISIT: we want the testlib to expose its back navigation capability + // ~ could look like: await wdi5.wz.iNavigateBack() await FioriElementsFacade.execute((Given, When, Then) => { Then.onTheMainPage.iSeeThisPage() From 43f99f75160ce479bbd74a99824bc9e2d75367ce Mon Sep 17 00:00:00 2001 From: Volker Buzek Date: Tue, 29 Aug 2023 15:22:32 +0200 Subject: [PATCH 13/17] refactor: only hotwire back nav when necessary --- src/lib/wdi5-fe.ts | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/lib/wdi5-fe.ts b/src/lib/wdi5-fe.ts index 39f7847c..e2f0aa01 100644 --- a/src/lib/wdi5-fe.ts +++ b/src/lib/wdi5-fe.ts @@ -31,14 +31,18 @@ export class WDI5FE { private browserInstance: any, private shell?: any ) { - this.onTheShell = { - iNavigateBack: async () => { - await this.toShell() - // eslint-disable-next-line @typescript-eslint/no-unused-vars - await this.shell.execute((Given, When, Then) => { - When.onTheShell.iNavigateBack() - }) - await this.toApp() + // only in the workzone context + // do we need to hotwire a back navigation on the fiori shell + if (shell) { + this.onTheShell = { + iNavigateBack: async () => { + await this.toShell() + // eslint-disable-next-line @typescript-eslint/no-unused-vars + await this.shell.execute((Given, When, Then) => { + When.onTheShell.iNavigateBack() + }) + await this.toApp() + } } } } From 7e1b37b2ea5e281618886c208b0a889419021fb3 Mon Sep 17 00:00:00 2001 From: Volker Buzek Date: Tue, 29 Aug 2023 15:24:58 +0200 Subject: [PATCH 14/17] docs: tag eventual testlib proxy for revisit --- src/wdi5.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wdi5.ts b/src/wdi5.ts index 849f8e9d..73cd2423 100644 --- a/src/wdi5.ts +++ b/src/wdi5.ts @@ -11,7 +11,7 @@ export class wdi5 { return Logger.getInstance(sPrefix) } - //// not yet + //// REVISIT: not yet/if still needed :) // static wz = new Proxy(this, { // get(target, prop, receiver) { // browser.switchToParentFrame() From 7e1558d375ef9f692200af41fcf59c8c24b8650c Mon Sep 17 00:00:00 2001 From: Volker Buzek Date: Tue, 29 Aug 2023 15:37:27 +0200 Subject: [PATCH 15/17] ci: tame the pipeline --- examples/fe-app/wdio.conf.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/fe-app/wdio.conf.js b/examples/fe-app/wdio.conf.js index 2506b99c..9dd43ac2 100644 --- a/examples/fe-app/wdio.conf.js +++ b/examples/fe-app/wdio.conf.js @@ -4,7 +4,7 @@ exports.config = { wdi5: { screenshotPath: join("app", "incidents", "webapp", "wdi5-test", "__screenshots__"), logLevel: "verbose", // error | verbose | silent - waitForUI5Timeout: 90000 + waitForUI5Timeout: 30000 }, //// wdio runner config specs: [join("webapp", "wdi5-test", "**/*.test.js")], From 2e034d12c2a3b85bd81837262f98e8ff61d5fd58 Mon Sep 17 00:00:00 2001 From: Volker Buzek Date: Tue, 29 Aug 2023 15:37:42 +0200 Subject: [PATCH 16/17] refactor: unify browser timeouts at instance level --- src/lib/wdi5-bridge.ts | 12 ++++++++++-- src/service.ts | 10 ---------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/lib/wdi5-bridge.ts b/src/lib/wdi5-bridge.ts index d2faa9d6..03d2bf2f 100644 --- a/src/lib/wdi5-bridge.ts +++ b/src/lib/wdi5-bridge.ts @@ -109,7 +109,7 @@ function checkUI5Version(ui5Version: string) { } /** - * function library to setup the webdriver to UI5 bridge, it runs alle the initial setup + * function library to setup the webdriver to UI5 bridge, it runs all 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' */ @@ -118,7 +118,15 @@ export async function injectUI5(config: wdi5Config, browserInstance) { let result = true // unify timeouts across Node.js- and browser-scope - await (browserInstance as WebdriverIO.Browser).setTimeout({ script: waitForUI5Timeout }) + // align browser script timeout with wdi5 setting (+ leverage) + // this mostly affects browser.executeAsync() + const timeout = waitForUI5Timeout + 1000 + await (browserInstance as WebdriverIO.Browser).setTimeout({ script: timeout }) + + Logger.debug(`browser script timeout set to ${timeout}`) + if (typeof browserInstance.getTimeouts === "function") { + Logger.debug(`browser timeouts are ${JSON.stringify(await browserInstance.getTimeouts(), null, 2)}`) + } const version = await (browserInstance as WebdriverIO.Browser).getUI5Version() await checkUI5Version(version) diff --git a/src/service.ts b/src/service.ts index 48505e76..87776a3d 100644 --- a/src/service.ts +++ b/src/service.ts @@ -25,16 +25,6 @@ export default class Service implements Services.ServiceInstance { await setup(this._config) Logger.info("setup complete") - // align browser script timeout with wdi5 setting (+ leverage) - // this mostly affects browser.executeAsync() - const timeout = (this._config["wdi5"]["waitForUI5Timeout"] || 15000) + 5000 - await browser.setTimeout({ script: timeout }) - - Logger.debug(`browser script timeout set to ${timeout}`) - if (typeof browser.getTimeouts === "function") { - Logger.debug(`browser timeouts are ${JSON.stringify(await browser.getTimeouts(), null, 2)}`) - } - if (browser.isMultiremote) { for (const name of (browser as unknown as MultiRemoteBrowser).instances) { if (this._capabilities[name].capabilities["wdi5:authentication"]) { From 9e851dc89e1146708464ea52f7b18f3b5756dbc1 Mon Sep 17 00:00:00 2001 From: Volker Buzek Date: Tue, 29 Aug 2023 16:13:48 +0200 Subject: [PATCH 17/17] fix: ensure cjs dir exists --- .retrofit-pkg-json.js | 1 + 1 file changed, 1 insertion(+) diff --git a/.retrofit-pkg-json.js b/.retrofit-pkg-json.js index 15cbec0c..cf6369cb 100644 --- a/.retrofit-pkg-json.js +++ b/.retrofit-pkg-json.js @@ -5,5 +5,6 @@ import pkgJson from "./package.json" assert { type: "json" } delete pkgJson[section] }) ;(async () => { + await fs.mkdir("./cjs", { recursive: true }) await fs.writeFile("./cjs/package.json", JSON.stringify(pkgJson, null, 2)) })()