Skip to content

feat(testing): add wdi5 e2e scope #23

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 16 commits into
base: testing
Choose a base branch
from
2 changes: 1 addition & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": ["./tsconfig.json"],
"project": ["./tsconfig.json", "./webapp/test/e2e/tsconfig.json"],
"sourceType": "module"
},
"plugins": [
Expand Down
31 changes: 20 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,25 @@ This app demonstrates a TypeScript setup for developing UI5 applications. For ge

:construction: **WORK IN PROGRESS** :construction:

* [Unit tests (QUnit)](./webapp/test/unit/) look good: run `npm start` and open http://localhost:8080/test/unit/unitTests.qunit.html
* OPA tests still need additional boilerplate code and do NOT represent the final state! OPA APIs do not fit a typed language very well. Type definitions as well as APIs are being improved. Nevertheless, the OPA test code in this repository does work and is streamlined to some degree. The integration tests can be run at http://localhost:8080/test/integration/opaTests.qunit.html.
- [Unit tests (QUnit)](./webapp/test/unit/) look good: run `npm start` and open http://localhost:8080/test/unit/unitTests.qunit.html
- OPA tests still need additional boilerplate code and do NOT represent the final state! OPA APIs do not fit a typed language very well. Type definitions as well as APIs are being improved. Nevertheless, the OPA test code in this repository does work and is streamlined to some degree. The integration tests can be run at http://localhost:8080/test/integration/opaTests.qunit.html.
- [End-to-end tests](./webapp/test/e2e/) are implemented with [`wdi5`](https://github.com/ui5-community/wdi5) can be run via
- (Terminal 1) `npm start`
- (Terminal 2) `npm run wdi5`

## Testing

### General Setup

The tests can be executed either manually or in an automated way using Karma. The setup provides the following options:
The tests can be executed either manually or in an automated way using Karma/`wdi5`. The setup provides the following options:

1. *Manual execution* using `npm start` and execute the tests by opening the [testsuite](http://localhost:8080/test/testsuite.qunit.html) in your browser (but you can also open the QUnit or Integration testsuite directly)
2. *Test-driven* development by running Karma in watch mode using `npm run karma` (which triggers the test each time a source file changes)
3. *Headless testing* by running Karma either without coverage reporting using `npm run karma-ci` or with using `npm run karma-ci-cov`
1. _Manual execution_ using `npm start` and ...
- (Unit/Integration) execute the tests by opening the [testsuite](http://localhost:8080/test/testsuite.qunit.html) in your browser (but you can also open the QUnit or Integration testsuite directly)
- (end-to-end) execute the tests by running `npm run wdi5`
2. _Test-driven_ development by running Karma in watch mode using `npm run karma` (which triggers the test each time a source file changes)
3. _Headless testing_
- (Unit/Integration) by running Karma either without coverage reporting using `npm run karma-ci` or with using `npm run karma-ci-cov`
- (end-to-end) `npm run wdi5 -- --headless`

### Unit Tests (QUnit)

Expand All @@ -40,23 +47,23 @@ In the (very minimal) actual tests in [`webapp/test/unit/controller/App.qunit.ts
### Integration Tests (OPA)

**IMPORTANT:**
*The OPA tests in TypeScript are experimental work in progress! This means they **do** work as shown in this sample project, **but** the UI5 team is working on making the OPA APIs work better with TypeScript. Therefore, it is expected that the recommended way of writing OPA tests in TypeScript will change and be simplified over time!*
_The OPA tests in TypeScript are experimental work in progress! This means they **do** work as shown in this sample project, **but** the UI5 team is working on making the OPA APIs work better with TypeScript. Therefore, it is expected that the recommended way of writing OPA tests in TypeScript will change and be simplified over time!_

#### The entry point and the list of tests

Just like for the unit tests, [`webapp/test/integration/opaTests.qunit.html`](webapp/test/integration/opaTests.qunit.html) is the entry point for running the OPA tests, which loads the list of journeys configured in [`webapp/test/integration/opaTests.qunit.ts`](webapp/test/integration/opaTests.qunit.ts).

#### The "App" page object

The page objects are written as *classes* extending `Opa5`. Inside such classes the actions and assertions are defined as instance methods. Apart from these changes, the implementation of the actions and assertions is done just like in plain JavaScript.
The page objects are written as _classes_ extending `Opa5`. Inside such classes the actions and assertions are defined as instance methods. Apart from these changes, the implementation of the actions and assertions is done just like in plain JavaScript.

#### The "Hello" Journey

The test journey [`webapp/test/integration/HelloJourney.ts`](webapp/test/integration/HelloJourney.ts) is actually pretty straightforward. The main differences are the following:

* Page objects are imported and an instance is being created
* `Given`/`When`/`Then` parameters in the `opaTest(...)` callbacks are omitted
* On the page object the actions and assertions are called
- Page objects are imported and an instance is being created
- `Given`/`When`/`Then` parameters in the `opaTest(...)` callbacks are omitted
- On the page object the actions and assertions are called

## Requirements

Expand Down Expand Up @@ -93,6 +100,8 @@ As shown in the terminal after executing this command, the app is then running o

Open http://localhost:8080/test/unit/unitTests.qunit.html to run the QUnit tests.

Execute `npm run wdi5` to run the end-to-end tests.

## Limitations

The ways how OPA tests are written in TypeScript are still being improved. There might be a different option in the future but the current option continues to work!
Expand Down
22 changes: 14 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,26 +15,32 @@
"karma": "karma start",
"karma-ci": "karma start karma-ci.conf.js",
"karma-ci-cov": "karma start karma-ci-cov.conf.js",
"test": "npm run lint && npm run karma-ci-cov"
"test": "npm run lint && npm run karma-ci-cov",
"wdi5": "wdio run ./webapp/test/e2e/wdio.conf.ts"
},
"repository": {
"type": "git",
"url": "https://github.com/SAP-samples/ui5-typescript-helloworld.git"
},
"devDependencies": {
"@types/openui5": "1.116.1",
"@typescript-eslint/eslint-plugin": "^6.3.0",
"@typescript-eslint/parser": "^6.3.0",
"@ui5/cli": "^3.4.0",
"eslint": "^8.46.0",
"@typescript-eslint/eslint-plugin": "^6.7.0",
"@typescript-eslint/parser": "^6.7.0",
"@ui5/cli": "^3.6.0",
"@wdio/cli": "^8",
"@wdio/local-runner": "^8",
"@wdio/mocha-framework": "^8",
"@wdio/spec-reporter": "^8",
"eslint": "^8.49.0",
"karma": "^6.4.2",
"karma-chrome-launcher": "^3.2.0",
"karma-coverage": "^2.2.1",
"karma-ui5": "^3.0.3",
"karma-ui5-transpile": "^3.0.1",
"karma-ui5-transpile": "^3.2.3",
"rimraf": "^5.0.1",
"typescript": "^5.1.6",
"ts-node": "^10.9.1",
"ui5-middleware-livereload": "^3.0.0",
"ui5-tooling-transpile": "^3.0.1"
"ui5-tooling-transpile": "^3.2.3",
"wdio-ui5-service": "^2"
}
}
18 changes: 8 additions & 10 deletions step-by-step.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,11 @@ Now, let's get the TypeScript compiler and the UI5 type definitions:
npm install --save-dev typescript @types/[email protected]
```

When you are developing a SAPUI5 application (i.e. also using control libraries which are not available in OpenUI5), use the `@sapui5/ts-types-esm` types instead of the `@types/openui5` ones.
When you are developing a SAPUI5 application (i.e. also using control libraries which are not available in OpenUI5), use the `@sapui5/types` types instead of the `@types/openui5` ones.

> **Remark:** There are also `@openui5/ts-types-esm` types available - how do they differ from the `@types/openui5` ones?<br>
The one difference is in versioning: while the types in the `@openui5` namespace are exactly in sync with the respective OpenUI5 patch release, the ones in the `@types` namespace follow the DefinitelyTyped versioning and are only released *once* per minor release of OpenUI5 ([more details here](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/openui5#versioning)), not for every patch. In practice it shouldn't make any difference what you use, but note that in the `@types` namespace there is usually only the `*.*.0` patch release available.<br>
The other small difference is [described in detail here](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/openui5#jquery-and-qunit-references-and-their-versions). In essence, UI5 declares the jQuery and QUnit types as dependencies to make sure the type definitions are also loaded because types from those libraries are in some places exposed in UI5 APIs. The difference is that for `@types/openui5` the *latest* version of those types is referenced and for `@openui5/ts-types-esm` the *best matching* version is referenced. But in practice also this difference should not be something to worry about. To enforce using a specific version of the jQuery/QUnit types with the `@types/openui5` type definitions, you can always do e.g.:
> **Remark:** There are also `@openui5/types` types available - how do they differ from the `@types/openui5` ones?<br>
The content is basically the same, one difference is in versioning: while the types in the `@openui5` namespace are exactly in sync with the respective OpenUI5 patch release, the ones in the `@types` namespace follow the DefinitelyTyped versioning and are only released *once* per minor release of OpenUI5 ([more details here](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/openui5#versioning)), not for every patch. In practice it shouldn't make any difference what you use, but note that in the `@types` namespace there is usually only the `*.*.0` patch release available.<br>
The other small difference is [described in detail here](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/openui5#jquery-and-qunit-references-and-their-versions). In essence, UI5 declares the jQuery and QUnit types as dependencies to make sure the type definitions are also loaded because types from those libraries are in some places exposed in UI5 APIs. The difference is that for `@types/openui5` the *latest* version of those types is referenced and for `@openui5/types` the *best matching* version is referenced. But in practice also this difference should not be something to worry about. To enforce using a specific version of the jQuery/QUnit types with the `@types/openui5` type definitions, you can always do e.g.:
> ```sh
> npm install --save-dev @types/[email protected] @types/[email protected]
> ```
Expand Down Expand Up @@ -127,18 +127,16 @@ So we need to add a `tsconfig.json` configuration file to configure the right la
}
```

> **Note:** when you use the `@sapui5/ts-types-esm` (or `@openui5/ts-types-esm`) types instead, you need to add the following section to tsconfig.json:
> **Note:** when you use the `@sapui5/types` (or `@openui5/types`) types instead, you need to add the following section to tsconfig.json:
>
> ```json
> "typeRoots": [
> "./node_modules/@types"
> ],
> "types": [
> "@sapui5/ts-types-esm"
> "./node_modules/@types",
> "./node_modules/@sapui5/types"
> ],
>```
>
> Why? TypeScript automatically finds all type definition files in a dependency starting with `@types/...` (i.e. all `.d.ts` files in `node_modules/@types/...`). The jQuery d.ts files are there and found, but the SAPUI5 types are only available in a package starting with `@sapui5/...`, hence TypeScript must be explicitly pointed to these types. As this disables the automatic loading of other types from `node_modules/@types/...`, this path must be given as a type root.
> Why? TypeScript automatically finds all type definition files in a dependency starting with `@types/...` (i.e. all `.d.ts` files in `node_modules/@types/...`). The jQuery d.ts files are there and found, but the SAPUI5 types are only available in a package starting with `@sapui5/...`, hence TypeScript must be explicitly pointed to these types. As this disables the automatic loading of other types from `node_modules/@types/...`, this path must also be given as a type root.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, after removing the "types" section, this sentence does no longer apply. However, I think the current recommendation is using types, not typeRoots. Peter?


There are additional settings in this file, e.g. telling the compiler which files to compile (all matching `./webapp/**/*`) and how the modules should be resolved (`"moduleResolution": "node"`). And a couple of compiler options which are not so important right now. They determine how exactly the compiler behaves. The "paths" section informs TypeScript about the mapping of namespaces used in the app.

Expand Down
17 changes: 17 additions & 0 deletions webapp/test/e2e/dialog.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import AppPage from "./pages/AppPage"

describe("Hello!", () => {
it("Should open the Hello dialog", async () => {
await AppPage.iPressTheSayHelloWithDialogButton()
const dialogVisible = await AppPage.iShouldSeeTheHelloDialog()
expect(dialogVisible).toBeTruthy()
})

it("Should close the Hello dialog", async () => {
await AppPage.iPressTheOkButtonInTheDialog()
const dialogNotVisible = await AppPage.iShouldNotSeeTheHelloDialog()
expect(dialogNotVisible).toBeTruthy()
})
})
35 changes: 35 additions & 0 deletions webapp/test/e2e/pages/AppPage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import Button from "sap/m/Button"
import MessageBox from "sap/m/MessageBox"

import { buttonLocator, dialogLocator, OKButtonLocator } from "./locators"

class AppPage {
async iPressTheSayHelloWithDialogButton() {
const button = await browser.asControl<Button>(buttonLocator)
await button.press()
}

async iShouldSeeTheHelloDialog(): Promise<boolean> {
const dialog = await browser.asControl<MessageBox>(dialogLocator)
return await dialog.getVisible<boolean>()
}

async iPressTheOkButtonInTheDialog() {
await browser.asControl(OKButtonLocator).press()
}

async iShouldNotSeeTheHelloDialog(): Promise<boolean> {
const dialog = await browser.asControl<MessageBox>(dialogLocator)
// this returns "null" as the message box is not
// part of the DOM after being close, so
// no methods can be executed on the control
const visible = await dialog.getVisible()
return !visible
}
}

export default new AppPage()
25 changes: 25 additions & 0 deletions webapp/test/e2e/pages/locators.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
export const buttonLocator = {
selector: {
id: "container-ui5.typescript.helloworld---app--helloButton"
}
}

export const dialogLocator = {
selector: {
controlType: "sap.m.Dialog",
properties: {
type: "Message"
},
searchOpenDialogs: true
}
}

export const OKButtonLocator = {
selector: {
controlType: "sap.m.Button",
properties: {
text: "OK"
},
searchOpenDialogs: true
}
}
7 changes: 7 additions & 0 deletions webapp/test/e2e/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"compilerOptions": {
"moduleResolution": "node",
"types": ["node", "@openui5/types", "@wdio/globals/types", "@wdio/mocha-framework", "wdio-ui5-service/esm"],
"target": "es2019"
}
}
Loading