├── .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 | [](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