Skip to content

Commit

Permalink
feat: sap build workzone std ed enablement, ias authentication capabi…
Browse files Browse the repository at this point in the history
…lity (#520)

closes #378
  • Loading branch information
vobu committed Aug 29, 2023
1 parent a95f57f commit 8f6c217
Show file tree
Hide file tree
Showing 20 changed files with 472 additions and 196 deletions.
9 changes: 9 additions & 0 deletions .github/workflows/wdi5-tests_auth.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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}}
Expand Down Expand Up @@ -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
1 change: 1 addition & 0 deletions .retrofit-pkg-json.js
Original file line number Diff line number Diff line change
Expand Up @@ -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))
})()
70 changes: 69 additions & 1 deletion docs/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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!

<!-- tabs:start -->

#### **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
}
}
}
}
```

<!-- tabs:end -->

### Office 365

<!-- tabs:start -->
Expand Down
8 changes: 7 additions & 1 deletion docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <wdioUI5service>.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
}
// ...
}
Expand Down Expand Up @@ -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`.
Expand Down
79 changes: 79 additions & 0 deletions docs/fe-testlib.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion examples/fe-app/wdio.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -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")],
Expand Down
1 change: 1 addition & 0 deletions examples/ui5-ts-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
36 changes: 20 additions & 16 deletions examples/ui5-ts-app/test/e2e/Input.test.ts
Original file line number Diff line number Diff line change
@@ -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<Input>(inputText)
const input = await browser.asControl<Input>(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<Input>(inputSelector).setValue(newValue)
const input = await browser.asControl<Input>(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")
})
})
5 changes: 5 additions & 0 deletions examples/ui5-ts-app/test/e2e/workzone/regular-journey.test.ts
Original file line number Diff line number Diff line change
@@ -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 () => {})
})
59 changes: 59 additions & 0 deletions examples/ui5-ts-app/test/e2e/workzone/testlib-journey.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
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("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()

// 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()
})
})
})

0 comments on commit 8f6c217

Please sign in to comment.