├── .gitignore ├── src ├── support │ ├── constants │ │ ├── HTMLConstants.ts │ │ ├── DBConstants.ts │ │ ├── BrowserConstants.ts │ │ └── CommonConstants.ts │ ├── utils │ │ ├── EnvUtil.ts │ │ ├── CLIUtil.ts │ │ ├── XMLParserUtil.ts │ │ ├── PDFUtil.ts │ │ ├── DateUtil.ts │ │ ├── DBUtil.ts │ │ └── StringUtil.ts │ ├── playwright │ │ ├── API │ │ │ ├── RequestHeader.ts │ │ │ ├── RESTResponse.ts │ │ │ ├── SOAPRequest.ts │ │ │ ├── SOAPResponse.ts │ │ │ └── RESTRequest.ts │ │ ├── actions │ │ │ ├── AlertActions.ts │ │ │ ├── CheckBoxActions.ts │ │ │ ├── EditBoxActions.ts │ │ │ ├── DropDownActions.ts │ │ │ ├── UIElementActions.ts │ │ │ └── UIActions.ts │ │ └── asserts │ │ │ └── Assert.ts │ ├── reporter │ │ ├── CucumberReporter.ts │ │ └── HTMLReporter.ts │ ├── manager │ │ └── Browser.ts │ ├── logger │ │ └── Log.ts │ └── config │ │ └── hooks.ts ├── resources │ └── API │ │ ├── SOAP │ │ ├── add.xml │ │ ├── divide.xml │ │ ├── multiply.xml │ │ └── subtract.xml │ │ └── REST │ │ └── book.json ├── web │ ├── pages │ │ ├── HomePage.ts │ │ ├── SearchResultsPage.ts │ │ ├── CommonPage.ts │ │ └── RegisterUserPage.ts │ ├── constants │ │ └── Constants.ts │ └── steps │ │ ├── SearchProductSteps.ts │ │ └── RegisterUserSteps.ts └── api │ ├── steps │ ├── RESTAuthor.ts │ ├── SOAPCalculator.ts │ └── RESTBook.ts │ └── constants │ └── Constants.ts ├── tsconfig.json ├── features ├── web │ ├── search_product.feature │ └── register_user.feature ├── REST │ ├── author.feature │ └── book.feature └── SOAP │ └── calculator.feature ├── .env ├── .env.qa ├── cucumber.js ├── .github └── workflows │ └── main.yml ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /test-results 2 | /allure-results 3 | /allure-report 4 | /node_modules 5 | node_modules 6 | /.vscode 7 | @rerun.txt -------------------------------------------------------------------------------- /src/support/constants/HTMLConstants.ts: -------------------------------------------------------------------------------- 1 | export default class HTMLConstants { 2 | static readonly OPTION = "option"; 3 | static readonly SELECTED_OPTION = "option[selected='selected']"; 4 | } 5 | -------------------------------------------------------------------------------- /src/support/utils/EnvUtil.ts: -------------------------------------------------------------------------------- 1 | export default class EnvUtil { 2 | public static setEnv() { 3 | require('dotenv').config({ 4 | path: process.env.TEST_ENV ? `.env.${process.env.TEST_ENV}` : '.env', 5 | override: process.env.TEST_ENV ? true : false, 6 | }); 7 | } 8 | } -------------------------------------------------------------------------------- /src/resources/API/SOAP/add.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | {number1} 8 | {number2} 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/support/constants/DBConstants.ts: -------------------------------------------------------------------------------- 1 | export default class DBConstants { 2 | static readonly PROTOCOL = ';PROTOCOL=TCPIP'; 3 | static readonly CERTIFICATE = ';trustServerCertificate=true;encrypt=false'; 4 | static readonly USER = 'user:'; 5 | static readonly PASSWORD = 'password:'; 6 | static readonly CONNECTION_STRING = 'connectString:'; 7 | } 8 | -------------------------------------------------------------------------------- /src/resources/API/SOAP/divide.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | {number1} 8 | {number2} 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/support/playwright/API/RequestHeader.ts: -------------------------------------------------------------------------------- 1 | export default class RequestHeader { 2 | private map = new Map(); 3 | 4 | public set(key: string, value: any): RequestHeader { 5 | this.map.set(key, value); 6 | return this; 7 | } 8 | 9 | public get() { 10 | return Object.fromEntries(this.map); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/resources/API/SOAP/multiply.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | {number1} 8 | {number2} 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/resources/API/SOAP/subtract.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | {number1} 8 | {number2} 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/support/constants/BrowserConstants.ts: -------------------------------------------------------------------------------- 1 | export default class BrowserConstants { 2 | static readonly CHROME = "chrome"; 3 | static readonly FIREFOX = "firefox"; 4 | static readonly WEBKIT = "webkit"; 5 | static readonly MSEDGE = "msedge"; 6 | static readonly EDGE = "edge"; 7 | static readonly CHROMIUM = "chromium"; 8 | static readonly BLANK = ""; 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true, 4 | "target": "ESNext", 5 | "module": "CommonJS", 6 | "moduleResolution": "Node", 7 | "sourceMap": true, 8 | "outDir": "../tests-out", 9 | "resolveJsonModule": true, 10 | "esModuleInterop": true, 11 | "allowSyntheticDefaultImports": true 12 | } 13 | } -------------------------------------------------------------------------------- /src/resources/API/REST/book.json: -------------------------------------------------------------------------------- 1 | { 2 | "Author": { 3 | "Age": {age}, 4 | "Id": {authorID}, 5 | "Name": "{authorName}" 6 | }, 7 | "AuthorId": {authorID}, 8 | "DateAdded": "/Date({dateAdded})/", 9 | "DateAddedIso": "{dateAddedIso}T00:00:00", 10 | "Genre": { 11 | "Id": {genreId}, 12 | "Name": "{genreName}" 13 | }, 14 | "GenreId": {genreId}, 15 | "IsOutOfPrint": {available}, 16 | "Name": "{bookName}" 17 | } -------------------------------------------------------------------------------- /src/web/pages/HomePage.ts: -------------------------------------------------------------------------------- 1 | import UIActions from "../../support/playwright/actions/UIActions"; 2 | import Assert from "../../support/playwright/asserts/Assert"; 3 | import Constants from "../constants/Constants"; 4 | 5 | export default class HomePage { 6 | constructor(private web: UIActions) { } 7 | /** 8 | * async navigateToHomePage 9 | */ 10 | public async navigateToHomePage() { 11 | await this.web.goto(process.env.BASE_URL, "Home page"); 12 | } 13 | } -------------------------------------------------------------------------------- /src/support/utils/CLIUtil.ts: -------------------------------------------------------------------------------- 1 | export default class CLIUtil { 2 | /** 3 | * Gets the value of command line argument 4 | * @param argumentName 5 | * @returns 6 | */ 7 | public static getValueOf(argumentName: string): string { 8 | const argv = process.argv[2]; 9 | if (argv === undefined) { 10 | throw new Error(`${argumentName} is not defined, please send ${argumentName} through CLI`); 11 | } 12 | if (argv.toUpperCase().includes(argumentName)) { 13 | return argv.split("=")[1]; 14 | } 15 | throw new Error(`Please send command line argument ${argumentName} with value`); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /features/web/search_product.feature: -------------------------------------------------------------------------------- 1 | @search 2 | Feature: Scenarios related to search product 3 | 4 | Background: 5 | Given user is on home page 6 | 7 | @regression @sanity @validSearch 8 | Scenario Outline: serach for a product 9 | When the user searches for product "" 10 | Then user should see "" product displayed on search result 11 | Examples: 12 | | product | 13 | | iPod | 14 | | Mac | 15 | 16 | @regression @invalidSearch 17 | Scenario: Search with invalid product 18 | When the user searches for product "invalid product" 19 | Then user should see a search result message as "There is no product that matches the search criteria." -------------------------------------------------------------------------------- /src/web/constants/Constants.ts: -------------------------------------------------------------------------------- 1 | export default class Constants { 2 | static readonly PRODUCT = "Product"; 3 | static readonly SEARCH_BUTTON = "Search Button"; 4 | static readonly MESSAGE = "Message"; 5 | static readonly MY_ACCOUNT = "My Account"; 6 | static readonly LOGOUT = "Logout"; 7 | static readonly REGISTER = "Register"; 8 | static readonly FIRST_NAME = "First Name"; 9 | static readonly LAST_NAME = "Last Name"; 10 | static readonly EMAIL = "Email"; 11 | static readonly TELEPHONE = "Telephone"; 12 | static readonly PASSWORD = "Password"; 13 | static readonly CONFIRM_PASSWORD = "Confirm Password"; 14 | static readonly PRIVACY_POLICY = "Privacy Policy"; 15 | static readonly CONTINUE = "Continue"; 16 | } -------------------------------------------------------------------------------- /features/REST/author.feature: -------------------------------------------------------------------------------- 1 | @rest @author 2 | Feature: Scenarios related to Author REST API in Library Information System 3 | 4 | Background: 5 | Given user has access to Library Information System 6 | 7 | @regression @sanity @authors 8 | Scenario: Retrieve list of Books in the Library Information System 9 | When user makes a request to retrieves all the Authors in the System 10 | Then user should get a status code 200 11 | And user should get list of Authors 12 | 13 | @regression @sanity @singleAuthor 14 | Scenario: Retrieve a single book in the Library Information System 15 | When user makes a request to retrieve an Author with id 1 16 | Then user should get a status code 200 17 | And user should get the author with id 1 -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | BROWSER=chrome 2 | 3 | # These timeouts are set in minutes 4 | TEST_TIMEOUT=20 5 | BROWSER_LAUNCH_TIMEOUT=0 6 | WAIT_TIMEOUT=1 7 | 8 | # Execution configurations 9 | RETRIES=0 10 | PARALLEL_THREAD=2 11 | 12 | # Test application configurations 13 | ENVIRONMENT=STG 14 | BASE_URL=https://ecommerce-playground.lambdatest.io/index.php 15 | 16 | # API configurations 17 | SOAP_API_BASE_URL=http://www.dneonline.com/calculator.asmx 18 | REST_API_BASE_URL=https://www.libraryinformationsystem.org/Services/RestService.svc 19 | 20 | # DB configurations 21 | DB_CONFIG=Server=localhost,1433;Database=AutomationDB;User Id=SA;Password=Auto2021 22 | # Sample DB2 CONFIG=> DATABASE=;HOSTNAME=;UID=;PWD=;PORT= 23 | # Sample ORACLE CONFIG=user:;password:;connectString::/ 24 | 25 | RECORD_VIDEO=true -------------------------------------------------------------------------------- /.env.qa: -------------------------------------------------------------------------------- 1 | BROWSER=firefox 2 | 3 | # These timeouts are set in minutes 4 | TEST_TIMEOUT=20 5 | BROWSER_LAUNCH_TIMEOUT=0 6 | WAIT_TIMEOUT=1 7 | 8 | # Execution configurations 9 | RETRIES=0 10 | PARALLEL_THREAD=2 11 | 12 | # Test application configurations 13 | ENVIRONMENT=QA 14 | BASE_URL=https://ecommerce-playground.lambdatest.io/index.php 15 | 16 | # API configurations 17 | SOAP_API_BASE_URL=http://www.dneonline.com/calculator.asmx 18 | REST_API_BASE_URL=https://www.libraryinformationsystem.org/Services/RestService.svc 19 | # DB configurations 20 | DB_CONFIG=Server=localhost,1433;Database=AutomationDB;User Id=SA;Password=Auto2021 21 | # Sample DB2 CONFIG=> DATABASE=;HOSTNAME=;UID=;PWD=;PORT= 22 | # Sample ORACLE CONFIG=user:;password:;connectString::/ 23 | 24 | RECORD_VIDEO=true -------------------------------------------------------------------------------- /src/web/steps/SearchProductSteps.ts: -------------------------------------------------------------------------------- 1 | import { Given, Then, When } from "@cucumber/cucumber"; 2 | import CommonPage from "../pages/CommonPage"; 3 | import HomePage from "../pages/HomePage"; 4 | import SearchResultsPage from "../pages/SearchResultsPage"; 5 | 6 | Given('user is on home page', async function () { 7 | await new HomePage(this.web).navigateToHomePage(); 8 | }); 9 | 10 | When('the user searches for product {string}', async function (product: string) { 11 | await new CommonPage(this.web).searchProduct(product); 12 | }); 13 | 14 | Then('user should see {string} product displayed on search result', async function (product: string) { 15 | await new SearchResultsPage(this.web).verifySearchResult(product); 16 | }); 17 | 18 | Then('user should see a search result message as {string}', async function (message: string) { 19 | await new SearchResultsPage(this.web).verifyInvalidSearchMessage(message); 20 | }); -------------------------------------------------------------------------------- /features/web/register_user.feature: -------------------------------------------------------------------------------- 1 | @register 2 | Feature: Scenarios related to register user 3 | 4 | Background: 5 | Given user is on home page 6 | 7 | @regression 8 | @sanity 9 | Scenario Outline: register a new user 10 | Given user navigate to registration page 11 | When the user enters the registration details "", "", "", "", "", "", "" 12 | Then user should see a message "Your Account Has Been Created!" 13 | Then user logs out of application 14 | Then user should see a message "Account Logout" 15 | Examples: 16 | | firstName | lastName | email | telephone | password | confirmPassword | subscribe | 17 | | John | Doe | john_{0}@email.com | 9876543210 | test321 | test321 | yes | 18 | | Jane | Doe | jane_{0}@email.com | 4321098765 | test123 | test123 | no | -------------------------------------------------------------------------------- /src/support/playwright/actions/AlertActions.ts: -------------------------------------------------------------------------------- 1 | import { Page } from "@playwright/test"; 2 | 3 | export default class AlertActions { 4 | constructor(private page: Page) {} 5 | 6 | /** 7 | * Accept alert and return alert message 8 | * @param promptText A text to enter in prompt. It is optional for alerts. 9 | * @returns alert message 10 | */ 11 | public async accept(promptText?: string): Promise { 12 | return this.page.waitForEvent("dialog").then(async (dialog) => { 13 | if (dialog.type() === "prompt") { 14 | await dialog.accept(promptText); 15 | } else { 16 | await dialog.accept(); 17 | } 18 | return dialog.message().trim(); 19 | }); 20 | } 21 | 22 | /** 23 | * Dismiss alert and return alert message 24 | * @returns alert message 25 | */ 26 | public async dismiss(): Promise { 27 | return this.page.waitForEvent("dialog").then(async (d) => { 28 | await d.dismiss(); 29 | return d.message().trim(); 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/support/utils/XMLParserUtil.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const xpath = require('xpath'); 3 | const Dom = require('xmldom').DOMParser; 4 | 5 | export default class XMLParserUtil { 6 | /** 7 | * Get content of tag in XML using xpath 8 | * @param xPathExpression xpath for the tag 9 | * @param xml as string 10 | */ 11 | public static getTagContentByXpath(xml: string, xPathExpression: string): string { 12 | const doc = new Dom().parseFromString(xml); 13 | const text = xpath.select(`string(${xPathExpression})`, doc); 14 | return text; 15 | } 16 | 17 | /** 18 | * Get value of attribute in XML using xpath 19 | * @param xPathExpression xpath for the attribute 20 | * @param xml as string 21 | */ 22 | public static getAttributeValueByXpath(xml: string, xPathExpression: string): string { 23 | const doc = new Dom().parseFromString(xml); 24 | const text = xpath.select1(xPathExpression, doc).value; 25 | return text; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /cucumber.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config({ 2 | path: process.env.TEST_ENV ? `.env.${process.env.TEST_ENV}` : '.env', 3 | override: process.env.TEST_ENV ? true : false, 4 | }); 5 | require('fs-extra').ensureDir('./test-results/reports'); 6 | require('fs-extra').remove('./test-results/screenshots'); 7 | require('fs-extra').remove('./test-results/videos'); 8 | 9 | let options = [ 10 | '--require-module ts-node/register', 11 | '--require **/steps/*.ts', 12 | '--require ./src/support/config/hooks.ts', 13 | '--format summary', 14 | '--format rerun:@rerun.txt', 15 | '--format json:./test-results/reports/cucumber.json', 16 | '--publish-quiet true', 17 | `--parallel=${process.env.PARALLEL_THREAD}`, 18 | `--format-options '{"snippetInterface":"async-await"}'`, 19 | `--retry=${process.env.RETRIES}`, 20 | `--tags "not @ignore"`, 21 | ].join(' '); 22 | 23 | let runner = [ 24 | './features/', 25 | options, 26 | ].join(' '); 27 | 28 | let rerun = [ 29 | '@rerun.txt', 30 | options, 31 | ].join(' '); 32 | 33 | module.exports = { runner, rerun } 34 | -------------------------------------------------------------------------------- /src/support/constants/CommonConstants.ts: -------------------------------------------------------------------------------- 1 | export default class CommonConstants { 2 | static readonly SEMICOLON = ';'; 3 | static readonly BLANK = ''; 4 | static readonly ZERO = 0; 5 | static readonly ONE = 1; 6 | static readonly TWO = 2; 7 | static readonly THREE = 3; 8 | static readonly HALF = 0.5; 9 | static readonly ONE_THOUSAND = 1000; 10 | static readonly DOWNLOAD_PATH = "./test-results/downloads/"; 11 | static readonly SOAP_XML_REQUEST_PATH = "src/resources/API/SOAP/"; 12 | static readonly REST_JSON_REQUEST_PATH = "src/resources/API/REST/"; 13 | static readonly TEST_FOLDER_PATH = "../../tests/"; 14 | static readonly TEST_SUITE_FILE_FORMAT = ".test.ts"; 15 | static readonly PARALLEL_MODE = "parallel"; 16 | static readonly SERIAL_MODE = "serial"; 17 | static readonly REPORT_TITLE = "Test Execution Report"; 18 | static readonly RESULTS_PATH = "./test-results/results"; 19 | static readonly JUNIT_RESULTS_PATH = `${CommonConstants.RESULTS_PATH}/results.xml`; 20 | static readonly SIXTY = 60; 21 | static readonly WAIT = parseInt(process.env.WAIT_TIME, 10) * CommonConstants.ONE_THOUSAND * CommonConstants.SIXTY; 22 | } 23 | -------------------------------------------------------------------------------- /src/web/steps/RegisterUserSteps.ts: -------------------------------------------------------------------------------- 1 | import { Given, Then, When } from "@cucumber/cucumber"; 2 | import StringUtil from "../../support/utils/StringUtil"; 3 | import CommonPage from "../pages/CommonPage"; 4 | import RegisterUserPage from "../pages/RegisterUserPage"; 5 | 6 | Given('user navigate to registration page', async function () { 7 | await new CommonPage(this.web).navigateToRegisterUser(); 8 | }); 9 | 10 | When('the user enters the registration details {string}, {string}, {string}, {string}, {string}, {string}, {string}', 11 | async function (firstName, lastName, email, telephone, password, confirmPassword, subscribe) { 12 | email = StringUtil.formatString(email, StringUtil.randomNumberString(5)); 13 | await new RegisterUserPage(this.web).enterRegistrationDetails(firstName, lastName, email, telephone, password, confirmPassword, subscribe); 14 | await new RegisterUserPage(this.web).agreePrivacyPolicy(); 15 | await new RegisterUserPage(this.web).clickContinueButton(); 16 | }); 17 | 18 | Then('user should see a message {string}', async function (message) { 19 | await new CommonPage(this.web).verifyTitleMessage(message); 20 | }); 21 | 22 | Then('user logs out of application', async function () { 23 | await new CommonPage(this.web).logout(); 24 | }); -------------------------------------------------------------------------------- /src/web/pages/SearchResultsPage.ts: -------------------------------------------------------------------------------- 1 | import UIActions from "../../support/playwright/actions/UIActions"; 2 | import Assert from "../../support/playwright/asserts/Assert"; 3 | import Constants from "../constants/Constants"; 4 | 5 | export default class SearchResultsPage { 6 | constructor(private web: UIActions) { } 7 | 8 | private SEARCH_RESULT_PRODUCT_TEXT = ".product-thumb .title a"; 9 | private SEARCH_MESSAGE_TEXT = ".entry-content.content-products p"; 10 | 11 | /** 12 | * Verify the product search results 13 | * @param product 14 | */ 15 | public async verifySearchResult(product: string) { 16 | const products = await this.web.element(this.SEARCH_RESULT_PRODUCT_TEXT, Constants.PRODUCT).getAllTextContent(); 17 | for(const prod of products) { 18 | await Assert.assertContainsIgnoreCase(prod, product, Constants.PRODUCT); 19 | } 20 | } 21 | /** 22 | * Verify the message displayed when searched for invalid product 23 | * @param message 24 | */ 25 | public async verifyInvalidSearchMessage(message: string) { 26 | const actualMsg = await this.web.element(this.SEARCH_MESSAGE_TEXT, Constants.MESSAGE).getTextContent(); 27 | await Assert.assertEquals(actualMsg, message, Constants.MESSAGE); 28 | } 29 | } -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # 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 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Automation Test Execution 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | 10 | jobs: 11 | tests: 12 | name: Cucumer features execution 13 | runs-on: windows-latest 14 | steps: 15 | - name: Checkout code from repository 16 | uses: actions/checkout@v3 17 | - name: Setting up Node.js 16 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: 16 21 | cache: 'npm' 22 | - name: Install dependencies 23 | run: npm ci 24 | - name: Test execution 25 | run: npm run test:tags "@validSearch" 26 | - name: Upload test results 27 | if: always() 28 | uses: actions/upload-artifact@v3 29 | with: 30 | name: test-results 31 | path: test-results 32 | - name: Generating cucubmer report 33 | if: always() 34 | uses: deblockt/cucumber-report-annotations-action@v1.7 35 | with: 36 | access-token: ${{ secrets.GITHUB_TOKEN }} 37 | path: "**/cucumber.json" 38 | -------------------------------------------------------------------------------- /src/support/reporter/CucumberReporter.ts: -------------------------------------------------------------------------------- 1 | import DateUtil from "../utils/DateUtil"; 2 | import EnvUtil from "../utils/EnvUtil"; 3 | 4 | var reporter = require('cucumber-html-reporter'); 5 | 6 | export default class CucumberReporter { 7 | public static generate() { 8 | // require('dotenv').config(); 9 | EnvUtil.setEnv(); 10 | const options = { 11 | brandTitle: "Acceptance Test Report", 12 | theme: 'bootstrap', 13 | jsonFile: 'test-results/reports/cucumber.json', 14 | output: 'test-results/reports/cucumber.html', 15 | reportSuiteAsScenarios: true, 16 | scenarioTimestamp: true, 17 | launchReport: false, 18 | columnLayout: 1, 19 | metadata: { 20 | "Execution Date": DateUtil.dateGenerator("DD/MM/YYYY", 0, 0, 0), 21 | "Base URL": process.env.BASE_URL, 22 | "Environment": process.env.ENVIRONMENT, 23 | "SOAP Endpoint": process.env.SOAP_API_BASE_URL, 24 | "Browser": process.env.BROWSER, 25 | "REST Endpoint": process.env.REST_API_BASE_URL, 26 | "DB Config": process.env.DB_CONFIG, 27 | } 28 | }; 29 | reporter.generate(options); 30 | } 31 | } 32 | CucumberReporter.generate(); -------------------------------------------------------------------------------- /src/support/playwright/actions/CheckBoxActions.ts: -------------------------------------------------------------------------------- 1 | import { Locator } from "@playwright/test"; 2 | import CommonConstants from "../../constants/CommonConstants"; 3 | import Log from "../../logger/Log"; 4 | 5 | export default class CheckBoxActions { 6 | private locator: Locator; 7 | private description: string; 8 | 9 | /** 10 | * Sets the locator with description 11 | * @param locator 12 | * @param description 13 | * @returns 14 | */ 15 | public setLocator(locator: Locator, description: string): CheckBoxActions { 16 | this.locator = locator; 17 | this.description = description; 18 | return this; 19 | } 20 | 21 | /** 22 | * check checkbox or radio button 23 | */ 24 | public async check() { 25 | Log.info(`Check ${this.description}`); 26 | await this.locator.check(); 27 | return this; 28 | } 29 | 30 | /** 31 | * uncheck checkbox or radio button 32 | */ 33 | public async uncheck() { 34 | Log.info(`Uncheck ${this.description}`); 35 | await this.locator.uncheck(); 36 | return this; 37 | } 38 | 39 | /** 40 | * Returns the status of the checkbox 41 | * @returns 42 | */ 43 | public async isChecked(): Promise { 44 | Log.info(`Checking status of checkbox ${this.description}`); 45 | const element = this.locator; 46 | await element.waitFor({ state: "visible", timeout: CommonConstants.WAIT }); 47 | return await this.locator.isChecked(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/support/utils/PDFUtil.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import pdfParse from "pdf-parse"; 3 | 4 | export default class PDFUtil { 5 | /** 6 | * Gets the text content of the pdf file 7 | * @param filePath File path 8 | * @returns PDF as text 9 | */ 10 | public static async getText(filePath: string): Promise { 11 | const buffer = fs.readFileSync(filePath); 12 | try { 13 | const data = await pdfParse(buffer); 14 | return data.text; 15 | } catch (err) { 16 | throw new Error(err); 17 | } 18 | } 19 | 20 | /** 21 | * Gets number of pages in pdf file 22 | * @param filePath File path 23 | * @returns Number of pages 24 | */ 25 | public static async getNumberOfPages(filePath: string): Promise { 26 | const buffer = fs.readFileSync(filePath); 27 | try { 28 | const data = await pdfParse(buffer); 29 | return data.numpages; 30 | } catch (err) { 31 | throw new Error(err); 32 | } 33 | } 34 | 35 | /** 36 | * Gets the information about the pdf file 37 | * @param filePath File path 38 | * @returns PDF document info 39 | */ 40 | public static async getInfo(filePath: string): Promise { 41 | const buffer = fs.readFileSync(filePath); 42 | try { 43 | const data = await pdfParse(buffer); 44 | return data.info; 45 | } catch (err) { 46 | throw new Error(err); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/support/utils/DateUtil.ts: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | 3 | export default class DateUtil { 4 | /** 5 | * Generates date based on the input 6 | * @param format date format 7 | * @param days increment OR decrement the days 8 | * @param months increment OR decrement the months 9 | * @param years increment OR decrement the years 10 | * @returns 11 | */ 12 | public static dateGenerator(format: string, days: number, months: number, years: number): string { 13 | const date = moment().add(days, 'd').add(months, 'M').add(years, 'y').format(format); 14 | return date; 15 | } 16 | 17 | /** 18 | * Customizes the date that has been given as input based on other input parameter 19 | * @param date to be customized 20 | * @param format date format 21 | * @param days increment OR decrement the days 22 | * @param months increment OR decrement the months 23 | * @param years increment OR decrement the years 24 | * @returns 25 | */ 26 | public static dateCustomizer(date: string, format: string, days: number, months: number, years: number): string { 27 | const customDate = moment(date, format).add(days, 'd').add(months, 'M').add(years, 'y').format(format); 28 | return customDate; 29 | } 30 | 31 | /** 32 | * Generates time in hr:min format based on the input 33 | * @param format time format 34 | * @param hours increment OR decrement the hours 35 | * @param minutes increment OR decrement the minutes 36 | * @returns 37 | */ 38 | public static timeGenerator(format: string, hours: number, minutes: number): string { 39 | const time = moment().add(minutes, 'm').add(hours, 'h').format(format); 40 | return time; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/support/manager/Browser.ts: -------------------------------------------------------------------------------- 1 | import { chromium, ChromiumBrowser, firefox, FirefoxBrowser, LaunchOptions, webkit, WebKitBrowser } from "@playwright/test"; 2 | import BrowserConstants from "../constants/BrowserConstants"; 3 | 4 | const browserOptions: LaunchOptions = { 5 | slowMo: 50, 6 | args: ["--start-maximized", "--disable-extensions", "--disable-plugins"], 7 | firefoxUserPrefs: { 8 | 'media.navigator.streams.fake': true, 9 | 'media.navigator.permission.disabled': true, 10 | }, 11 | headless: false, 12 | timeout: Number.parseInt(process.env.BROWSER_LAUNCH_TIMEOUT, 10), 13 | downloadsPath: "./test-results/downloads", 14 | }; 15 | 16 | export default class Browser { 17 | public static async launch() { 18 | const browserType = process.env.BROWSER; 19 | let browser: ChromiumBrowser | FirefoxBrowser | WebKitBrowser; 20 | if (BrowserConstants.FIREFOX === browserType) { 21 | browser = await firefox.launch(browserOptions); 22 | } else if (BrowserConstants.WEBKIT === browserType) { 23 | browser = await webkit.launch(browserOptions); 24 | } else { 25 | browser = await chromium.launch(browserOptions); 26 | } 27 | return browser; 28 | } 29 | /* 30 | public static channel() { 31 | const browser = process.env.BROWSER.toLowerCase(); 32 | let browserChannel; 33 | if (browser === BrowserConstants.CHROME) { 34 | browserChannel = BrowserConstants.CHROME; 35 | } else if (browser === BrowserConstants.EDGE) { 36 | browserChannel = BrowserConstants.MSEDGE; 37 | } else { 38 | browserChannel = BrowserConstants.BLANK; 39 | } 40 | return browserChannel; 41 | } */ 42 | } 43 | -------------------------------------------------------------------------------- /src/support/playwright/API/RESTResponse.ts: -------------------------------------------------------------------------------- 1 | import jp from "jsonpath"; 2 | import Log from "../../logger/Log"; 3 | 4 | export default class RESTResponse { 5 | public constructor(private headers: any, private body: string, private status: number, 6 | private description: string) { } 7 | 8 | /** 9 | * Get content of tag in response body using JSON path 10 | * @param jsonPath 11 | * @param description 12 | * @returns 13 | */ 14 | public async getTagContentByJsonPath(jsonPath: string, description: string): Promise { 15 | Log.info(`Getting content of ${description}`); 16 | // eslint-disable-next-line prefer-destructuring 17 | return jp.query(JSON.parse(this.body), jsonPath)[0]; 18 | } 19 | 20 | /** 21 | * Get header value by header key 22 | * @param key 23 | * @returns 24 | */ 25 | public async getHeaderValueByKey(key: string): Promise { 26 | Log.info(`Getting header value of ${key}`); 27 | const jsonHeaders = await JSON.parse(JSON.stringify(this.headers)); 28 | return jsonHeaders[key]; 29 | } 30 | 31 | /** 32 | * Get response status code 33 | * @returns 34 | */ 35 | public async getStatusCode(): Promise { 36 | Log.info(`Getting status code of ${this.description}`); 37 | return this.status; 38 | } 39 | 40 | /** 41 | * Get response body 42 | * @returns 43 | */ 44 | public async getBody(): Promise { 45 | Log.info(`Getting response body of ${this.description}`); 46 | return this.body; 47 | } 48 | 49 | /** 50 | * Get response headers 51 | * @returns 52 | */ 53 | public async getHeaders(): Promise { 54 | Log.info(`Getting response Headers of ${this.description}`); 55 | return this.headers; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/support/reporter/HTMLReporter.ts: -------------------------------------------------------------------------------- 1 | import DateUtil from '../utils/DateUtil'; 2 | import EnvUtil from '../utils/EnvUtil'; 3 | 4 | export default class HTMLReporter { 5 | public static generateReport() { 6 | const os = require('node:os'); 7 | const report = require('multiple-cucumber-html-reporter'); 8 | // require('dotenv').config(); 9 | EnvUtil.setEnv(); 10 | report.generate({ 11 | jsonDir: './test-results/reports/', 12 | reportPath: './test-results/reports/html/', 13 | pageTitle: 'Test Execution Report', 14 | reportName: 'Execution Results', 15 | displayDuration: false, 16 | displayReportTime: false, 17 | hideMetadata: false, 18 | customMetadata: false, 19 | metadata: { 20 | browser: { 21 | name: process.env.BROWSER, 22 | version: 'latest' 23 | }, 24 | device: os.hostname(), 25 | platform: { 26 | name: os.type(), 27 | version: os.version(), 28 | } 29 | }, 30 | customData: { 31 | title: 'Run Info', 32 | data: [ 33 | { label: 'Execution Date', value: DateUtil.dateGenerator("DD/MM/YYYY", 0, 0, 0) }, 34 | { label: 'Base URL', value: process.env.BASE_URL }, 35 | { label: 'Environment', value: process.env.ENVIRONMENT }, 36 | { label: 'SOAP Endpoint', value: process.env.SOAP_API_BASE_URL }, 37 | { label: 'REST Endpoint', value: process.env.REST_API_BASE_URL }, 38 | { label: 'DB Config', value: process.env.DB_CONFIG }, 39 | ] 40 | } 41 | }); 42 | } 43 | } 44 | HTMLReporter.generateReport(); 45 | -------------------------------------------------------------------------------- /features/SOAP/calculator.feature: -------------------------------------------------------------------------------- 1 | @soap @calculator 2 | Feature: Scenarios related to Calculator SOAP service 3 | 4 | @regression @sanity @add @ignore 5 | Scenario Outline: Verify ADD SOAP service 6 | When user adds two numbers and in the calculator 7 | Then user should get a status code 200 8 | Then user should get the result of addition as "" 9 | Examples: 10 | | number1 | number2 | result | 11 | | 10 | 20 | 30 | 12 | | 23 | 32 | 55 | 13 | 14 | @regression @sanity @subtract 15 | Scenario Outline: Verify SUBTRACT SOAP service 16 | When user subtracts two numbers and in the calculator 17 | Then user should get a status code 200 18 | Then user should get the result of subtraction as "" 19 | Examples: 20 | | number1 | number2 | result | 21 | | 100 | 20 | 80 | 22 | | 135 | 32 | 103 | 23 | 24 | @regression @sanity @multiply 25 | Scenario Outline: Verify MULTIPLY SOAP service 26 | When user multiplies two numbers and in the calculator 27 | Then user should get a status code 200 28 | Then user should get the result of multiplication as "" 29 | Examples: 30 | | number1 | number2 | result | 31 | | 10 | 20 | 200 | 32 | | 2 | 32 | 64 | 33 | 34 | @regression @sanity @divide 35 | Scenario Outline: Verify DIVIDE SOAP service 36 | When user divides two numbers and in the calculator 37 | Then user should get a status code 200 38 | Then user should get the result of division as "" 39 | Examples: 40 | | number1 | number2 | result | 41 | | 100 | 20 | 5 | 42 | | 21 | 3 | 7 | 43 | -------------------------------------------------------------------------------- /src/web/pages/CommonPage.ts: -------------------------------------------------------------------------------- 1 | import UIActions from "../../support/playwright/actions/UIActions"; 2 | import Assert from "../../support/playwright/asserts/Assert"; 3 | import StringUtil from "../../support/utils/StringUtil"; 4 | import Constants from "../constants/Constants"; 5 | 6 | export default class CommonPage { 7 | constructor(private web: UIActions) { } 8 | 9 | private SUCCESS_MESSAGE_TEXT = "h1.page-title"; 10 | private SEARCH_TEXTBOX = "[name='search']"; 11 | private SEARCH_BUTTON = ".search-button"; 12 | private MY_ACCOUNT_LINK = "//li[contains(@class,'dropdown')]//span[contains(text(),'My account')]"; 13 | private MENU_LINK = "//ul[contains(@class,'dropdown-menu')]//span[contains(text(),'{0}')]"; 14 | 15 | /** 16 | * Search for a product from header banner 17 | * @param product 18 | */ 19 | public async searchProduct(product: string) { 20 | await this.web.editBox(this.SEARCH_TEXTBOX, Constants.PRODUCT).fill(product); 21 | await this.web.element(this.SEARCH_BUTTON, Constants.SEARCH_BUTTON).click(); 22 | } 23 | 24 | public async logout() { 25 | await this.web.element(this.MY_ACCOUNT_LINK, Constants.MY_ACCOUNT).hover(); 26 | await this.web.element(StringUtil.formatString(this.MENU_LINK, Constants.LOGOUT), Constants.LOGOUT).click(); 27 | } 28 | 29 | public async navigateToRegisterUser() { 30 | await this.web.element(this.MY_ACCOUNT_LINK, Constants.MY_ACCOUNT).hover(); 31 | await this.web.element(StringUtil.formatString(this.MENU_LINK, Constants.REGISTER), Constants.REGISTER).click(); 32 | } 33 | 34 | /** 35 | * Verify the message displayed on title of the page 36 | * @param message 37 | */ 38 | public async verifyTitleMessage(message: string) { 39 | const actualMsg = await this.web.element(this.SUCCESS_MESSAGE_TEXT, Constants.MESSAGE).getTextContent(); 40 | await Assert.assertEquals(actualMsg, message, Constants.MESSAGE); 41 | } 42 | 43 | } -------------------------------------------------------------------------------- /src/support/playwright/actions/EditBoxActions.ts: -------------------------------------------------------------------------------- 1 | import { Locator } from "@playwright/test"; 2 | import Log from "../../logger/Log"; 3 | import UIElementActions from "./UIElementActions"; 4 | 5 | export default class EditBoxActions extends UIElementActions { 6 | /** 7 | * Sets the selector with description 8 | * @param selector 9 | * @param description 10 | * @returns 11 | */ 12 | public setEditBox(selector: string, description: string): EditBoxActions { 13 | this.setElement(selector, description); 14 | return this; 15 | } 16 | 17 | /** 18 | * Sets the locator with description 19 | * @param locator 20 | * @returns 21 | */ 22 | public setLocator(locator: Locator, description: string): EditBoxActions { 23 | super.setLocator(locator, description); 24 | return this; 25 | } 26 | 27 | /** 28 | * Clear and enter text 29 | * @param value 30 | * @returns 31 | */ 32 | public async fill(value: string) { 33 | Log.info(`Entering ${this.description} as ${value}`); 34 | await this.getLocator().fill(value); 35 | return this; 36 | } 37 | 38 | /** 39 | * Types the value to text field 40 | * @param value 41 | * @returns 42 | */ 43 | public async type(value: string) { 44 | Log.info(`Typing ${this.description} as ${value}`); 45 | await this.getLocator().type(value); 46 | return this; 47 | } 48 | 49 | /** 50 | * Enter text and hit tab key 51 | * @param value 52 | * @returns 53 | */ 54 | public async fillAndTab(value: string) { 55 | Log.info(`Entering ${this.description} as ${value} and Tab`); 56 | await this.getLocator().fill(value); 57 | await this.getLocator().press("Tab"); 58 | return this; 59 | } 60 | 61 | /** 62 | * Typing text and hit tab key 63 | * @param value 64 | * @returns 65 | */ 66 | public async typeAndTab(value: string) { 67 | Log.info(`Entering ${this.description} as ${value} and Tab`); 68 | await this.getLocator().type(value); 69 | await this.getLocator().press("Tab"); 70 | return this; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/support/playwright/API/SOAPRequest.ts: -------------------------------------------------------------------------------- 1 | import soapRequest from "easy-soap-request"; 2 | import format from "xml-formatter"; 3 | import fs from 'fs'; 4 | import SOAPResponse from "./SOAPResponse"; 5 | import StringUtil from "../../utils/StringUtil"; 6 | import CommonConstants from "../../constants/CommonConstants"; 7 | import Log from "../../logger/Log"; 8 | import { ICreateAttachment } from "@cucumber/cucumber/lib/runtime/attachment_manager"; 9 | 10 | export default class SOAPRequest { 11 | /** 12 | * Creates request body by replacing the input parameters 13 | * @param xmlFileName 14 | * @param data 15 | * @returns 16 | */ 17 | private async createRequestBody(attach: ICreateAttachment, xmlFileName: string, data: any): Promise { 18 | let xml = fs.readFileSync(CommonConstants.SOAP_XML_REQUEST_PATH + xmlFileName, 'utf-8'); 19 | xml = StringUtil.formatStringValue(xml, data); 20 | Log.attachText(attach, `SOAP request : \n${format(xml, { collapseContent: true })}`); 21 | return xml; 22 | } 23 | 24 | /** 25 | * Make POST request and return response 26 | * @param endPoint 27 | * @param requestHeader 28 | * @param fileName 29 | * @param gData 30 | * @param data 31 | * @param description 32 | * @returns 33 | */ 34 | public async post(attach: ICreateAttachment, endPoint: string, requestHeader: any, fileName: string, 35 | requestData: any, description: string): Promise { 36 | Log.info(`Making SOAP request for ${description}`); 37 | Log.attachText(attach, `URL: ${endPoint}`); 38 | const xml = await this.createRequestBody(attach, fileName, requestData); 39 | const { response } = await soapRequest({ url: endPoint, headers: requestHeader, xml: xml }); 40 | const { headers, body, statusCode } = response; 41 | Log.attachText(attach, `SOAP Response: \n${format(body, { collapseContent: true })}`); 42 | return new SOAPResponse(headers, body, statusCode, description); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/support/logger/Log.ts: -------------------------------------------------------------------------------- 1 | import { ICreateAttachment } from '@cucumber/cucumber/lib/runtime/attachment_manager'; 2 | import winston from 'winston'; 3 | 4 | const Logger = winston.createLogger({ 5 | transports: [ 6 | new winston.transports.Console({ 7 | format: winston.format.combine( 8 | winston.format.uncolorize({ level: true, message: true, raw: true }), 9 | winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), 10 | winston.format.align(), 11 | winston.format.printf((info) => `${info.timestamp} ${info.level}: ${info.message}`), 12 | ), 13 | }), 14 | new winston.transports.File({ 15 | filename: 'test-results/logs/execution.log', 16 | format: winston.format.combine( 17 | winston.format.uncolorize({ level: true, message: true, raw: true }), 18 | winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), 19 | winston.format.align(), 20 | winston.format.printf((info) => `${info.timestamp} ${info.level}: ${info.message}`), 21 | ), 22 | }), 23 | ], 24 | }); 25 | 26 | const TEST_SEPARATOR = "##############################################################################"; 27 | 28 | export default class Log { 29 | public static testBegin(scenario: string): void { 30 | this.printLogs(`Scenario: ${scenario} - Started`, TEST_SEPARATOR); 31 | } 32 | 33 | public static testEnd(scenario: string, status: string): void { 34 | this.printLogs(`Scenario: ${scenario} - ${status}`, TEST_SEPARATOR); 35 | } 36 | 37 | private static printLogs(msg: string, separator: string) { 38 | Logger.info(separator); 39 | Logger.info(`${msg.toUpperCase()}`); 40 | Logger.info(separator); 41 | } 42 | 43 | public static info(message: string): void { 44 | Logger.info(message); 45 | } 46 | 47 | public static error(error: string): void { 48 | Logger.error(error); 49 | } 50 | 51 | public static attachText(attach: ICreateAttachment, message: string): void { 52 | Logger.info(message); 53 | attach(message); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/support/playwright/API/SOAPResponse.ts: -------------------------------------------------------------------------------- 1 | import Log from "../../logger/Log"; 2 | import XMLParserUtil from "../../utils/XMLParserUtil"; 3 | 4 | export default class SOAPResponse { 5 | public constructor(private headers: any, private body: any, private status: number, private description: string) { } 6 | /** 7 | * Get content of tag in response body using xpath 8 | * @param xPathExpression xpath for the tag 9 | * @param description 10 | */ 11 | public async getTagContentByXpath(xPathExpression: string, description: string): Promise { 12 | Log.info(`Getting tag value of action ${description}`); 13 | return XMLParserUtil.getTagContentByXpath(this.body, xPathExpression); 14 | } 15 | 16 | /** 17 | * Get value of attribute in response body using xpath 18 | * @param xPathExpression xpath for the attribute 19 | * @param description 20 | */ 21 | public async getAttributeValueByXpath(xPathExpression: string, description: string): Promise { 22 | Log.info(`Getting attribute value of action ${description}`); 23 | return XMLParserUtil.getAttributeValueByXpath(this.body, xPathExpression); 24 | } 25 | 26 | /** 27 | * Get header value by header key 28 | * @param key 29 | * @returns 30 | */ 31 | public async getHeaderValueByKey(key: string): Promise { 32 | Log.info(`Getting header value of ${key}`); 33 | const jsonHeaders = await JSON.parse(JSON.stringify(this.headers)); 34 | return jsonHeaders[key]; 35 | } 36 | 37 | /** 38 | * Get response status code 39 | * @returns 40 | */ 41 | public async getStatusCode(): Promise { 42 | Log.info(`Getting status code of ${this.description}`); 43 | return this.status; 44 | } 45 | 46 | /** 47 | * Get response body 48 | * @returns 49 | */ 50 | public async getBody(): Promise { 51 | Log.info(`Getting response body of ${this.description}`); 52 | return this.body; 53 | } 54 | 55 | /** 56 | * Get response headers 57 | * @returns 58 | */ 59 | public async getHeaders(): Promise { 60 | Log.info(`Getting response Headers of ${this.description}`); 61 | return JSON.stringify(this.headers); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/web/pages/RegisterUserPage.ts: -------------------------------------------------------------------------------- 1 | import UIActions from "../../support/playwright/actions/UIActions"; 2 | import Assert from "../../support/playwright/asserts/Assert"; 3 | import StringUtil from "../../support/utils/StringUtil"; 4 | import Constants from "../constants/Constants"; 5 | 6 | export default class RegisterUserPage { 7 | constructor(private web: UIActions) { } 8 | 9 | private FIRST_NAME_TEXTBOX = "#input-firstname"; 10 | private LAST_NAME_TEXTBOX = "#input-lastname"; 11 | private EMAIL_TEXTBOX = "#input-email"; 12 | private TELEPHONE_TEXTBOX = "#input-telephone"; 13 | private PASSWORD_TEXTBOX = "#input-password"; 14 | private CONFIRM_PASSWORD_TEXTBOX = "#input-confirm"; 15 | private SUBSCRIBE_RADIO = "[for='input-newsletter-{0}']"; 16 | private PRIVACY_POLICY_CHECKBOX = "[for='input-agree']"; 17 | private PRIVACY_POLICY_LINK = "//a/b[text()='Privacy Policy']"; 18 | private CONTINUE_BUTTON = "[value='Continue']"; 19 | 20 | public async enterRegistrationDetails(firstName: string, lastName: string, email: string, telephone: string, password: string, confirmPassword: string, subscribe: string) { 21 | await this.web.editBox(this.FIRST_NAME_TEXTBOX, Constants.FIRST_NAME).fill(firstName); 22 | await this.web.editBox(this.LAST_NAME_TEXTBOX, Constants.LAST_NAME).fill(lastName); 23 | await this.web.editBox(this.EMAIL_TEXTBOX, Constants.EMAIL).fill(email); 24 | await this.web.editBox(this.TELEPHONE_TEXTBOX, Constants.TELEPHONE).fill(telephone); 25 | await this.web.editBox(this.PASSWORD_TEXTBOX, Constants.PASSWORD).fill(password); 26 | await this.web.editBox(this.CONFIRM_PASSWORD_TEXTBOX, Constants.CONFIRM_PASSWORD).fill(confirmPassword); 27 | await this.web.element(StringUtil.formatString(this.SUBSCRIBE_RADIO, subscribe.toLowerCase()), subscribe.toUpperCase()).click(); 28 | } 29 | 30 | public async agreePrivacyPolicy() { 31 | await Assert.assertTrue(await this.web.element(this.PRIVACY_POLICY_LINK, Constants.PRIVACY_POLICY).isVisible(1), 32 | Constants.PRIVACY_POLICY); 33 | await this.web.element(this.PRIVACY_POLICY_CHECKBOX, Constants.PRIVACY_POLICY).click(); 34 | } 35 | 36 | public async clickContinueButton() { 37 | await this.web.element(this.CONTINUE_BUTTON, Constants.CONTINUE).click(); 38 | } 39 | } -------------------------------------------------------------------------------- /src/support/playwright/actions/DropDownActions.ts: -------------------------------------------------------------------------------- 1 | import { Locator } from "@playwright/test"; 2 | import CommonConstants from "../../constants/CommonConstants"; 3 | import HTMLConstants from "../../constants/HTMLConstants"; 4 | import Log from "../../logger/Log"; 5 | 6 | export default class DropDownActions { 7 | private locator: Locator; 8 | private description: string; 9 | 10 | /** 11 | * Sets the locator with description 12 | * @param locator 13 | * @param description 14 | * @returns 15 | */ 16 | public setLocator(locator: Locator, description: string): DropDownActions { 17 | this.locator = locator; 18 | this.description = description; 19 | return this; 20 | } 21 | 22 | /** 23 | * Select the dropdown by value 24 | * @param value 25 | * @returns 26 | */ 27 | public async selectByValue(value: string) { 28 | Log.info(`Selecting value ${value} from ${this.description}`); 29 | await this.locator.selectOption({ value }); 30 | return this; 31 | } 32 | 33 | /** 34 | * Select the dropdown by Label 35 | * @param text 36 | * @returns 37 | */ 38 | public async selectByVisibleText(text: string) { 39 | Log.info(`Selecting text ${text} from ${this.description}`); 40 | await this.locator.selectOption({ label: text }); 41 | return this; 42 | } 43 | 44 | /** 45 | * Select the dropdown by index 46 | * @param index 47 | * @returns 48 | */ 49 | public async selectByIndex(index: number) { 50 | Log.info(`Selecting index ${index} of ${this.description}`); 51 | await this.locator.selectOption({ index }); 52 | return this; 53 | } 54 | 55 | /** 56 | * Gets all the options in dropdown 57 | * @param index 58 | * @returns 59 | */ 60 | public async getAllOptions(): Promise { 61 | Log.info(`Getting all the options of ${this.description}`); 62 | await this.locator.waitFor({state: "visible", timeout: CommonConstants.WAIT}); 63 | return await this.locator.locator(HTMLConstants.OPTION).allTextContents(); 64 | } 65 | 66 | /** 67 | * Gets all the selected options in dropdown 68 | * @param index 69 | * @returns 70 | */ 71 | public async getAllSelectedOptions(): Promise { 72 | Log.info(`Getting all the selected options of ${this.description}`); 73 | await this.locator.waitFor({ state: "visible", timeout: CommonConstants.WAIT }); 74 | return await this.locator.locator(HTMLConstants.SELECTED_OPTION).allTextContents(); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playwright-cucumber-sample-project", 3 | "version": "1.0.0", 4 | "description": "Sample project with Playwright and Cucumber", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "npx cucumber-js -p runner & npx ts-node ./src/support/reporter/HTMLReporter.ts & npx ts-node ./src/support/reporter/CucumberReporter.ts", 8 | "test:tags": "npx cucumber-js -p runner --tags", 9 | "failed:test": "npx cucumber-js -p rerun", 10 | "report": "npx ts-node ./src/support/reporter/HTMLReporter.ts & npx ts-node ./src/support/reporter/CucumberReporter.ts", 11 | "qa:test": "cross-env TEST_ENV=qa npx cucumber-js -p runner & cross-env TEST_ENV=qa npx ts-node ./src/support/reporter/HTMLReporter.ts & cross-env TEST_ENV=qa npx ts-node ./src/support/reporter/CucumberReporter.ts", 12 | "dry:test": "npx cucumber-js -p runner --dry-run" 13 | }, 14 | "keywords": [], 15 | "author": "Vinay Kumar B M", 16 | "license": "ISC", 17 | "devDependencies": { 18 | "@cucumber/cucumber": "8.7.0", 19 | "@cucumber/pretty-formatter": "1.0.0", 20 | "@playwright/test": "1.27.1", 21 | "@types/cucumber-html-reporter": "5.0.1", 22 | "@types/easy-soap-request": "4.1.1", 23 | "@types/express": "^4.17.13", 24 | "@types/fs-extra": "^9.0.13", 25 | "@types/ibm_db": "2.0.11", 26 | "@types/jsonpath": "^0.2.0", 27 | "@types/mssql": "^7.1.4", 28 | "@types/oracledb": "5.2.3", 29 | "@types/pdf-parse": "^1.1.1", 30 | "@types/randomstring": "^1.1.8", 31 | "@types/string-format": "^2.0.0", 32 | "@types/xmldom": "^0.1.31", 33 | "@typescript-eslint/eslint-plugin": "^5.16.0", 34 | "@typescript-eslint/parser": "^5.16.0", 35 | "dotenv": "16.0.0", 36 | "easy-soap-request": "^4.6.0", 37 | "eslint": "^8.12.0", 38 | "eslint-config-airbnb-typescript": "16.1.4", 39 | "eslint-plugin-playwright": "0.8.0", 40 | "fetch-to-curl": "0.5.2", 41 | "fs-extra": "10.1.0", 42 | "ibm_db": "2.8.1", 43 | "jsonpath": "1.1.1", 44 | "moment": "2.29.4", 45 | "mssql": "^7.2.1", 46 | "oracledb": "5.3.0", 47 | "pdf-parse": "1.1.1", 48 | "playwright": "1.27.1", 49 | "randomstring": "1.2.2", 50 | "string-format": "2.0.0", 51 | "ts-node": "10.9.1", 52 | "typescript": "4.8.4", 53 | "winston": "3.8.2", 54 | "xml-formatter": "2.6.1", 55 | "xmldom": "0.6.0", 56 | "xpath": "0.0.32", 57 | "multiple-cucumber-html-reporter": "3.0.1", 58 | "os": "0.1.2", 59 | "cross-env": "7.0.3" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/api/steps/RESTAuthor.ts: -------------------------------------------------------------------------------- 1 | import { Given, Then, When } from "@cucumber/cucumber"; 2 | import RequestHeader from "../../support/playwright/API/RequestHeader"; 3 | import RESTResponse from "../../support/playwright/API/RESTResponse"; 4 | import Assert from "../../support/playwright/asserts/Assert"; 5 | import StringUtil from "../../support/utils/StringUtil"; 6 | import Constants from "../constants/Constants"; 7 | 8 | function getHeader() { 9 | return new RequestHeader().set(Constants.CONTENT_TYPE, Constants.APPLICATION_JSON) 10 | .set(Constants.ACCEPT, Constants.APPLICATION_JSON) 11 | .set(Constants.AUTHORIZATION, `${Constants.BASIC} ${Buffer.from(`${Constants.USER}:${Constants.USER}`) 12 | .toString(Constants.BASE64)}`).get(); 13 | } 14 | 15 | Given('user has access to Library Information System', async function () { 16 | const endPoint = `${process.env.REST_API_BASE_URL}${Constants.SESSION_EP}`; 17 | const response: RESTResponse = await this.rest.get(this.attach, endPoint, getHeader(), Constants.SESSION); 18 | await Assert.assertEquals(await response.getStatusCode(), 200, Constants.STATUS_CODE); 19 | this.id = await response.getBody(); 20 | }); 21 | 22 | When('user makes a request to retrieves all the Authors in the System', async function () { 23 | const endPoint = `${process.env.REST_API_BASE_URL}${Constants.AUTHOR_EP}${this.id}`; 24 | this.response = await this.rest.get(this.attach, endPoint, getHeader(), Constants.AUTHORS); 25 | }); 26 | 27 | Then('user should get a status code {int}', async function (status: number) { 28 | const response: RESTResponse = this.response; 29 | await Assert.assertEquals(await response.getStatusCode(), status, Constants.STATUS_CODE); 30 | }); 31 | 32 | Then('user should get list of Authors', async function () { 33 | const response: RESTResponse = this.response; 34 | await Assert.assertNotNull(await response.getBody(), Constants.AUTHORS); 35 | }); 36 | 37 | When('user makes a request to retrieve an Author with id {int}', async function (id: number) { 38 | const endPoint = `${process.env.REST_API_BASE_URL}${StringUtil.formatString(Constants.SINGLE_AUTHOR_EP, id.toString(), this.id)}`; 39 | this.response = await this.rest.get(this.attach, endPoint, getHeader(), Constants.SINGLE_AUTHOR); 40 | }); 41 | 42 | Then('user should get the author with id {int}', async function (id: number) { 43 | const response: RESTResponse = this.response; 44 | await Assert.assertEquals(await response.getTagContentByJsonPath(Constants.ID_JSON_PATH, Constants.SINGLE_AUTHOR), id, Constants.SINGLE_AUTHOR); 45 | }); -------------------------------------------------------------------------------- /src/support/config/hooks.ts: -------------------------------------------------------------------------------- 1 | import { Before, BeforeAll, AfterAll, After, setDefaultTimeout, ITestCaseHookParameter, Status, formatterHelpers } from "@cucumber/cucumber"; 2 | import { Browser } from "@playwright/test"; 3 | import WebBrowser from "../manager/Browser"; 4 | import fse from "fs-extra"; 5 | import UIActions from "../playwright/actions/UIActions"; 6 | import Log from "../logger/Log"; 7 | import RESTRequest from "../playwright/API/RESTRequest"; 8 | import SOAPRequest from "../playwright/API/SOAPRequest"; 9 | 10 | const timeInMin: number = 60 * 1000; 11 | setDefaultTimeout(Number.parseInt(process.env.TEST_TIMEOUT, 10) * timeInMin); 12 | let browser: Browser; 13 | 14 | // launch the browser 15 | BeforeAll(async function () { 16 | browser = await WebBrowser.launch(); 17 | }); 18 | 19 | // close the browser 20 | AfterAll(async function () { 21 | await browser.close(); 22 | }); 23 | 24 | // Create a new browser context and page per scenario 25 | Before(async function ({ pickle, gherkinDocument }: ITestCaseHookParameter) { 26 | const { line } = formatterHelpers.PickleParser.getPickleLocation({ gherkinDocument, pickle }) 27 | Log.testBegin(`${pickle.name}: ${line}`); 28 | this.context = await browser.newContext({ 29 | viewport: null, 30 | ignoreHTTPSErrors: true, 31 | acceptDownloads: true, 32 | recordVideo: process.env.RECORD_VIDEO === "true" ? { dir: './test-results/videos' } : undefined, 33 | }); 34 | this.page = await this.context?.newPage(); 35 | this.web = new UIActions(this.page); 36 | this.rest = new RESTRequest(this.page); 37 | this.soap = new SOAPRequest(); 38 | }); 39 | 40 | // Cleanup after each scenario 41 | After(async function ({ result, pickle, gherkinDocument }: ITestCaseHookParameter) { 42 | const { line } = formatterHelpers.PickleParser.getPickleLocation({ gherkinDocument, pickle }) 43 | const status = result.status; 44 | const scenario = pickle.name; 45 | const videoPath = await this.page?.video()?.path(); 46 | if (status === Status.FAILED) { 47 | const image = await this.page?.screenshot({ path: `./test-results/screenshots/${scenario} (${line}).png`, fullPage: true }); 48 | await this.attach(image, 'image/png'); 49 | Log.error(`${scenario}: ${line} - ${status}\n${result.message}`); 50 | } 51 | await this.page?.close(); 52 | await this.context?.close(); 53 | if (process.env.RECORD_VIDEO === "true") { 54 | if (status === Status.FAILED) { 55 | fse.renameSync(videoPath, `./test-results/videos/${scenario}(${line}).webm`); 56 | await this.attach(fse.readFileSync(`./test-results/videos/${scenario}(${line}).webm`), 'video/webm'); 57 | } else { 58 | fse.unlinkSync(videoPath); 59 | } 60 | } 61 | Log.testEnd(`${scenario}: ${line}`, status); 62 | }); -------------------------------------------------------------------------------- /src/api/constants/Constants.ts: -------------------------------------------------------------------------------- 1 | export default class Constants{ 2 | // REST Endpoints 3 | static readonly SESSION_EP = "/session"; 4 | static readonly AUTHOR_EP = "/author?session_id="; 5 | static readonly SINGLE_AUTHOR_EP = "/author/{0}?session_id={1}"; 6 | static readonly BOOK_EP = "/book?session_id="; 7 | static readonly SINGLE_BOOK_EP = "/book/{0}?session_id={1}"; 8 | static readonly SEARCH_BOOK_EP = "/book/search?session_id="; 9 | 10 | // REST JSON path 11 | static readonly ID_JSON_PATH = "$.Id"; 12 | static readonly AUTHOR_ID_JSON_PATH = "$.AuthorId"; 13 | static readonly DATE_ADDED_ISO_JSON_PATH = "$.DateAddedIso"; 14 | static readonly GENRE_ID_JSON_PATH = "$.GenreId"; 15 | static readonly OUT_OF_PRINT_JSON_PATH = "$.IsOutOfPrint"; 16 | static readonly NAME_JSON_PATH = "$.Name"; 17 | static readonly FIRST_ID_JSON_PATH = "$[0].Id"; 18 | static readonly FIRST_AUTHOR_ID_JSON_PATH = "$[0].AuthorId"; 19 | static readonly FIRST_DATE_ADDED_ISO_JSON_PATH = "$[0].DateAddedIso"; 20 | static readonly FIRST_GENRE_ID_JSON_PATH = "$[0].GenreId"; 21 | static readonly FIRST_OUT_OF_PRINT_JSON_PATH = "$[0].IsOutOfPrint"; 22 | static readonly FIRST_NAME_JSON_PATH = "$[0].Name"; 23 | 24 | // SOAP Actions 25 | static readonly ADD_SOAP_ACTION = "http://tempuri.org/Add"; 26 | static readonly SUBTRACT_SOAP_ACTION = "http://tempuri.org/Subtract"; 27 | static readonly MULTIPLY_SOAP_ACTION = "http://tempuri.org/Multiply"; 28 | static readonly DIVIDE_SOAP_ACTION = "http://tempuri.org/Divide"; 29 | 30 | // SOAP Xpath 31 | static readonly ADD_RESULT_XPATH = "//*[local-name()='AddResult']/text()"; 32 | static readonly SUBTRACT_RESULT_XPATH = "//*[local-name()='SubtractResult']/text()"; 33 | static readonly MULTIPLY_RESULT_XPATH = "//*[local-name()='MultiplyResult']/text()"; 34 | static readonly DIVIDE_RESULT_XPATH = "//*[local-name()='DivideResult']/text()"; 35 | 36 | // Constants 37 | static readonly USER = "librarian"; 38 | static readonly CONTENT_TYPE = "content-type"; 39 | static readonly APPLICATION_JSON = "application/json"; 40 | static readonly ACCEPT = "accept"; 41 | static readonly AUTHORIZATION = "authorization"; 42 | static readonly BASIC = "Basic"; 43 | static readonly BASE64 = "base64"; 44 | static readonly STATUS_CODE = "Status Code"; 45 | static readonly SESSION = "Session"; 46 | static readonly AUTHORS = "Authors" 47 | static readonly SINGLE_AUTHOR = "Single Author"; 48 | static readonly BOOKS = "Books" 49 | static readonly SINGLE_BOOK = "Single Book"; 50 | static readonly TEXT_XML = "text/xml;charset=UTF-8" 51 | static readonly SOAP_ACTION = "SOAPAction"; 52 | static readonly XML_FORMAT = ".xml"; 53 | static readonly ADD = "add"; 54 | static readonly SUBTRACT = "subtract"; 55 | static readonly MULTIPLY = "multiply"; 56 | static readonly DIVIDE = "divide"; 57 | static readonly BOOK_JSON = "book.json"; 58 | static readonly SEARCH_BOOK = "Search Book"; 59 | } -------------------------------------------------------------------------------- /src/support/utils/DBUtil.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-shadow */ 2 | /* eslint-disable global-require */ 3 | /* eslint-disable @typescript-eslint/no-var-requires */ 4 | import * as sql from "mssql"; 5 | import oracledb from "oracledb"; 6 | import CommonConstants from "../constants/CommonConstants"; 7 | import DBConstants from "../constants/DBConstants"; 8 | 9 | export default class DBUtil { 10 | /** 11 | * Executes the query on MSSQL database 12 | * @param dbConfig data base configuration 13 | * @param query to be executed 14 | * @returns record set 15 | */ 16 | public static async executeMSSQLQuery(dbConfig: string, query: string) { 17 | try { 18 | const pool = await sql.connect(`${dbConfig}${DBConstants.CERTIFICATE}`); 19 | const result = await pool.request().query(query); 20 | return { rows: result.recordset, rowsAffected: result.rowsAffected }; 21 | } catch (err) { 22 | throw new Error(`Error while executing query\n${err.message}`); 23 | } 24 | } 25 | 26 | /** 27 | * Executes the query on Oracle database 28 | * @param dbConfig data base configuration 29 | * @param query to be executed 30 | * @returns record set 31 | */ 32 | public static async executeOracleQuery(dbConfig: string, query: string) { 33 | const configs = dbConfig.split(CommonConstants.SEMICOLON); 34 | const config = { 35 | user: configs[0].replace(DBConstants.USER, CommonConstants.BLANK).trim(), 36 | password: configs[1].replace(DBConstants.PASSWORD, CommonConstants.BLANK).trim(), 37 | connectString: configs[2].replace(DBConstants.CONNECTION_STRING, CommonConstants.BLANK).trim(), 38 | }; 39 | let connection: oracledb.Connection; 40 | try { 41 | connection = await oracledb.getConnection(config); 42 | const result = await connection.execute(query); 43 | return { rows: result.rows, rowsAffected: result.rowsAffected }; 44 | } catch (err) { 45 | throw new Error(`Error while executing query\n${err.message}`); 46 | } finally { 47 | if (connection) { 48 | try { 49 | await connection.close(); 50 | } catch (err) { 51 | console.error(err); 52 | } 53 | } 54 | } 55 | } 56 | 57 | /** 58 | * Executes the query on DB2 database 59 | * @param dbConfig data base configuration 60 | * @param query to be executed 61 | * @returns record set 62 | */ 63 | public static async executeDB2Query(dbConfig: string, query: string) { 64 | const ibmdb = require('ibm_db'); 65 | let connection: any; 66 | try { 67 | connection = ibmdb.openSync(`${dbConfig}${DBConstants.PROTOCOL}`); 68 | const result = connection.querySync(query); 69 | return { rows: result, rowsAffected: result.length }; 70 | } catch (error) { 71 | throw new Error(`Error while executing query\n${error.message}`); 72 | } finally { 73 | if (connection) { 74 | try { 75 | connection.closeSync(); 76 | } catch (err) { 77 | console.error(err); 78 | } 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/api/steps/SOAPCalculator.ts: -------------------------------------------------------------------------------- 1 | import { Then, When } from "@cucumber/cucumber"; 2 | import { ICreateAttachment } from "@cucumber/cucumber/lib/runtime/attachment_manager"; 3 | import RequestHeader from "../../support/playwright/API/RequestHeader"; 4 | import SOAPRequest from "../../support/playwright/API/SOAPRequest"; 5 | import SOAPResponse from "../../support/playwright/API/SOAPResponse"; 6 | import Assert from "../../support/playwright/asserts/Assert"; 7 | import Constants from "../constants/Constants"; 8 | 9 | async function makePostRequest(attach: ICreateAttachment, soap: SOAPRequest, soapAction: string, operation: string, 10 | number1: number, number2: number) { 11 | const header = new RequestHeader().set(Constants.CONTENT_TYPE, Constants.TEXT_XML) 12 | .set(Constants.SOAP_ACTION, soapAction).get(); 13 | const requestData = { 14 | number1: number1, 15 | number2: number2, 16 | }; 17 | return await soap.post(attach, process.env.SOAP_API_BASE_URL, header, `${operation}${Constants.XML_FORMAT}`, 18 | requestData, operation); 19 | } 20 | 21 | async function validateResult(response: SOAPResponse, xpath: string, operation: string, result: string) { 22 | await Assert.assertEquals((await response.getTagContentByXpath(xpath, operation)), result, operation); 23 | } 24 | 25 | When('user adds two numbers {int} and {int} in the calculator', async function (number1: number, number2: number) { 26 | this.response = await makePostRequest(this.attach, this.soap, Constants.ADD_SOAP_ACTION, Constants.ADD, number1, number2); 27 | }); 28 | 29 | Then('user should get the result of addition as {string}', async function (result: string) { 30 | await validateResult(this.response, Constants.ADD_RESULT_XPATH, Constants.ADD, result); 31 | }); 32 | 33 | When('user subtracts two numbers {int} and {int} in the calculator', async function (number1: number, number2: number) { 34 | this.response = await makePostRequest(this.attach, this.soap, Constants.SUBTRACT_SOAP_ACTION, Constants.SUBTRACT, number1, number2); 35 | }); 36 | 37 | When('user multiplies two numbers {int} and {int} in the calculator', async function (number1: number, number2: number) { 38 | this.response = await makePostRequest(this.attach, this.soap, Constants.MULTIPLY_SOAP_ACTION, Constants.MULTIPLY, number1, number2); 39 | }); 40 | 41 | When('user divides two numbers {int} and {int} in the calculator', async function (number1: number, number2: number) { 42 | this.response = await makePostRequest(this.attach, this.soap, Constants.DIVIDE_SOAP_ACTION, Constants.DIVIDE, number1, number2); 43 | }); 44 | 45 | Then('user should get the result of subtraction as {string}', async function (result: string) { 46 | await validateResult(this.response, Constants.SUBTRACT_RESULT_XPATH, Constants.SUBTRACT, result); 47 | }); 48 | 49 | Then('user should get the result of multiplication as {string}', async function (result: string) { 50 | await validateResult(this.response, Constants.MULTIPLY_RESULT_XPATH, Constants.MULTIPLY, result); 51 | }); 52 | 53 | Then('user should get the result of division as {string}', async function (result: string) { 54 | await validateResult(this.response, Constants.DIVIDE_RESULT_XPATH, Constants.DIVIDE, result); 55 | }); -------------------------------------------------------------------------------- /src/support/utils/StringUtil.ts: -------------------------------------------------------------------------------- 1 | import randomString from "randomstring"; 2 | import format from "string-format"; 3 | 4 | export default class StringUtil { 5 | /** 6 | * This method will return the formatted String by replacing value in {\d} 7 | * @param str : String to be formatted 8 | * @param replaceValue : value to replaced in formatted string 9 | * @returns str 10 | */ 11 | public static formatString(str: string, ...replaceValue: string[]): string { 12 | for (let i = 0; i < replaceValue.length; i++) { 13 | // eslint-disable-next-line no-param-reassign 14 | str = str.split(`{${i}}`).join(replaceValue[i]); 15 | } 16 | return str; 17 | } 18 | 19 | /** 20 | * This method will return the formatted String by replacing value in {key} 21 | * @param str : String to be formatted 22 | * @param replaceValue : value to replaced in formatted string 23 | * @returns str 24 | */ 25 | public static formatStringValue(str: string, replaceValue: any): string { 26 | // eslint-disable-next-line no-restricted-syntax 27 | for (const [key, value] of Object.entries(replaceValue)) { 28 | // eslint-disable-next-line no-param-reassign 29 | str = str.split(`{${key}}`).join(`${value}`); 30 | } 31 | return str; 32 | } 33 | 34 | /** 35 | * Replaces text in a string, using an string that supports replacement within a string. 36 | * @param str Original string 37 | * @param searchValue searches for and replace matches within the string. 38 | * @param replaceValue A string containing the text to replace for every successful match of searchValue in this string. 39 | * @returns 40 | */ 41 | public static replaceAll(str: string, searchValue: string, replaceValue: string): string { 42 | const replacer = new RegExp(searchValue, 'g'); 43 | const replacedStr = str.replace(replacer, replaceValue); 44 | return replacedStr; 45 | } 46 | 47 | /** 48 | * replaces the regex with string value 49 | * @param str 50 | * @param regex 51 | * @param value 52 | * @returns 53 | */ 54 | public static getRegXLocator(str: string, regex: RegExp, value: string) { 55 | return str.replace(regex, value); 56 | } 57 | 58 | /** 59 | * Generates random alphanumeric string of given length 60 | * @param length 61 | * @returns 62 | */ 63 | public static randomAlphanumericString(length: number): string { 64 | const str = randomString.generate(length); 65 | return str; 66 | } 67 | 68 | /** 69 | * Generates random string of given length 70 | * @param length 71 | * @returns 72 | */ 73 | public static randomAlphabeticString(length: number): string { 74 | const str = randomString.generate({ length: length, charset: 'alphabetic' }); 75 | return str; 76 | } 77 | 78 | /** 79 | * Generates random string of given length with all letters a as uppercase 80 | * @param length 81 | * @returns 82 | */ 83 | public static randomUppercaseString(length: number): string { 84 | const str = randomString.generate({ length: length, charset: 'alphabetic', capitalization: "uppercase" }); 85 | return str; 86 | } 87 | 88 | /** 89 | * Generates random string of given length with all letters a as lowercase 90 | * @param length 91 | * @returns 92 | */ 93 | public static randomLowercaseString(length: number): string { 94 | const str = randomString.generate({ length: length, charset: 'alphabetic', capitalization: "lowercase" }); 95 | return str; 96 | } 97 | 98 | /** 99 | * Generates random number string of given length 100 | * @param length 101 | * @returns 102 | */ 103 | public static randomNumberString(length: number): string { 104 | const str = randomString.generate({ length: length, charset: 'numeric' }); 105 | return str; 106 | } 107 | 108 | /** 109 | * This method will return the formatted String by replacing value in {key} from Object 110 | * @param str 111 | * @param obj 112 | * @returns 113 | */ 114 | public static formatStringFromObject(str: string, obj: any): string { 115 | return format(str, obj); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /features/REST/book.feature: -------------------------------------------------------------------------------- 1 | @rest @book 2 | Feature: Scenarios related to Book REST API in Library Information System 3 | 4 | Background: 5 | Given user has access to Library Information System 6 | 7 | @regression @sanity @books 8 | Scenario: Retrieve list of Books in the Library Information System 9 | When user makes a request to retrieves all the Books in the System 10 | Then user should get a status code 200 11 | And user should get list of Books 12 | 13 | @regression @sanity @singleBook 14 | Scenario: Retrieve a single book in the Library Information System 15 | When user makes a request to retrieve an Book with id 1 16 | Then user should get a status code 200 17 | And user should get the Book with id 1 18 | 19 | @regression @sanity @addBook 20 | Scenario Outline: Add a new book into Library Information System 21 | When user adds a book with details "", "", , "", , "", , "", "" 22 | Then user should get a status code 200 23 | And user should be able to added Book "", "", , , "" 24 | Examples: 25 | | bookName | available | genreId | genreName | authorID | authorName | age | dateAdded | dateAddedIso | 26 | | To Kill a Mockingbird | true | 21 | Southern Gothic | 31 | Harper Lee | 45 | 1960-07-11 | 2015-01-01 | 27 | | Alice in Wonderland | false | 51 | Fantasy Fiction | 19 | Lewis Carroll | 68 | 1865-11-01 | 1990-10-01 | 28 | 29 | @regression @sanity @deleteBook 30 | Scenario Outline: Delete the book in the Library Information System 31 | When user adds a book with details "", "", , "", , "", , "", "" 32 | Then user should get a status code 200 33 | Then user deletes the book that was added 34 | Then user should get a status code 200 35 | Examples: 36 | | bookName | available | genreId | genreName | authorID | authorName | age | dateAdded | dateAddedIso | 37 | | To Kill a Mockingbird | true | 21 | Southern Gothic | 31 | Harper Lee | 45 | 1960-07-11 | 2015-01-01 | 38 | 39 | @regression @sanity @updateBook 40 | Scenario Outline: Update the book in the Library Information System 41 | When user adds a book with details "", "", , "", , "", , "", "" 42 | Then user should get a status code 200 43 | Then user updates the book that was added "", , 44 | Then user should get a status code 200 45 | Then user should see that book details "", "", , , "" are updated 46 | Examples: 47 | | bookName | available | genreId | genreName | authorID | authorName | age | dateAdded | dateAddedIso | updateAvailable | updateGenreId | updateAuthorID | 48 | | To Kill a Mockingbird | true | 21 | Southern Gothic | 31 | Harper Lee | 45 | 1960-07-11 | 2015-01-01 | false | 50 | 70 | 49 | 50 | @regression @sanity @searchBook 51 | Scenario Outline: Search for books in Library Information System by providing date range 52 | When user adds a book with details "", "", , "", , "", , "", "" 53 | Then user should get a status code 200 54 | Then user searches for books within date range "" to "" 55 | Then user should get a status code 200 56 | And user should see book in search result with details "", "", , , "" 57 | Examples: 58 | | bookName | available | genreId | genreName | authorID | authorName | age | dateAdded | dateAddedIso | startDateIso | endDateIso | 59 | | Alice in Wonderland | false | 51 | Fantasy Fiction | 19 | Lewis Carroll | 68 | 1865-11-01 | 1990-10-01 | 1990-10-01 | 1990-10-31 | 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Test Execution](https://github.com/VinayKumarBM/playwright-cucumber-sample/actions/workflows/main.yml/badge.svg)](https://github.com/VinayKumarBM/playwright-cucumber-sample/actions/workflows/main.yml) 2 |
3 | # playwright-cucumber-sample 4 | 5 | ## **Overview:** 6 | 7 | This is a sample test automation framework developed using **Playwright** with **Cucumber**. 8 | 9 | **Playwright** is a framework for Web Testing and Automation. It allows testing Chromium, Firefox and WebKit with a single API. Playwright is built to enable cross-browser web automation that is ever-green, capable, reliable and fast. 10 | 11 | **Cucumber** is a tool for running automated tests written in plain language. Because they're written in plain language, they can be read by anyone on your team. Because they can be read by anyone, you can use them to help improve communication, collaboration and trust on your team. Cucumber supports behavior-driven development. Central to the Cucumber BDD approach is its ordinary language parser called Gherkin. 12 | 13 | For Demo purpose web UI test cases are created on [ecommerce-playground.lambdatest.io](https://ecommerce-playground.lambdatest.io/index.php) site and API test cases are created on these [SOAP Calculator API](http://www.dneonline.com/calculator.asmx) & [REST Library Information System API](https://www.libraryinformationsystem.org/Services/RestService.svc) endpoints. 14 | 15 | ## Features 16 | 17 | - This testing framework supports Behavior Driven Development (BDD). Tests are written in plain English text called Gherkin 18 | - Framework has built in library to operate on UI, API (both SOAP & REST API) and DB (MSSQL, DB2 & Oracle). 19 | - Supports execution of tests in different browsers. 20 | - Supports running scenarios in parallel mode. It runs 2 scenarios in parallel by default. 21 | - Flaky scenario can be Retried multiple times until either it passes or the maximum number of attempts is reached. You can enable this via the retry configuration option. 22 | - Supports rerun of the failed scenarios. 23 | - Scenarios can be easily skipped by adding @ignore tag to scenarios 24 | - Supports dry run of scenarios this helps to identifies the undefined and ambiguous steps. 25 | - Has utility built in for file download, Read PDF files etc. 26 | - Generates Cucumber HTML Report & HTML Report. 27 | - HTML reports are included with snapshots and video in case of failed scenarios. 28 | - Test execution logs are captured in the log file. 29 | - All the configuration are controlled by .env file and environment variables can be modified at runtime. 30 | - Easy and simple integration to CI/CD tools like Jenkins. 31 | 32 | ## Supported Browsers 33 | 34 | 1. Chrome - default browser 35 | 2. Firefox 36 | 3. MS Edge 37 | 4. WebKit - web browser engine used by Safari 38 | 39 | 40 | #### Steps to use 41 | ##### 1. Installation 42 | 43 | Playwright framework requires [Node.js](https://nodejs.org/) v14+ to run. 44 | 45 | Code from github need to be [download](https://github.com/VinayKumarBM/playwright-cucumber-sample/archive/refs/heads/master.zip) OR [cloned](https://github.com/VinayKumarBM/playwright-cucumber-sample.git) using git command. 46 | 47 | Installing the dependencies. 48 | ```sh 49 | npm ci 50 | ``` 51 | ##### 2. Test creation 52 | - Test scenarios are organized into features and these feature files should be placed inside features folder. 53 | - Step definitions connect Gherkin steps in feature files to programming code. A step definition carries out the action that should be performed by the scenario steps. These step definitions should placed inside steps folder in different packages. 54 | - For web UI based tests maintain all the selectors inside pages folder. 55 | 56 | ##### 3. Execution 57 | To run test scenarios use below command. 58 | ```sh 59 | npm run test 60 | ``` 61 | To run specific scenario, use tags command. Below are few examples. 62 | ```sh 63 | npm run test:tags @sanity 64 | npm run test:tags "@calculator or @author" 65 | npm run test:tags "@rest and @author" 66 | ``` 67 | To dry run test scenarios use below command. 68 | ```sh 69 | npm run dry:test 70 | ``` 71 | To rerun the failed test scenarios use below command. 72 | ```sh 73 | npm run failed:test 74 | ``` 75 | To change any environment configuration in .env file at run time use set command. 76 | Eg: To change browser to Firefox use below command 77 | ```sh 78 | set BROWSER=firefox 79 | ``` 80 | Similar command can be used to update other environment configuration 81 | 82 | To generate HTML and Cucumber report use below command 83 | ```sh 84 | npm run report 85 | ``` 86 | ##### 4. Report & Logs 87 | Cucumber HTML report will be present inside 88 | ```sh 89 | test-results/reports/cucumber.html 90 | ``` 91 | HTML report will be present inside 92 | ```sh 93 | test-results/reports/html/index.html 94 | ``` 95 | Execution log will be present in the log file. 96 | ```sh 97 | test-results/logs/execution.log 98 | ``` 99 | ## Before you Go 100 | **:pencil: If you find my work interesting don't forget to give a Star & Follow me :busts_in_silhouette:** 101 | -------------------------------------------------------------------------------- /src/support/playwright/API/RESTRequest.ts: -------------------------------------------------------------------------------- 1 | import { Page, APIResponse } from '@playwright/test'; 2 | import fs from 'fs'; 3 | import fetchToCurl from 'fetch-to-curl'; 4 | import CommonConstants from '../../constants/CommonConstants'; 5 | import StringUtil from '../../utils/StringUtil'; 6 | import RESTResponse from "./RESTResponse"; 7 | import Log from '../../logger/Log'; 8 | import { ICreateAttachment } from '@cucumber/cucumber/lib/runtime/attachment_manager'; 9 | 10 | export default class RESTRequest { 11 | constructor(private page: Page) { } 12 | /** 13 | * Creates request body from JSON file by replacing the input parameters 14 | * @param jsonFileName 15 | * @param data 16 | * @returns 17 | */ 18 | public async createRequestBody(jsonFileName: string, data: any): Promise { 19 | let json = fs.readFileSync(CommonConstants.REST_JSON_REQUEST_PATH + jsonFileName, 'utf-8'); 20 | json = StringUtil.formatStringValue(json, data); 21 | return json; 22 | } 23 | /** 24 | * Make POST request and return response 25 | * @param endPoint 26 | * @param requestHeader 27 | * @param jsonAsString 28 | * @param description 29 | * @returns 30 | */ 31 | public async post(attach: ICreateAttachment, endPoint: string, requestHeader: any, jsonAsString: string, 32 | description: string): Promise { 33 | const headersAsJson = JSON.parse(JSON.stringify(requestHeader)); 34 | Log.info(`Making POST request for ${description}`); 35 | this.printRequest(attach, endPoint, headersAsJson, jsonAsString, 'post'); 36 | const response = await this.page.request.post(endPoint, 37 | { headers: headersAsJson, data: JSON.parse(jsonAsString) }); 38 | return await this.setRestResponse(attach, response, description); 39 | } 40 | /** 41 | * Sets the API Response into RestResponse object 42 | * @param response 43 | * @param description 44 | * @returns RestResponse object 45 | */ 46 | private async setRestResponse(attach: ICreateAttachment, response: APIResponse, description: string): Promise { 47 | const body = await response.text(); 48 | const headers = response.headers(); 49 | const statusCode = response.status(); 50 | const restResponse: RESTResponse = new RESTResponse(headers, body, statusCode, description); 51 | const responseBody = body === CommonConstants.BLANK ? CommonConstants.BLANK : JSON.stringify(JSON.parse(body), undefined, 2); 52 | Log.attachText(attach, `Response body: ${responseBody}`); 53 | return restResponse; 54 | } 55 | /** 56 | * Make Get request and return response 57 | * @param endPoint 58 | * @param requestHeader 59 | * @param description 60 | * @returns 61 | */ 62 | public async get(attach: ICreateAttachment, endPoint: string, requestHeader: any, description: string): Promise { 63 | const headersAsJson = JSON.parse(JSON.stringify(requestHeader)); 64 | Log.info(`Making GET request for ${description}`); 65 | this.printRequest(attach ,endPoint, headersAsJson, null, 'get'); 66 | const response = await this.page.request.get(endPoint, { headers: headersAsJson }); 67 | return await this.setRestResponse(attach, response, description); 68 | } 69 | /** 70 | * Make Put request and return response 71 | * @param endPoint 72 | * @param requestHeader 73 | * @param jsonAsString 74 | * @param description 75 | * @returns 76 | */ 77 | public async put(attach: ICreateAttachment, endPoint: string, requestHeader: any, jsonAsString: any, 78 | description: string): Promise { 79 | const headersAsJson = JSON.parse(JSON.stringify(requestHeader)); 80 | Log.info(`Making PUT request for ${description}`); 81 | this.printRequest(attach, endPoint, headersAsJson, jsonAsString, 'put'); 82 | const response = await this.page.request.put(endPoint, 83 | { headers: headersAsJson, data: JSON.parse(jsonAsString) }); 84 | return await this.setRestResponse(attach, response, description); 85 | } 86 | /** 87 | * Make Patch request and return response 88 | * @param endPoint 89 | * @param requestHeader 90 | * @param jsonAsString 91 | * @param description 92 | * @returns 93 | */ 94 | public async patch(attach: ICreateAttachment, endPoint: string, requestHeader: any, jsonAsString: any, 95 | description: string): Promise { 96 | const headersAsJson = JSON.parse(JSON.stringify(requestHeader)); 97 | Log.info(`Making PATCH request for ${description}`); 98 | this.printRequest(attach, endPoint, headersAsJson, jsonAsString, 'patch'); 99 | const response = await this.page.request.patch(endPoint, 100 | { headers: headersAsJson, data: JSON.parse(jsonAsString) }); 101 | return await this.setRestResponse(attach, response, description); 102 | } 103 | /** 104 | * Make Delete request and return response 105 | * @param endPoint 106 | * @param requestHeader 107 | * @param description 108 | * @returns 109 | */ 110 | public async delete(attach: ICreateAttachment, endPoint: string, requestHeader: any, description: string): Promise { 111 | const headersAsJson = JSON.parse(JSON.stringify(requestHeader)); 112 | Log.info(`Making DELETE request for ${description}`); 113 | this.printRequest(attach, endPoint, headersAsJson, null, 'delete'); 114 | const response = await this.page.request.delete(endPoint, { headers: headersAsJson }); 115 | return await this.setRestResponse(attach, response, description); 116 | } 117 | /** 118 | * Prints the API request on console in curl format 119 | * @param endPoint 120 | * @param requestHeader 121 | * @param jsonRequestBody 122 | * @param method 123 | */ 124 | private printRequest(attach: ICreateAttachment, endPoint: string, requestHeader: any, jsonRequestBody: string, method: string) { 125 | let requestBody = jsonRequestBody; 126 | if (jsonRequestBody !== null) { 127 | requestBody = JSON.stringify(JSON.parse(jsonRequestBody), undefined, 2); 128 | } 129 | Log.attachText(attach, `Request: ${fetchToCurl({ 130 | url: endPoint, 131 | headers: requestHeader, 132 | body: requestBody, 133 | method: method, 134 | })}`); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/support/playwright/actions/UIElementActions.ts: -------------------------------------------------------------------------------- 1 | import { Locator, Page } from "@playwright/test"; 2 | import CommonConstants from "../../constants/CommonConstants"; 3 | import Log from "../../logger/Log"; 4 | 5 | export default class UIElementActions { 6 | protected locator: Locator; 7 | protected description: string; 8 | protected selector: string; 9 | 10 | constructor(private page: Page) { } 11 | 12 | /** 13 | * Returns the first locator 14 | * @returns 15 | */ 16 | public getLocator(): Locator { 17 | return this.locator.first(); 18 | } 19 | 20 | /** 21 | * Returns the all the locators 22 | * @returns 23 | */ 24 | public getLocators(): Locator { 25 | return this.locator; 26 | } 27 | 28 | /** 29 | * Sets the locator using the selector * 30 | * @param selector 31 | * @param description 32 | * @returns 33 | */ 34 | public setElement(selector: string, description: string): UIElementActions { 35 | this.selector = selector; 36 | this.locator = this.page.locator(this.selector); 37 | this.description = description; 38 | return this; 39 | } 40 | 41 | /** 42 | * Sets the locator with description 43 | * @param locator 44 | * @param description 45 | * @returns 46 | */ 47 | public setLocator(locator: Locator, description: string): UIElementActions { 48 | this.locator = locator; 49 | this.description = description; 50 | return this; 51 | } 52 | 53 | /** 54 | * Click on element 55 | * @returns 56 | */ 57 | public async click() { 58 | Log.info(`Clicking on ${this.description}`); 59 | await this.getLocator().click(); 60 | return this; 61 | } 62 | 63 | /** 64 | * Double click on element 65 | * @returns 66 | */ 67 | public async doubleClick() { 68 | Log.info(`Double Clicking ${this.description}`); 69 | await this.getLocator().dblclick(); 70 | return this; 71 | } 72 | 73 | /** 74 | * scroll element into view, unless it is completely visible 75 | * @returns 76 | */ 77 | public async scrollIntoView() { 78 | Log.info(`Scroll to element ${this.description}`); 79 | await this.getLocator().scrollIntoViewIfNeeded(); 80 | return this; 81 | } 82 | 83 | /** 84 | * Wait for element to be invisible 85 | * @returns 86 | */ 87 | public async waitTillInvisible() { 88 | Log.info(`Waiting for ${this.description} to be invisible`); 89 | await this.getLocator().waitFor({ state: "hidden", timeout: CommonConstants.WAIT }); 90 | return this; 91 | } 92 | 93 | /** 94 | * wait for element not to be present in DOM 95 | * @returns 96 | */ 97 | public async waitTillDetached() { 98 | Log.info(`Wait for ${this.description} to be detached from DOM`); 99 | await this.getLocator().waitFor({ state: "detached", timeout: CommonConstants.WAIT }); 100 | return this; 101 | } 102 | 103 | /** 104 | * wait for element to be visible 105 | * @returns 106 | */ 107 | public async waitTillVisible() { 108 | Log.info(`Wait for ${this.description} to be visible in DOM`); 109 | await this.getLocator().waitFor({ state: "visible", timeout: CommonConstants.WAIT }); 110 | return this; 111 | } 112 | 113 | /** 114 | * wait for element to be attached to DOM 115 | * @returns 116 | */ 117 | public async waitForPresent() { 118 | Log.info(`Wait for ${this.description} to attach to DOM`); 119 | await this.getLocator().waitFor({ state: "attached", timeout: CommonConstants.WAIT }); 120 | return this; 121 | } 122 | 123 | /** 124 | * This method hovers over the element 125 | */ 126 | public async hover() { 127 | Log.info(`Hovering on ${this.description}`); 128 | await this.getLocator().hover(); 129 | return this; 130 | } 131 | 132 | /** 133 | * Returns input.value for or