├── .github └── workflows │ ├── api.yml │ └── web.yml ├── .gitignore ├── .vscode ├── extensions.json └── settings.json ├── LICENSE ├── README.md ├── api ├── .mocharc.js ├── config │ └── setup.ts ├── package-lock.json ├── package.json ├── resources │ └── payloads.ts ├── services │ └── endpoints.ts ├── test │ └── reqres.ts ├── tsconfig.json └── utils │ ├── formatter.ts │ └── httpCalls.ts ├── images ├── appium-logo.svg ├── setup.gif └── wdio-logo.svg ├── mobile ├── README.md ├── app │ └── android │ │ └── ApiDemos-debug.apk ├── config │ └── capabilities.ts ├── package-lock.json ├── package.json ├── pages │ ├── base.page.ts │ └── elements.page.ts ├── sample │ ├── android_config.png │ ├── appium_driver_list.png │ └── report.png ├── specs │ └── apiDemoApp.spec.ts ├── static │ ├── constants.ts │ └── pathconstants.ts ├── tsconfig.json ├── wdio.conf.parallel.ts └── wdio.conf.ts ├── package-lock.json ├── package.json ├── start.js └── web ├── .env.example ├── config ├── capabilities.ts ├── wdio.conf.docker.ts ├── wdio.conf.e2e.docker.ts ├── wdio.conf.e2e.ts └── wdio.conf.ts ├── docker-compose.yml ├── generator ├── bddEmail.ts ├── emailBody.ts ├── index.ts └── mochaEmail.ts ├── package-lock.json ├── package.json ├── pages ├── basePage.ts ├── form.page.ts ├── frameShadowDom.page.ts ├── login.page.ts ├── secure.page.ts └── webTables.page.ts ├── resources ├── logindata.ts └── testdata.json ├── static ├── frameworkConstants.ts └── pathConstants.ts ├── tests ├── cucumber │ ├── features │ │ ├── BackgroundDatatable.feature │ │ └── ExamplesTable.feature │ └── steps │ │ ├── BackgroundDatatable.steps..ts │ │ └── ExamplesTable.steps.ts ├── mocha │ ├── formElements.spec.ts │ ├── frameShadowDom.spec.ts │ ├── herokuAppLogin.spec.ts │ └── webTables.spec.ts └── smoke.spec.ts ├── tsconfig.json ├── types ├── customTypes.d.ts ├── external.d.ts └── webelements.d.ts └── utils ├── base64Utils.ts ├── envReader.ts ├── fileSystem.ts └── mailer.ts /.github/workflows/api.yml: -------------------------------------------------------------------------------- 1 | name: API CI 2 | 3 | on: 4 | pull_request: 5 | branches: [ main ] 6 | 7 | jobs: 8 | 9 | ApiTest: 10 | runs-on: ubuntu-latest 11 | defaults: 12 | run: 13 | working-directory: ./api 14 | steps: 15 | - name: Checkout project 16 | uses: actions/checkout@v3 17 | - name: Setup Node.js 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: 18 21 | cache: 'npm' 22 | cache-dependency-path: './api/package-lock.json' 23 | - run: npm install 24 | - run: npm run test 25 | 26 | - name: Generate API Mochawesome Report 27 | if: always() 28 | uses: actions/upload-artifact@v3 29 | with: 30 | name: APIMochaReport 31 | path: api/reports -------------------------------------------------------------------------------- /.github/workflows/web.yml: -------------------------------------------------------------------------------- 1 | name: Web CI 2 | 3 | on: 4 | pull_request: 5 | branches: [ main ] 6 | 7 | jobs: 8 | 9 | smoke: 10 | runs-on: ubuntu-latest 11 | defaults: 12 | run: 13 | working-directory: ./web 14 | steps: 15 | - name: Checkout project 16 | uses: actions/checkout@v3 17 | - name: Setup Node.js 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: 18 21 | cache: 'npm' 22 | cache-dependency-path: './web/package-lock.json' 23 | - run: npm install --legacy-peer-deps 24 | - run: npm run smoke 25 | 26 | MochaTests: 27 | needs: [smoke] 28 | runs-on: ubuntu-latest 29 | defaults: 30 | run: 31 | working-directory: ./web 32 | steps: 33 | - name: Checkout project 34 | uses: actions/checkout@v3 35 | - name: Setup Node.js 36 | uses: actions/setup-node@v3 37 | with: 38 | node-version: 18 39 | cache: 'npm' 40 | cache-dependency-path: './web/package-lock.json' 41 | - run: npm install --legacy-peer-deps 42 | - run: npm run test 43 | 44 | - name: Generate Mochawesome Report 45 | if: always() 46 | run: npm run report:mocha:ci 47 | 48 | - name: Export Mochawesome Report 49 | if: always() 50 | uses: actions/upload-artifact@v3 51 | with: 52 | name: MochaHTMLReport 53 | path: web/mochawesome-report 54 | 55 | CucumberBDDTests: 56 | needs: [smoke] 57 | runs-on: ubuntu-latest 58 | defaults: 59 | run: 60 | working-directory: ./web 61 | steps: 62 | - name: Checkout project 63 | uses: actions/checkout@v3 64 | - name: Setup Node.js 65 | uses: actions/setup-node@v3 66 | with: 67 | node-version: 18 68 | cache: 'npm' 69 | cache-dependency-path: './web/package-lock.json' 70 | - run: npm install --legacy-peer-deps 71 | - run: npm run test:e2e 72 | 73 | - name: Generate Cucumber HTML Report 74 | if: always() 75 | run: npm run report:cucumber 76 | 77 | - name: Export Cucumber HTML Report 78 | if: always() 79 | uses: actions/upload-artifact@v3 80 | with: 81 | name: CucumberHTMLReport 82 | path: web/reports/cucumber/cucumber-report.html -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | api/node_modules/ 2 | api/reports/ 3 | mobile/node_modules/ 4 | mobile/reports/ 5 | web/node_modules/ 6 | web/reports/ 7 | web/mochawesome-report/ 8 | web/tmp/ 9 | web/.env 10 | notes.TODO 11 | node_modules/ 12 | .DS_Store -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "akamud.vscode-theme-onedark", 4 | "alexkrechik.cucumberautocomplete", 5 | "pkief.material-icon-theme", 6 | "ms-azuretools.vscode-docker" 7 | ] 8 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cucumberautocomplete.steps": [ 3 | "./web/tests/cucumber/steps/*.ts" 4 | ], 5 | "cucumberautocomplete.strictGherkinCompletion": true, 6 | "editor.codeActionsOnSave": { 7 | "source.fixAll.tslint": "explicit", 8 | "source.organizeImports": "explicit" 9 | }, 10 | "typescript.updateImportsOnFileMove.enabled": "always", 11 | "editor.formatOnSave": true 12 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 MD SADAB SAQIB 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Web, API and Mobile Test Automation Framework

2 | 3 |

4 | 5 | WebdriverIO 6 | 7 |

8 | 9 | 10 |

11 | 12 | [![Web CI](https://github.com/sadabnepal/WebdriverIOTypeScriptE2E/actions/workflows/web.yml/badge.svg)](https://github.com/sadabnepal/WebdriverIOTypeScriptE2E/actions/workflows/web.yml) 13 | [![API CI](https://github.com/sadabnepal/WebdriverIOTypeScriptE2E/actions/workflows/api.yml/badge.svg)](https://github.com/sadabnepal/WebdriverIOTypeScriptE2E/actions/workflows/api.yml) 14 | 15 |

16 | 17 | 18 | 19 | #### Pre-requisite 20 | [![NodeJs](https://img.shields.io/badge/-NodeJS-grey?logo=node.js)](https://nodejs.org/en/download/) 21 | [![Docker](https://img.shields.io/badge/-Docker-0db7ed?logo=docker&logoColor=white)](https://docs.docker.com/engine/install/) 22 | [![VSCode](https://img.shields.io/badge/-Visual%20Studio%20Code-%233178C6?logo=visual-studio-code)](https://code.visualstudio.com/download) 23 | [![Appium-Inspector](https://img.shields.io/badge/-Appium%20Inspector-662d91?logo=appium&logoColor=black)](https://github.com/appium/appium-inspector/releases) 24 | [![AndroidStudio](https://img.shields.io/badge/-Android%20Studio-3DDC84?logo=android-studio&logoColor=white)](https://developer.android.com/studio) 25 | [![JDK](https://img.shields.io/badge/-JDK-white?logo=openjdk&logoColor=black&)](https://www.azul.com/downloads/#zulu) 26 | 27 | 28 | #### Clone Repository 29 | ```bash 30 | git clone https://github.com/sadabnepal/web-mobile-api-test-framework.git 31 | cd web-mobile-api-test-framework 32 | ``` 33 | ----- 34 | 35 | ### Interactive CLI to run test: 36 | > Make sure mobile setup has been completed if selecting mobile as CLI option. See [Mobile Test](./mobile/README.md) for setup instructions. Before running actual test, presence of node_modules folder will be validated and if not not found installation will take place before proceeding any further. 37 | ```bash 38 | npm start 39 | ``` 40 | It start wizard with test module options, based on user selection either of the below module will start locally or inside docker container. Code to control wizard and user selection is available in 'start.js' which is built using [enquirer](https://www.npmjs.com/package/enquirer) node package.
41 | Test Module Options : | UI | API | Mobile |
42 | ![cli_demo](./images/setup.gif) 43 | 44 | 45 | ----- 46 | 47 | ### Web Test 48 | Install dependencies: 49 | > Navigate to "web" folder and then run below command 50 | ```bash 51 | npm install 52 | ``` 53 | 54 | Setup .env file:
55 | create `.env` file inside web folder and update content with reference to `.env.example` 56 | 57 | Run test in local: 58 | > By default test will run in HEADLESS mode. 59 | > Update MODE=LOCAL in .env file to see test running in browser. 60 | ```bash 61 | npm test [ Mocha tests ] 62 | npm run test:e2e [ Cucumber BDD tests ] 63 | ``` 64 | 65 | Run test in Docker: 66 | ```bash 67 | npm run test:docker [ Mocha tests] 68 | npm run test:e2e:docker [ Cucumber BDD tests ] 69 | ``` 70 | > Pre and Post script will handle start and stop of docker containers automatically. 71 | > If containers does not stop automatically run "docker-compose down" command. 72 | 73 | Generate Report: 74 | ```bash 75 | npm run report:mocha 76 | npm run report:cucumber 77 | ``` 78 | 79 | Report Paths: 80 | ```bash 81 | mocha: web/mochawesome-report/mochawesome-report.html 82 | cucumber: web/reports/cucumber/cucumber-report.html 83 | ``` 84 | 85 | Send Report: 86 | > Update .env file details with reference of .env.example file 87 | ```bash 88 | npm run mailCucumberReport 89 | npm run mailMochaResult 90 | ``` 91 | ----- 92 | 93 | ### API Test 94 | Install dependencies: 95 | > Navigate to "api" folder and then run below command 96 | ```bash 97 | npm install 98 | ``` 99 | 100 | Run test: 101 | ```bash 102 | npm test 103 | ``` 104 | 105 | Report Paths: 106 | ```bash 107 | api/reports/mochawesome.html 108 | ``` 109 | 110 | ----- 111 | 112 | ### Mobile Test 113 | 114 | Appium setup: [Click here to open Appium SetUp README](/mobile/README.md) 115 | 116 | Install dependencies: 117 | > Navigate to "mobile" folder and then run below command 118 | ```bash 119 | npm install 120 | ``` 121 | 122 | Run in local: 123 | > Make sure android virtual device is up and running before starting mobile test. 124 | ```bash 125 | npm run test [ Mobile tests ] 126 | ``` 127 | 128 | Generate Report: 129 | ```bash 130 | npm run report 131 | ``` 132 | 133 | Report Paths: 134 | ```bash 135 | mobile: mobile/reports/mobile.html 136 | ``` 137 | 138 | ----- 139 | 140 | #### Features: 141 | - Web, Mobile and API Testing 142 | - Mocha and Cucumber BDD framework 143 | - Page Object Design pattern 144 | - Docker with VNC integration 145 | - Parallel execution 146 | - Cross browser testing 147 | - Retry failed test 148 | - Screenshot in report for failed tests 149 | - Github actions 150 | - Send test report to list of Gmail 151 | - Use of types for method params optimization 152 | - Improved import statement using tsconfig path 153 | 154 | #### Tech stacks: 155 | [![WebdriverIO](https://img.shields.io/badge/-WebdriverI/O-EA5906?logo=WebdriverIO&logoColor=white)](https://webdriver.io/) 156 | [![TypeScript](https://img.shields.io/badge/-TypeScript-%233178C6?logo=Typescript&logoColor=black)](https://www.typescriptlang.org/) 157 | [![Mocha](https://img.shields.io/badge/-Mocha-%238D6748?logo=Mocha&logoColor=white)](https://mochajs.org/) 158 | [![CucumberIO](https://img.shields.io/badge/-Cucumber.io-brightgreen?logo=cucumber&logoColor=white)](https://cucumber.io/) 159 | [![ChaiJS](https://img.shields.io/badge/-ChaiJS-FEDABD?logo=Chai&logoColor=black)](https://www.chaijs.com/) 160 | [![SuperTest](https://img.shields.io/badge/-SuperTest-07BA82?logoColor=white)](https://github.com/visionmedia/supertest) 161 | [![Enquirer](https://img.shields.io/badge/-Enquirer-f0db4f?logoColor=white)](https://github.com/enquirer/enquirer) 162 | [![Docker](https://img.shields.io/badge/-Docker-0db7ed?logo=docker&logoColor=white)](https://www.docker.com/) 163 | [![Appium](https://img.shields.io/badge/-Appium-662d91?logo=appium&logoColor=black)](https://github.com/appium/appium) 164 | [![dotenv](https://img.shields.io/badge/-dotenv-grey?logo=.env&logoColor=#ECD53F)](https://github.com/appium/appium) 165 | [![Node-Mailer](https://img.shields.io/badge/-Node%20Mailer-grey?logo=gmail&logoColor=blue)](https://github.com/nodemailer/nodemailer) 166 | 167 | #### Folder Structure: 168 | ![e2e_framework_folders](https://user-images.githubusercontent.com/65847528/168474570-5eca8112-25b7-45ca-b411-355d0ce39079.png) 169 | 170 | #### Sample Email Report: 171 | ![email_report](https://user-images.githubusercontent.com/65847528/168474717-26236fd6-4f30-4cc0-bcb9-cf9ae0deadce.png) 172 | -------------------------------------------------------------------------------- /api/.mocharc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | spec: ['test/**/*.ts'], 3 | package: './package.json', 4 | require: ['ts-node/register', 'tsconfig-paths/register'], 5 | extension: ['ts'], 6 | timeout: 20 * 1000, 7 | grep: '', 8 | ignore: [''], 9 | reporter: 'mochawesome', 10 | 'reporter-option': [ 11 | 'reportDir=reports', 12 | 'reportFilename=index', 13 | 'reportTitle=API Test Report', 14 | 'charts=true', 15 | 'code=false', 16 | 'inline=true', 17 | 'autoOpen=false', 18 | 'showPassed=true', 19 | 'showFailed=true', 20 | 'showPending=true', 21 | 'showSkipped=true', 22 | 'showHooks=failed' 23 | ] 24 | }; -------------------------------------------------------------------------------- /api/config/setup.ts: -------------------------------------------------------------------------------- 1 | export const REQ_RES_BASE_URI = "https://reqres.in" -------------------------------------------------------------------------------- /api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "@faker-js/faker": "^6.3.1", 14 | "@types/chai": "^4.3.1", 15 | "@types/mocha": "^9.1.1", 16 | "@types/mochawesome": "^6.2.1", 17 | "@types/supertest": "^2.0.12", 18 | "chai": "^4.3.6", 19 | "dotenv": "^16.0.0", 20 | "mocha": "^10.0.0", 21 | "mochawesome": "^7.1.3", 22 | "supertest": "^6.2.3", 23 | "ts-node": "^10.7.0", 24 | "tsconfig-paths": "^4.0.0", 25 | "typescript": "^4.6.4" 26 | } 27 | } -------------------------------------------------------------------------------- /api/resources/payloads.ts: -------------------------------------------------------------------------------- 1 | import { faker } from "@faker-js/faker" 2 | 3 | export const createUserPayload = { 4 | "name": faker.name.firstName() + " " + faker.name.lastName(), 5 | "job": faker.name.jobTitle() 6 | } -------------------------------------------------------------------------------- /api/services/endpoints.ts: -------------------------------------------------------------------------------- 1 | export enum endpoints { 2 | USERS_SERVICE = "/api/users", 3 | USER_BY_ID_SERVICE = "/api/users/%s" 4 | } -------------------------------------------------------------------------------- /api/test/reqres.ts: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai'; 2 | import { createUserPayload } from "resources/payloads"; 3 | import { endpoints } from "services/endpoints"; 4 | import { logResponseToMochaReport, stringFormatter } from 'utils/formatter'; 5 | import { makeDELETECall, makeGETCall, makePOSTCall } from 'utils/httpCalls'; 6 | 7 | describe('REQ RES users api validation', () => { 8 | 9 | it('should verify POST user call', async function () { 10 | const response = await makePOSTCall(endpoints.USERS_SERVICE, createUserPayload) 11 | logResponseToMochaReport(this, response); 12 | assert.equal(response.statusCode, 201) 13 | assert.equal(response.body.name, createUserPayload.name) 14 | assert.equal(response.body.job, createUserPayload.job) 15 | }); 16 | 17 | it('should verify GET user/{id} call', async function () { 18 | const userByID = stringFormatter(endpoints.USER_BY_ID_SERVICE, 2) 19 | const response = await makeGETCall(userByID) 20 | logResponseToMochaReport(this, response); 21 | assert.equal(response.statusCode, 200) 22 | assert.equal(response.body.data.id, 2) 23 | }); 24 | 25 | it('should verify DELETE user/{id} call', async function () { 26 | const userByID = stringFormatter(endpoints.USER_BY_ID_SERVICE, 2) 27 | const response = await makeDELETECall(userByID) 28 | logResponseToMochaReport(this, response); 29 | assert.equal(response.statusCode, 204) 30 | }); 31 | }); -------------------------------------------------------------------------------- /api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "esModuleInterop": true, 5 | "resolveJsonModule": true 6 | } 7 | } -------------------------------------------------------------------------------- /api/utils/formatter.ts: -------------------------------------------------------------------------------- 1 | import addContext from 'mochawesome/addContext'; 2 | import supertest from "supertest"; 3 | import util from "util"; 4 | 5 | export const stringFormatter = (data: string, value: string | number) => { 6 | return util.format(data, value) 7 | } 8 | 9 | const formatResponse = (response: supertest.Response) => { 10 | return `Response: ${JSON.stringify(response.body, null, 4)}`; 11 | } 12 | 13 | export const logResponseToMochaReport = (context: Mocha.Context, response: supertest.Response) => { 14 | addContext(context, formatResponse(response)) 15 | } -------------------------------------------------------------------------------- /api/utils/httpCalls.ts: -------------------------------------------------------------------------------- 1 | import { REQ_RES_BASE_URI } from 'config/setup'; 2 | import { endpoints } from "services/endpoints"; 3 | import supertest, { Response } from "supertest"; 4 | 5 | const request = supertest(REQ_RES_BASE_URI) 6 | 7 | export const makeGETCall = async (endpoint: endpoints | string, payload?: object, headersAPI?: Record): Promise => { 8 | if (payload && headersAPI) return request.get(endpoint).set(headersAPI).send(payload); 9 | else if (payload) return request.get(endpoint).send(payload); 10 | else return request.get(endpoint); 11 | } 12 | 13 | export const makePOSTCall = async (endpoint: endpoints | string, payload: string | object, headers?: Record): Promise => { 14 | if (headers) return request.post(endpoint).set(headers); 15 | return request.post(endpoint).send(payload); 16 | } 17 | 18 | export const makeDELETECall = async (endpoint: endpoints | string, payload?: object): Promise => { 19 | if (payload) return request.delete(endpoint).send(payload); 20 | return request.delete(endpoint); 21 | } -------------------------------------------------------------------------------- /images/appium-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /images/setup.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sadabnepal/web-mobile-api-test-framework/d7e71af4527b003647890678cdd4eb62aee636de/images/setup.gif -------------------------------------------------------------------------------- /images/wdio-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Robot-Edit 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /mobile/README.md: -------------------------------------------------------------------------------- 1 | #### Install Appium Server 2 | ``` 3 | npm install -g appium [ install appium CLI version ] 4 | npm install -g appium-doctor [ install appium doctor ] 5 | appium --version [ To check appium version ] 6 | ``` 7 | 8 | #### Verify drivers 9 | ``` 10 | appium driver list [ To check available drivers ] 11 | appium driver install uiautomator2 [ install android driver] 12 | appium driver install xcuitest [ install ios driver] 13 | ``` 14 | 15 | #### Setup Android SDK path environment variable 16 | ``` 17 | - ANDROID_HOME = 18 | - %ANDROID_HOME%\tools [path variable] 19 | - %ANDROID_HOME%\tools\bin [path variable] 20 | - %ANDROID_HOME%\platform-tools [path variable] 21 | ``` 22 | 23 | #### Setup/Create virtual device on Android studio: 24 | ``` 25 | 1] Open Android Studio 26 | 2] Click on More Actions 27 | --> AVD Manager 28 | --> Create Virtual Device 29 | --> Select the device and OS version [ Refer Device Configurations ] 30 | --> Finish 31 | 3] Once Virtual device is created, click on Launch this AVD in the emulator. 32 | 4] Command to view the list of devices attached `adb devices` 33 | ``` 34 | 35 | Device Configurations: 36 | ``` 37 | Device 1: Pixel 3 - version 11 38 | Device 2: Nexus 6 - version 10 [ if you want to run tests in parallel ] 39 | ``` 40 | 41 | 42 | #### Verify all setup 43 | ``` 44 | appium-doctor --android [ To check Android set up ] 45 | appium-doctor --ios [ To check ios set up ] 46 | ``` 47 | all options should be green checked as shown in below image to start. 48 | ![android_config.png](sample/android_config.png) 49 | 50 | [Go Back to main README](../../README.md) 51 | -------------------------------------------------------------------------------- /mobile/app/android/ApiDemos-debug.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sadabnepal/web-mobile-api-test-framework/d7e71af4527b003647890678cdd4eb62aee636de/mobile/app/android/ApiDemos-debug.apk -------------------------------------------------------------------------------- /mobile/config/capabilities.ts: -------------------------------------------------------------------------------- 1 | import { ANDROID_APP_PATH } from "../static/pathConstants"; 2 | 3 | export const androidDeviceCapabilities = [ 4 | { 5 | platformName: "Android", 6 | "appium:platformVersion": "11", 7 | "appium:deviceName": "Pixel 3", 8 | "appium:systemPort": 8200, 9 | "appium:automationName": "UiAutomator2", 10 | "appium:app": ANDROID_APP_PATH, 11 | 'appium:noReset': false, 12 | 'appium:newCommandTimeout': 30, 13 | "appium:autoGrantPermissions": true, 14 | "appium:avd": "Pixel_3", 15 | "appium:avdLaunchTimeout": 180000 16 | } 17 | ] 18 | 19 | export const androidMultiDeviceCapabilities = [ 20 | ...androidDeviceCapabilities, 21 | { 22 | platformName: "Android", 23 | "appium:platformVersion": "10", 24 | "appium:deviceName": "Nexus 6", 25 | "appium:systemPort": 8201, 26 | "appium:automationName": "UiAutomator2", 27 | "appium:app": ANDROID_APP_PATH, 28 | 'appium:noReset': false, 29 | 'appium:newCommandTimeout': 30, 30 | "appium:autoGrantPermissions": true, 31 | "appium:avd": "Pixel_3", 32 | "appium:avdLaunchTimeout": 180000 33 | } 34 | ] -------------------------------------------------------------------------------- /mobile/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mobile", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "wdio run wdio.conf.ts", 8 | "test:parallel": "wdio run wdio.conf.parallel.ts", 9 | "report": "marge ./reports/wdio-ma-merged.json --reportTitle 'AppiumReport' --reportDir=./reports/ && move ./reports.html ./reports" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "ISC", 14 | "devDependencies": { 15 | "@wdio/appium-service": "^8.36.0", 16 | "@wdio/cli": "^8.36.0", 17 | "@wdio/json-reporter": "^8.36.0", 18 | "@wdio/local-runner": "^8.36.0", 19 | "@wdio/mocha-framework": "^8.36.0", 20 | "@wdio/spec-reporter": "^8.36.0", 21 | "mochawesome-report-generator": "^6.2.0", 22 | "ts-node": "^10.9.2", 23 | "typescript": "^5.4.5" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /mobile/pages/base.page.ts: -------------------------------------------------------------------------------- 1 | import { APP_PACKAGE } from "../static/constants"; 2 | 3 | export default class BasePage { 4 | 5 | findByTextContains(partialText: string) { 6 | return $(`android=new UiSelector().textContains("${partialText}")`); 7 | } 8 | 9 | async scrollAndClickByText(text: string) { 10 | await $(`android=new UiScrollable(new UiSelector()).scrollTextIntoView("${text}")`).click(); 11 | } 12 | 13 | async scrollHorizontally() { 14 | await $('android=new UiScrollable(new UiSelector()).setAsHorizontalList().scrollForward(2)'); 15 | } 16 | 17 | async openUsingPackage(packageName: string) { 18 | await driver.startActivity(APP_PACKAGE, packageName) 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /mobile/pages/elements.page.ts: -------------------------------------------------------------------------------- 1 | import BasePage from "./base.page" 2 | 3 | class PageElements extends BasePage { 4 | get appNameHeader() { return $('android.widget.TextView') } 5 | get allMenuItemsElements() { return $$(".android.widget.TextView") } 6 | get appMenuElement() { return $('~App') } 7 | get viewsMenuElement() { return $('~Views') } 8 | get listDialogueElement() { return $("//*[@content-desc='List dialog']") } 9 | get commandTwoElement() { return $("//*[@text='Command two']") } 10 | get commandTwoMsgElement() { return $('//*[@resource-id="android:id/message"]') } 11 | get okCancelElement() { return $('~OK Cancel dialog with a message') } 12 | get alertTitleElement() { return $('//*[@resource-id="android:id/alertTitle"]') } 13 | get actionBar() { return $('~Action Bar') } 14 | get activitySubMenu() { return $('~Activity') } 15 | get countryInputElement() { return $('//*[@resource-id="io.appium.android.apis:id/edit"]') } 16 | get dateWidgetMenu() { return $('~Date Widgets') } 17 | get dialogOption() { return $('~1. Dialog') } 18 | get dateElement() { return $('//*[@resource-id="io.appium.android.apis:id/dateDisplay"]'); } 19 | get changeDateButton() { return $('~change the date') } 20 | get dateOKButton() { return $('//android.widget.Button[@text="OK"]') } 21 | get wallpaperTextElement() { return $('//*[@resource-id="io.appium.android.apis:id/text"]') } 22 | 23 | async openMainMenu() { 24 | await this.openUsingPackage(".ApiDemos") 25 | } 26 | 27 | async openAlertPage() { 28 | await this.openUsingPackage(".app.AlertDialogSamples") 29 | } 30 | 31 | async openCountryInputPage() { 32 | await this.openUsingPackage(".view.AutoComplete1") 33 | } 34 | 35 | async openGalleryPage() { 36 | await this.openUsingPackage(".view.Gallery1") 37 | } 38 | 39 | async clickOnAppMenu() { 40 | await this.appMenuElement.click(); 41 | } 42 | 43 | async clickOnViewsMenu() { 44 | await this.viewsMenuElement.click() 45 | } 46 | 47 | async navigateToCommandTwoPopup() { 48 | await this.openAlertPage() 49 | await this.listDialogueElement.click(); 50 | await this.commandTwoElement.click(); 51 | } 52 | 53 | async clickOnOkCancelDialogue() { 54 | await this.okCancelElement.click(); 55 | } 56 | 57 | async clickOnActivityMenu() { 58 | await this.activitySubMenu.click() 59 | } 60 | 61 | async selectDay(day: string) { 62 | await $(`//android.view.View[@text='${day}']`).click() 63 | } 64 | 65 | async openDateDialogueMenu() { 66 | await this.dateWidgetMenu.click() 67 | await this.dialogOption.click() 68 | } 69 | 70 | async scrollToNextMonthAndSelectDay(day: string) { 71 | await this.scrollHorizontally() 72 | await this.selectDay(day) 73 | await this.dateOKButton.click() 74 | } 75 | 76 | async scrollGalleryHorizontally() { 77 | await this.scrollHorizontally() 78 | } 79 | 80 | async scrollAndClickOnWallpaperMenu() { 81 | await this.scrollAndClickByText('Wallpaper') 82 | } 83 | 84 | 85 | } 86 | export default new PageElements() -------------------------------------------------------------------------------- /mobile/sample/android_config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sadabnepal/web-mobile-api-test-framework/d7e71af4527b003647890678cdd4eb62aee636de/mobile/sample/android_config.png -------------------------------------------------------------------------------- /mobile/sample/appium_driver_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sadabnepal/web-mobile-api-test-framework/d7e71af4527b003647890678cdd4eb62aee636de/mobile/sample/appium_driver_list.png -------------------------------------------------------------------------------- /mobile/sample/report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sadabnepal/web-mobile-api-test-framework/d7e71af4527b003647890678cdd4eb62aee636de/mobile/sample/report.png -------------------------------------------------------------------------------- /mobile/specs/apiDemoApp.spec.ts: -------------------------------------------------------------------------------- 1 | import elementsPage from '../pages/elements.page'; 2 | import * as constants from '../static/constants'; 3 | 4 | describe('API Demo Android APP tests', () => { 5 | 6 | it('should validate app name', async () => { 7 | await expect(elementsPage.appNameHeader).toHaveText(constants.APP_HEADER); 8 | }) 9 | 10 | it('should validate all menu items', async () => { 11 | const actualMenuItems = await elementsPage.allMenuItemsElements.map(async menuItem => menuItem.getText()); 12 | expect(actualMenuItems).toEqual(constants.MENU_ITEMS) 13 | expect(await elementsPage.allMenuItemsElements.length).toBeGreaterThan(0); 14 | }) 15 | 16 | it('should open Action bar menu item', async () => { 17 | await elementsPage.clickOnAppMenu(); 18 | await expect(elementsPage.actionBar).toBeExisting(); 19 | }) 20 | 21 | it('should validate command two menu with app activity', async () => { 22 | await elementsPage.navigateToCommandTwoPopup() 23 | await expect(elementsPage.commandTwoMsgElement).toHaveText(constants.COMMAND_TWO_POPUP_MSG); 24 | }) 25 | 26 | it('should validate screen top send keys', async () => { 27 | await elementsPage.openCountryInputPage() 28 | await elementsPage.countryInputElement.setValue('Nepal') 29 | await expect(elementsPage.countryInputElement).toHaveText('Nepal') 30 | }) 31 | 32 | it('should validate alert text and accept alert', async () => { 33 | await elementsPage.openAlertPage() 34 | await elementsPage.clickOnOkCancelDialogue() 35 | expect(await driver.getAlertText()).toEqual(constants.ALERT_TEXT) 36 | await driver.acceptAlert() 37 | await expect(elementsPage.alertTitleElement).not.toExist() 38 | }) 39 | 40 | it('should validate alert text and dismiss alert', async () => { 41 | await elementsPage.openAlertPage() 42 | await elementsPage.clickOnOkCancelDialogue() 43 | expect(await driver.getAlertText()).toEqual(constants.ALERT_TEXT) 44 | await driver.dismissAlert() 45 | await expect(elementsPage.alertTitleElement).not.toExist() 46 | }) 47 | 48 | it('should validate vertical scrolling', async () => { 49 | await elementsPage.openMainMenu() 50 | await elementsPage.clickOnAppMenu() 51 | await elementsPage.clickOnActivityMenu() 52 | await elementsPage.scrollAndClickOnWallpaperMenu() 53 | await expect(elementsPage.wallpaperTextElement).toHaveText(expect.stringContaining(constants.WALLPAPER_TEXT)); 54 | }) 55 | 56 | it('should validate horizontal scrolling', async () => { 57 | await elementsPage.openGalleryPage() 58 | await elementsPage.scrollGalleryHorizontally() 59 | }) 60 | 61 | it('should validate next month date selection using scroll', async () => { 62 | await elementsPage.openMainMenu() 63 | await elementsPage.clickOnViewsMenu() 64 | await elementsPage.openDateDialogueMenu() 65 | const currentDate = await elementsPage.dateElement.getText() 66 | await elementsPage.changeDateButton.click() 67 | await elementsPage.scrollToNextMonthAndSelectDay('10') 68 | const updateDate = await elementsPage.dateElement.getText() 69 | expect(updateDate).not.toEqual(currentDate) 70 | }) 71 | 72 | }) -------------------------------------------------------------------------------- /mobile/static/constants.ts: -------------------------------------------------------------------------------- 1 | export const APP_PACKAGE = "io.appium.android.apis"; 2 | export const APP_HEADER = "API Demos"; 3 | export const COMMAND_TWO_POPUP_MSG = "You selected: 1 , Command two"; 4 | export const MENU_ITEMS = ['API Demos', 'Accessibility', 'Animation', 'App', 'Content', 'Graphics', 'Media', 'NFC', 'OS', 'Preference', 'Text', 'Views']; 5 | export const ALERT_TEXT = `Lorem ipsum dolor sit aie consectetur adipiscing 6 | Plloaso mako nuto siwuf cakso dodtos anr koop.`; 7 | export const WALLPAPER_TEXT = 'Example of how you can make an activity have a translucent background'; -------------------------------------------------------------------------------- /mobile/static/pathconstants.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | 3 | export const ANDROID_APP_PATH = join(process.cwd(), 'app', 'android', 'ApiDemos-debug.apk') 4 | export const JSON_OUTPUT_DIR = join(process.cwd(), 'reports'); -------------------------------------------------------------------------------- /mobile/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": false, 4 | "resolveJsonModule": true, 5 | "target": "es2019", 6 | "module": "commonjs", 7 | "strict": true, 8 | "noImplicitAny": false, 9 | "types": [ 10 | "node", 11 | "@wdio/globals/types", 12 | "@wdio/mocha-framework", 13 | "expect-webdriverio", 14 | "@wdio/appium-service" 15 | ], 16 | "allowSyntheticDefaultImports": true, 17 | "esModuleInterop": true, 18 | "experimentalDecorators": true, 19 | "emitDecoratorMetadata": true 20 | } 21 | } -------------------------------------------------------------------------------- /mobile/wdio.conf.parallel.ts: -------------------------------------------------------------------------------- 1 | import type { Options } from '@wdio/types'; 2 | import { androidMultiDeviceCapabilities } from './config/capabilities'; 3 | import { config as baseConfig } from './wdio.conf'; 4 | 5 | export const config: Options.Testrunner = { 6 | ...baseConfig, 7 | capabilities: androidMultiDeviceCapabilities, 8 | } -------------------------------------------------------------------------------- /mobile/wdio.conf.ts: -------------------------------------------------------------------------------- 1 | import type { Options } from '@wdio/types'; 2 | import { androidDeviceCapabilities } from './config/capabilities'; 3 | import { JSON_OUTPUT_DIR } from './static/pathConstants'; 4 | 5 | export const config: Options.Testrunner = { 6 | // ===================== 7 | // ts-node Configurations 8 | // ===================== 9 | autoCompileOpts: { 10 | autoCompile: true, 11 | tsNodeOpts: { 12 | transpileOnly: true, 13 | project: './tsconfig.json' 14 | } 15 | }, 16 | // ==================== 17 | // Runner Configuration 18 | // ==================== 19 | port: 4723, 20 | // ================== 21 | // Specify Test Files 22 | // ================== 23 | specs: [ 24 | './specs/**/*.ts' 25 | ], 26 | exclude: [ 27 | // 'path/to/excluded/files' 28 | ], 29 | // ============ 30 | // Capabilities 31 | // ============ 32 | maxInstances: 10, 33 | capabilities: androidDeviceCapabilities, 34 | // =================== 35 | // Test Configurations 36 | // =================== 37 | // Level of logging verbosity: trace | debug | info | warn | error | silent 38 | logLevel: 'info', 39 | bail: 0, 40 | baseUrl: 'http://localhost', 41 | waitforTimeout: 10000, 42 | connectionRetryTimeout: 120000, 43 | connectionRetryCount: 3, 44 | services: ['appium'], 45 | framework: 'mocha', 46 | // specFileRetries: 1, 47 | // specFileRetriesDelay: 0, 48 | reporters: ['spec', 49 | ['json', { 50 | outputDir: JSON_OUTPUT_DIR, 51 | outputFileFormat: (opts: any) => { 52 | return `results-${opts.cid}.${opts.capabilities.platformName}.json` 53 | } 54 | }]], 55 | mochaOpts: { 56 | compilers: [], 57 | ui: 'bdd', 58 | timeout: 60000 59 | }, 60 | // 61 | // ===== 62 | // Hooks 63 | // ===== 64 | // WebdriverIO provides several hooks you can use to interfere with the test process in order to enhance 65 | // it and to build services around it. You can either apply a single function or an array of 66 | // methods to it. If one of them returns with a promise, WebdriverIO will wait until that promise got 67 | // resolved to continue. 68 | /** 69 | * Gets executed once before all workers get launched. 70 | * @param {Object} config wdio configuration object 71 | * @param {Array.} capabilities list of capabilities details 72 | */ 73 | // onPrepare: function (config, capabilities) { 74 | // }, 75 | /** 76 | * Gets executed before a worker process is spawned and can be used to initialise specific service 77 | * for that worker as well as modify runtime environments in an async fashion. 78 | * @param {String} cid capability id (e.g 0-0) 79 | * @param {[type]} caps object containing capabilities for session that will be spawn in the worker 80 | * @param {[type]} specs specs to be run in the worker process 81 | * @param {[type]} args object that will be merged with the main configuration once worker is initialised 82 | * @param {[type]} execArgv list of string arguments passed to the worker process 83 | */ 84 | // onWorkerStart: function (cid, caps, specs, args, execArgv) { 85 | // }, 86 | /** 87 | * Gets executed just before initialising the webdriver session and test framework. It allows you 88 | * to manipulate configurations depending on the capability or spec. 89 | * @param {Object} config wdio configuration object 90 | * @param {Array.} capabilities list of capabilities details 91 | * @param {Array.} specs List of spec file paths that are to be run 92 | * @param {String} cid worker id (e.g. 0-0) 93 | */ 94 | // beforeSession: function (config, capabilities, specs, cid) { 95 | // }, 96 | /** 97 | * Gets executed before test execution begins. At this point you can access to all global 98 | * variables like `browser`. It is the perfect place to define custom commands. 99 | * @param {Array.} capabilities list of capabilities details 100 | * @param {Array.} specs List of spec file paths that are to be run 101 | * @param {Object} browser instance of created browser/device session 102 | */ 103 | // before: function (capabilities, specs) { 104 | // }, 105 | /** 106 | * Runs before a WebdriverIO command gets executed. 107 | * @param {String} commandName hook command name 108 | * @param {Array} args arguments that command would receive 109 | */ 110 | // beforeCommand: function (commandName, args) { 111 | // }, 112 | /** 113 | * Hook that gets executed before the suite starts 114 | * @param {Object} suite suite details 115 | */ 116 | // beforeSuite: function (suite) { 117 | // }, 118 | /** 119 | * Function to be executed before a test (in Mocha/Jasmine) starts. 120 | */ 121 | // beforeTest: function (test, context) { 122 | // }, 123 | /** 124 | * Hook that gets executed _before_ a hook within the suite starts (e.g. runs before calling 125 | * beforeEach in Mocha) 126 | */ 127 | // beforeHook: function (test, context) { 128 | // }, 129 | /** 130 | * Hook that gets executed _after_ a hook within the suite starts (e.g. runs after calling 131 | * afterEach in Mocha) 132 | */ 133 | // afterHook: function (test, context, { error, result, duration, passed, retries }) { 134 | // }, 135 | /** 136 | * Function to be executed after a test (in Mocha/Jasmine only) 137 | * @param {Object} test test object 138 | * @param {Object} context scope object the test was executed with 139 | * @param {Error} result.error error object in case the test fails, otherwise `undefined` 140 | * @param {Any} result.result return object of test function 141 | * @param {Number} result.duration duration of test 142 | * @param {Boolean} result.passed true if test has passed, otherwise false 143 | * @param {Object} result.retries informations to spec related retries, e.g. `{ attempts: 0, limit: 0 }` 144 | */ 145 | // eslint-disable-next-line no-unused-vars 146 | afterTest: async function (test, context, { error, result, duration, passed, retries }) { 147 | if (!passed) { 148 | await driver.takeScreenshot(); 149 | } 150 | }, 151 | 152 | 153 | /** 154 | * Hook that gets executed after the suite has ended 155 | * @param {Object} suite suite details 156 | */ 157 | // afterSuite: function (suite) { 158 | // }, 159 | /** 160 | * Runs after a WebdriverIO command gets executed 161 | * @param {String} commandName hook command name 162 | * @param {Array} args arguments that command would receive 163 | * @param {Number} result 0 - command success, 1 - command error 164 | * @param {Object} error error object if any 165 | */ 166 | // afterCommand: function (commandName, args, result, error) { 167 | // }, 168 | /** 169 | * Gets executed after all tests are done. You still have access to all global variables from 170 | * the test. 171 | * @param {Number} result 0 - test pass, 1 - test fail 172 | * @param {Array.} capabilities list of capabilities details 173 | * @param {Array.} specs List of spec file paths that ran 174 | */ 175 | // after: function (result, capabilities, specs) { 176 | // }, 177 | /** 178 | * Gets executed right after terminating the webdriver session. 179 | * @param {Object} config wdio configuration object 180 | * @param {Array.} capabilities list of capabilities details 181 | * @param {Array.} specs List of spec file paths that ran 182 | */ 183 | // afterSession: function (config, capabilities, specs) { 184 | // }, 185 | /** 186 | * Gets executed after all workers got shut down and the process is about to exit. An error 187 | * thrown in the onComplete hook will result in the test run failing. 188 | * @param {Object} exitCode 0 - success, 1 - fail 189 | * @param {Object} config wdio configuration object 190 | * @param {Array.} capabilities list of capabilities details 191 | * @param {} results object containing test results 192 | */ 193 | // eslint-disable-next-line no-unused-vars 194 | onComplete: function (exitCode, config, capabilities, results) { 195 | const mergeResults = require('@wdio/json-reporter/mergeResults'); 196 | mergeResults(JSON_OUTPUT_DIR, "results-*"); 197 | }, 198 | /** 199 | * Gets executed when a refresh happens. 200 | * @param {String} oldSessionId session ID of the old session 201 | * @param {String} newSessionId session ID of the new session 202 | */ 203 | //onReload: function(oldSessionId, newSessionId) { 204 | //} 205 | } 206 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webdriverio_typescript_e2e", 3 | "version": "1.0.0", 4 | "description": "Web, API and Mobile Test Automation Framework", 5 | "main": "start.js", 6 | "scripts": { 7 | "start": "node start.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/sadabnepal/WebdriverIOTypeScriptE2E.git" 12 | }, 13 | "keywords": [ 14 | "webdriverio", 15 | "typescript", 16 | "supertest", 17 | "appium" 18 | ], 19 | "author": "MD SADAB SAQIB", 20 | "license": "ISC", 21 | "bugs": { 22 | "url": "https://github.com/sadabnepal/WebdriverIOTypeScriptE2E/issues" 23 | }, 24 | "homepage": "https://github.com/sadabnepal/WebdriverIOTypeScriptE2E#readme", 25 | "dependencies": { 26 | "enquirer": "^2.3.6" 27 | }, 28 | "devDependencies": { 29 | "npm-check-updates": "^16.14.18" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /start.js: -------------------------------------------------------------------------------- 1 | const { execSync } = require('child_process') 2 | const { Select } = require('enquirer'); 3 | const { join } = require("path"); 4 | const { existsSync } = require('fs'); 5 | const TEST_MODULE = new Select({ 6 | name: 'framework', 7 | message: 'Which test module you want to run?', 8 | choices: ['UI', 'API', 'Mobile'] 9 | }) 10 | 11 | const RUNNER_SERVICE = new Select({ 12 | name: 'runMode', 13 | message: 'Where do you want to run your tests?', 14 | choices: ['Local', 'Docker'] 15 | }) 16 | 17 | const UI_TEST_TYPE = new Select({ 18 | name: 'runMode', 19 | message: 'Which framework do you want to run?', 20 | choices: ['Mocha', 'Cucumber'] 21 | }) 22 | 23 | const runnerCommand = { 24 | apiRunner: () => { execSync('cd api&&npm run test', { stdio: 'inherit' }) }, 25 | mobileRunner: () => { execSync('cd mobile&&npm run test', { stdio: 'inherit' }) }, 26 | localMochaRunner: () => execSync('cd web&&npm run test', { stdio: 'inherit' }), 27 | dockerMochaRunner: () => { execSync('cd web&&npm run test:docker', { stdio: 'inherit' }) }, 28 | localBDDRunner: () => execSync('cd web&&npm run test:e2e', { stdio: 'inherit' }), 29 | dockerBDDRunner: () => { execSync('cd web&&npm run test:e2e:docker', { stdio: 'inherit' }) } 30 | } 31 | 32 | const API_NODE_MODULES_PATH = join(process.cwd(), 'api', 'node_modules'); 33 | const MOBILE_NODE_MODULES_PATH = join(process.cwd(), 'mobile', 'node_modules'); 34 | const WEB_NODE_MODULES_PATH = join(process.cwd(), 'web', 'node_modules'); 35 | 36 | const isNodeModuleDoesNotExists = (path) => { 37 | if (!existsSync(path)) { 38 | console.log(`'node_modules' folder is missing!!! Starting installation...`); 39 | return true; 40 | } 41 | else return false; 42 | } 43 | 44 | const installerCommand = { 45 | api: () => { execSync('cd api&&npm install', { stdio: 'inherit' }) }, 46 | mobile: () => { execSync('cd mobile&&npm install', { stdio: 'inherit' }) }, 47 | web: () => execSync('cd web&&npm install', { stdio: 'inherit' }), 48 | } 49 | 50 | const nodeModuleInstaller = { 51 | api: () => { 52 | if (isNodeModuleDoesNotExists(API_NODE_MODULES_PATH)) 53 | installerCommand.api(); 54 | }, 55 | mobile: () => { 56 | if (isNodeModuleDoesNotExists(MOBILE_NODE_MODULES_PATH)) 57 | installerCommand.mobile(); 58 | }, 59 | web: () => { 60 | if (isNodeModuleDoesNotExists(WEB_NODE_MODULES_PATH)) 61 | installerCommand.web(); 62 | } 63 | } 64 | 65 | const configRunner = async () => { 66 | const answers = await TEST_MODULE.run(); 67 | switch (answers) { 68 | case "API": 69 | nodeModuleInstaller.api(); 70 | runnerCommand.apiRunner(); 71 | break; 72 | case "Mobile": 73 | nodeModuleInstaller.mobile(); 74 | runnerCommand.mobileRunner(); 75 | break; 76 | case "UI": 77 | nodeModuleInstaller.web(); 78 | const webTestType = await UI_TEST_TYPE.run(); 79 | if (webTestType == 'Mocha') { 80 | const mochaRunMode = await RUNNER_SERVICE.run(); 81 | if (mochaRunMode == 'Local') { runnerCommand.localMochaRunner() } 82 | else if (mochaRunMode == 'Docker') { runnerCommand.dockerMochaRunner() } 83 | } 84 | else if (webTestType == 'Cucumber') { 85 | const bddRunMode = await RUNNER_SERVICE.run(); 86 | if (bddRunMode == 'Local') { runnerCommand.localBDDRunner() } 87 | else if (bddRunMode == 'Docker') { runnerCommand.dockerBDDRunner() } 88 | } 89 | break; 90 | default: throw new Error("Please select option from :: api | web | mobile") 91 | } 92 | } 93 | 94 | configRunner() -------------------------------------------------------------------------------- /web/.env.example: -------------------------------------------------------------------------------- 1 | # Options: LOCAL | REMOTE 2 | MODE=LOCAL 3 | 4 | # Email Notification Specific 5 | SENDER_GMAIL=xxxx@gmail.com 6 | GMAIL_PASSWORD=XXXXX 7 | SENDER_DISPLAY_NAME=TestAutomationHub 8 | MAIL_LIST=xxxx1@gmail.com,xxxx2@gmail.com,xxxx3@gmail.com -------------------------------------------------------------------------------- /web/config/capabilities.ts: -------------------------------------------------------------------------------- 1 | import { RUN_MODE } from "../utils/envReader"; 2 | 3 | const browserOptions = { 4 | args: [ 5 | '--no-sandbox', 6 | '--disable-infobars', 7 | '--disable-gpu', 8 | '--window-size=1440,735' 9 | ], 10 | } 11 | 12 | const browserOptionsHeadless = { 13 | args: [...browserOptions.args, "--headless"] 14 | } 15 | 16 | export const chromeCapabilities = [ 17 | { 18 | maxInstances: 1, 19 | browserName: 'chrome', 20 | acceptInsecureCerts: true, 21 | 'goog:chromeOptions': RUN_MODE === "LOCAL" ? browserOptions : browserOptionsHeadless 22 | } 23 | ] 24 | 25 | export const multipleBrowserCapabilities = [ 26 | { 27 | maxInstances: 1, 28 | browserName: 'chrome', 29 | acceptInsecureCerts: true, 30 | 'goog:chromeOptions': RUN_MODE === "LOCAL" ? browserOptions : browserOptionsHeadless 31 | }, 32 | { 33 | maxInstances: 1, 34 | browserName: 'MicrosoftEdge', 35 | acceptInsecureCerts: true, 36 | 'ms:edgeOptions': RUN_MODE === "LOCAL" ? browserOptions : browserOptionsHeadless 37 | }, 38 | // { 39 | // maxInstances: 1, 40 | // browserName: 'firefox', 41 | // acceptInsecureCerts: true, 42 | // 'moz:firefoxOptions': RUN_MODE === "LOCAL" ? browserOptions : browserOptionsHeadless 43 | // } 44 | ] 45 | 46 | export const DockerBrowserCapabilities = [ 47 | ...chromeCapabilities, 48 | { 49 | maxInstances: 1, 50 | browserName: 'MicrosoftEdge', 51 | acceptInsecureCerts: true, 52 | 'ms:edgeOptions': browserOptionsHeadless 53 | }, 54 | // { 55 | // maxInstances: 1, 56 | // browserName: 'firefox', 57 | // acceptInsecureCerts: true, 58 | // 'moz:firefoxOptions': browserOptionsHeadless 59 | // } 60 | ] -------------------------------------------------------------------------------- /web/config/wdio.conf.docker.ts: -------------------------------------------------------------------------------- 1 | import { DockerBrowserCapabilities } from "./capabilities"; 2 | import { config as mochaConfig } from "./wdio.conf"; 3 | 4 | export const config: WebdriverIO.Config = { 5 | ...mochaConfig, 6 | hostname: 'localhost', 7 | port: 4444, 8 | path: '/', 9 | maxInstances: 5, 10 | capabilities: DockerBrowserCapabilities, 11 | services: ['docker'] 12 | } 13 | -------------------------------------------------------------------------------- /web/config/wdio.conf.e2e.docker.ts: -------------------------------------------------------------------------------- 1 | import { DockerBrowserCapabilities } from './capabilities'; 2 | import { config as bddConfig } from './wdio.conf.e2e'; 3 | 4 | export const config: WebdriverIO.Config = { 5 | ...bddConfig, 6 | hostname: 'localhost', 7 | port: 4444, 8 | path: '/', 9 | maxInstances: 5, 10 | capabilities: DockerBrowserCapabilities, 11 | services: ['docker'] 12 | } 13 | -------------------------------------------------------------------------------- /web/config/wdio.conf.e2e.ts: -------------------------------------------------------------------------------- 1 | import type { Options } from '@wdio/types'; 2 | import { join } from 'path'; 3 | import cucumberJson from 'wdio-cucumberjs-json-reporter'; 4 | import { CUCUMBER_JSON_REPORT_DIR, CUCUMBER_REPORT_DIR, MAIL_JSON_CUCUMBER_DIR } from '../static/pathConstants'; 5 | import { deleteDirectory } from '../utils/fileSystem'; 6 | import { chromeCapabilities } from "./capabilities"; 7 | 8 | export const config: Options.Testrunner = { 9 | // ==================== 10 | // Runner Configuration 11 | // ==================== 12 | 13 | // ================== 14 | // Specify Test Files 15 | // ================== 16 | specs: [ 17 | join(process.cwd(), 'tests', 'cucumber', 'features', '*.feature') 18 | ], 19 | exclude: [], 20 | 21 | // ============ 22 | // Capabilities 23 | // ============ 24 | maxInstances: 1, 25 | capabilities: chromeCapabilities, 26 | 27 | // =================== 28 | // Test Configurations 29 | // =================== 30 | // Level of logging verbosity: trace | debug | info | warn | error | silent 31 | logLevel: 'error', 32 | bail: 0, 33 | baseUrl: 'http://localhost', 34 | waitforTimeout: 10000, 35 | connectionRetryTimeout: 120000, 36 | connectionRetryCount: 3, 37 | // services: [], 38 | framework: 'cucumber', 39 | specFileRetries: 0, 40 | specFileRetriesDelay: 0, 41 | specFileRetriesDeferred: false, 42 | reporters: ['spec', 43 | ['cucumberjs-json', { 44 | jsonFolder: CUCUMBER_JSON_REPORT_DIR, 45 | language: 'en', 46 | }], 47 | ['json', { 48 | outputDir: MAIL_JSON_CUCUMBER_DIR, 49 | outputFileFormat: (opts: any) => { 50 | return `results-${opts.cid}.${opts.capabilities}.json` 51 | } 52 | }] 53 | ], 54 | 55 | cucumberOpts: { 56 | retry: 0, 57 | require: ['./tests/cucumber/steps/*.ts'], 58 | backtrace: false, 59 | requireModule: [], 60 | dryRun: false, 61 | failFast: false, 62 | format: ['pretty'], 63 | snippets: true, 64 | source: true, 65 | profile: [], 66 | strict: false, 67 | tagExpression: '', 68 | timeout: 60000, 69 | ignoreUndefinedDefinitions: false 70 | }, 71 | 72 | // 73 | // ===== 74 | // Hooks 75 | // ===== 76 | // WebdriverIO provides several hooks you can use to interfere with the test process in order to enhance 77 | // it and to build services around it. You can either apply a single function or an array of 78 | // methods to it. If one of them returns with a promise, WebdriverIO will wait until that promise got 79 | // resolved to continue. 80 | /** 81 | * Gets executed once before all workers get launched. 82 | * @param {Object} config wdio configuration object 83 | * @param {Array.} capabilities list of capabilities details 84 | */ 85 | onPrepare: function (config, capabilities) { 86 | deleteDirectory(CUCUMBER_REPORT_DIR); 87 | deleteDirectory(MAIL_JSON_CUCUMBER_DIR); 88 | }, 89 | /** 90 | * Gets executed before a worker process is spawned and can be used to initialise specific service 91 | * for that worker as well as modify runtime environments in an async fashion. 92 | * @param {String} cid capability id (e.g 0-0) 93 | * @param {[type]} caps object containing capabilities for session that will be spawn in the worker 94 | * @param {[type]} specs specs to be run in the worker process 95 | * @param {[type]} args object that will be merged with the main configuration once worker is initialised 96 | * @param {[type]} execArgv list of string arguments passed to the worker process 97 | */ 98 | // onWorkerStart: function (cid, caps, specs, args, execArgv) { 99 | // }, 100 | /** 101 | * Gets executed just before initialising the webdriver session and test framework. It allows you 102 | * to manipulate configurations depending on the capability or spec. 103 | * @param {Object} config wdio configuration object 104 | * @param {Array.} capabilities list of capabilities details 105 | * @param {Array.} specs List of spec file paths that are to be run 106 | */ 107 | // beforeSession: function (config, capabilities, specs) { 108 | // }, 109 | /** 110 | * Gets executed before test execution begins. At this point you can access to all global 111 | * variables like `browser`. It is the perfect place to define custom commands. 112 | * @param {Array.} capabilities list of capabilities details 113 | * @param {Array.} specs List of spec file paths that are to be run 114 | * @param {Object} browser instance of created browser/device session 115 | */ 116 | // before: function (capabilities, specs) { 117 | // }, 118 | /** 119 | * Runs before a WebdriverIO command gets executed. 120 | * @param {String} commandName hook command name 121 | * @param {Array} args arguments that command would receive 122 | */ 123 | // beforeCommand: function (commandName, args) { 124 | // }, 125 | /** 126 | * Runs before a Cucumber feature 127 | */ 128 | // beforeFeature: function (uri, feature) { 129 | // }, 130 | /** 131 | * Runs before a Cucumber scenario 132 | */ 133 | // beforeScenario: function (world) { 134 | // }, 135 | /** 136 | * Runs before a Cucumber step 137 | */ 138 | // beforeStep: function (step, context) { 139 | // }, 140 | /** 141 | * Runs after a Cucumber step 142 | */ 143 | afterStep: async function (step, scenario, result, context) { 144 | if (!result.passed) { 145 | cucumberJson.attach(await browser.takeScreenshot(), 'image/png'); 146 | } 147 | }, 148 | /** 149 | * Runs after a Cucumber scenario 150 | */ 151 | // afterScenario: function (world) { 152 | // }, 153 | /** 154 | * Runs after a Cucumber feature 155 | */ 156 | // afterFeature: function (uri, feature) { 157 | // }, 158 | 159 | /** 160 | * Runs after a WebdriverIO command gets executed 161 | * @param {String} commandName hook command name 162 | * @param {Array} args arguments that command would receive 163 | * @param {Number} result 0 - command success, 1 - command error 164 | * @param {Object} error error object if any 165 | */ 166 | // afterCommand: function (commandName, args, result, error) { 167 | // }, 168 | /** 169 | * Gets executed after all tests are done. You still have access to all global variables from 170 | * the test. 171 | * @param {Number} result 0 - test pass, 1 - test fail 172 | * @param {Array.} capabilities list of capabilities details 173 | * @param {Array.} specs List of spec file paths that ran 174 | */ 175 | // after: function (result, capabilities, specs) { 176 | // }, 177 | /** 178 | * Gets executed right after terminating the webdriver session. 179 | * @param {Object} config wdio configuration object 180 | * @param {Array.} capabilities list of capabilities details 181 | * @param {Array.} specs List of spec file paths that ran 182 | */ 183 | // afterSession: function (config, capabilities, specs) { 184 | // }, 185 | /** 186 | * Gets executed after all workers got shut down and the process is about to exit. An error 187 | * thrown in the onComplete hook will result in the test run failing. 188 | * @param {Object} exitCode 0 - success, 1 - fail 189 | * @param {Object} config wdio configuration object 190 | * @param {Array.} capabilities list of capabilities details 191 | * @param {} results object containing test results 192 | */ 193 | onComplete: function (exitCode, config, capabilities, results) { 194 | const mergeResults = require('@wdio/json-reporter/mergeResults'); 195 | mergeResults(MAIL_JSON_CUCUMBER_DIR, 'results-*', '/merged_result.json'); 196 | }, 197 | /** 198 | * Gets executed when a refresh happens. 199 | * @param {String} oldSessionId session ID of the old session 200 | * @param {String} newSessionId session ID of the new session 201 | */ 202 | //onReload: function(oldSessionId, newSessionId) { 203 | //} 204 | } 205 | -------------------------------------------------------------------------------- /web/config/wdio.conf.ts: -------------------------------------------------------------------------------- 1 | import type { Options } from '@wdio/types'; 2 | import { join } from 'path'; 3 | import { MOCHA_OUTPUT_DIR } from "../static/pathConstants"; 4 | import { deleteDirectory } from "../utils/fileSystem"; 5 | import { chromeCapabilities } from "./capabilities"; 6 | 7 | export const config: Options.Testrunner = { 8 | // ==================== 9 | // Runner Configuration 10 | // ==================== 11 | runner: 'local', 12 | 13 | // ================== 14 | // Specify Test Files 15 | // ================== 16 | specs: [ 17 | join(process.cwd(), 'tests', 'mocha', '*.ts') 18 | ], 19 | exclude: [ 20 | join(process.cwd(), 'tests', 'mocha', 'frameShadowDom.spec.ts') 21 | ], 22 | 23 | suites: { 24 | smoke: [join(process.cwd(), 'tests', 'smoke.spec.ts')] 25 | }, 26 | // ============ 27 | // Capabilities 28 | // ============ 29 | maxInstances: 1, 30 | capabilities: chromeCapabilities, 31 | 32 | // =================== 33 | // Test Configurations 34 | // =================== 35 | // Level of logging verbosity: trace | debug | info | warn | error | silent 36 | logLevel: 'error', 37 | bail: 0, 38 | baseUrl: 'http://localhost', 39 | waitforTimeout: 10000, 40 | connectionRetryTimeout: 120000, 41 | connectionRetryCount: 3, 42 | // services: [], 43 | framework: 'mocha', 44 | specFileRetries: 0, 45 | specFileRetriesDelay: 0, 46 | specFileRetriesDeferred: false, 47 | reporters: ['spec', 48 | ['json', { 49 | outputDir: MOCHA_OUTPUT_DIR, 50 | outputFileFormat: (opts: any) => { 51 | return `results-${opts.cid}.${opts.capabilities.browserName}.json` 52 | } 53 | }] 54 | ], 55 | mochaOpts: { 56 | ui: 'bdd', 57 | timeout: 60000, 58 | mochawesomeOpts: { 59 | includeScreenshots: true, 60 | screenshotUseRelativePath: true 61 | }, 62 | }, 63 | // 64 | // ===== 65 | // Hooks 66 | // ===== 67 | // WebdriverIO provides several hooks you can use to interfere with the test process in order to enhance 68 | // it and to build services around it. You can either apply a single function or an array of 69 | // methods to it. If one of them returns with a promise, WebdriverIO will wait until that promise got 70 | // resolved to continue. 71 | /** 72 | * Gets executed once before all workers get launched. 73 | * @param {Object} config wdio configuration object 74 | * @param {Array.} capabilities list of capabilities details 75 | */ 76 | onPrepare: function (config, capabilities) { 77 | deleteDirectory(MOCHA_OUTPUT_DIR); 78 | deleteDirectory('mochawesome-report'); 79 | }, 80 | /** 81 | * Gets executed before a worker process is spawned and can be used to initialise specific service 82 | * for that worker as well as modify runtime environments in an async fashion. 83 | * @param {String} cid capability id (e.g 0-0) 84 | * @param {[type]} caps object containing capabilities for session that will be spawn in the worker 85 | * @param {[type]} specs specs to be run in the worker process 86 | * @param {[type]} args object that will be merged with the main configuration once worker is initialised 87 | * @param {[type]} execArgv list of string arguments passed to the worker process 88 | */ 89 | // onWorkerStart: function (cid, caps, specs, args, execArgv) { 90 | // }, 91 | /** 92 | * Gets executed just before initialising the webdriver session and test framework. It allows you 93 | * to manipulate configurations depending on the capability or spec. 94 | * @param {Object} config wdio configuration object 95 | * @param {Array.} capabilities list of capabilities details 96 | * @param {Array.} specs List of spec file paths that are to be run 97 | */ 98 | // beforeSession: function (config, capabilities, specs) { 99 | // }, 100 | /** 101 | * Gets executed before test execution begins. At this point you can access to all global 102 | * variables like `browser`. It is the perfect place to define custom commands. 103 | * @param {Array.} capabilities list of capabilities details 104 | * @param {Array.} specs List of spec file paths that are to be run 105 | * @param {Object} browser instance of created browser/device session 106 | */ 107 | // before: function (capabilities, specs) { 108 | // }, 109 | /** 110 | * Runs before a WebdriverIO command gets executed. 111 | * @param {String} commandName hook command name 112 | * @param {Array} args arguments that command would receive 113 | */ 114 | // beforeCommand: function (commandName, args) { 115 | // }, 116 | /** 117 | * Hook that gets executed before the suite starts 118 | * @param {Object} suite suite details 119 | */ 120 | // beforeSuite: function (suite) { 121 | // }, 122 | /** 123 | * Function to be executed before a test (in Mocha/Jasmine) starts. 124 | */ 125 | // beforeTest: function (test, context) { 126 | // }, 127 | /** 128 | * Hook that gets executed _before_ a hook within the suite starts (e.g. runs before calling 129 | * beforeEach in Mocha) 130 | */ 131 | // beforeHook: function (test, context) { 132 | // }, 133 | /** 134 | * Hook that gets executed _after_ a hook within the suite starts (e.g. runs after calling 135 | * afterEach in Mocha) 136 | */ 137 | // afterHook: function (test, context, { error, result, duration, passed, retries }) { 138 | // }, 139 | /** 140 | * Function to be executed after a test (in Mocha/Jasmine). 141 | */ 142 | afterTest: async function (test, context, { error, result, duration, passed, retries }) { 143 | if (!passed) { 144 | await browser.takeScreenshot(); 145 | } 146 | }, 147 | 148 | /** 149 | * Hook that gets executed after the suite has ended 150 | * @param {Object} suite suite details 151 | */ 152 | // afterSuite: function (suite) { 153 | // }, 154 | /** 155 | * Runs after a WebdriverIO command gets executed 156 | * @param {String} commandName hook command name 157 | * @param {Array} args arguments that command would receive 158 | * @param {Number} result 0 - command success, 1 - command error 159 | * @param {Object} error error object if any 160 | */ 161 | // afterCommand: function (commandName, args, result, error) { 162 | // }, 163 | /** 164 | * Gets executed after all tests are done. You still have access to all global variables from 165 | * the test. 166 | * @param {Number} result 0 - test pass, 1 - test fail 167 | * @param {Array.} capabilities list of capabilities details 168 | * @param {Array.} specs List of spec file paths that ran 169 | */ 170 | // after: function (result, capabilities, specs) { 171 | // }, 172 | /** 173 | * Gets executed right after terminating the webdriver session. 174 | * @param {Object} config wdio configuration object 175 | * @param {Array.} capabilities list of capabilities details 176 | * @param {Array.} specs List of spec file paths that ran 177 | */ 178 | // afterSession: function (config, capabilities, specs) { 179 | // }, 180 | /** 181 | * Gets executed after all workers got shut down and the process is about to exit. An error 182 | * thrown in the onComplete hook will result in the test run failing. 183 | * @param {Object} exitCode 0 - success, 1 - fail 184 | * @param {Object} config wdio configuration object 185 | * @param {Array.} capabilities list of capabilities details 186 | * @param {} results object containing test results 187 | */ 188 | onComplete: function (exitCode, config, capabilities, results) { 189 | const mergeResults = require('@wdio/json-reporter/mergeResults'); 190 | mergeResults(MOCHA_OUTPUT_DIR, "results-*", "wdio-ma-merged.json"); 191 | }, 192 | /** 193 | * Gets executed when a refresh happens. 194 | * @param {String} oldSessionId session ID of the old session 195 | * @param {String} newSessionId session ID of the new session 196 | */ 197 | //onReload: function(oldSessionId, newSessionId) { 198 | //} 199 | } 200 | -------------------------------------------------------------------------------- /web/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | 4 | selenium-hub: 5 | image: selenium/hub:latest 6 | container_name: selenium-hub 7 | restart: always 8 | ports: 9 | - "4442:4442" 10 | - "4443:4443" 11 | - "4444:4444" 12 | 13 | chrome: 14 | image: selenium/node-chrome:latest 15 | shm_size: 2gb 16 | depends_on: 17 | - selenium-hub 18 | environment: 19 | - SE_EVENT_BUS_HOST=selenium-hub 20 | - SE_EVENT_BUS_PUBLISH_PORT=4442 21 | - SE_EVENT_BUS_SUBSCRIBE_PORT=4443 22 | - VNC_NO_PASSWORD=1 23 | - SE_NODE_MAX_SESSIONS=2 24 | 25 | edge: 26 | image: selenium/node-edge:latest 27 | shm_size: 2gb 28 | depends_on: 29 | - selenium-hub 30 | environment: 31 | - SE_EVENT_BUS_HOST=selenium-hub 32 | - SE_EVENT_BUS_PUBLISH_PORT=4442 33 | - SE_EVENT_BUS_SUBSCRIBE_PORT=4443 34 | - VNC_NO_PASSWORD=1 35 | - SE_NODE_MAX_SESSIONS=2 36 | 37 | # firefox: 38 | # image: selenium/node-firefox:latest 39 | # shm_size: 2gb 40 | # depends_on: 41 | # - selenium-hub 42 | # environment: 43 | # - SE_EVENT_BUS_HOST=selenium-hub 44 | # - SE_EVENT_BUS_PUBLISH_PORT=4442 45 | # - SE_EVENT_BUS_SUBSCRIBE_PORT=4443 46 | # - VNC_NO_PASSWORD=1 47 | # - SE_NODE_MAX_SESSIONS=2 48 | 49 | chrome_video: 50 | image: selenium/video:ffmpeg-4.3.1-20220217 51 | volumes: 52 | - ./tmp/videos:/videos 53 | depends_on: 54 | - chrome 55 | environment: 56 | - DISPLAY_CONTAINER_NAME=chrome 57 | - FILE_NAME=chrome_video.mp4 58 | 59 | edge_video: 60 | image: selenium/video:ffmpeg-4.3.1-20220217 61 | volumes: 62 | - ./tmp/videos:/videos 63 | depends_on: 64 | - edge 65 | environment: 66 | - DISPLAY_CONTAINER_NAME=edge 67 | - FILE_NAME=edge_video.mp4 68 | 69 | # firefox_video: 70 | # image: selenium/video:ffmpeg-4.3.1-20220217 71 | # volumes: 72 | # - ./tmp/videos:/videos 73 | # depends_on: 74 | # - firefox 75 | # environment: 76 | # - DISPLAY_CONTAINER_NAME=firefox 77 | # - FILE_NAME=firefox_video.mp4 -------------------------------------------------------------------------------- /web/generator/bddEmail.ts: -------------------------------------------------------------------------------- 1 | import { MAILER_PATH } from "../static/pathConstants"; 2 | import { env_receiver_list, env_sender_gmail, env_sender_name } from "../utils/envReader"; 3 | import { parseJsonFile, zipFolder } from "../utils/fileSystem"; 4 | import { mailSender } from "../utils/mailer"; 5 | import { emailBodyTemplate } from "./emailBody"; 6 | 7 | 8 | const bddJsonData = parseJsonFile(MAILER_PATH.WDIO_JSON_CUCUMBER_FILE); 9 | let result_state = bddJsonData.state 10 | 11 | let bdd_test_passed = result_state.passed; 12 | let bdd_test_failed = result_state.failed; 13 | let bdd_test_skipped = result_state.skipped; 14 | let bdd_test_total = bdd_test_passed + bdd_test_failed + bdd_test_skipped; 15 | 16 | zipFolder(MAILER_PATH.SOURCE_CUCUMBER_HTML, MAILER_PATH.DESTINATION_CUCUMBER_COMPRESS) 17 | 18 | let bddEMailOptions = { 19 | from: `"${env_sender_name}" <${env_sender_gmail}>`, 20 | to: env_receiver_list, 21 | subject: "Automation Execution Report", 22 | html: emailBodyTemplate(bdd_test_total, bdd_test_passed, bdd_test_failed, bdd_test_skipped), 23 | attachments: [ 24 | { 25 | filename: 'cucumber-report.zip', 26 | path: MAILER_PATH.DESTINATION_CUCUMBER_COMPRESS 27 | } 28 | ] 29 | }; 30 | 31 | mailSender(bddEMailOptions) -------------------------------------------------------------------------------- /web/generator/emailBody.ts: -------------------------------------------------------------------------------- 1 | export const emailBodyTemplate = (total: number, passed: number, failed: number, skipped: number) => { 2 | return ` 3 | 4 | 5 | 6 | 7 | 8 | 9 | 15 | 16 | 22 | 27 | 33 | 34 |

