diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..1663b5c --- /dev/null +++ b/.eslintignore @@ -0,0 +1,2 @@ +node_modules +src/framework/config/BBConfig.ts \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..547b51d --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,54 @@ +{ + "env": { + "commonjs": true, + "es6": true + }, + "extends": [ + "eslint:recommended", + "airbnb-base", + "airbnb-typescript/base", + "plugin:@typescript-eslint/recommended", + "plugin:playwright/playwright-test" + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "project": "./tsconfig.json", + "ecmaVersion": "latest", + "sourceType": "module" + }, + "plugins": [ + "@typescript-eslint" + ], + "rules": { + "indent": "off", + "@typescript-eslint/indent": "off", + "linebreak-style": "off", + "quotes": "off", + "no-trailing-spaces": "off", + "lines-between-class-members": "off", + "@typescript-eslint/lines-between-class-members": "off", + "@typescript-eslint/quotes": "off", + "no-underscore-dangle": "off", + "semi": [ + "error", + "always" + ], + "object-shorthand": "off", + "no-console":"off", + "class-methods-use-this": "off", + "no-plusplus": "off", + "@typescript-eslint/no-explicit-any": "off", + "no-await-in-loop": "off", + "function-paren-newline": "off", + "function-call-argument-newline": "off", + "import/no-extraneous-dependencies": "off", + "max-len": ["warn", + { + "code": 120 , + "ignoreComments": true, + "ignoreTrailingComments": true, + "ignoreTemplateLiterals": true, + "ignoreUrls": true + }] + } +} diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml new file mode 100644 index 0000000..0934ab0 --- /dev/null +++ b/.github/workflows/node.js.yml @@ -0,0 +1,83 @@ +# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions + +name: Automation Test Execution + +on: + push: + branches: [ master ] + +jobs: + tests: + name: Test Execution + runs-on: windows-latest + steps: + - name: Checkout code from repository + uses: actions/checkout@v4 + - name: Setting up Node.js 20 + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: 'npm' + - name: Install dependencies + run: npm ci && npx playwright install + - name: Creating test suite + run: npm run create:suite SHEET=Regression --if-present + - name: Test execution + run: npm test + - name: Generating execution report + if: always() + run: npx ts-node ./src/framework/reporter/HTMLReporter.ts + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results + path: test-results + - name: Upload allure-results artifact + if: always() + uses: actions/upload-artifact@master + with: + name: allure-results + path: allure-results + retention-days: 30 + + generate_report: + name: Allure Report + runs-on: ubuntu-latest + if: always() + needs: [ tests ] + steps: + - name: Download Artifacts + uses: actions/download-artifact@v4 + id: download + with: + name: allure-results + path: allure-results + + - name: Get Allure history + uses: actions/checkout@v4 + if: always() + continue-on-error: true + with: + ref: gh-pages + path: gh-pages + + - name: Allure Report action + uses: simple-elf/allure-report-action@master + if: always() + id: allure-report + with: + allure_results: allure-results + gh_pages: gh-pages + allure_report: allure-report + allure_history: allure-history + + - name: Deploy allure report to Github Pages + if: always() + uses: peaceiris/actions-gh-pages@v2 + env: + PERSONAL_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PUBLISH_BRANCH: gh-pages + PUBLISH_DIR: allure-history + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bcd6637 --- /dev/null +++ b/.gitignore @@ -0,0 +1,246 @@ +### Intellij+all ### +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Intellij+all Patch ### +# Ignore everything but code style settings and run configurations +# that are supposed to be shared within teams. + +.idea/* + +!.idea/codeStyles +!.idea/runConfigurations + +### Node ### +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +### Node Patch ### +# Serverless Webpack directories +.webpack/ + +# Optional stylelint cache + +# SvelteKit build / generate output +.svelte-kit + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide diff --git a/README.md b/README.md index 5415746..67a0e4f 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,95 @@ -# playwright-demo +# Playwright-Demo +## **Overview:** + +This is a sample Automation project using Playwright and Typescript and uses playwright-testrunner to execute test cases. This is a Data Driven framework focused on separating the test scripts logic and the test data from each other. This allows us to create test automation scripts by passing different sets of test data. The test data set is kept in an external Excel Sheet. The test scripts connect to the external Excel sheet to get the test data. This framework significantly reduces the number of test scripts compared to a modular based framework when we need to test for multiple sets of data for same functionality. + +For Demo purpose UI test cases are created on [advantageonlineshopping.com](http://advantageonlineshopping.com/) site and API test cases are created on these [SOAP API](https://www.advantageonlineshopping.com/accountservice/ws/accountservice.wsdl) & [REST API](https://fakestoreapi.com) endpoints. + +## Features + +- This framework has built in library to operate on UI, API (both SOAP & REST API) and DB (MSSQL, DB2 & Oracle). +- Supports execution of tests in different browsers. +- Test data is stored in an Excel sheet and from this Excel sheet user can control the test cases that needs to be run. +- User also has full control to run test in different modes from the Excel sheet. +- Allows transfer of data between test cases. +- Has utility built in for file download, Read PDF files etc. +- Generates Playwright's HTML Report, Allure Report & JUnit Report in HTML format for each exaction. +- Allure & Playwright report including snapshots and video in case of test failure. +- Test execution logs are captured in the log file. +- You Can execute local tests in Playwright's UI Mode, that comes with a built-in watch mode. Which helps in running and debuging of tests. +- All the playwright related config is controlled by playwright config file. +- Environment variables can be modified at runtime and its controlled by .env file. +- Easy and simple integration to CI/CD tools like Jenkins. + +#### Supported Browsers +1. Chrome - default browser +2. Firefox +3. MS Edge +4. WebKit - web browser engine used by Safari + +#### Run Mode Details +| Mode | Execl Value |Description | +| ------ | ------ | ------ | +|Normal|Blank| Runs the tests sequentially| +|Serial|serial| Runs the tests sequentially. On test failure, all subsequent tests are skipped| +|Parallel|parallel| Runs the tests parallelly, this is ideal when tests in the scenario are independent of one another| + +#### Steps to use +##### 1. Installation + +Playwright framework requires [Node.js](https://nodejs.org/) v14+ to run. + +Code from github need to be [download](https://github.com/VinayKumarBM/playwright-sample-project/archive/refs/heads/master.zip) OR [cloned](https://github.com/VinayKumarBM/playwright-sample-project.git) using git command. + +Installing the dependencies. +```sh +npm ci +``` +##### 2. Test creation +- Create Test file with extenstion .spec.ts. Eg LoginTest.spec.ts +- In the testData excel create a sheet with name of test. Eg. LoginTest +- Create a execution sheet and make an entry of new test case. Eg. in the Regression sheet add a row for new test LoginTest and update other columns like run, mode etc. + +##### 3. Execution +To run test suite use below command. +```sh +npm run create:suite SHEET= && npm test +``` +**Note:** SheetName needs to be updated. + +To run individual test locally use below command. +```sh +set TEST_NAME= && npm run local:test +``` +**Note:** Using set command we are setting the local TestFileName. + +To run individual test locally in [UI Mode](https://playwright.dev/docs/test-ui-mode) use below command. +```sh +set TEST_NAME= && npm run local:test:ui +``` +**Note:** Using set command we are setting the local TestFileName. + +To change any environment configuration in .env file at run time use set command. +Eg: To change browser to MS Edge use below command +```sh +set BROWSER=edge +``` +Similar command can be used to update other environment configuration + +To generate Allure report use below command +```sh +npm run report +``` + +##### 4. Report & Logs +Playwright HTML report will be present inside +```sh +test-results/results/index.html +``` +Execution log will be present in the log file. +```sh +test-results/logs/execution.log +``` +## ## +**:pencil: If you find my work interesting don't forget to give a Star :star: & Follow me :busts_in_silhouette:** diff --git a/docs/_config.yml b/docs/_config.yml new file mode 100644 index 0000000..2f7efbe --- /dev/null +++ b/docs/_config.yml @@ -0,0 +1 @@ +theme: jekyll-theme-minimal \ No newline at end of file diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..33a5a11 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,86 @@ +# playwright-sample-project + +## **Overview:** + +This is a sample Playwright project using Typescript as scripting language and uses playwright-testrunner to execute test cases. This is a Data Driven framework focused on separating the test scripts logic and the test data from each other. This allows us to create test automation scripts by passing different sets of test data. The test data set is kept in an external Excel Sheet. The test scripts connect to the external Excel sheet to get the test data. This framework significantly reduces the number of test scripts compared to a modular based framework when we need to test for multiple sets of data for same functionality. + +For Demo purpose UI test cases are created on [advantageonlineshopping.com](http://advantageonlineshopping.com/) site and API test cases are created on these [SOAP API](https://www.advantageonlineshopping.com/accountservice/ws/accountservice.wsdl) & [REST API](https://fakestoreapi.com) endpoints. + +## Features + +- This framework has built in library to operate on UI, API (both SOAP & REST API) and DB (MSSQL, DB2 & Oracle). +- Supports execution of tests in different browsers. +- Test data is stored in an Excel sheet and from this Excel sheet user can control the test cases that needs to be run. +- User also has full control to run test in different modes from the Excel sheet. +- Allows transfer of data between test cases. +- Has utility built in for file download, Read PDF files etc. +- Generates Playwright's HTML Report, Allure Report & JUnit Report in HTML format for each exaction. +- Allure & Playwright report including snapshots and video in case of test failure. +- Test execution logs are captured in the log file. +- All the playwright related config is controlled by playwright config file. +- Environment variables can be modified at runtime and its controlled by .env file. +- Easy and simple integration to CI/CD tools like Jenkins. + +#### Supported Browsers +1. Chrome - default browser +2. Firefox +3. MS Edge +4. WebKit - web browser engine used by Safari + +#### Run Mode Details +| Mode | Execl Value |Description | +| ------ | ------ | ------ | +|Normal|Blank| Runs the tests sequentially| +|Serial|serial| Runs the tests sequentially. On test failure, all subsequent tests are skipped| +|Parallel|parallel| Runs the tests parallelly, this is ideal when tests in the scenario are independent of one another| + +#### Steps to use +##### 1. Installation + +Playwright framework requires [Node.js](https://nodejs.org/) v14+ to run. + +Code from github need to be [download](https://github.com/VinayKumarBM/playwright-sample-project/archive/refs/heads/master.zip) OR [cloned](https://github.com/VinayKumarBM/playwright-sample-project.git) using git command. + +Installing the dependencies. +```sh +npm ci +``` +##### 2. Test creation +- Create Test file with extenstion .spec.ts. Eg LoginTest.spec.ts +- In the testData excel create a sheet with name of test. Eg. LoginTest +- Create a execution sheet and make an entry of new test case. Eg. in the Regression sheet add a row for new test LoginTest and update other columns like run, mode etc. + +##### 3. Execution +To run test suite use below command. +```sh +npm run create:suite SHEET= && npm test +``` +**Note:** SheetName needs to be updated. + +To run individual test locally use below command. +```sh +set TEST_NAME= && npm run local:test +``` +**Note:** Using set command we are setting the local TestFileName. + +To change any environment configuration in .env file at run time use set command. +Eg: To change browser to MS Edge use below command +```sh +set BROWSER=edge +``` +Similar command can be used to update other environment configuration + +To generate Allure report use below command +```sh +npm run report +``` + +##### 4. Report & Logs +Playwright HTML report will be present inside +```sh +test-results/results/index.html +``` +Execution log will be present in the log file. +```sh +test-results/logs/execution.log +``` diff --git a/package.json b/package.json new file mode 100644 index 0000000..ba3f503 --- /dev/null +++ b/package.json @@ -0,0 +1,60 @@ +{ + "name": "playwright-demo", + "version": "1.0.0", + "description": "Playwright Demo framework", + "main": "index.js", + "repository": { + "type": "git", + "url": "" + }, + "scripts": { + "lint": "eslint . --ext .ts", + "report": "allure serve", + "create:suite": "cd ./src/framework/manager & npx ts-node SuiteManager.ts", + "test": "playwright test --project=suite", + "local:test": "playwright test --project=local", + "local:test:ui": "playwright test --project=local --ui" + }, + "keywords": [], + "author": "YuCheng", + "license": "MIT", + "devDependencies": { + "@playwright/test": "1.54.1", + "@types/easy-soap-request": "4.1.1", + "@types/express": "^4.17.13", + "@types/ibm_db": "2.0.10", + "@types/mssql": "^9.1.7", + "@types/oracledb": "5.2.3", + "@types/pdf-parse": "^1.1.1", + "@types/randomstring": "^1.1.8", + "@types/string-format": "^2.0.0", + "@types/xmldom": "^0.1.31", + "@typescript-eslint/eslint-plugin": "^5.16.0", + "@typescript-eslint/parser": "^5.16.0", + "allure-playwright": "2.5.0", + "dotenv": "17.2.1", + "easy-soap-request": "^4.6.0", + "eslint": "^8.12.0", + "eslint-config-airbnb-typescript": "16.1.4", + "eslint-plugin-playwright": "0.8.0", + "ibm_db": "2.8.1", + "jsonpath": "1.1.1", + "moment": "2.29.4", + "mssql": "^7.2.1", + "oracledb": "5.3.0", + "pdf-parse": "1.1.1", + "randomstring": "1.2.2", + "string-format": "2.0.0", + "ts-node": "^10.4.0", + "typescript": "^4.5.4", + "winston": "^3.4.0", + "xml-formatter": "2.6.1", + "xmldom": "0.6.0", + "xpath": "0.0.32", + "@types/convert-excel-to-json": "1.7.1", + "convert-excel-to-json": "1.7.0", + "jasmine-xml2html-converter": "0.0.2", + "fetch-to-curl": "0.5.2", + "monocart-reporter": "1.0.7" + } +} diff --git a/playwright-demo.iml b/playwright-demo.iml new file mode 100644 index 0000000..8021953 --- /dev/null +++ b/playwright-demo.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..53cfc24 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,69 @@ +import { PlaywrightTestConfig } from "@playwright/test"; +import dotenv from 'dotenv'; +import Browser from "./src/framework/manager/Browser"; + +dotenv.config(); + +const timeInMin: number = 60 * 1000; +const config: PlaywrightTestConfig = { + use: { + browserName: Browser.type(process.env.BROWSER.toLowerCase()), + headless: false, + channel: Browser.channel(process.env.BROWSER.toLowerCase()), + launchOptions: { + args: ["--start-maximized", "--disable-extensions", "--disable-plugins"], + headless: false, + timeout: Number.parseInt(process.env.BROWSER_LAUNCH_TIMEOUT, 10), + slowMo: 100, + downloadsPath: "./test-results/downloads", + }, + viewport: null, + ignoreHTTPSErrors: true, + acceptDownloads: true, + actionTimeout: Number.parseInt(process.env.ACTION_TIMEOUT, 10) * timeInMin, + navigationTimeout: Number.parseInt(process.env.NAVIGATION_TIMEOUT, 10) * timeInMin, + screenshot: { + mode: "only-on-failure", + fullPage: true, + }, + video: "retain-on-failure", + }, + testDir: "./src/tests", + outputDir: "./test-results/failure", + retries: Number.parseInt(process.env.RETRIES, 10), + preserveOutput: "failures-only", + reportSlowTests: null, + timeout: Number.parseInt(process.env.TEST_TIMEOUT, 10) * timeInMin, + workers: Number.parseInt(process.env.PARALLEL_THREAD, 10), + reporter: [ + ["dot"], + ["allure-playwright", { + detail: false, + suiteTitle: false, + environmentInfo: { + OS: process.platform.toUpperCase(), + BROWSER: process.env.BROWSER.toUpperCase(), + BASE_URL: process.env.BASE_URL, + }, + }], + ['html', { open: 'never', outputFolder: "./test-results/report" }], + ["junit", { outputFile: "./test-results/results/results.xml" }], + ["json", { outputFile: "./test-results/results/results.json" }], + ["./src/framework/logger/TestListener.ts"], + ['monocart-reporter', { + name: "Automation Report", + outputFile: './test-results/report/execution.html', + }], + ], + projects: [ + { + name: "local", + testMatch: `*${process.env.TEST_NAME.trim()}*`, + }, + { + name: "suite", + testMatch: "*.test.ts", + }, + ], +}; +export default config; diff --git a/src/API/REST/constants/RESTConstants.ts b/src/API/REST/constants/RESTConstants.ts new file mode 100644 index 0000000..fb1a336 --- /dev/null +++ b/src/API/REST/constants/RESTConstants.ts @@ -0,0 +1,6 @@ +export default class RESTConstants { + static readonly CONTENT_TYPE = 'Content-Type'; + static readonly ACCEPT = 'Accept'; + static readonly CONTENT_JSON = "application/json"; + static readonly STATUS_CODE = "Status Code"; +} diff --git a/src/API/REST/steps/UserSteps.ts b/src/API/REST/steps/UserSteps.ts new file mode 100644 index 0000000..20d4207 --- /dev/null +++ b/src/API/REST/steps/UserSteps.ts @@ -0,0 +1,81 @@ +import test, { Page } from "@playwright/test"; +import APIActions from "@apiActions/APIActions"; +import RESTResponse from "@apiActions/RESTResponse"; +import Assert from "@asserts/Assert"; +import RESTConstants from "@restConstants/RESTConstants"; + +export default class UserSteps { + private api: APIActions; + private BASE_URL = process.env.REST_API_BASE_URL; + constructor(private page: Page) { + this.api = new APIActions(this.page); + } + private get header() { + return this.api.header.set(RESTConstants.CONTENT_TYPE, RESTConstants.CONTENT_JSON) + .set(RESTConstants.ACCEPT, RESTConstants.CONTENT_JSON).get(); + } + + public async get(endPoint: string, operation: string): Promise { + let response: RESTResponse; + await test.step(`Making call to GET ${operation}`, async () => { + response = await this.api.rest.get(this.BASE_URL + endPoint, this.header, operation); + }); + return response; + } + + public async post(endPoint: string, requestBodyFile: string, requestData: any, + operation: string): Promise { + let response: RESTResponse; + await test.step(`Making POST call to ${operation}`, async () => { + const requestJSON = await this.api.rest.createRequestBody(requestBodyFile, requestData); + response = await this.api.rest.post(this.BASE_URL + endPoint, this.header, requestJSON, operation); + }); + return response; + } + + public async put(endPoint: string, requestBodyFile: string, requestData: any, + operation: string): Promise { + let response: RESTResponse; + await test.step(`Making PUT call to ${operation}`, async () => { + const requestJSON = await this.api.rest.createRequestBody(requestBodyFile, requestData); + response = await this.api.rest.put(this.BASE_URL + endPoint, this.header, requestJSON, operation); + }); + return response; + } + + public async delete(endPoint: string, operation: string): Promise { + let response: RESTResponse; + await test.step(`Making DELETE call to ${operation}`, async () => { + response = await this.api.rest.delete(this.BASE_URL + endPoint, this.header, operation); + }); + return response; + } + + public async verifyStatusCode(response: RESTResponse, statusCode: string) { + await test.step(`Verifying that status code is ${statusCode}`, async () => { + await Assert.assertEquals(await response.getStatusCode(), statusCode, RESTConstants.STATUS_CODE); + }); + } + + public async extractResponseValue(response: RESTResponse, jsonPath: string, operation: string) { + let value: string; + await test.step(`Extract value from ${operation} response`, async () => { + value = await response.getTagContentByJsonPath(jsonPath, operation); + }); + return value; + } + + public async verifyContent(response: RESTResponse, jsonPath: string, expectedValue: string, description: string) { + await test.step(`Verifying that ${description} has value ${expectedValue}`, async () => { + const value = await response.getTagContentByJsonPath(jsonPath, description); + await Assert.assertEquals(value, expectedValue, description); + }); + } + + public async verifyContentIsNotNull(response: RESTResponse, jsonPath: string, description: string) { + await test.step(`Verifying that ${description} content is NOT NULL`, async () => { + const value = await response.getTagContentByJsonPath(jsonPath, description); + await Assert.assertNotNull(value, description); + }); + } +} diff --git a/src/API/SOAP/constants/SOAPConstants.ts b/src/API/SOAP/constants/SOAPConstants.ts new file mode 100644 index 0000000..175149e --- /dev/null +++ b/src/API/SOAP/constants/SOAPConstants.ts @@ -0,0 +1,5 @@ +export default class SOAPConstants { + static readonly CONTENT_TYPE = 'Content-Type'; + static readonly SOAP_ACTION = 'SoapAction'; + static readonly CONTENT_TEXT = "text/xml;charset=UTF-8"; +} diff --git a/src/API/SOAP/steps/AccountServiceSteps.ts b/src/API/SOAP/steps/AccountServiceSteps.ts new file mode 100644 index 0000000..c175465 --- /dev/null +++ b/src/API/SOAP/steps/AccountServiceSteps.ts @@ -0,0 +1,45 @@ +import test, { Page } from "@playwright/test"; +import APIActions from "@apiActions/APIActions"; +import SOAPResponse from "@apiActions/SOAPResponse"; +import Assert from "@asserts/Assert"; +import SOAPConstants from "@soapConstants/SOAPConstants"; + +export default class AccountServiceSteps { + private api: APIActions; + + constructor(private page: Page) { + this.api = new APIActions(this.page); + } + + public async request(endPoint: string, requestBody: string, requestData: any, + operation: string): Promise { + let response: SOAPResponse; + await test.step(`SOAP request to ${operation}`, async () => { + const requestHeaders = this.api.header.set(SOAPConstants.CONTENT_TYPE, SOAPConstants.CONTENT_TEXT).get(); + response = await this.api.soap.post(endPoint, requestHeaders, requestBody, requestData, operation); + }); + return response; + } + + public async verifyResponse(response: SOAPResponse, xpath: string, result: string, operation: string) { + await test.step(`Verifying that result of ${operation} is ${result}`, async () => { + const actualResult = await response.getTagContentByXpath(xpath, operation); + await Assert.assertEquals(actualResult, result.toString(), operation); + }); + } + + public async verifyResponseContains(response: SOAPResponse, xpath: string, result: string, operation: string) { + await test.step(`Verifying that result of ${operation} contains ${result}`, async () => { + const actualResult = await response.getTagContentByXpath(xpath, operation); + await Assert.assertContains(actualResult, result, operation); + }); + } + + public async getResponseContent(response: SOAPResponse, xpath: string, operation: string) { + let content: string; + await test.step(`Getting content from response of ${operation}`, async () => { + content = await response.getTagContentByXpath(xpath, operation); + }); + return content; + } +} diff --git a/src/advantage/constants/CommonConstants.ts b/src/advantage/constants/CommonConstants.ts new file mode 100644 index 0000000..37337bc --- /dev/null +++ b/src/advantage/constants/CommonConstants.ts @@ -0,0 +1,7 @@ +export default class CommonConstants { + static readonly FALSE = 'false'; + static readonly TRUE = 'true'; + static readonly TEN = 10; + static readonly TWO = 2; + static readonly DOWNLOADS_PATH = "./test-results/downloads/"; +} diff --git a/src/advantage/constants/ConfigurationConstants.ts b/src/advantage/constants/ConfigurationConstants.ts new file mode 100644 index 0000000..896b61b --- /dev/null +++ b/src/advantage/constants/ConfigurationConstants.ts @@ -0,0 +1,3 @@ +export default class ConfigurationConstants { + static readonly AOS_BACKEND = "AOS Backend"; +} diff --git a/src/advantage/constants/HomePageConstants.ts b/src/advantage/constants/HomePageConstants.ts new file mode 100644 index 0000000..e6e77ca --- /dev/null +++ b/src/advantage/constants/HomePageConstants.ts @@ -0,0 +1,23 @@ +export default class HomePageConstants { + static readonly USER_ICON = "User Icon"; + static readonly USER_NAME = "User Name"; + static readonly PASSWORD = "Password"; + static readonly REMEMBER_ME_CHECKBOX = "Remember Me"; + static readonly SIGN_IN_BUTTON = "Sign In Button"; + static readonly SIGN_IN_ERROR_MESSAGE = "Error Message"; + static readonly SIGN_OUT_LINK = "Sign Out Link"; + static readonly HOME_PAGE = "Home Page"; + static readonly CREATE_NEW_ACCOUNT_LINK = "Create New Account Link"; + static readonly CATEGORY_DROPDOWN = "Category Dropdown"; + static readonly PRODUCT_DROPDOWN = "Product Dropdown"; + static readonly SUBJECT_TEXTAREA = "ContactUs Subject"; + static readonly EMAIL_TEXTBOX = "ContactUs email"; + static readonly SEND_BUTTON = "Send Button"; + static readonly CONTACT_US_MESSAGE = "ContactUs Success Message"; + static readonly SEARCH_ICON = "Search Icon"; + static readonly SEARCH_TEXTBOX = "Search Box"; + static readonly SEARCH_CLOSE_IMAGE = "Close Search"; + static readonly ENTER_KEY: 'Enter'; + static readonly HELP_ICON = "Help Icon"; + static readonly MANAGEMENT_CONSOLE_LINK = "Management Console Link"; +} diff --git a/src/advantage/constants/RegistrationPageConstants.ts b/src/advantage/constants/RegistrationPageConstants.ts new file mode 100644 index 0000000..32109c7 --- /dev/null +++ b/src/advantage/constants/RegistrationPageConstants.ts @@ -0,0 +1,19 @@ +export default class RegistrationPageConstants { + static readonly USER_NAME = "User Name"; + static readonly PASSWORD = "Password"; + static readonly EMAIL = "Email"; + static readonly CONFIRM_PASSWORD = "Confirm Password"; + static readonly FIRST_NAME = "First Name"; + static readonly LAST_NAME = "Last Name"; + static readonly PHONE_NUMBER = "Phone Number"; + static readonly COUNTRY = "Country"; + static readonly CITY = "City"; + static readonly ADDRESS = "Address"; + static readonly STATE = "State"; + static readonly POSTAL_CODE = "Postal Code"; + static readonly PROMOTION = "Promotion"; + static readonly TERMS_AND_CONDITIONS = "Terms & Conditions"; + static readonly REGISTER_BUTTON = "Register Button"; + static readonly ALREADY_HAVE_AN_ACCOUNT_LINK = "Already have an Account Link"; + static readonly ERROR_MESSAGE = "Error Message"; +} diff --git a/src/advantage/pages/ConfigurationPage.ts b/src/advantage/pages/ConfigurationPage.ts new file mode 100644 index 0000000..dac19f1 --- /dev/null +++ b/src/advantage/pages/ConfigurationPage.ts @@ -0,0 +1,3 @@ +export default class ConfigurationPage { + static readonly AOS_BACK_END_LINK = "//ul[@class='nav_user_guide_list']/li/a[text()='AOS back end']"; +} diff --git a/src/advantage/pages/HomePage.ts b/src/advantage/pages/HomePage.ts new file mode 100644 index 0000000..99403b0 --- /dev/null +++ b/src/advantage/pages/HomePage.ts @@ -0,0 +1,22 @@ +export default class HomePage { + static readonly USER_ICON = "#menuUser"; + static readonly USER_NAME_TEXTBOX = "[name='username']"; + static readonly PASSWORD_TEXTBOX = "[name='password']"; + static readonly REMEMBER_ME_CHECKBOX = "[name='remember_me']"; + static readonly SIGN_IN_BUTTON = "#sign_in_btn"; + static readonly LOGGED_IN_USER = "a#menuUserLink>span.hi-user"; + static readonly SIGN_IN_ERROR_MESSAGE = "#signInResultMessage.invalid"; + static readonly SIGN_OUT_LINK = "#loginMiniTitle>[translate='Sign_out']"; + static readonly CREATE_NEW_ACCOUNT_LINK = "[translate='CREATE_NEW_ACCOUNT']"; + static readonly CATEGORY_DROPDOWN = "[name='categoryListboxContactUs']"; + static readonly PRODUCT_DROPDOWN = "[name='productListboxContactUs']"; + static readonly SUBJECT_TEXTAREA = "[name='subjectTextareaContactUs']"; + static readonly EMAIL_TEXTBOX = "[name='emailContactUs']"; + static readonly SEND_BUTTON = "#send_btn"; + static readonly CONTACT_US_MESSAGE = ".roboto-regular.successMessage"; + static readonly SEARCH_ICON = "#searchSection"; + static readonly SEARCH_TEXTBOX = "#autoComplete"; + static readonly SEARCH_CLOSE_IMAGE = "div.autoCompleteCover>div>img"; + static readonly HELP_ICON = "#menuHelp"; + static readonly MANAGEMENT_CONSOLE_LINK = "div#helpMiniTitle [translate='CONFIG_TOOL']"; +} diff --git a/src/advantage/pages/RegistrationPage.ts b/src/advantage/pages/RegistrationPage.ts new file mode 100644 index 0000000..4afbb14 --- /dev/null +++ b/src/advantage/pages/RegistrationPage.ts @@ -0,0 +1,19 @@ +export default class RegistrationPage { + static readonly USER_NAME_TEXTBOX = "[name='usernameRegisterPage']"; + static readonly PASSWORD_TEXTBOX = "[name='passwordRegisterPage']"; + static readonly EMAIL_TEXTBOX = "[name='emailRegisterPage']"; + static readonly PASSWORD_CONFIRM_TEXTBOX = "[name='confirm_passwordRegisterPage']"; + static readonly FIRST_NAME_TEXTBOX = "[name='first_nameRegisterPage']"; + static readonly LAST_NAME_TEXTBOX = "[name='last_nameRegisterPage']"; + static readonly PHONE_NUMBER_TEXTBOX = "[name='phone_numberRegisterPage']"; + static readonly COUNTRY_DROPDOWN = "[name='countryListboxRegisterPage']"; + static readonly CITY_TEXTBOX = "[name='cityRegisterPage']"; + static readonly ADDRESS_TEXTBOX = "[name='addressRegisterPage']"; + static readonly STATE_TEXTBOX = "[name='state_/_province_/_regionRegisterPage']"; + static readonly POSTAL_CODE_TEXTBOX = "[name='postal_codeRegisterPage']"; + static readonly PROMOTION_CHECKBOX = "[name='allowOffersPromotion']"; + static readonly PRIVACY_POLICY_CHECKBOX = "[name='i_agree']"; + static readonly REGISTER_BUTTON = "#register_btn"; + static readonly ALREADY_HAVE_AN_ACCOUNT_LINK = "[translate='ALREADY_HAVE_AN_ACCOUNT']"; + static readonly MANDATORY_FIELD_ERROR_MESSAGE = "div.inputContainer>label.invalid"; +} diff --git a/src/advantage/steps/ConfigurationSteps.ts b/src/advantage/steps/ConfigurationSteps.ts new file mode 100644 index 0000000..e14d7f6 --- /dev/null +++ b/src/advantage/steps/ConfigurationSteps.ts @@ -0,0 +1,38 @@ +import test, { Page } from "@playwright/test"; +import ConfigurationPage from "@pages/ConfigurationPage"; +import Assert from "@asserts/Assert"; +import UIActions from "@uiActions/UIActions"; +import PDFUtil from "@utils/PDFUtil"; +import CommonConstants from "@uiConstants/CommonConstants"; +import ConfigurationConstants from "@uiConstants/ConfigurationConstants"; + +export default class ConfigurationSteps { + private ui: UIActions; + + constructor(private page: Page) { + this.ui = new UIActions(page); + } + + public async downloadAOSBackendPDF() { + let fileName: string; + await test.step(`Downloading the AOS backend PDF file`, async () => { + fileName = await this.ui.downloadFile(ConfigurationPage.AOS_BACK_END_LINK, + ConfigurationConstants.AOS_BACKEND); + }); + return fileName; + } + + public async verifyPDFFilePageCount(fileName: string, pages: number) { + await test.step(`Verify that ${fileName} file has ${pages} pages`, async () => { + await Assert.assertEquals(await PDFUtil.getNumberOfPages(CommonConstants.DOWNLOADS_PATH + fileName), + pages, fileName); + }); + } + + public async verifyPDFFileText(fileName: string, content: string) { + await test.step(`Verify that ${fileName} has content ${content}`, async () => { + await Assert.assertContains(await PDFUtil.getText(CommonConstants.DOWNLOADS_PATH + fileName), + content, fileName); + }); + } +} diff --git a/src/advantage/steps/HomeSteps.ts b/src/advantage/steps/HomeSteps.ts new file mode 100644 index 0000000..d62e851 --- /dev/null +++ b/src/advantage/steps/HomeSteps.ts @@ -0,0 +1,147 @@ +import test, { Page } from "@playwright/test"; +import UIActions from "@uiActions/UIActions"; +import Assert from "@asserts/Assert"; +import CommonConstants from "@uiConstants/CommonConstants"; +import HomePageConstants from "@uiConstants/HomePageConstants"; +import HomePage from "@pages/HomePage"; + +export default class HomeSteps { + private ui: UIActions; + + constructor(private page: Page) { + this.ui = new UIActions(page); + } + /** + * Launch the Application + */ + public async launchApplication() { + await test.step(`Launching the application`, async () => { + await this.ui.goto(process.env.BASE_URL, HomePageConstants.HOME_PAGE); + }); + } + /** + * Log into the application + * @param userName + * @param password + */ + public async login(userName: string, password: string) { + await test.step(`Login to application credentials as ${userName} & ${password}`, async () => { + await this.ui.element(HomePage.USER_ICON, HomePageConstants.USER_ICON).click(); + await this.enterLoginDetails(userName, password); + }); + } + /** + * Enter login details + * @param userName + * @param password + */ + public async enterLoginDetails(userName: string, password: string) { + await test.step(`Enter login credentials as ${userName} & ${password}`, async () => { + await this.ui.editBox(HomePage.USER_NAME_TEXTBOX, HomePageConstants.USER_NAME).fill(userName); + await this.ui.editBox(HomePage.PASSWORD_TEXTBOX, HomePageConstants.PASSWORD).fill(password); + await this.ui.checkbox(HomePage.REMEMBER_ME_CHECKBOX, HomePageConstants.REMEMBER_ME_CHECKBOX).check(); + await this.ui.element(HomePage.SIGN_IN_BUTTON, HomePageConstants.SIGN_IN_BUTTON).click(); + }); + } + /** + * Validate logged in user + * @param userName + */ + public async validateLogin(userName: string) { + await test.step(`Verify that user is successfully logged in as ${userName}`, async () => { + const user = await this.ui.element(HomePage.LOGGED_IN_USER, HomePageConstants.USER_NAME).getTextContent(); + await Assert.assertEquals(user, userName, HomePageConstants.USER_NAME); + }); + } + /** + * Validate invalid login + * @param errorMessage + */ + public async validateInvalidLogin(errorMessage: string) { + await test.step(`Verify that error message ${errorMessage}`, async () => { + const user = await this.ui.element(HomePage.SIGN_IN_ERROR_MESSAGE, HomePageConstants.SIGN_IN_ERROR_MESSAGE) + .getTextContent(); + await Assert.assertEquals(user, errorMessage, HomePageConstants.SIGN_IN_ERROR_MESSAGE); + }); + } + /** + * Log out of the application + */ + public async logout() { + await test.step(`Logged out of application`, async () => { + await this.ui.element(HomePage.LOGGED_IN_USER, HomePageConstants.USER_NAME).click(); + await this.ui.element(HomePage.SIGN_OUT_LINK, HomePageConstants.SIGN_OUT_LINK).click(); + await this.ui.pauseInSecs(CommonConstants.TWO); + }); + } + /** + * Navigate to Create Account page + */ + public async navigateToCreateAccount() { + await test.step(`Navigate to Create Account page`, async () => { + await this.ui.element(HomePage.USER_ICON, HomePageConstants.USER_ICON).click(); + await this.ui.element(HomePage.CREATE_NEW_ACCOUNT_LINK, HomePageConstants.CREATE_NEW_ACCOUNT_LINK).click(); + }); + } + /** + * Enters details into Contact Us + * @param category + * @param product + * @param email + * @param subject + */ + public async enterContactUsDetails(category: string, product: string, email: string, subject: string) { + await test.step(`Entering Contact Us details`, async () => { + await this.ui.dropdown(HomePage.CATEGORY_DROPDOWN, HomePageConstants.CATEGORY_DROPDOWN) + .selectByVisibleText(category); + await this.ui.dropdown(HomePage.PRODUCT_DROPDOWN, HomePageConstants.PRODUCT_DROPDOWN) + .selectByVisibleText(product); + await this.ui.editBox(HomePage.EMAIL_TEXTBOX, HomePageConstants.EMAIL_TEXTBOX).fill(email); + await this.ui.editBox(HomePage.SUBJECT_TEXTAREA, HomePageConstants.SUBJECT_TEXTAREA).fill(subject); + }); + } + /** + * Click on Send button of Contact Us + */ + public async sendMessage() { + await test.step(`Click on Send button of Contact Us`, async () => { + await this.ui.element(HomePage.SEND_BUTTON, HomePageConstants.SEND_BUTTON).click(); + }); + } + /** + * Verify the success message of Contact Us + * @param message + */ + public async verifySuccessMessage(message: string) { + await test.step(`Verifying Success Message of Contact Us`, async () => { + const actualMessage = await this.ui.element(HomePage.CONTACT_US_MESSAGE, + HomePageConstants.CONTACT_US_MESSAGE).getTextContent(); + await Assert.assertEquals(actualMessage, message, HomePageConstants.CONTACT_US_MESSAGE); + }); + } + /** + * Search for Product + * @param product + */ + public async searchProduct(product: string) { + await test.step(`Searching for product '${product}'`, async () => { + await this.ui.element(HomePage.SEARCH_ICON, HomePageConstants.SEARCH_ICON).click(); + await (await this.ui.editBox(HomePage.SEARCH_TEXTBOX, HomePageConstants.SEARCH_TEXTBOX).type(product)) + .keyPress(HomePageConstants.ENTER_KEY); + await this.ui.element(HomePage.SEARCH_CLOSE_IMAGE, HomePageConstants.SEARCH_CLOSE_IMAGE).click(); + }); + } + /** + * Navigate to Management Console screen + */ + public async navigateToManagementConsole() { + let newPage: Page; + await test.step(`Navigate to Management Console screen`, async () => { + await this.ui.waitForLoadingImage(); + await this.ui.element(HomePage.HELP_ICON, HomePageConstants.HELP_ICON).click(); + newPage = await this.ui.switchToNewWindow(HomePage.MANAGEMENT_CONSOLE_LINK, + HomePageConstants.MANAGEMENT_CONSOLE_LINK); + }); + return newPage; + } +} diff --git a/src/advantage/steps/RegistrationSteps.ts b/src/advantage/steps/RegistrationSteps.ts new file mode 100644 index 0000000..f4af6a2 --- /dev/null +++ b/src/advantage/steps/RegistrationSteps.ts @@ -0,0 +1,84 @@ +import test, { Page } from "@playwright/test"; +import UIActions from "@uiActions/UIActions"; +import Assert from "@asserts/Assert"; +import StringUtil from "@utils/StringUtil"; +import CommonConstants from "@uiConstants/CommonConstants"; +import RegistrationPageConstants from "@uiConstants/RegistrationPageConstants"; +import RegistrationPage from "@pages/RegistrationPage"; + +export default class RegistrationSteps { + private ui: UIActions; + + constructor(private page: Page) { + this.ui = new UIActions(page); + } + /** + * Creating a new Account + * @param email + * @param password + * @param confirmPassword + * @param firstName + * @param lastName + * @param phoneNumber + * @param country + * @param city + * @param address + * @param state + * @param postalCode + * @param allowOffersPromotion + * @returns + */ + public async createAccount(email: string, password: string, confirmPassword: string, firstName: string, + lastName: string, phoneNumber: string, country: string, city: string, address: string, state: string, + postalCode: string, allowOffersPromotion: string) { + let userName: string; + await test.step(`Create New Account`, async () => { + userName = StringUtil.randomAlphabeticString(CommonConstants.TEN); + await this.ui.editBox(RegistrationPage.USER_NAME_TEXTBOX, + RegistrationPageConstants.USER_NAME).fill(userName); + await this.ui.editBox(RegistrationPage.EMAIL_TEXTBOX, RegistrationPageConstants.EMAIL).fill(email); + await this.ui.editBox(RegistrationPage.PASSWORD_TEXTBOX, RegistrationPageConstants.PASSWORD).fill(password); + await this.ui.editBox(RegistrationPage.PASSWORD_CONFIRM_TEXTBOX, RegistrationPageConstants.CONFIRM_PASSWORD) + .fill(confirmPassword); + await this.ui.editBox(RegistrationPage.FIRST_NAME_TEXTBOX, RegistrationPageConstants.FIRST_NAME) + .fill(firstName); + await this.ui.editBox(RegistrationPage.LAST_NAME_TEXTBOX, RegistrationPageConstants.LAST_NAME) + .fill(lastName); + await this.ui.editBox(RegistrationPage.PHONE_NUMBER_TEXTBOX, RegistrationPageConstants.PHONE_NUMBER) + .fill(phoneNumber); + await this.ui.dropdown(RegistrationPage.COUNTRY_DROPDOWN, RegistrationPageConstants.COUNTRY) + .selectByVisibleText(country); + await this.ui.editBox(RegistrationPage.CITY_TEXTBOX, RegistrationPageConstants.CITY).fill(city); + await this.ui.editBox(RegistrationPage.ADDRESS_TEXTBOX, RegistrationPageConstants.ADDRESS).fill(address); + await this.ui.editBox(RegistrationPage.STATE_TEXTBOX, RegistrationPageConstants.STATE).fill(state); + await this.ui.editBox(RegistrationPage.POSTAL_CODE_TEXTBOX, RegistrationPageConstants.POSTAL_CODE) + .fill(postalCode); + await Assert.assertTrue(await this.ui.checkbox(RegistrationPage.PROMOTION_CHECKBOX, + RegistrationPageConstants.PROMOTION).isChecked(), RegistrationPageConstants.PROMOTION); + if (allowOffersPromotion.toLowerCase() === CommonConstants.FALSE) { + await this.ui.checkbox(RegistrationPage.PROMOTION_CHECKBOX, RegistrationPageConstants.PROMOTION) + .uncheck(); + } + await this.ui.checkbox(RegistrationPage.PRIVACY_POLICY_CHECKBOX, + RegistrationPageConstants.TERMS_AND_CONDITIONS).check(); + }); + return userName; + } + /** + * Saves the registration details + */ + public async saveRegistration() { + await test.step(`Save registration details`, async () => { + await this.ui.element(RegistrationPage.REGISTER_BUTTON, RegistrationPageConstants.REGISTER_BUTTON).click(); + }); + } + /** + * Click on link Already having Account + */ + public async alreadyHaveAccount() { + await test.step(`Click on Already have an account link`, async () => { + await this.ui.element(RegistrationPage.ALREADY_HAVE_AN_ACCOUNT_LINK, + RegistrationPageConstants.ALREADY_HAVE_AN_ACCOUNT_LINK).click(); + }); + } +} diff --git a/src/database/constants/DatabaseConstants.ts b/src/database/constants/DatabaseConstants.ts new file mode 100644 index 0000000..c4dda06 --- /dev/null +++ b/src/database/constants/DatabaseConstants.ts @@ -0,0 +1,3 @@ +export default class DatabaseConstants { + static readonly QUERY_EXECUTION = "Query Execution"; +} diff --git a/src/database/steps/DatabaseStep.ts b/src/database/steps/DatabaseStep.ts new file mode 100644 index 0000000..9cbdae9 --- /dev/null +++ b/src/database/steps/DatabaseStep.ts @@ -0,0 +1,40 @@ +import test from "@playwright/test"; +import { IRecordSet } from "mssql"; +import Assert from "@asserts/Assert"; +import DBUtil from "@utils/DBUtil"; +import DatabaseConstants from "@dbConstants/DatabaseConstants"; + +export default class DatabaseStep { + public async executeMSSQLQuery(query: string) { + let result: { rows: IRecordSet; rowsAffected: number[]; }; + await test.step('Executing query in MS SQL db', async () => { + result = await DBUtil.executeMSSQLQuery(process.env.DB_CONFIG, query); + console.log(result); + }); + return result; + } + + public async executeDB2Query(query: string) { + let result: { rows: any; rowsAffected: any; }; + await test.step('Executing query in DB2 db', async () => { + result = await DBUtil.executeDB2Query(process.env.DB_CONFIG, query); + console.log(result); + }); + return result; + } + + public async executeOracleQuery(query: string) { + let result: { rows: unknown[]; rowsAffected: number; }; + await test.step('Executing query in Oracle db', async () => { + result = await DBUtil.executeOracleQuery(process.env.DB_CONFIG, query); + console.log(result); + }); + return result; + } + + public async verifyExecutionSuccess(rowsAffected: number) { + await test.step('Verify query execution is success', async () => { + await Assert.assertTrue(rowsAffected > 0, DatabaseConstants.QUERY_EXECUTION); + }); + } +} diff --git a/src/framework/config/base-test.ts b/src/framework/config/base-test.ts new file mode 100644 index 0000000..b003c97 --- /dev/null +++ b/src/framework/config/base-test.ts @@ -0,0 +1,17 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { test as base } from '@playwright/test'; + +export const test = base.extend<{ MyFixtures }, { gData: Map }>({ + gData: [async ({ }, use) => { + const data = new Map(); + data.set("SPACE", " "); + data.set("HYPHEN", "-"); + data.set("UNDERSCORE", "_"); + await use(data); + }, { scope: 'worker' }], + + page: async ({ page, gData }, use) => { + await use(page); + }, +}); +export { expect } from '@playwright/test'; diff --git a/src/framework/constants/BrowserConstants.ts b/src/framework/constants/BrowserConstants.ts new file mode 100644 index 0000000..d34459b --- /dev/null +++ b/src/framework/constants/BrowserConstants.ts @@ -0,0 +1,9 @@ +export default class BrowserConstants { + static readonly CHROME = "chrome"; + static readonly FIREFOX = "firefox"; + static readonly WEBKIT = "webkit"; + static readonly MSEDGE = "msedge"; + static readonly EDGE = "edge"; + static readonly CHROMIUM = "chromium"; + static readonly BLANK = ""; +} diff --git a/src/framework/constants/CommonConstants.ts b/src/framework/constants/CommonConstants.ts new file mode 100644 index 0000000..a4d148c --- /dev/null +++ b/src/framework/constants/CommonConstants.ts @@ -0,0 +1,20 @@ +export default class CommonConstants { + static readonly SEMICOLON = ';'; + static readonly BLANK = ''; + static readonly ZERO = 0; + static readonly ONE = 1; + static readonly TWO = 2; + static readonly THREE = 3; + static readonly HALF = 0.5; + static readonly ONE_THOUSAND = 1000; + static readonly DOWNLOAD_PATH = "./test-results/downloads/"; + static readonly SOAP_XML_REQUEST_PATH = "src/resources/API/SOAP/"; + static readonly REST_JSON_REQUEST_PATH = "src/resources/API/REST/"; + static readonly TEST_FOLDER_PATH = "../../tests/"; + static readonly TEST_SUITE_FILE_FORMAT = ".test.ts"; + static readonly PARALLEL_MODE = "parallel"; + static readonly SERIAL_MODE = "serial"; + static readonly REPORT_TITLE = "Test Execution Report"; + static readonly RESULTS_PATH = "./test-results/results"; + static readonly JUNIT_RESULTS_PATH = `${CommonConstants.RESULTS_PATH}/results.xml`; +} diff --git a/src/framework/constants/DBConstants.ts b/src/framework/constants/DBConstants.ts new file mode 100644 index 0000000..91c3476 --- /dev/null +++ b/src/framework/constants/DBConstants.ts @@ -0,0 +1,7 @@ +export default class DBConstants { + static readonly PROTOCOL = ';PROTOCOL=TCPIP'; + static readonly CERTIFICATE = ';trustServerCertificate=true;encrypt=false'; + static readonly USER = 'user:'; + static readonly PASSWORD = 'password:'; + static readonly CONNECTION_STRING = 'connectString:'; +} diff --git a/src/framework/constants/ExcelConstants.ts b/src/framework/constants/ExcelConstants.ts new file mode 100644 index 0000000..0897665 --- /dev/null +++ b/src/framework/constants/ExcelConstants.ts @@ -0,0 +1,5 @@ +export default class ExcelConstants { + static readonly TEST_PATH = './src/resources/data/testData.xlsx'; + static readonly SUITE_PATH = '../../resources/data/testData.xlsx'; + static readonly YES = "YES"; +} diff --git a/src/framework/constants/HTMLConstants.ts b/src/framework/constants/HTMLConstants.ts new file mode 100644 index 0000000..1240e58 --- /dev/null +++ b/src/framework/constants/HTMLConstants.ts @@ -0,0 +1,5 @@ +export default class HTMLConstants { + static readonly LOADING_IMAGE = "body>.loader"; + static readonly OPTION = "option"; + static readonly SELECTED_OPTION = "option[selected='selected']"; +} diff --git a/src/framework/logger/Logger.ts b/src/framework/logger/Logger.ts new file mode 100644 index 0000000..5306ea7 --- /dev/null +++ b/src/framework/logger/Logger.ts @@ -0,0 +1,24 @@ +import winston from 'winston'; + +const Logger = winston.createLogger({ + transports: [ + new winston.transports.Console({ + format: winston.format.combine( + winston.format.uncolorize({ level: true, message: true, raw: true }), + winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), + winston.format.align(), + winston.format.printf((info) => `${info.timestamp} ${info.level}: ${info.message}`), + ), + }), + new winston.transports.File({ + filename: 'test-results/logs/execution.log', + format: winston.format.combine( + winston.format.uncolorize({ level: true, message: true, raw: true }), + winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), + winston.format.align(), + winston.format.printf((info) => `${info.timestamp} ${info.level}: ${info.message}`), + ), + }), + ], +}); +export default Logger; diff --git a/src/framework/logger/TestListener.ts b/src/framework/logger/TestListener.ts new file mode 100644 index 0000000..d57f3af --- /dev/null +++ b/src/framework/logger/TestListener.ts @@ -0,0 +1,57 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { + Reporter, TestCase, TestError, TestResult, TestStep, +} from "@playwright/test/reporter"; +import Logger from "./Logger"; + +const TEST_SEPARATOR = "##############################################################################"; +const STEP_SEPARATOR = "------------------------------------------------------------------------------"; + +export default class TestListener implements Reporter { + onTestBegin(test: TestCase, result: TestResult): void { + this.printLogs(`Test: ${test.title} - Started`, TEST_SEPARATOR); + } + + onTestEnd(test: TestCase, result: TestResult): void { + if (result.status === 'failed') { + Logger.error(`Test: ${test.title} - ${result.status}\n${result.error.stack}`); + } + this.printLogs(`Test: ${test.title} - ${result.status}`, TEST_SEPARATOR); + } + + onStdOut(chunk: string | Buffer, test?: TestCase, result?: TestResult): void { + Logger.info(chunk); + } + + onStdErr(chunk: string | Buffer, test?: TestCase, result?: TestResult): void { + Logger.error(chunk); + } + + onStepBegin(test: TestCase, result: TestResult, step: TestStep): void { + if (step.category === "test.step") { + if (typeof step.parent !== "undefined") { + Logger.info(step.title); + } else { + this.printLogs(`Started Step: ${step.title}`, STEP_SEPARATOR); + } + } + } + + onStepEnd(test: TestCase, result: TestResult, step: TestStep): void { + if (step.category === "test.step" && typeof step.parent === "undefined") { + this.printLogs(`Completed Step: ${step.title}`, STEP_SEPARATOR); + } + } + + onError(error: TestError): void { + Logger.error(`Message: ${error.message}`); + Logger.error(`Stack: ${error.stack}`); + Logger.error(`Value: ${error.value}`); + } + + private printLogs(msg: string, separator: string) { + Logger.info(separator); + Logger.info(`${msg.toUpperCase()}`); + Logger.info(separator); + } +} diff --git a/src/framework/manager/Browser.ts b/src/framework/manager/Browser.ts new file mode 100644 index 0000000..623b067 --- /dev/null +++ b/src/framework/manager/Browser.ts @@ -0,0 +1,27 @@ +import BrowserConstants from "../constants/BrowserConstants"; + +export default class Browser { + public static type(browser: string) { + let browserType; + if (browser === BrowserConstants.FIREFOX) { + browserType = BrowserConstants.FIREFOX; + } else if (browser === BrowserConstants.WEBKIT) { + browserType = BrowserConstants.WEBKIT; + } else { + browserType = BrowserConstants.CHROMIUM; + } + return browserType; + } + + public static channel(browser: string) { + let browserChannel; + if (browser === BrowserConstants.CHROME) { + browserChannel = BrowserConstants.CHROME; + } else if (browser === BrowserConstants.EDGE) { + browserChannel = BrowserConstants.MSEDGE; + } else { + browserChannel = BrowserConstants.BLANK; + } + return browserChannel; + } +} diff --git a/src/framework/manager/SuiteManager.ts b/src/framework/manager/SuiteManager.ts new file mode 100644 index 0000000..b7d246b --- /dev/null +++ b/src/framework/manager/SuiteManager.ts @@ -0,0 +1,37 @@ +/* eslint-disable no-tabs */ +/* eslint-disable no-restricted-syntax */ +import fs from "fs"; +import path from 'path'; +import CommonConstants from "../constants/CommonConstants"; +import SuiteTemplate from "../template/SuiteTemplate"; +import CLIUtil from "../utils/CLIUtil"; +import ExcelUtil from "../utils/ExcelUtil"; + +export default class SuiteManager { + public static createSuite() { + const sheet = CLIUtil.getValueOf("SHEET"); + this.deleteFiles(CommonConstants.TEST_FOLDER_PATH); + let testList = CommonConstants.BLANK; + for (const { TestName, Mode } of ExcelUtil.getSuiteTests(sheet)) { + let modeOfRun = CommonConstants.BLANK; + if (Mode !== undefined && Mode !== null && Mode !== CommonConstants.BLANK) { + modeOfRun = `\n\ttest.describe.configure({ mode: '${Mode.toLowerCase()}' });`; + } + testList += `\ntest.describe("${TestName}", () => {${modeOfRun} + require("./${TestName}.spec.ts"); +});`; + } + fs.writeFileSync(`${CommonConstants.TEST_FOLDER_PATH}${sheet}${CommonConstants.TEST_SUITE_FILE_FORMAT}`, + SuiteTemplate.getTemplate(sheet, testList)); + console.log(" Completed!! "); + } + + private static deleteFiles(directory: string) { + const files = fs.readdirSync(directory); + for (const file of files) { + if (file.includes(CommonConstants.TEST_SUITE_FILE_FORMAT)) { fs.unlinkSync(path.join(directory, file)); } + } + } +} + +SuiteManager.createSuite(); diff --git a/src/framework/playwright/API/APIActions.ts b/src/framework/playwright/API/APIActions.ts new file mode 100644 index 0000000..2c9325a --- /dev/null +++ b/src/framework/playwright/API/APIActions.ts @@ -0,0 +1,31 @@ +import { Page } from "@playwright/test"; +import RequestHeader from "./RequestHeader"; +import RESTRequest from "./RESTRequest"; +import SOAPRequest from "./SOAPRequest"; + +export default class APIActions { + constructor(private page: Page) { } + /** + * Returns REST Request instance + * @returns + */ + public get rest(): RESTRequest { + return new RESTRequest(this.page); + } + + /** + * Returns SOAP Request instance + * @returns + */ + public get soap(): SOAPRequest { + return new SOAPRequest(); + } + + /** + * Returns Request header instance + * @returns + */ + public get header(): RequestHeader { + return new RequestHeader(); + } +} diff --git a/src/framework/playwright/API/RESTRequest.ts b/src/framework/playwright/API/RESTRequest.ts new file mode 100644 index 0000000..a1cc330 --- /dev/null +++ b/src/framework/playwright/API/RESTRequest.ts @@ -0,0 +1,148 @@ +import { test, Page, APIResponse } from '@playwright/test'; +import fs from 'fs'; +import fetchToCurl from 'fetch-to-curl'; +import CommonConstants from '../../constants/CommonConstants'; +import StringUtil from '../../utils/StringUtil'; +import RESTResponse from "./RESTResponse"; + +export default class RESTRequest { + constructor(private page: Page) { } + /** + * Creates request body from JSON file by replacing the input parameters + * @param jsonFileName + * @param data + * @returns + */ + public async createRequestBody(jsonFileName: string, data: any): Promise { + let json = fs.readFileSync(CommonConstants.REST_JSON_REQUEST_PATH + jsonFileName, 'utf-8'); + json = StringUtil.formatStringValue(json, data); + return json; + } + /** + * Make POST request and return response + * @param endPoint + * @param requestHeader + * @param jsonAsString + * @param description + * @returns + */ + public async post(endPoint: string, requestHeader: any, jsonAsString: string, + description: string): Promise { + const headersAsJson = JSON.parse(JSON.stringify(requestHeader)); + let restResponse: RESTResponse; + await test.step(`Making POST request for ${description}`, async () => { + this.printRequest(endPoint, headersAsJson, jsonAsString, 'post'); + const response = await this.page.request.post(endPoint, + { headers: headersAsJson, data: JSON.parse(jsonAsString) }); + restResponse = await this.setRestResponse(response, description); + }); + return restResponse; + } + /** + * Sets the API Response into RestResponse object + * @param response + * @param description + * @returns RestResponse object + */ + private async setRestResponse(response: APIResponse, description: string): Promise { + const body = await response.text(); + const headers = response.headers(); + const statusCode = response.status(); + const restResponse: RESTResponse = new RESTResponse(headers, body, statusCode, description); + console.log(`Response body: ${JSON.stringify(JSON.parse(body), undefined, 2)}`); + return restResponse; + } + /** + * Make Get request and return response + * @param endPoint + * @param requestHeader + * @param description + * @returns + */ + public async get(endPoint: string, requestHeader: any, description: string): Promise { + const headersAsJson = JSON.parse(JSON.stringify(requestHeader)); + let restResponse: RESTResponse; + await test.step(`Making GET request for ${description}`, async () => { + this.printRequest(endPoint, headersAsJson, null, 'get'); + const response = await this.page.request.get(endPoint, { headers: headersAsJson }); + restResponse = await this.setRestResponse(response, description); + }); + return restResponse; + } + /** + * Make Put request and return response + * @param endPoint + * @param requestHeader + * @param jsonAsString + * @param description + * @returns + */ + public async put(endPoint: string, requestHeader: any, jsonAsString: any, + description: string): Promise { + const headersAsJson = JSON.parse(JSON.stringify(requestHeader)); + let restResponse: RESTResponse; + await test.step(`Making PUT request for ${description}`, async () => { + this.printRequest(endPoint, headersAsJson, jsonAsString, 'put'); + const response = await this.page.request.put(endPoint, + { headers: headersAsJson, data: JSON.parse(jsonAsString) }); + restResponse = await this.setRestResponse(response, description); + }); + return restResponse; + } + /** + * Make Patch request and return response + * @param endPoint + * @param requestHeader + * @param jsonAsString + * @param description + * @returns + */ + public async patch(endPoint: string, requestHeader: any, jsonAsString: any, + description: string): Promise { + const headersAsJson = JSON.parse(JSON.stringify(requestHeader)); + let restResponse: RESTResponse; + await test.step(`Making PATCH request for ${description}`, async () => { + this.printRequest(endPoint, headersAsJson, jsonAsString, 'patch'); + const response = await this.page.request.patch(endPoint, + { headers: headersAsJson, data: JSON.parse(jsonAsString) }); + restResponse = await this.setRestResponse(response, description); + }); + return restResponse; + } + /** + * Make Delete request and return response + * @param endPoint + * @param requestHeader + * @param description + * @returns + */ + public async delete(endPoint: string, requestHeader: any, description: string): Promise { + const headersAsJson = JSON.parse(JSON.stringify(requestHeader)); + let restResponse: RESTResponse; + await test.step(`Making DELETE request for ${description}`, async () => { + this.printRequest(endPoint, headersAsJson, null, 'delete'); + const response = await this.page.request.delete(endPoint, { headers: headersAsJson }); + restResponse = await this.setRestResponse(response, description); + }); + return restResponse; + } + /** + * Prints the API request on console in curl format + * @param endPoint + * @param requestHeader + * @param jsonRequestBody + * @param method + */ + private printRequest(endPoint: string, requestHeader: any, jsonRequestBody: string, method: string) { + let requestBody = jsonRequestBody; + if (jsonRequestBody !== null) { + requestBody = JSON.stringify(JSON.parse(jsonRequestBody), undefined, 2); + } + console.log("Request: ", fetchToCurl({ + url: endPoint, + headers: requestHeader, + body: requestBody, + method: method, + })); + } +} diff --git a/src/framework/playwright/API/RESTResponse.ts b/src/framework/playwright/API/RESTResponse.ts new file mode 100644 index 0000000..a99aa79 --- /dev/null +++ b/src/framework/playwright/API/RESTResponse.ts @@ -0,0 +1,72 @@ +import test from "@playwright/test"; +import jp from "jsonpath"; + +export default class RESTResponse { + public constructor(private headers: any, private body: string, private status: number, + private description: string) { } + + /** + * Get content of tag in response body using JSON path + * @param jsonPath + * @param description + * @returns + */ + public async getTagContentByJsonPath(jsonPath: string, description: string): Promise { + let text: string; + await test.step(`Getting content of ${description}`, async () => { + // eslint-disable-next-line prefer-destructuring + text = jp.query(JSON.parse(this.body), jsonPath)[0]; + }); + return text; + } + + /** + * Get header value by header key + * @param key + * @returns + */ + public async getHeaderValueByKey(key: string): Promise { + let value: string; + await test.step(`Getting header value of ${key}`, async () => { + const jsonHeaders = await JSON.parse(JSON.stringify(this.headers)); + value = jsonHeaders[key]; + }); + return value; + } + + /** + * Get response status code + * @returns + */ + public async getStatusCode(): Promise { + let status: number; + await test.step(`Getting status code of ${this.description}`, async () => { + status = this.status; + }); + return status; + } + + /** + * Get response body + * @returns + */ + public async getBody(): Promise { + let body: string; + await test.step(`Getting response body of ${this.description}`, async () => { + body = this.body; + }); + return body; + } + + /** + * Get response headers + * @returns + */ + public async getHeaders(): Promise { + let headers: string; + await test.step(`Getting response Headers of ${this.description}`, async () => { + headers = this.headers; + }); + return headers; + } +} diff --git a/src/framework/playwright/API/RequestHeader.ts b/src/framework/playwright/API/RequestHeader.ts new file mode 100644 index 0000000..58edbf9 --- /dev/null +++ b/src/framework/playwright/API/RequestHeader.ts @@ -0,0 +1,12 @@ +export default class RequestHeader { + private map = new Map(); + + public set(key: string, value: any): RequestHeader { + this.map.set(key, value); + return this; + } + + public get() { + return Object.fromEntries(this.map); + } +} diff --git a/src/framework/playwright/API/SOAPRequest.ts b/src/framework/playwright/API/SOAPRequest.ts new file mode 100644 index 0000000..338275c --- /dev/null +++ b/src/framework/playwright/API/SOAPRequest.ts @@ -0,0 +1,47 @@ +import test from "@playwright/test"; +import soapRequest from "easy-soap-request"; +import format from "xml-formatter"; +import fs from 'fs'; +import SOAPResponse from "./SOAPResponse"; +import StringUtil from "../../utils/StringUtil"; +import CommonConstants from "../../constants/CommonConstants"; + +export default class SOAPRequest { + /** + * Creates request body by replacing the input parameters + * @param xmlFileName + * @param data + * @returns + */ + private async createRequestBody(xmlFileName: string, data: any): Promise { + let xml = fs.readFileSync(CommonConstants.SOAP_XML_REQUEST_PATH + xmlFileName, 'utf-8'); + xml = StringUtil.formatStringValue(xml, data); + console.log(`SOAP request : \n${format(xml, { collapseContent: true })}`); + return xml; + } + + /** + * Make POST request and return response + * @param endPoint + * @param requestHeader + * @param fileName + * @param gData + * @param data + * @param description + * @returns + */ + public async post(endPoint: string, requestHeader: any, fileName: string, + requestData : any, description: string): Promise { + let soapResponse: SOAPResponse; + await test.step(`Making post request for ${description}`, async () => { + const url = process.env.SOAP_API_BASE_URL + endPoint; + console.log(`URL: ${url}`); + const xml = await this.createRequestBody(fileName, requestData); + const { response } = await soapRequest({ url: url, headers: requestHeader, xml: xml }); + const { headers, body, statusCode } = response; + soapResponse = new SOAPResponse(headers, body, statusCode, description); + console.log(`SOAP Response: \n${format(body, { collapseContent: true })}`); + }); + return soapResponse; + } +} diff --git a/src/framework/playwright/API/SOAPResponse.ts b/src/framework/playwright/API/SOAPResponse.ts new file mode 100644 index 0000000..14c156a --- /dev/null +++ b/src/framework/playwright/API/SOAPResponse.ts @@ -0,0 +1,82 @@ +import test from "@playwright/test"; +import XMLParserUtil from "../../utils/XMLParserUtil"; + +export default class SOAPResponse { + public constructor(private headers: any, private body: any, private status: number, private description: string) { } + /** + * Get content of tag in response body using xpath + * @param xPathExpression xpath for the tag + * @param description + */ + public async getTagContentByXpath(xPathExpression: string, description: string): Promise { + let text: string; + await test.step(`Getting tag value of action ${description}`, async () => { + text = XMLParserUtil.getTagContentByXpath(this.body, xPathExpression); + }); + return text; + } + + /** + * Get value of attribute in response body using xpath + * @param xPathExpression xpath for the attribute + * @param description + */ + public async getAttributeValueByXpath(xPathExpression: string, description: string): Promise { + let text: string; + await test.step(`Getting attribute value of action ${description}`, async () => { + text = XMLParserUtil.getAttributeValueByXpath(this.body, xPathExpression); + }); + return text; + } + + /** + * Get header value by header key + * @param key + * @param description + * @returns + */ + public async getHeaderValueByKey(key: string): Promise { + let value:string; + await test.step(`Getting header value of ${key}`, async () => { + const jsonHeaders = await JSON.parse(JSON.stringify(this.headers)); + value = jsonHeaders[key]; + }); + return value; + } + + /** + * Get response status code + * @returns + */ + public async getStatusCode(): Promise { + let status:number; + await test.step(`Getting status code of ${this.description}`, async () => { + status = this.status; + }); + return status; + } + + /** + * Get response body + * @returns + */ + public async getBody(): Promise { + let body:string; + await test.step(`Getting response body of ${this.description}`, async () => { + body = this.body; + }); + return body; + } + + /** + * Get response headers + * @returns + */ + public async getHeaders(): Promise { + let headers:string; + await test.step(`Getting response Headers of ${this.description}`, async () => { + headers = JSON.stringify(this.headers); + }); + return headers; + } +} diff --git a/src/framework/playwright/actions/AlertActions.ts b/src/framework/playwright/actions/AlertActions.ts new file mode 100644 index 0000000..31a9847 --- /dev/null +++ b/src/framework/playwright/actions/AlertActions.ts @@ -0,0 +1,32 @@ +import { Page } from "@playwright/test"; + +export default class AlertActions { + constructor(private page: Page) {} + + /** + * Accept alert and return alert message + * @param promptText A text to enter in prompt. It is optional for alerts. + * @returns alert message + */ + public async accept(promptText?: string): Promise { + return this.page.waitForEvent("dialog").then(async (dialog) => { + if (dialog.type() === "prompt") { + await dialog.accept(promptText); + } else { + await dialog.accept(); + } + return dialog.message().trim(); + }); + } + + /** + * Dismiss alert and return alert message + * @returns alert message + */ + public async dismiss(): Promise { + return this.page.waitForEvent("dialog").then(async (d) => { + await d.dismiss(); + return d.message().trim(); + }); + } +} diff --git a/src/framework/playwright/actions/CheckBoxActions.ts b/src/framework/playwright/actions/CheckBoxActions.ts new file mode 100644 index 0000000..69e2925 --- /dev/null +++ b/src/framework/playwright/actions/CheckBoxActions.ts @@ -0,0 +1,53 @@ +import { test, Locator } from "@playwright/test"; + +export default class CheckBoxActions { + private locator: Locator; + private description: string; + + /** + * Sets the locator with description + * @param locator + * @param description + * @returns + */ + public setLocator(locator: Locator, description: string): CheckBoxActions { + this.locator = locator; + this.description = description; + return this; + } + + /** + * check checkbox or radio button + */ + public async check() { + await test.step(`Check ${this.description}`, async () => { + await this.locator.check(); + }); + return this; + } + + /** + * uncheck checkbox or radio button + */ + public async uncheck() { + await await test.step(`Uncheck ${this.description}`, async () => { + await this.locator.uncheck(); + }); + return this; + } + + /** + * Returns the status of the checkbox + * @returns + */ + public async isChecked(): Promise { + let status: boolean; + await test.step(`Checking status of checkbox ${this.description}`, async () => { + const element = this.locator; + await element.waitFor(); + status = await element.isChecked(); + }, + ); + return status; + } +} diff --git a/src/framework/playwright/actions/DropDownActions.ts b/src/framework/playwright/actions/DropDownActions.ts new file mode 100644 index 0000000..09da71c --- /dev/null +++ b/src/framework/playwright/actions/DropDownActions.ts @@ -0,0 +1,89 @@ +import { test, Locator } from "@playwright/test"; +import HTMLConstants from "../../constants/HTMLConstants"; + +export default class DropDownActions { + private locator: Locator; + private description: string; + + /** + * Sets the locator with description + * @param locator + * @param description + * @returns + */ + public setLocator(locator: Locator, description: string): DropDownActions { + this.locator = locator; + this.description = description; + return this; + } + + /** + * Select the dropdown by value + * @param value + * @returns + */ + public async selectByValue(value: string) { + await test.step(`Selecting value ${value} from ${this.description}`, async () => { + await this.locator.selectOption({ value }); + }); + return this; + } + + /** + * Select the dropdown by Label + * @param text + * @returns + */ + public async selectByVisibleText(text: string) { + await test.step(`Selecting text ${text} from ${this.description}`, async () => { + await this.locator.selectOption({ label: text }); + }); + return this; + } + + /** + * Select the dropdown by index + * @param index + * @returns + */ + public async selectByIndex(index: number) { + await test.step(`Selecting index ${index} of ${this.description}`, async () => { + await this.locator.selectOption({ index }); + }); + return this; + } + + /** + * Gets all the options in dropdown + * @param index + * @returns + */ + public async getAllOptions(): Promise { + let selectOptions: string[]; + await test.step( + `Getting all the options of ${this.description}`, + async () => { + selectOptions = await this.locator.locator(HTMLConstants.OPTION).allTextContents(); + }, + ); + return selectOptions; + } + + /** + * Gets all the selected options in dropdown + * @param index + * @returns + */ + public async getAllSelectedOptions(): Promise { + let selectOptions: string[]; + await test.step( + `Getting all the selected options of ${this.description}`, + async () => { + selectOptions = await this.locator + .locator(HTMLConstants.SELECTED_OPTION) + .allTextContents(); + }, + ); + return selectOptions; + } +} diff --git a/src/framework/playwright/actions/EditBoxActions.ts b/src/framework/playwright/actions/EditBoxActions.ts new file mode 100644 index 0000000..8602e00 --- /dev/null +++ b/src/framework/playwright/actions/EditBoxActions.ts @@ -0,0 +1,75 @@ +import { test, Locator } from "@playwright/test"; +import UIElementActions from "./UIElementActions"; + +export default class EditBoxActions extends UIElementActions { + /** + * Sets the selector with description + * @param selector + * @param description + * @returns + */ + public setEditBox(selector: string, description: string): EditBoxActions { + this.setElement(selector, description); + return this; + } + + /** + * Sets the locator with description + * @param locator + * @returns + */ + public setLocator(locator: Locator, description: string): EditBoxActions { + super.setLocator(locator, description); + return this; + } + + /** + * Clear and enter text + * @param value + * @returns + */ + public async fill(value: string) { + await test.step(`Entering ${this.description} as ${value}`, async () => { + await this.getLocator().fill(value); + }); + return this; + } + + /** + * Types the value to text field + * @param value + * @returns + */ + public async type(value: string) { + await test.step(`Typing ${this.description} as ${value}`, async () => { + await this.getLocator().type(value); + }); + return this; + } + + /** + * Enter text and hit tab key + * @param value + * @returns + */ + public async fillAndTab(value: string) { + await test.step(`Entering ${this.description} as ${value} and Tab`, async () => { + await this.getLocator().fill(value); + await this.getLocator().press("Tab"); + }); + return this; + } + + /** + * Typing text and hit tab key + * @param value + * @returns + */ + public async typeAndTab(value: string) { + await test.step(`Entering ${this.description} as ${value} and Tab`, async () => { + await this.getLocator().type(value); + await this.getLocator().press("Tab"); + }); + return this; + } +} diff --git a/src/framework/playwright/actions/UIActions.ts b/src/framework/playwright/actions/UIActions.ts new file mode 100644 index 0000000..36f4884 --- /dev/null +++ b/src/framework/playwright/actions/UIActions.ts @@ -0,0 +1,316 @@ +import { test, Page } from "@playwright/test"; +import CommonConstants from "../../constants/CommonConstants"; +import HTMLConstants from "../../constants/HTMLConstants"; +import AlertActions from "./AlertActions"; +import CheckBoxActions from "./CheckBoxActions"; +import DropDownActions from "./DropDownActions"; +import EditBoxActions from "./EditBoxActions"; +import UIElementActions from "./UIElementActions"; + +export default class UIActions { + private elementAction: UIElementActions; + private editBoxAction: EditBoxActions; + private checkboxAction: CheckBoxActions; + private dropdownAction: DropDownActions; + private alertAction: AlertActions; + + constructor(private page: Page) { + this.elementAction = new UIElementActions(page); + this.editBoxAction = new EditBoxActions(page); + this.checkboxAction = new CheckBoxActions(); + this.dropdownAction = new DropDownActions(); + this.alertAction = new AlertActions(this.page); + } + + /** + * Returns page object + * @returns + */ + public getPage(): Page { + return this.page; + } + + /** + * Sets the page + * @param page + */ + public setPage(page: Page) { + this.page = page; + this.elementAction = new UIElementActions(page); + this.editBoxAction = new EditBoxActions(page); + this.alertAction = new AlertActions(this.page); + } + + /** + * Close page + * @returns + */ + public closePage() { + this.page.close(); + } + + /** + * Returns the instance of Alert + * @returns + */ + public alert() { + return this.alertAction; + } + + /** + * Returns the instance of editbox actions + * @param selector + * @param description + * @returns + */ + public editBox(selector: string, description: string) { + return this.editBoxAction.setEditBox(selector, description); + } + + /** + * Returns the instance of UIElements actions + * @param selector + * @param description + * @returns + */ + public element(selector: string, description: string) { + return this.elementAction.setElement(selector, description); + } + + /** + * Returns the instance of Dropdown actions + * @param selector + * @param description + * @returns + */ + public dropdown(selector: string, description: string) { + return this.dropdownAction.setLocator( + this.elementAction.setElement(selector, description).getLocator(), + description, + ); + } + + /** + * Returns the instance of CheckBox actions + * @param selector + * @param description + * @returns + */ + public checkbox(selector: string, description: string) { + return this.checkboxAction.setLocator( + this.elementAction.setElement(selector, description).getLocator(), + description, + ); + } + + /** + * Navigate to specified URL + * @param URL + * @param description + */ + public async goto(URL: string, description: string) { + await test.step(`Navigate to ${description}`, async () => { + await this.page.goto(URL); + }); + } + + /** + * Navigate to previous URL + * @param description + */ + public async goBack(description: string) { + await test.step(`Go to the previous ${description}`, async () => { + await this.page.goBack(); + }); + } + + /** + * Navigate to next URL + * @param description + */ + public async goForward(description: string) { + await test.step(`Go to the next ${description}`, async () => { + await this.page.goForward(); + }); + } + + /** + * Page Refresh + */ + public async pageRefresh() { + await test.step(`Page Refresh`, async () => { + await this.page.reload(); + }); + } + + /** + * Press a key on web page + * @param key + * @param description + */ + public async keyPress(key: string, description: string) { + await test.step(`Pressing ${description}`, async () => { + await this.page.keyboard.press(key); + }); + } + + /** + * Waits for the main frame navigation and returns the main resource response. + */ + public async waitForNavigation() { + await test.step(`Waiting for navigation`, async () => { + await this.page.waitForNavigation(); + }); + } + + /** + * Returns when the required load state has been reached. + */ + public async waitForLoadState() { + await test.step(`Waiting for load event`, async () => { + await this.page.waitForLoadState(); + }); + } + + /** + * Returns when the required dom content is in loaded state. + */ + public async waitForDomContentLoaded() { + await test.step(`Waiting for load event`, async () => { + await this.page.waitForLoadState("domcontentloaded", { timeout: 5000 }); + }); + } + + /** + * Gets the handle of the new window + * @param selector + * @param description + */ + public async switchToNewWindow( + selector: string, + description: string, + ): Promise { + let [newPage] = [this.page]; + await test.step(`Opening ${description} Window`, async () => { + [newPage] = await Promise.all([ + this.page.context().waitForEvent("page"), + await this.elementAction.setElement(selector, description).click(), + ]); + await newPage.waitForLoadState("domcontentloaded"); + }); + return newPage; + } + + /** + * Clicks the an element, accepts the alert and returns the alert message + * @param selector selector of the element + * @param description description of element + * @returns alert message + */ + public async acceptAlertOnElementClick( + selector: string, + description: string, + ): Promise { + const message = this.alert().accept(); + return this.handleAlert(selector, description, message); + } + + /** + * Clicks the an element, dismisses the alert and returns the alert message + * @param selector selector of the element + * @param description description of element + * @returns alert message + */ + public async dismissAlertOnElementClick( + selector: string, + description: string, + ): Promise { + const message = this.alert().dismiss(); + return this.handleAlert(selector, description, message); + } + + /** + * Clicks the an element, accepts the alert prompt and returns the alert message + * @param selector selector of the element + * @param description description of element + * @param promptText A text to enter in prompt. + * @returns alert message + */ + public async acceptPromptOnElementClick( + selector: string, + description: string, + promptText: string, + ): Promise { + const message = this.alert().accept(promptText); + return this.handleAlert(selector, description, message); + } + + private async handleAlert( + selector: string, + description: string, + message: Promise, + ) { + await this.elementAction.setElement(selector, description).click(); + return message; + } + + /** + * Gets the page Title + * @returns + */ + public async getPageTitle(): Promise { + let title: string; + await test.step(`Getting Page Title`, async () => { + title = await this.page.title(); + }); + return title; + } + + /** + * Downloads the file and returns the downloaded file name + * @param selector element that results in file download + * @param description description of the element + * @returns downloaded file name + */ + public async downloadFile(selector: string, description: string): Promise { + let fileName: string; + await test.step(`Downloading ${description} file`, async () => { + const [download] = await Promise.all([ + this.page.waitForEvent('download'), + await this.page.locator(selector).click({ modifiers: ["Alt"] }), + ]); + fileName = download.suggestedFilename(); + const filePath = `${CommonConstants.DOWNLOAD_PATH}${fileName}`; + await download.saveAs(filePath); + await download.delete(); + }); + return fileName; + } + /** + * Pause the execution in seconds + * @param sec + */ + public async pauseInSecs(sec: number) { + // eslint-disable-next-line no-promise-executor-return + return new Promise((resolve) => setTimeout(resolve, sec * CommonConstants.ONE_THOUSAND)); + } + + /** + * Wait For Page loading image to disappear + * @param page + */ + public async waitForLoadingImage() { + await test.step("Waiting for Loading Image to disappear", async () => { + try { + await this.page.locator(HTMLConstants.LOADING_IMAGE).waitFor({ + state: "visible", + timeout: CommonConstants.ONE_THOUSAND * CommonConstants.THREE, + }); + } catch (error) { + // console.log("Loading Image was not displayed"); + } + await this.page.locator(HTMLConstants.LOADING_IMAGE).waitFor({ state: "hidden" }); + await this.pauseInSecs(CommonConstants.HALF); + }); + } +} diff --git a/src/framework/playwright/actions/UIElementActions.ts b/src/framework/playwright/actions/UIElementActions.ts new file mode 100644 index 0000000..70776bd --- /dev/null +++ b/src/framework/playwright/actions/UIElementActions.ts @@ -0,0 +1,314 @@ +import { test, Locator, Page } from "@playwright/test"; + +export default class UIElementActions { + protected locator: Locator; + protected description: string; + protected selector: string; + + constructor(private page: Page) { } + + /** + * Returns the first locator + * @returns + */ + public getLocator(): Locator { + return this.locator.first(); + } + + /** + * Returns the all the locators + * @returns + */ + public getLocators(): Locator { + return this.locator; + } + + /** + * Sets the locator using the selector * + * @param selector + * @param description + * @returns + */ + public setElement(selector: string, description: string): UIElementActions { + this.selector = selector; + this.locator = this.page.locator(this.selector); + this.description = description; + return this; + } + + /** + * Sets the locator with description + * @param locator + * @param description + * @returns + */ + public setLocator(locator: Locator, description: string): UIElementActions { + this.locator = locator; + this.description = description; + return this; + } + + /** + * Click on element + * @returns + */ + public async click() { + await test.step(`Clicking on ${this.description}`, async () => { + await this.getLocator().click(); + }); + return this; + } + + /** + * Double click on element + * @returns + */ + public async doubleClick() { + await test.step(`Double Clicking ${this.description}`, async () => { + await this.getLocator().dblclick(); + }); + return this; + } + + /** + * scroll element into view, unless it is completely visible + * @returns + */ + public async scrollIntoView() { + await test.step(`Scroll to element ${this.description}`, async () => { + await this.getLocator().scrollIntoViewIfNeeded(); + }); + return this; + } + + /** + * Wait for element to be invisible + * @returns + */ + public async waitTillInvisible() { + await test.step(`Waiting for ${this.description} to be invisible`, async () => { + await this.getLocator().waitFor({ state: "hidden" }); + }); + return this; + } + + /** + * wait for element not to be present in DOM + * @returns + */ + public async waitTillDetached() { + await test.step(`Wait for ${this.description} to be detached from DOM`, async () => { + await this.getLocator().waitFor({ state: "detached" }); + }); + return this; + } + + /** + * wait for element to be visible + * @param wait time for element is visible + * @returns + */ + public async waitTillVisible(sec: number) { + await test.step(`Wait for ${this.description} to be visible in DOM`, async () => { + await this.getLocator().waitFor({ state: "visible", timeout: sec * 1000 }); + }); + return this; + } + + /** + * wait for element to be attached to DOM + * @returns + */ + public async waitForPresent() { + await test.step(`Wait for ${this.description} to attach to DOM`, async () => { + await this.getLocator().waitFor({ state: "attached" }); + }); + return this; + } + + /** + * This method hovers over the element + */ + public async hover() { + await test.step(`Hovering on ${this.description}`, async () => { + await this.getLocator().hover(); + }); + return this; + } + + /** + * Returns input.value for or