Test Automation Report

10 | 11 | 12 | 13 |
${total}
Total
14 |
17 | 18 | 19 | 20 |
${passed}
Passed
21 |
23 | 24 | 25 | 26 |
${failed}
Failed
28 | 29 | 30 | 31 |
${skipped}
Skipped
32 |
35 | 36 | ` 37 | } -------------------------------------------------------------------------------- /web/generator/index.ts: -------------------------------------------------------------------------------- 1 | import { generate, Options } from 'cucumber-html-reporter'; 2 | import { CUCUMBER_JSON_REPORT_DIR, CUCUMBER_REPORT_DIR, CUCUMBER_SCREENSHOT_REPORT_DIR } from '../static/pathConstants'; 3 | 4 | let options: Options = { 5 | theme: 'bootstrap', 6 | brandTitle: "WebdriverIO Cucumber Report", 7 | jsonDir: CUCUMBER_JSON_REPORT_DIR, 8 | output: `${CUCUMBER_REPORT_DIR}/cucumber-report.html`, 9 | screenshotsDirectory: CUCUMBER_SCREENSHOT_REPORT_DIR, 10 | storeScreenshots: true, 11 | reportSuiteAsScenarios: true, 12 | scenarioTimestamp: true, 13 | ignoreBadJsonFile: true, 14 | launchReport: false, 15 | metadata: { 16 | "App Version": "0.3.2", 17 | "Test Environment": "STAGING", 18 | "Browser": "Chrome", 19 | "Platform": process.platform, 20 | "Parallel": "Features", 21 | "Executed": "Local" 22 | } 23 | }; 24 | 25 | generate(options); -------------------------------------------------------------------------------- /web/generator/mochaEmail.ts: -------------------------------------------------------------------------------- 1 | import { MAILER_PATH } from "../static/pathConstants"; 2 | import { env_receiver_list, env_sender_gmail, env_sender_name } from "../utils/envReader"; 3 | import { parseJsonFile } from "../utils/fileSystem"; 4 | import { mailSender } from "../utils/mailer"; 5 | import { emailBodyTemplate } from "./emailBody"; 6 | 7 | const bddJsonData = parseJsonFile(MAILER_PATH.WDIO_JSON_MOCHA_FILE); 8 | let state = bddJsonData.stats 9 | 10 | let mochaEMailOptions = { 11 | from: `"${env_sender_name}" <${env_sender_gmail}>`, 12 | to: env_receiver_list, 13 | subject: "Automation Execution Report", 14 | html: emailBodyTemplate(state.tests, state.passes, state.failures, state.pending), 15 | }; 16 | 17 | mailSender(mochaEMailOptions) -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webdriverio_typescript_e2e", 3 | "version": "1.0.0", 4 | "description": "Boilerplate project using webdriverio with mocha and BDD framework", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "wdio run ./config/wdio.conf.ts", 8 | "smoke": "wdio run ./config/wdio.conf.ts --suite smoke", 9 | "pretest:docker": "docker-compose up -d", 10 | "test:docker": "wdio run ./config/wdio.conf.docker.ts", 11 | "posttest:docker": "docker-compose down", 12 | "report:mocha": "marge ./reports/mocha/wdio-ma-merged.json --reportTitle 'WebAppMochaReport' && move mochawesome-report.html ./mochawesome-report", 13 | "mailMochaResult": "ts-node ./generator/mochaEmail.ts", 14 | "report:mocha:ci": "marge ./reports/mocha/wdio-ma-merged.json", 15 | "test:e2e": "wdio run ./config/wdio.conf.e2e.ts", 16 | "pretest:e2e:docker": "docker-compose up -d", 17 | "test:e2e:docker": "wdio run ./config/wdio.conf.e2e.docker.ts", 18 | "posttest:e2e:docker": "docker-compose down", 19 | "report:cucumber": "ts-node ./generator/index.ts", 20 | "mailCucumberReport": "npm run report:cucumber && ts-node ./generator/bddEmail.ts" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/sadabnepal/WebdriverIOTypeScriptE2E.git" 25 | }, 26 | "author": "MD SADAB SAQIB", 27 | "license": "ISC", 28 | "bugs": { 29 | "url": "https://github.com/sadabnepal/WebdriverIOTypeScriptE2E/issues" 30 | }, 31 | "homepage": "https://github.com/sadabnepal/WebdriverIOTypeScriptE2E#readme", 32 | "devDependencies": { 33 | "@faker-js/faker": "^8.4.1", 34 | "@types/nodemailer": "^6.4.14", 35 | "@wdio/cli": "^8.35.1", 36 | "@wdio/cucumber-framework": "^8.35.0", 37 | "@wdio/json-reporter": "^8.36.0", 38 | "@wdio/junit-reporter": "^8.32.4", 39 | "@wdio/local-runner": "^8.35.1", 40 | "@wdio/mocha-framework": "^8.35.0", 41 | "@wdio/spec-reporter": "^8.32.4", 42 | "cucumber-html-reporter": "^7.1.1", 43 | "dotenv": "^16.4.5", 44 | "nodemailer": "^6.9.13", 45 | "ts-node": "^10.9.2", 46 | "typescript": "^5.4.4", 47 | "wdio-cucumberjs-json-reporter": "^5.2.1", 48 | "wdio-docker-service": "^3.2.1" 49 | }, 50 | "dependencies": { 51 | "zip-local": "^0.3.5" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /web/pages/basePage.ts: -------------------------------------------------------------------------------- 1 | import { WebdriverIOElement, WebdriverIOElements } from "../types/webelements"; 2 | 3 | const logStep = (logMessage: string) => console.log(`STEP || ${logMessage}`); 4 | 5 | export default class BasePage { 6 | 7 | protected async open(path: string) { 8 | await browser.maximizeWindow(); 9 | await browser.url(path) 10 | } 11 | 12 | protected async clickElement(element: WebdriverIOElement) { 13 | await element.click() 14 | logStep(`Clicked on Element: ${await element.selector}`); 15 | } 16 | 17 | protected async waitAndClick(element: WebdriverIOElement, waitTime?: number) { 18 | await element.waitForClickable({ timeout: waitTime ? waitTime : 10000 }) 19 | await element.click() 20 | logStep(`clicked on Element: ${await element.selector}`); 21 | } 22 | 23 | protected async enterData(element: WebdriverIOElement, value: string | number) { 24 | await element.clearValue(); 25 | await element.setValue(value); 26 | logStep(`Entered value : ${value} on element: ${await element.selector}`); 27 | } 28 | 29 | protected async waitAndEnterData(element: WebdriverIOElement, value: string | number, waitTime?: number) { 30 | await element.waitForEnabled({ timeout: waitTime ? waitTime : 10000 }); 31 | await element.clearValue(); 32 | await element.setValue(value); 33 | logStep(`Entered value: ${value} on Element: ${await element.selector} `); 34 | } 35 | 36 | protected async scrollToElement(element: WebdriverIOElement) { 37 | await element.scrollIntoView(); 38 | logStep(`Scrolled to Element: ${await element.selector}`); 39 | } 40 | 41 | protected async selectDropdownByText(element: WebdriverIOElement, text: string) { 42 | await element.selectByVisibleText(text) 43 | logStep(`Selected Element: ${await element.selector} by visible text: ${text}`); 44 | } 45 | 46 | protected async clickOnMatchingText(elements: WebdriverIOElements, expectedText: string) { 47 | await elements.forEach(async element => { 48 | if (await element.getText() === expectedText) { 49 | await element.click(); 50 | logStep(`Clicked on matching text: ${expectedText}`); 51 | } 52 | }) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /web/pages/form.page.ts: -------------------------------------------------------------------------------- 1 | import { FormFieldTypes } from "../types/customTypes" 2 | import BasePage from "./basePage" 3 | 4 | class FormElements extends BasePage { 5 | 6 | get nameTextBox() { return $('#cname') } 7 | get emailTextBox() { return $('#cemail') } 8 | get phoneTextBox() { return $('#cphone') } 9 | get phoneRadioButtons() { return $$('.radio-container') } 10 | get messageTextBox() { return $('#cmessage') } 11 | get questionDropdown() { return $('#cselect') } 12 | get simulateMsgCheckBox() { return $('#csuccess') } 13 | get submitBtn() { return $('#submit') } 14 | get submittedMsg() { return $('#cmsgSubmit') } 15 | 16 | async openApp() { 17 | await super.open("https://aquabottesting.com/") 18 | } 19 | 20 | async submitContactForm(formData: FormFieldTypes) { 21 | await this.scrollToElement(this.nameTextBox) 22 | await this.enterData(this.nameTextBox, formData.name) 23 | await this.enterData(this.emailTextBox, formData.email) 24 | await this.enterData(this.phoneTextBox, formData.contactNo) 25 | await this.clickOnMatchingText(this.phoneRadioButtons, formData.contactType) 26 | if (formData.message) { await this.enterData(this.messageTextBox, formData.message) } 27 | await this.selectDropdownByText(this.questionDropdown, formData.question) 28 | await this.clickElement(this.simulateMsgCheckBox) 29 | await this.clickElement(this.submitBtn) 30 | } 31 | 32 | } 33 | export default new FormElements() -------------------------------------------------------------------------------- /web/pages/frameShadowDom.page.ts: -------------------------------------------------------------------------------- 1 | import BasePage from "./basePage"; 2 | 3 | class FrameShadowDom extends BasePage { 4 | 5 | async openApp() { 6 | await super.open('https://selectorshub.com/xpath-practice-page/'); 7 | } 8 | 9 | get snacksFrame() { return $("#pact") } 10 | get snacksShadowDom() { return $("#snacktime") } 11 | get username() { return $("#userName") } 12 | get country() { return $("#jex") } 13 | get teaShadowElement() { return this.snacksShadowDom.shadow$("#tea") } 14 | 15 | async enterSnacks(value: string) { 16 | await browser.switchToFrame(await this.snacksFrame) 17 | await this.enterData(this.teaShadowElement, value) 18 | } 19 | 20 | async enterCountry(value: string) { 21 | await browser.switchToFrame(await this.username.shadow$("#pact1")); 22 | await this.enterData(this.country, value); 23 | } 24 | 25 | } 26 | export default new FrameShadowDom() -------------------------------------------------------------------------------- /web/pages/login.page.ts: -------------------------------------------------------------------------------- 1 | import BasePage from "./basePage"; 2 | 3 | class LoginPage extends BasePage { 4 | 5 | get inputUsername() { return $('#username') } 6 | get inputPassword() { return $('#password') } 7 | get btnSubmit() { return $('button[type="submit"]') } 8 | 9 | async login(username: string, password: string) { 10 | await this.waitAndEnterData(this.inputUsername, username); 11 | await this.waitAndEnterData(this.inputPassword, password); 12 | await this.waitAndClick(this.btnSubmit); 13 | } 14 | 15 | async openApp() { 16 | await super.open('https://the-internet.herokuapp.com/login'); 17 | } 18 | } 19 | 20 | export default new LoginPage(); 21 | -------------------------------------------------------------------------------- /web/pages/secure.page.ts: -------------------------------------------------------------------------------- 1 | import BasePage from "./basePage"; 2 | 3 | class SecurePage extends BasePage { 4 | 5 | get flashAlert() { return $('#flash') } 6 | } 7 | 8 | export default new SecurePage(); 9 | -------------------------------------------------------------------------------- /web/pages/webTables.page.ts: -------------------------------------------------------------------------------- 1 | import BasePage from "./basePage" 2 | 3 | class WebTablePage extends BasePage { 4 | 5 | get dashboardHeader() { return $("

") } 6 | get exampleTable1() { return $("#table1") } 7 | 8 | async openApp() { 9 | await super.open("https://the-internet.herokuapp.com/tables") 10 | } 11 | 12 | private setColumnData(personObject: any, index: number, value: string) { 13 | if (index === 0) personObject["LastName"] = value 14 | if (index === 1) personObject["FirstName"] = value 15 | if (index === 2) personObject["Email"] = value 16 | if (index === 3) personObject["Due"] = value 17 | if (index === 4) personObject["Website"] = value 18 | if (index === 5) personObject["Action"] = value 19 | } 20 | 21 | async getTableDataAsListOfMap() { 22 | await this.exampleTable1.scrollIntoView() 23 | let rowCount = (await this.exampleTable1.$$("tbody>tr")).length; 24 | let columnCount = (await this.exampleTable1.$$("thead>tr>th")).length; 25 | 26 | let personDetails: any[] = []; 27 | 28 | for (let i = 0; i < rowCount; i++) { 29 | let personObject = {}; 30 | for (let j = 0; j < columnCount; j++) { 31 | let cellValue = await this.exampleTable1.$(`tbody>tr:nth-child(${i + 1})>td:nth-child(${j + 1})`).getText() 32 | this.setColumnData(personObject, j, cellValue) 33 | } 34 | personDetails.push(personObject) //push table cell as oject to array 35 | } 36 | return personDetails; 37 | } 38 | 39 | 40 | } 41 | export default new WebTablePage() -------------------------------------------------------------------------------- /web/resources/logindata.ts: -------------------------------------------------------------------------------- 1 | import { faker } from '@faker-js/faker'; 2 | import { decodeFromBase64String } from '../utils/base64Utils'; 3 | 4 | const herokuAppBase64EncodedPassword = "U3VwZXJTZWNyZXRQYXNzd29yZCE="; 5 | 6 | export const herokuAppLoginData = { 7 | validUserName: 'tomsmith', 8 | validPassword: () => decodeFromBase64String(herokuAppBase64EncodedPassword), 9 | invalidUserName: faker.internet.userName(), 10 | invalidPassword: faker.internet.password(6) 11 | } -------------------------------------------------------------------------------- /web/resources/testdata.json: -------------------------------------------------------------------------------- 1 | { 2 | "shadowDomData": { 3 | "tea": "Lemon Tea", 4 | "country": "Nepal" 5 | }, 6 | "formDataWithMandateFieldsOnly": { 7 | "name": "testuser", 8 | "email": "testuser@test.com", 9 | "contactNo": 1234567890, 10 | "contactType": "Mobile Phone", 11 | "question": "Frameworks" 12 | }, 13 | "formDataWithAllFields": { 14 | "name": "testuser", 15 | "email": "testuser@test.com", 16 | "contactNo": 1234567890, 17 | "contactType": "Home Phone", 18 | "question": "Frameworks", 19 | "message": "This is test input" 20 | } 21 | } -------------------------------------------------------------------------------- /web/static/frameworkConstants.ts: -------------------------------------------------------------------------------- 1 | export default class FrameworkConstants { 2 | static LOGIN_SUCCESS_MSG = "You logged into a secure area!" 3 | static LOGIN_FAILED_MSG = "Your username is invalid!" 4 | static FORM_SUBMITTED_MSG = "Message Submitted!" 5 | } -------------------------------------------------------------------------------- /web/static/pathConstants.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | export const MOCHA_OUTPUT_DIR = path.join(process.cwd(), 'reports', 'mocha'); 4 | export const CUCUMBER_REPORT_DIR = path.join(process.cwd(), 'reports', 'cucumber'); 5 | export const CUCUMBER_JSON_REPORT_DIR = path.join(CUCUMBER_REPORT_DIR); 6 | export const CUCUMBER_SCREENSHOT_REPORT_DIR = path.join(CUCUMBER_REPORT_DIR, "screenshots"); 7 | export const MAIL_JSON_CUCUMBER_DIR = path.join(process.cwd(), 'reports', 'cucumermail'); 8 | 9 | export const MAILER_PATH = { 10 | SOURCE_CUCUMBER_HTML: path.join(CUCUMBER_REPORT_DIR, 'cucumber-report.html'), 11 | DESTINATION_CUCUMBER_COMPRESS: path.join(process.cwd(), 'reports', 'cucumber-report.zip'), 12 | WDIO_JSON_CUCUMBER_FILE: path.join(MAIL_JSON_CUCUMBER_DIR, 'merged_result.json'), 13 | WDIO_JSON_MOCHA_FILE: path.join(MOCHA_OUTPUT_DIR, 'wdio-ma-merged.json'), 14 | } -------------------------------------------------------------------------------- /web/tests/cucumber/features/BackgroundDatatable.feature: -------------------------------------------------------------------------------- 1 | Feature: Login feature for background and datable demo 2 | 3 | Background: open herokuapp 4 | Given I open the heroku app login page 5 | 6 | Scenario: As a valid user, I can log into the secure area 7 | When I login with given username and password 8 | | username | password | 9 | | tomsmith | SuperSecretPassword! | 10 | Then I should see a success flash message 11 | 12 | Scenario: As a invalid user, I can not log into the secure area 13 | When I login with given username and password 14 | | username | password | 15 | | foobar | barfoo | 16 | Then I should see a failed flash message -------------------------------------------------------------------------------- /web/tests/cucumber/features/ExamplesTable.feature: -------------------------------------------------------------------------------- 1 | Feature: Login feature for examples table demo 2 | 3 | Scenario Outline: As a user, I can log into the secure area 4 | 5 | Given I am on the login page 6 | When I login with and 7 | Then I should see a flash message saying 8 | 9 | Examples: 10 | | username | password | message | 11 | | tomsmith | SuperSecretPassword! | You logged into a secure area! | 12 | | foobar | barfoo | Your username is invalid! | 13 | -------------------------------------------------------------------------------- /web/tests/cucumber/steps/BackgroundDatatable.steps..ts: -------------------------------------------------------------------------------- 1 | import { Given, Then, When } from '@cucumber/cucumber'; 2 | import loginPage from '../../../pages/login.page'; 3 | import securePage from '../../../pages/secure.page'; 4 | import FrameworkConstants from '../../../static/frameworkConstants'; 5 | 6 | Given(/^I open the heroku app login page$/, async () => { 7 | await loginPage.openApp(); 8 | }); 9 | 10 | When(/^I login with given username and password$/, async (dataTable) => { 11 | await loginPage.login(dataTable.hashes()[0].username, dataTable.hashes()[0].password) 12 | }); 13 | 14 | Then(/^I should see a (success|failed) flash message$/, async (status: string) => { 15 | await expect(securePage.flashAlert).toBeExisting(); 16 | 17 | switch (status) { 18 | case "success": await expect(securePage.flashAlert).toHaveTextContaining(FrameworkConstants.LOGIN_SUCCESS_MSG); 19 | break; 20 | case "failed": await expect(securePage.flashAlert).toHaveTextContaining(FrameworkConstants.LOGIN_FAILED_MSG); 21 | break; 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /web/tests/cucumber/steps/ExamplesTable.steps.ts: -------------------------------------------------------------------------------- 1 | import { Given, Then, When } from '@cucumber/cucumber'; 2 | import loginPage from '../../../pages/login.page'; 3 | import securePage from '../../../pages/secure.page'; 4 | 5 | Given(/^I am on the login page$/, async () => { 6 | await loginPage.openApp() 7 | }); 8 | 9 | When(/^I login with (\w+) and (.+)$/, async (username, password) => { 10 | await loginPage.login(username, password) 11 | }); 12 | 13 | Then(/^I should see a flash message saying (.*)$/, async (message) => { 14 | await expect(securePage.flashAlert).toBeExisting(); 15 | await expect(securePage.flashAlert).toHaveTextContaining(message); 16 | }); 17 | 18 | -------------------------------------------------------------------------------- /web/tests/mocha/formElements.spec.ts: -------------------------------------------------------------------------------- 1 | import formPage from "../../pages/form.page"; 2 | import loginData from "../../resources/testdata.json"; 3 | import FrameworkConstants from "../../static/frameworkConstants"; 4 | 5 | describe('test form submission and use of interface', () => { 6 | 7 | beforeEach(async () => { 8 | await formPage.openApp() 9 | }) 10 | 11 | it('should submit form with mandatory fields only', async () => { 12 | await formPage.submitContactForm(loginData.formDataWithMandateFieldsOnly) 13 | await expect(formPage.submittedMsg).toHaveText(FrameworkConstants.FORM_SUBMITTED_MSG) 14 | }); 15 | 16 | it('should submit form with all mandate and non-mandate fields', async () => { 17 | await formPage.submitContactForm(loginData.formDataWithAllFields) 18 | await expect(formPage.submittedMsg).toHaveText(FrameworkConstants.FORM_SUBMITTED_MSG) 19 | }); 20 | 21 | }); -------------------------------------------------------------------------------- /web/tests/mocha/frameShadowDom.spec.ts: -------------------------------------------------------------------------------- 1 | import frameShadowDomPage from "../../pages/frameShadowDom.page"; 2 | import loginData from "../../resources/testdata.json"; 3 | 4 | describe('Shadow dom and frame demo', () => { 5 | 6 | beforeEach(async () => { 7 | await frameShadowDomPage.openApp() 8 | }) 9 | 10 | it("should switch to frame and handle shadow dom element", async () => { 11 | await frameShadowDomPage.enterSnacks(loginData.shadowDomData.tea) 12 | await expect(frameShadowDomPage.teaShadowElement).toHaveValue(loginData.shadowDomData.tea) 13 | }) 14 | 15 | it("should switch to frame inside shadow dom", async () => { 16 | await frameShadowDomPage.enterCountry(loginData.shadowDomData.country); 17 | await expect(frameShadowDomPage.country).toHaveValue(loginData.shadowDomData.country) 18 | }) 19 | 20 | }) 21 | -------------------------------------------------------------------------------- /web/tests/mocha/herokuAppLogin.spec.ts: -------------------------------------------------------------------------------- 1 | import loginPage from "../../pages/login.page"; 2 | import securePage from "../../pages/secure.page"; 3 | import { herokuAppLoginData } from "../../resources/logindata"; 4 | import FrameworkConstants from "../../static/frameworkConstants"; 5 | 6 | 7 | describe('Test heroku app application login', () => { 8 | 9 | beforeEach(async () => { 10 | await loginPage.openApp(); 11 | }) 12 | 13 | it("should login with valid credentials", async () => { 14 | await loginPage.login(herokuAppLoginData.validUserName, herokuAppLoginData.validPassword()); 15 | await expect(securePage.flashAlert).toBeExisting(); 16 | await expect(securePage.flashAlert).toHaveText(expect.stringContaining(FrameworkConstants.LOGIN_SUCCESS_MSG)) 17 | }); 18 | 19 | it("should not login with invalid credentials", async () => { 20 | await loginPage.login(herokuAppLoginData.invalidUserName, herokuAppLoginData.invalidPassword); 21 | await expect(securePage.flashAlert).toBeExisting(); 22 | await expect(securePage.flashAlert).toHaveText(expect.stringContaining(FrameworkConstants.LOGIN_FAILED_MSG)); 23 | }); 24 | 25 | }); -------------------------------------------------------------------------------- /web/tests/mocha/webTables.spec.ts: -------------------------------------------------------------------------------- 1 | import webTablesPage from "../../pages/webTables.page"; 2 | import { table1DataOptions } from "../../types/customTypes"; 3 | 4 | describe('validate web table elements', () => { 5 | 6 | let example1TableData: any; 7 | 8 | beforeEach(async () => { 9 | await webTablesPage.openApp() 10 | }) 11 | 12 | it("should retrieve example1 table data", async () => { 13 | await expect(webTablesPage.dashboardHeader).toHaveText("Data Tables") 14 | example1TableData = await webTablesPage.getTableDataAsListOfMap() 15 | console.table(example1TableData); 16 | expect(example1TableData.length).toBeGreaterThan(0) 17 | }) 18 | 19 | it("should filter user data with due > $50", async () => { 20 | const filterDataGreaterThan50 = (data: table1DataOptions) => +(data.Due.split("$")[1]) > 50; 21 | const filteredUserData = example1TableData.filter(filterDataGreaterThan50) 22 | console.table(filteredUserData) 23 | expect(filteredUserData.every(filterDataGreaterThan50)).toBeTruthy() 24 | }) 25 | 26 | it("should calculate total due amount", async () => { 27 | const filterDueAmount = (data: table1DataOptions) => parseFloat(data.Due.split("$")[1]); 28 | const totalDue = example1TableData.map(filterDueAmount).reduce((pre: number, current: number) => pre + current) 29 | expect(totalDue).toEqual(251) 30 | }) 31 | 32 | it("should add $30 due amount if it is <= $50", async () => { 33 | const updateData = example1TableData.map((data: table1DataOptions) => { 34 | if (Number(data.Due.split("$")[1]) <= 50) { 35 | data.Due = "$" + (parseFloat(data.Due.split("$")[1]) + 30).toFixed(2) 36 | } 37 | return data; 38 | }) 39 | console.table(updateData) 40 | const isAllDueGreaterThan30 = (data: table1DataOptions) => +(data.Due.split("$")[1]) > 30; 41 | expect(updateData.every(isAllDueGreaterThan30)).toBeTruthy() 42 | }) 43 | 44 | }) -------------------------------------------------------------------------------- /web/tests/smoke.spec.ts: -------------------------------------------------------------------------------- 1 | import loginPage from "../pages/login.page"; 2 | import securePage from "../pages/secure.page"; 3 | import { herokuAppLoginData } from "../resources/logindata"; 4 | import FrameworkConstants from "../static/frameworkConstants"; 5 | 6 | describe('@smoke suite', () => { 7 | 8 | it("should validate login page", async () => { 9 | await loginPage.openApp(); 10 | await loginPage.login(herokuAppLoginData.validUserName, herokuAppLoginData.validPassword()); 11 | await expect(securePage.flashAlert).toBeExisting(); 12 | await expect(securePage.flashAlert).toHaveText(expect.stringContaining(FrameworkConstants.LOGIN_SUCCESS_MSG)) 13 | }); 14 | 15 | }); 16 | 17 | 18 | -------------------------------------------------------------------------------- /web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": false, 4 | "resolveJsonModule": true, 5 | "target": "ESNext", 6 | "moduleResolution": "node", 7 | "strict": true, 8 | "noImplicitAny": false, 9 | "types": [ 10 | "node", 11 | "@wdio/globals/types", 12 | "@wdio/mocha-framework", 13 | "@wdio/cucumber-framework", 14 | "expect-webdriverio" 15 | ], 16 | "allowSyntheticDefaultImports": true, 17 | "esModuleInterop": true, 18 | "experimentalDecorators": true, 19 | "emitDecoratorMetadata": true 20 | } 21 | } -------------------------------------------------------------------------------- /web/types/customTypes.d.ts: -------------------------------------------------------------------------------- 1 | export type FormFieldTypes = { 2 | name: string, 3 | email: string, 4 | contactNo: number, 5 | contactType: string, 6 | question: string, 7 | message?: string 8 | } 9 | 10 | export type table1DataOptions = { 11 | LastName: string, 12 | FirstName: string, 13 | Email: string, 14 | Due: string, 15 | Website: string, 16 | Action: string 17 | } -------------------------------------------------------------------------------- /web/types/external.d.ts: -------------------------------------------------------------------------------- 1 | import Mail from "nodemailer/lib/mailer"; 2 | 3 | export type NodeMailOptions = Mail.Options; -------------------------------------------------------------------------------- /web/types/webelements.d.ts: -------------------------------------------------------------------------------- 1 | import { ChainablePromiseArray, ChainablePromiseElement } from 'webdriverio'; 2 | 3 | export type WebdriverIOElement = ChainablePromiseElement; 4 | export type WebdriverIOElements = ChainablePromiseArray 5 | -------------------------------------------------------------------------------- /web/utils/base64Utils.ts: -------------------------------------------------------------------------------- 1 | export const encodeToBase64String = (data: string) => { 2 | return Buffer.from(data).toString('base64'); 3 | } 4 | 5 | export const decodeFromBase64String = (data: string) => { 6 | return Buffer.from(data, 'base64').toString() 7 | } -------------------------------------------------------------------------------- /web/utils/envReader.ts: -------------------------------------------------------------------------------- 1 | import env from 'dotenv' 2 | env.config() 3 | 4 | export const RUN_MODE = process.env.MODE 5 | 6 | export const env_sender_gmail = process.env.SENDER_GMAIL 7 | export const env_password = process.env.GMAIL_PASSWORD 8 | export const env_sender_name = process.env.SENDER_DISPLAY_NAME 9 | export const env_receiver_list = process.env.MAIL_LIST -------------------------------------------------------------------------------- /web/utils/fileSystem.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | let zipper = require('zip-local'); 3 | 4 | export const deleteDirectory = (path: string) => { 5 | if (fs.existsSync(path)) { 6 | fs.rmdirSync(path, { recursive: true }) 7 | console.log(`Removed directory: ${path} !!!`) 8 | } 9 | } 10 | 11 | export const zipFolder = (sourceFolder: string, targetFolder: string) => { 12 | zipper.zip(sourceFolder, (error: any, zipped: any) => { 13 | if (!error) { 14 | zipped.compress(); 15 | zipped.save(targetFolder, (err: any) => { 16 | if (!err) console.log("Folder zipped successfully !!!"); 17 | }); 18 | } 19 | }); 20 | } 21 | 22 | export const parseJsonFile = (filepath: string) => { 23 | return JSON.parse(fs.readFileSync(filepath, "utf-8")) 24 | } -------------------------------------------------------------------------------- /web/utils/mailer.ts: -------------------------------------------------------------------------------- 1 | import nodemailer from "nodemailer"; 2 | import { NodeMailOptions } from "../types/external"; 3 | import { env_password, env_sender_gmail } from "./envReader"; 4 | 5 | const GmailCredObject = { user: env_sender_gmail, pass: env_password } 6 | 7 | export const mailSender = async (mailOptions: NodeMailOptions) => { 8 | nodemailer 9 | .createTransport({ service: "gmail", auth: GmailCredObject }) 10 | .sendMail(mailOptions, (error, info) => error 11 | ? console.log(`Email did not deliver: ${error}`) 12 | : console.log(`Email Sent: ${info.response}`)) 13 | } --------------------------------------------------------------------------------