├── src ├── utils │ ├── utils.ts │ ├── constants.ts │ ├── dbUtils.ts │ ├── config │ │ ├── env │ │ │ ├── dev.env │ │ │ ├── qa.env │ │ │ └── local.env │ │ ├── artillery │ │ │ └── artillery-script.yml │ │ └── azure │ │ │ ├── azure-pipeline.yml │ │ │ ├── playwright.service.config.ts │ │ │ └── playwright.config.ts │ ├── error │ │ ├── ApiErrorHandler.ts │ │ └── ErrorManager.ts │ ├── dataStore.js │ ├── report │ │ ├── sendEmail.ts │ │ ├── ReportAction.ts │ │ ├── CustomReporterConfig.ts │ │ └── Logger.ts │ ├── reader │ │ ├── config.json │ │ └── jsonReader.ts │ └── harAnalyser.ts ├── globals │ ├── global-setup.ts │ ├── global-teardown.ts │ └── healthcheck.setup.ts └── helper │ ├── index.ts │ ├── Helper.ts │ ├── terminal │ └── SSHHelper.ts │ └── api │ └── apiHelper.ts ├── environments ├── dev.env ├── local.env └── qe.env ├── tests ├── api │ └── example │ │ ├── apiTestDemo.spec.ts │ │ ├── user.spec.ts │ │ ├── post.spec.ts │ │ ├── api1.spec.ts │ │ ├── api.spec.ts │ │ └── dashboard.spec.ts ├── web │ ├── example │ │ ├── demo-fixture.spec.ts │ │ ├── blockunnecessary.spec.ts │ │ ├── runTestParallel.spect.ts │ │ ├── screenshot.spec.ts │ │ ├── runTestSerial.spec.ts │ │ ├── launchChrome.spec.js │ │ ├── generateHarFfile.spec.ts │ │ ├── uploadfile.spec.ts │ │ ├── slider.spec.ts │ │ ├── runMultipleTestInSingleBrowserWindow.spec.js │ │ ├── saveHarFile.spec.ts │ │ ├── downloadfile.spec.ts │ │ ├── frame.spec.ts │ │ ├── UIBasics.spec.js │ │ ├── multipleLocator.spec.js │ │ ├── networkTest.spec.ts │ │ ├── basicAuthentication.spec.ts │ │ ├── localization.spec.ts │ │ ├── verifybrokenlink.spec.js │ │ ├── multipleTabs.spec.ts │ │ ├── autoWait.spec.ts │ │ ├── globalLogin.spec.ts │ │ ├── multiplePages.spec.ts │ │ ├── DragAndDrop.spec.ts │ │ ├── webTable.spec.ts │ │ ├── launchEdge.spec.js │ │ ├── launchBrowserUsingConfig.spec.ts │ │ ├── route.spec.ts │ │ ├── authenticationUsingAuthJSON.spec.ts │ │ ├── mockApi.spec.ts │ │ ├── alert.spec.ts │ │ ├── launchMultipleBrowserInSingle.spec.js │ │ ├── ConsoleAndPageError.spec.ts │ │ ├── locator.spec.ts │ │ ├── inputFieldOperation.spec.ts │ │ ├── accessibilitytest.spec.ts │ │ ├── trackApiResponse.spec.js │ │ └── assertion.spec.ts │ ├── orangeHrm.spec.ts │ └── IntercpetFailingAPI.js ├── e2e │ └── moatazeldebsy │ │ └── form.spec.ts ├── fixtures │ ├── test-options.ts │ └── customFixtures.ts ├── mobile │ └── example │ │ ├── mobiletest.spec.js │ │ ├── healthcheckiOS.test.ts │ │ └── healthcheckAndroid.test.ts ├── resources │ └── uiMap │ │ ├── moataeldebsy.json │ │ └── orangeHRM.json └── load │ └── BrowerLoadTest.spec.js ├── .vscode └── settings.json ├── .mocharc.js ├── middleware.js ├── .gitignore ├── data ├── routes.json └── db.json ├── lint-staged.config.js ├── .env ├── testConfig.ts ├── .prettierrc ├── DockerFile ├── auth.json ├── .github └── workflows │ └── playwright.yml ├── tsconfig.json ├── eslint.config.mjs ├── config.ts ├── playwright.config.ts ├── package.json ├── logs ├── combined.log └── rejections.log ├── Notes ├── .windsurf └── rules │ └── rules.md └── README.md /src/utils/utils.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /environments/dev.env: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /environments/local.env: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /environments/qe.env: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/utils/constants.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/utils/dbUtils.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/globals/global-setup.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/utils/config/env/dev.env: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/utils/config/env/qa.env: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/globals/global-teardown.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/globals/healthcheck.setup.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/utils/config/env/local.env: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/utils/error/ApiErrorHandler.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/api/example/apiTestDemo.spec.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true 3 | } -------------------------------------------------------------------------------- /.mocharc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | timeout: 20000, // define a 20 seconds timeout for all tests 3 | }; 4 | -------------------------------------------------------------------------------- /middleware.js: -------------------------------------------------------------------------------- 1 | module.exports = (req, res, next) => { 2 | res.header('X-Hello', 'World') 3 | next() 4 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /test-results/ 3 | /playwright-report/ 4 | /blob-report/ 5 | /playwright/.cache/ 6 | logs/info.log 7 | -------------------------------------------------------------------------------- /data/routes.json: -------------------------------------------------------------------------------- 1 | { 2 | "/api/*": "/$1", 3 | "/mobiles/:name": "/mobiles?name_like=:name", 4 | "/reviews/:id": "/reviews?id=:id" 5 | } -------------------------------------------------------------------------------- /src/helper/index.ts: -------------------------------------------------------------------------------- 1 | export {ApiHelper} from "../helper/api/apiHelper" 2 | export {SshHelper} from "../helper/terminal/SSHHelper" 3 | export {WebHelper} from "../helper/web/webHelper" -------------------------------------------------------------------------------- /lint-staged.config.js: -------------------------------------------------------------------------------- 1 | // lint-staged.config.js 2 | module.exports = { 3 | '*.{ts,js}': [ 4 | 'eslint --fix', // Run ESLint with auto-fix on TypeScript files 5 | 'prettier --write', // Run Prettier to format code 6 | ], 7 | }; -------------------------------------------------------------------------------- /tests/web/example/demo-fixture.spec.ts: -------------------------------------------------------------------------------- 1 | import { test } from "../../fixtures/customFixtures"; 2 | 3 | test.describe('Demo website',()=>{ 4 | 5 | test('Verify homepage', async({web})=>{ 6 | await web.navigateToUrl("https://google.co.in"); 7 | }) 8 | }) -------------------------------------------------------------------------------- /tests/web/example/blockunnecessary.spec.ts: -------------------------------------------------------------------------------- 1 | import test from "@playwright/test"; 2 | 3 | /* 4 | Blocking Unnecessary requests, can significantly speed up your tests 5 | */ 6 | test("Block Unnecessary Requests", async ({ page }) => { 7 | await page.route("**/analytics/**", (route) => route.abort()); 8 | }); 9 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | #Browser variable 2 | HEADLESS_BROWSER=false 3 | 4 | #Paralleism, reties, timeouts 5 | WORKERS=10 6 | RETRY_FAILED=2 7 | MAX_TEST_RUNTIME=60000 8 | 9 | #Environment 10 | BASE_URL='https://moatazeldebsy.github.io/test-automation-practices' 11 | 12 | #Credentials 13 | USER_NAME='ABC' 14 | PASSWORd='ABC' 15 | 16 | -------------------------------------------------------------------------------- /src/utils/dataStore.js: -------------------------------------------------------------------------------- 1 | /* 2 | This components acts as a in-memory data store. It is used to save data that needs to be shared between different test case*/ 3 | let store = {}; 4 | 5 | function saveData(key, data) { 6 | store[key] = data; 7 | } 8 | 9 | function getData(key) { 10 | return store[key]; 11 | } 12 | module.exports = { saveData, getData }; 13 | -------------------------------------------------------------------------------- /tests/e2e/moatazeldebsy/form.spec.ts: -------------------------------------------------------------------------------- 1 | import {test} from "@tests/fixtures/customFixtures" 2 | 3 | test("Enter value in forms @form", async ({ page,web,api }) => { 4 | await page.goto("https://moatazeldebsy.github.io/test-automation-practices/#/forms"); 5 | 6 | await web.changeValueOnUi({elementName:"Basic.Forms.UsernameText"}) 7 | }); 8 | 9 | 10 | -------------------------------------------------------------------------------- /tests/fixtures/test-options.ts: -------------------------------------------------------------------------------- 1 | //This file is used for merging all extended test fixtures 2 | import { mergeTests, test as base } from "@playwright/test"; 3 | import {test as customFixture} from "./customFixtures"; 4 | // import {test as harTest} from "./networkLogFixture"; 5 | 6 | const test = mergeTests(customFixture); 7 | 8 | const expect = base.expect; 9 | export {test,expect}; -------------------------------------------------------------------------------- /testConfig.ts: -------------------------------------------------------------------------------- 1 | export const testConfig = { 2 | qa: `https://demoqa.com`, 3 | dev: ``, 4 | qaApi: `https://reqres.in`, 5 | devApi: ``, 6 | username: `demouat@gmail.com`, 7 | password: `U2FsdGVkX18/eMdsOJpvI4hJZ/w7hNgwSRFaDvAcZx4=`, 8 | waitForElement: 120000, 9 | dbUsername: ``, 10 | dbPassword: ``, 11 | dbServerName: ``, 12 | dbPort: ``, 13 | dbName: `` 14 | } -------------------------------------------------------------------------------- /tests/web/example/runTestParallel.spect.ts: -------------------------------------------------------------------------------- 1 | import test from "@playwright/test"; 2 | 3 | //1st way to set run tests in parallel 4 | test.describe.configure({ mode: "parallel" }); 5 | 6 | //2nd way to set run tests in parallel 7 | test.describe.parallel("Run All Tests in Parallel", async () => { 8 | test("TestOne", async ({ page }) => {}); 9 | 10 | test("TestTwo", async ({ page }) => {}); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/web/example/screenshot.spec.ts: -------------------------------------------------------------------------------- 1 | import test from "@playwright/test"; 2 | 3 | test("Take Screenshot of Page", async ({ page }) => { 4 | //Take screenshot of full page 5 | await page.screenshot({ path: "screenshot.png" }); 6 | }); 7 | 8 | test("Take Screenshot of Element", async ({ page }) => { 9 | //Take screenshot of an element 10 | await page.goto("http://xxx.com"); 11 | await page.getByTestId("todo-item").screenshot({ path: "screenshot.png" }); 12 | }); 13 | -------------------------------------------------------------------------------- /tests/web/example/runTestSerial.spec.ts: -------------------------------------------------------------------------------- 1 | import test from "@playwright/test"; 2 | 3 | //1st way to define test suite to run tests in sequence 4 | test.describe.configure({ mode: "serial" }); 5 | 6 | //2nd way to define test suite to run tests in sequence 7 | test.describe.serial("Run all test in serial", async () => { 8 | test("TestOne", async ({ page }) => {}); 9 | 10 | test("TestTwo", async ({ page }) => {}); 11 | 12 | test("TestThree", async ({ page }) => {}); 13 | }); 14 | -------------------------------------------------------------------------------- /tests/web/example/launchChrome.spec.js: -------------------------------------------------------------------------------- 1 | import { chromium, test } from "@playwright/test"; 2 | 3 | test('Launch Chrome', async () => { 4 | const browser = await chromium.launch({ 5 | channel: 'chrome' //Use system installed chrome 6 | , headless: false //Show browser 7 | }); 8 | 9 | const page = await browser.newPage(); 10 | await page.goto('https://www.google.com'); 11 | console.log(await page.title()); 12 | await browser.close(); 13 | }) 14 | 15 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 4, 4 | "userTabs": false, 5 | "semi": true, 6 | "singleQuote": false, 7 | "trailingComma": "es5", 8 | "bracketSpacing": true, 9 | "jsxSingleQuote": false, 10 | "arrowParens": "always", 11 | "proseWrap": "preserve", 12 | "htmlWhitespaceSensitivity": "strict", 13 | "endOfLine": "lf", 14 | "overrides": [ 15 | { 16 | "files": "*.json", 17 | "options": { 18 | "printWidth": 80, 19 | "singleQuote": false 20 | } 21 | } 22 | ] 23 | } -------------------------------------------------------------------------------- /tests/web/example/generateHarFfile.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "../../fixtures/customFixtures" 2 | 3 | test.describe('Sample UI test', () => { 4 | test('Capture network logs', { tag: '@network1' }, async ({ web }) => { 5 | await web.navigateToUrl('https://practicesoftwaretesting.com/') 6 | const locator = await web.findElement("xpath","//*[@src='assets/img/products/pliers01.avif']"); 7 | if (locator) { 8 | await locator.click(); 9 | } 10 | 11 | //To Do : Add more steps to 12 | }) 13 | }) -------------------------------------------------------------------------------- /src/utils/config/artillery/artillery-script.yml: -------------------------------------------------------------------------------- 1 | config: 2 | target: https://opensource-demo.orangehrmlive.com/web/index.php/auth/login 3 | phases: 4 | - duration: 100 5 | arrivalRate: 1 6 | maxVusers: 1 7 | engines: 8 | playwright: 9 | showAllPageMetrics: true, 10 | extendedMetrics: true 11 | aggregateByName: true 12 | launchOptions: 13 | headless: false 14 | processor: ./tests/load/BrowerLoadTest.spec.js 15 | scenarios: 16 | - name: "Basic Load" 17 | engine: playwright 18 | flowFunction: "browserloadTest" 19 | -------------------------------------------------------------------------------- /DockerFile: -------------------------------------------------------------------------------- 1 | # Use the official Playwright Docker image 2 | FROM mcr.microsoft.com/playwright:focal 3 | 4 | # Set the working directory inside the container 5 | WORKDIR /app 6 | 7 | # Copy the package.json and package-lock.json (if available) 8 | COPY package*.json ./ 9 | 10 | # Install dependencies 11 | RUN npm install 12 | 13 | # Copy the entire project to the working directory 14 | COPY . . 15 | 16 | # Install Playwright browsers (Chromium, Firefox, WebKit) 17 | RUN npx playwright install --with-deps 18 | 19 | # Command to run the Playwright tests 20 | CMD ["npx", "playwright", "test"] -------------------------------------------------------------------------------- /src/utils/report/sendEmail.ts: -------------------------------------------------------------------------------- 1 | var nodeoutlook = require("nodejs-nodemailer-outlook"); 2 | 3 | function sendEmailReport() { 4 | nodeoutlook.sendEmail({ 5 | auth: { 6 | user: "xxxxx", 7 | pass: "password", 8 | }, 9 | from: "emailid", 10 | to: "emailid", 11 | subject: "subject", 12 | html: "Pls open attached HTML file", 13 | replyTo: "abhaybharti@gmail.com", 14 | attachments: [ 15 | { filename: "index.html", path: "../../../playwright-report/index.html" }, 16 | ], 17 | onError: (e: any) => console.log(e), 18 | onSuccess: (i: any) => console.log(i), 19 | }); 20 | } 21 | sendEmailReport(); 22 | -------------------------------------------------------------------------------- /tests/web/example/uploadfile.spec.ts: -------------------------------------------------------------------------------- 1 | import test, { expect } from "@playwright/test"; 2 | import { WebHelper } from "../../../helper/web/webHelper"; 3 | import path from "path"; 4 | 5 | test("File Upload Test ", async ({ page, browser }) => { 6 | const context = await browser.newContext(); 7 | const webHelper = new WebHelper(page, context); 8 | 9 | //Arrange 10 | const fileName = "uploadFile.txt"; 11 | const filePath = path.resolve(__dirname, `../test-data/${fileName}`); 12 | 13 | //Action 14 | await webHelper.uploadFile(fileName, filePath, "locatorOfUploadLink"); 15 | 16 | //Assert 17 | expect(await webHelper.elementHasText("locatorOfUploadLink", fileName)); 18 | }); 19 | -------------------------------------------------------------------------------- /auth.json: -------------------------------------------------------------------------------- 1 | { 2 | "cookies": [ 3 | { 4 | "name": "session-username", 5 | "value": "standard_user", 6 | "domain": "www.saucedemo.com", 7 | "path": "/", 8 | "expires": 1758732605, 9 | "httpOnly": false, 10 | "secure": false, 11 | "sameSite": "Lax" 12 | } 13 | ], 14 | "origins": [ 15 | { 16 | "origin": "https://www.saucedemo.com", 17 | "localStorage": [ 18 | { 19 | "name": "backtrace-guid", 20 | "value": "6d346665-28b8-40b7-84bb-574bc9aed4c6" 21 | }, 22 | { 23 | "name": "backtrace-last-active", 24 | "value": "1758731628459" 25 | } 26 | ] 27 | } 28 | ] 29 | } -------------------------------------------------------------------------------- /tests/web/example/slider.spec.ts: -------------------------------------------------------------------------------- 1 | import { test } from "@tests/fixtures/customFixtures"; 2 | import {expect} from "@playwright/test"; 3 | 4 | 5 | test('Test Slider function', async ({web}) => { 6 | 7 | await web.navigateToUrl('https://the-internet.herokuapp.com/horizontal_slider') 8 | 9 | const slider = await web.findElement('xpath', "//input[@type='range']"); 10 | 11 | await slider?.evaluate(node=>{(node as HTMLInputElement).value = '3' 12 | node.dispatchEvent(new Event('change')) 13 | }) 14 | 15 | const result = await web.findElement('xpath', "//span[@id='range']") 16 | 17 | const resultText = await web.getText(result) 18 | 19 | await expect(resultText).toBe('3') 20 | }) -------------------------------------------------------------------------------- /src/utils/reader/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "John Doe", 3 | "age": 30, 4 | "email": "johndoe@example.com", 5 | "address": { 6 | "street": "123 Main St", 7 | "city": "New York", 8 | "state": "NY", 9 | "zip": "10001" 10 | }, 11 | "phone_numbers": [ 12 | { 13 | "type": "home", 14 | "number": "555-1234" 15 | }, 16 | { 17 | "type": "work", 18 | "number": "555-5678" 19 | } 20 | ], 21 | "is_active": true, 22 | "roles": ["admin", "editor"], 23 | "preferences": { 24 | "newsletter": true, 25 | "notifications": { 26 | "email": true, 27 | "sms": false 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/web/example/runMultipleTestInSingleBrowserWindow.spec.js: -------------------------------------------------------------------------------- 1 | test.describe('1 page multiple tests', () => { 2 | let page; 3 | test.beforeAll(async ({ browser }) => { 4 | const context = await browser.newContext(); 5 | page = await context.newPage(); 6 | await page.goto('https://example.com'); 7 | }); 8 | 9 | test.afterAll(async ({ browser }) => { 10 | browser.close; 11 | }); 12 | 13 | test('nav test', async () => { 14 | const name = await page.innerText('h1'); 15 | expect(name).toContain('Example'); 16 | }); 17 | 18 | test('header test', async () => { 19 | const name = await page.innerText('h1'); 20 | expect(name).toContain('Domain'); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /tests/web/example/saveHarFile.spec.ts: -------------------------------------------------------------------------------- 1 | import test from "@playwright/test"; 2 | 3 | test(`Generate HAR file`, async ({ page: Page }, testInfo) => { 4 | // To record HAR file, use "update:true", below code will create a directory named har and 5 | //store all the har related files in it 6 | await Page.routeFromHAR("har/example.har", { update: true }); 7 | 8 | /* The `await testInfo.attach(`HAR FILE`, { path: `../../../../har/example.har` });` line is 9 | attaching the generated HAR file to the test report. It allows the HAR file to be easily 10 | accessible and viewable alongside the test results. The `path` parameter specifies the location of 11 | the HAR file. */ 12 | await testInfo.attach(`HAR FILE`, { path: `../../../../har/example.har` }); 13 | }); 14 | -------------------------------------------------------------------------------- /.github/workflows/playwright.yml: -------------------------------------------------------------------------------- 1 | name: Playwright Tests 2 | on: 3 | push: 4 | branches: [ main, master ] 5 | pull_request: 6 | branches: [ main, master ] 7 | jobs: 8 | test: 9 | timeout-minutes: 60 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | - uses: actions/setup-node@v3 14 | with: 15 | node-version: 18 16 | - name: Install dependencies 17 | run: npm ci 18 | - name: Install Playwright Browsers 19 | run: npx playwright install --with-deps 20 | - name: Run Playwright tests 21 | run: npx playwright test 22 | - uses: actions/upload-artifact@v3 23 | if: always() 24 | with: 25 | name: playwright-report 26 | path: playwright-report/ 27 | retention-days: 30 28 | -------------------------------------------------------------------------------- /tests/web/example/downloadfile.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "../../fixtures/customFixtures" 2 | import fs from "fs"; 3 | import path from "path"; 4 | 5 | test("File Download Test ", async ({ web }) => { 6 | //Arrange 7 | const expectedFileName = "fileToDownload.txt"; 8 | const downloadFolderPath = path.resolve(__dirname, "../test-data"); // path to the folder where the downloaded file will be saved 9 | const savePath = path.join(downloadFolderPath, expectedFileName); 10 | 11 | //Action 12 | await web.downLoadFile( 13 | expectedFileName, 14 | downloadFolderPath, 15 | "locatorOfDownloadLink" 16 | ); 17 | 18 | //Assert 19 | expect(fs.existsSync(savePath)).toBeTruthy(); 20 | 21 | //Clean up : remove downloaded file 22 | fs.unlinkSync(savePath); 23 | }); 24 | -------------------------------------------------------------------------------- /tests/web/example/frame.spec.ts: -------------------------------------------------------------------------------- 1 | 2 | import { test, expect } from "../../fixtures/customFixtures" 3 | 4 | test("iframe", async ({ page }) => { 5 | await page.goto("http://rahulshettyacademy.com/AutomationPractice/"); 6 | const framesPage = await page.frameLocator("#courses-iframe"); 7 | framesPage.locator("li a[href*='lifetime-access]:visible").click(); 8 | const textCheck = await framesPage.locator(".text h2").textContent(); 9 | console.log(textCheck); 10 | }); 11 | 12 | test("Test 2 : Operation on frame", async ({ web, browser }) => { 13 | const context = await browser.newContext(); 14 | const page = await context.newPage(); 15 | await page.goto("http://rahulshettyacademy.com/AutomationPractice/"); 16 | const frameOne = await web.getFrame("iframe[name='courses-iframe']"); 17 | }); 18 | -------------------------------------------------------------------------------- /tests/web/example/UIBasics.spec.js: -------------------------------------------------------------------------------- 1 | import { test } from "@playwright/test"; 2 | 3 | //In general terminology, fixture are global variable which are available across your project. 4 | 5 | //What is context 6 | // 7 | test('Browser Context Playwright Test', async ({ browser, page }) => { 8 | // write test step here 9 | 10 | //Opens a new & fresh instance of browser 11 | const context = await browser.newContext() 12 | const pageLocal = await context.newPage(); 13 | await pageLocal.goto("https://sso.teachable.com/secure/9521/identity/login/otp") 14 | await pageLocal.close() 15 | }) 16 | 17 | test('Page Playwrigth Test', async function ({ page }) { 18 | //write test step here using page fixtures 19 | await page.goto("https://google.co.in") 20 | await page.close() 21 | }) 22 | -------------------------------------------------------------------------------- /tests/web/example/multipleLocator.spec.js: -------------------------------------------------------------------------------- 1 | import test from '@playwright/test'; 2 | import {logInfo, logError, logWarn} from "../../../src/utils/report/Logger" 3 | 4 | test('find multiple elements', async ({ page }) => { 5 | await page.goto('https://timesofindia.indiatimes.com/'); 6 | const links = await page.locator('a'); 7 | const links2 = await page.locator('a').all(); 8 | console.log(links); 9 | console.log(links2); 10 | 11 | // 2. Get the number of links, waiting for them to appear 12 | const count = await links.count(); 13 | 14 | // 3. Loop through them using nth() 15 | for (let i = 0; i < count; i++) { 16 | // For each link, Playwright finds it again and waits if necessary 17 | const linkText = await links.nth(i).textContent(); 18 | logInfo(linkText); 19 | 20 | } 21 | }) 22 | -------------------------------------------------------------------------------- /tests/mobile/example/mobiletest.spec.js: -------------------------------------------------------------------------------- 1 | import { remote } from "webdriverio"; 2 | 3 | const capabilities = { 4 | platformName: "Android", 5 | "appium:automationName": "UiAutomator2", 6 | "appium:deviceName": "Android", 7 | "appium:appPackage": "com.android.settings", 8 | "appium:appActivity": ".Settings", 9 | }; 10 | 11 | const wdOpts = { 12 | hostname: process.env.APPIUM_HOST || "localhost", 13 | port: parseInt(process.env.APPIUM_PORT, 10) || 4723, 14 | logLevel: "info", 15 | capabilities, 16 | }; 17 | 18 | // async function runTest() { 19 | // const driver = await remote(wdOpts); 20 | // try { 21 | // const batteryItem = await driver.$('//*[@text="Battery"]'); 22 | // await batteryItem.click(); 23 | // } finally { 24 | // await driver.pause(1000); 25 | // await driver.deleteSession(); 26 | // } 27 | // } 28 | 29 | // runTest().catch(console.error); 30 | -------------------------------------------------------------------------------- /tests/web/example/networkTest.spec.ts: -------------------------------------------------------------------------------- 1 | import test from "@playwright/test"; 2 | import { ApiHelper } from "../../../helper/api/apiHelper"; 3 | 4 | let token: string; 5 | 6 | test.beforeAll(async ({ browser }) => { 7 | const context = await browser.newContext(); 8 | const apiContent = await context.newPage(); 9 | const apiHelper = new ApiHelper(apiContent); 10 | 11 | //save token value returned from getToken() function in token variable 12 | token = await apiHelper.getToken(); 13 | }); 14 | 15 | test("Network Test -> Inject token generated through API into browser", async ({ 16 | page, 17 | }) => { 18 | //executed JavaScript to inject token into browser 19 | page.addInitScript((value) => { 20 | window.localStorage.setItem("token", value); 21 | }, token); 22 | 23 | //when script hits URL, browser will open as logged in using above generated token 24 | await page.goto("https://www.xxxx.com"); 25 | }); 26 | -------------------------------------------------------------------------------- /tests/web/example/basicAuthentication.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@tests/fixtures/test-options"; 2 | 3 | test.use({ 4 | baseURL: 'https://the-internet.herokuapp.com', 5 | httpCredentials: { 6 | username: 'admin', 7 | password: 'admin', 8 | } 9 | }) 10 | 11 | 12 | test('Basic Authetication', async ({ page }) => { 13 | await page.goto('/') 14 | await page.getByRole('link', { name: 'Basic Auth', exact: true }).click(); 15 | await expect(page.getByRole('heading', { name: 'Basic Auth' })).toBeVisible() 16 | }) 17 | 18 | test.only('Basic Authetication with Extra HTTP headers', async ({ page }) => { 19 | await page.goto('/') 20 | await page.setExtraHTTPHeaders({ 21 | Authorization: 'Basic '+btoa('admin:admin') 22 | }) 23 | await page.getByRole('link', { name: 'Basic Auth', exact: true }).click(); 24 | await expect(page.getByRole('heading', { name: 'Basic Auth' })).toBeVisible() 25 | }) -------------------------------------------------------------------------------- /tests/web/example/localization.spec.ts: -------------------------------------------------------------------------------- 1 | import { logInfo } from "@src/utils/report/Logger"; 2 | import { test } from "@tests/fixtures/test-options"; 3 | 4 | test.describe('Local testing', () => { 5 | 6 | test.use({ locale: 'ru-RU', timezoneId: 'Asia/Tokyo', baseURL:'https://www.google.com' }) 7 | 8 | test('Launch the Google website in Russian local', async ({ page }) => { 9 | await page.goto('/') 10 | const dateRes = await page.evaluate(() => { 11 | return { 12 | date: Date(), 13 | localization: navigator.language, 14 | timezoneId : Intl.DateTimeFormat().resolvedOptions().timeZone 15 | } 16 | }); 17 | logInfo('dateRes : ',dateRes) 18 | 19 | await page.goto('https://www.typescriptlang.org/') 20 | 21 | await page.goto('/') 22 | 23 | 24 | await page.goto('https://yahoo.com') 25 | 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /src/utils/report/ReportAction.ts: -------------------------------------------------------------------------------- 1 | import test from "@playwright/test"; 2 | 3 | export function step(_stepName?: string) { 4 | return function (target: any, context: ClassMethodDecoratorContext) { 5 | return function (...args: any) { 6 | // get the class and method details 7 | const methodDetails = this.constructor.name + '.' + (context.name as string) 8 | // Get the step details and add the parameter value with steps 9 | const name = _stepName ? `${_replacePlaceholders(_stepName, args)} - ${methodDetails} ` : methodDetails 10 | return test.step(name, async () => { 11 | return await target.call(this, ...args) 12 | }) 13 | } 14 | } 15 | } 16 | 17 | function _replacePlaceholders(template: string, values: any[]): string { 18 | values.forEach(value => { 19 | if (typeof value === 'string' || typeof value === 'number') { 20 | // add support for other data types if needed 21 | template = template.replace(/{(.*?)}/, value as any) 22 | } 23 | }) 24 | return template 25 | } -------------------------------------------------------------------------------- /tests/web/example/verifybrokenlink.spec.js: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test' 2 | 3 | const html = ` 4 | Example 5 | yc 6 | google 7 | yahoo 8 | `; 9 | 10 | test("All links are valid", async ({ page, request }) => { 11 | // await page.setContent(html); 12 | const links = await page.locator("a").evaluateAll(els => els.map(el => el.href)); 13 | 14 | 15 | const chunkSize = 3; 16 | 17 | 18 | for (let i = 0; i < links.length; i += chunkSize) { 19 | const chunkLinks = links.slice(i, i + chunkSize); 20 | console.log('chunkLinks : ',chunkLinks); 21 | const responses = await Promise.all(chunkLinks.map(url => request.get(url))); 22 | responses.forEach((response, index) => { 23 | const url = chunkLinks[index]; 24 | expect(response.ok(), `${url} is broken with status ${response.status()}`).toBe(true); 25 | }); 26 | } 27 | }) -------------------------------------------------------------------------------- /tests/web/example/multipleTabs.spec.ts: -------------------------------------------------------------------------------- 1 | import { chromium,test } from "@playwright/test" 2 | 3 | test('Multiple Tabs',async()=>{ 4 | //Launch Browser 5 | const browser = await chromium.launch({headless:false,args:["--start-maximized"]}) 6 | 7 | //Create New browser Context 8 | const context = await browser.newContext(); 9 | 10 | 11 | //Open the first tab (page) 12 | const tabOne = await context.newPage() 13 | await tabOne.goto('https://www.google.com') 14 | await tabOne.waitForTimeout(2000) 15 | console.log(await tabOne.url()) 16 | 17 | 18 | //Open the second tab (page) in same context 19 | const tabTwo = await context.newPage() 20 | await tabTwo.goto('https://timesofindia.indiatimes.com/') 21 | await tabTwo.waitForTimeout(2000) 22 | console.log(await tabTwo.url()) 23 | 24 | //Perform action on first tab, focus on first tab 25 | await tabOne.bringToFront(); 26 | 27 | //Perform action on second tab, focus on second tab 28 | await tabTwo.bringToFront(); 29 | 30 | //Close the browser 31 | await browser.close() 32 | }) 33 | 34 | 35 | -------------------------------------------------------------------------------- /tests/web/example/autoWait.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect, Request, Response } from "@playwright/test"; 2 | 3 | //Different wait available in Playwright 4 | // 1. waitForTimeout -- Thread.sleep() 5 | 6 | // 2. waitForRequest 7 | test("test waitForRequest demo", async ({ page }) => { 8 | const request: Request = await page.waitForRequest(/ohrm_logo.png/); 9 | console.log(request.url()); 10 | }); 11 | // 3. waitForResponse 12 | test("test waitForResponse demo", async ({ page }) => { 13 | const response: Response = await page.waitForResponse(/ohrm_logo.png/); 14 | console.log(response.request().url()); 15 | }); 16 | 17 | // 4. waitForUrl 18 | // 5. waitForLoadState 19 | // 6. waitForSelector 20 | 21 | test("test waitForSelector demo", async ({ page }) => { 22 | //use below approach than using waitForSelector 23 | await expect(page.getByAltText("OrangeHRM")).toBeVisible({ timeout: 3_000 }); 24 | }); 25 | // 7. waitForFunction 26 | test("test waitForFunction demo", async ({ page }) => { 27 | await page.waitForFunction(() => { 28 | window.scrollBy(0, 600); 29 | }); 30 | }); 31 | 32 | // 8. waitForEvent 33 | 34 | test("test waitForEvent demo", async ({ page }) => { 35 | await page.waitForEvent("domcontentloaded"); 36 | }); 37 | -------------------------------------------------------------------------------- /src/utils/error/ErrorManager.ts: -------------------------------------------------------------------------------- 1 | import { logError } from '../report/Logger'; 2 | 3 | // Define an interface for the expected error object 4 | interface ApiErrorResponse { 5 | response?: { 6 | status?: number; 7 | data?: any; // Replace 'any' with a more specific type if known 8 | }; 9 | } 10 | 11 | export class ApiError extends Error { 12 | constructor( 13 | message: string, 14 | public statusCode?: number, 15 | public response?: any 16 | ) { 17 | super(message); 18 | this.name = 'ApiError'; 19 | } 20 | } 21 | 22 | export const handleError = (error: any): never => { 23 | if (error instanceof ApiError) { 24 | logError(`API Error: ${error.message}`, { 25 | statusCode: error.statusCode, 26 | response: error.response 27 | }); 28 | } else { 29 | logError('Unexpected error occurred', error); 30 | } 31 | throw error; 32 | }; 33 | 34 | export const wrapAsync = async ( 35 | fn: () => Promise, 36 | errorMessage: string 37 | ): Promise => { 38 | try { 39 | return await fn(); 40 | } catch (error) { 41 | const apiError = error as ApiErrorResponse; // Cast error to the expected type 42 | throw new ApiError( 43 | errorMessage, 44 | apiError?.response?.status, 45 | apiError?.response?.data 46 | ); 47 | } 48 | }; -------------------------------------------------------------------------------- /src/helper/Helper.ts: -------------------------------------------------------------------------------- 1 | 2 | import { step } from "../utils/report/ReportAction"; 3 | 4 | export class Helper { 5 | // Internal storage for locators 6 | const locatorStorage = new Map(); 7 | 8 | /** 9 | * The `delay` function is an asynchronous function that waits for a specified amount of time before 10 | * resolving. 11 | * @param {number} time - The `time` parameter is a number that represents the duration of the delay 12 | * in seconds. 13 | * @returns a Promise that resolves to void. 14 | */ 15 | @step('delay') 16 | async delay(time: number): Promise { 17 | return new Promise(function (resolve) { 18 | setTimeout(resolve, time * 1000); 19 | }); 20 | } 21 | 22 | /** 23 | * Validates whether the provided string is a valid date parsable by `Date.parse()`. 24 | * @param {string} date - The date string to validate. 25 | * @returns {Promise} Resolves to true if the date string is valid; otherwise false. 26 | */ 27 | @step('isValidDate') 28 | async isValidDate(date: string): Promise { 29 | if (Date.parse(date)) { 30 | return true; 31 | } else { 32 | return false; 33 | } 34 | } 35 | 36 | /** 37 | * Clears all stored locators 38 | */ 39 | clearStoredLocators() { 40 | this.locatorStorage.clear(); 41 | } 42 | 43 | 44 | } 45 | -------------------------------------------------------------------------------- /src/utils/config/azure/azure-pipeline.yml: -------------------------------------------------------------------------------- 1 | # Starter pipeline 2 | # Start with a minimal pipeline that you can customize to build and deploy your code. 3 | # Add steps that build, run tests, deploy, and more: 4 | # https://aka.ms/yaml 5 | 6 | trigger: 7 | - none 8 | 9 | pool: 10 | vmImage: ubuntu-latest 11 | 12 | variables: 13 | "system.debug": false 14 | 15 | steps: 16 | - task: NodeTool@0 17 | inputs: 18 | versionSource: "spec" 19 | versionSpec: ">16.x" 20 | 21 | - task: PowerShell@2 22 | enabled: true 23 | displayName: "Install dependencies" 24 | inputs: 25 | targetType: "inline" 26 | script: "npm ci" 27 | workingDirectory: "$(System.DefaultWorkingDirectory)/samples/get-started" 28 | 29 | - task: PowerShell@2 30 | enabled: true 31 | displayName: "Run Playwright tests" 32 | env: 33 | PLAYWRIGHT_SERVICE_ACCESS_TOKEN: $(Secret) 34 | inputs: 35 | targetType: "inline" 36 | script: "npx playwright test -c playwright.service.config.ts --workers=20" 37 | workingDirectory: "$(System.DefaultWorkingDirectory)/samples/get-started" 38 | 39 | - task: PublishPipelineArtifact@1 40 | inputs: 41 | targetPath: "$(System.DefaultWorkingDirectory)/samples/get-started/playwright-report/" 42 | artifact: "Playwright tests" 43 | publishLocation: "pipeline" 44 | -------------------------------------------------------------------------------- /tests/web/example/globalLogin.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, BrowserContext } from "@playwright/test"; 2 | 3 | 4 | let webContext: any; 5 | 6 | /* The `test.beforeAll()` function is a hook provided by the Playwright test framework. It is used to 7 | run a setup function before all the tests in a test file. In this hook, we will login into application and save login details in state.json file*/ 8 | test.beforeAll( 9 | "Login into web app through browser and save login detail in JSON", 10 | async ({ browser }) => { 11 | const browserContext = await browser.newContext(); 12 | const webPage = await browserContext.newPage(); 13 | const webHelper = new WebHelper(webPage, browserContext); 14 | webHelper.navigateToUrl("www.gmail.com"); 15 | 16 | //write code to login in gmail 17 | 18 | await browserContext.storageState({ path: "state.json" }); 19 | webContext = await browserContext.newContext({ 20 | storageState: "state.json", 21 | }); 22 | await webPage.close(); 23 | } 24 | ); 25 | /* The code you provided is a test case that logs into a web application using the saved login state. */ 26 | test("Login into web app using saved login state", async () => { 27 | const webPage = await webContext.newPage(); 28 | 29 | const webHelper = new WebHelper(webPage, webContext); 30 | webHelper.navigateToUrl("www.gmail.com"); // Browser will open page using login details saved in test.beforeAll() step 31 | }); 32 | -------------------------------------------------------------------------------- /tests/web/example/multiplePages.spec.ts: -------------------------------------------------------------------------------- 1 | 2 | import { chromium, test } from "@playwright/test"; 3 | 4 | // Define a test case named 'Multiple Browser Window' 5 | test('Multiple Browser Window', async () => { 6 | 7 | // Launch the first browser instance Chromium 8 | const browserOne = await chromium.launch({ headless: false, args: ["--start-maximized"] }); 9 | 10 | // Create a new browser context for the first browser 11 | const contextOne = await browserOne.newContext(); 12 | 13 | // Open a new page (tab) in the first browser context 14 | const pageOne = await contextOne.newPage(); 15 | 16 | // Navigate the first page to Google 17 | await pageOne.goto('https://www.google.com'); 18 | 19 | // Launch the second browser instance Chromium 20 | const browserTwo = await chromium.launch({ headless: false, args: ["--start-maximized"] }); 21 | 22 | // Create a new browser context for the second browser 23 | const contextTwo = await browserTwo.newContext(); 24 | 25 | // Open a new page (tab) in the second browser context 26 | const pageTwo = await contextTwo.newPage(); 27 | 28 | // Navigate the second page to DEV.to 29 | await pageTwo.goto('https://dev.to/'); 30 | 31 | // Bring the first browser window to the focus 32 | await pageOne.bringToFront(); 33 | 34 | // Bring the second browser window to the focus 35 | await pageTwo.bringToFront(); 36 | 37 | // Close the first browser 38 | await browserOne.close(); 39 | 40 | // Close the second browser 41 | await browserTwo.close(); 42 | }); -------------------------------------------------------------------------------- /src/utils/report/CustomReporterConfig.ts: -------------------------------------------------------------------------------- 1 | import { 2 | FullConfig, 3 | FullResult, 4 | Reporter, 5 | Suite, 6 | TestCase, 7 | TestError, 8 | TestResult, 9 | TestStep, 10 | } from "@playwright/test/reporter"; 11 | import {logInfo} from './Logger' 12 | 13 | export default class CustomReporterConfig implements Reporter { 14 | constructor(options: { customOption?: string } = {}) { 15 | logInfo(`playwright-framework-template ${options.customOption}`); 16 | } 17 | 18 | onBegin(config: FullConfig, suite: Suite): void { 19 | logInfo(`info`, 20 | `Test Suite Started : ${suite.title} , ${suite.allTests().length} tests` 21 | ); 22 | } 23 | onTestBegin(test: TestCase): void { 24 | logInfo(`info`,`Test Case Started : ${test.title}`); 25 | } 26 | 27 | onTestEnd(test: TestCase, result: TestResult): void { 28 | logInfo(`info`,`Test Case Completed : ${test.title} Status : ${result.status}`); 29 | } 30 | 31 | onStepBegin(test: TestCase, result: TestResult, step: TestStep): void { 32 | if (step.category === `test.step`) { 33 | logInfo(`info`,`Executing Step : ${step.title}`); 34 | } 35 | } 36 | 37 | onError(error: TestError): void { 38 | logInfo(`error`,`TestError : ${error.message}`); 39 | } 40 | 41 | onEnd( 42 | result: FullResult 43 | ): void | Promise< 44 | | void 45 | | { status?: "passed" | "failed" | "timedout" | "interrupted" | undefined } 46 | | undefined 47 | > { 48 | console.log(`Test Suite Completed : ${result.status}`); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/web/example/DragAndDrop.spec.ts: -------------------------------------------------------------------------------- 1 | import { test } from "@tests/fixtures/test-options"; 2 | 3 | test('Drag and Drop', async ({ web }) => { // Define a test case named 'Drag and Drop' using the 'web' fixture. 4 | await web.navigateToUrl('https://the-internet.herokuapp.com/drag_and_drop') // Navigate the browser to the specified URL. 5 | 6 | const A = await web.findElement('xpath', "//div[@id='column-a']") // Find the source element (Column A) using XPath. 7 | const B = await web.findElement('xpath', "//div[@id='column-b']") // Find the target element (Column B) using XPath. 8 | 9 | await A!.dragTo(B!); // Perform the drag-and-drop action, moving element A onto element B.     10 | }) 11 | 12 | test('Drag and Drop Using Mouse Hover', async ({ web }) => { 13 | await web.navigateToUrl('https://the-internet.herokuapp.com/drag_and_drop') // Navigate to the drag and drop test page. 14 | 15 | const A = await web.findElement('xpath', "//div[@id='column-a']") // Locate the source element (Column A). 16 | const B = await web.findElement('xpath', "//div[@id='column-b']") // Locate the target element (Column B). 17 | 18 | await A!.hover() // Move the mouse pointer over the source element (Column A). 19 | await web.webPage.mouse.down(); // Press and hold the left mouse button to start the drag operation. 20 | await B!.hover(); // Move the mouse pointer to the target element (Column B) while still holding the button. 21 | await web.webPage.mouse.up(); // Release the left mouse button to complete the drop operation.     22 | }) 23 | 24 | -------------------------------------------------------------------------------- /tests/web/example/webTable.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | import { logInfo } from '@src/utils/report/Logger'; 3 | 4 | test('Read all data from table', async ({ page }) => { 5 | //navigate to URL 6 | //Locate Table 7 | //Find number of rows in table 8 | //Traverse rows 9 | //Find number of column in each row 10 | //Traverse columns & print values 11 | 12 | await page.goto('https://the-internet.herokuapp.com/tables') 13 | const rows = await page.locator("//*[@id='table1']").locator("tbody").locator("tr"); 14 | const columnHeader = await page.locator("//*[@id='table1']").locator("thead").locator("tr").locator("th"); 15 | const columnHeaderCount = await columnHeader.count(); 16 | const headerText = []; 17 | for (let i = 0; i < columnHeaderCount; i++) { 18 | headerText.push(await columnHeader.nth(i).textContent()); 19 | } 20 | logInfo(`Header Text: ${headerText.join('|')}`); 21 | 22 | 23 | const count = await rows.count(); 24 | logInfo(`Number of rows in table: ${count}`); 25 | for (let i = 0; i < count; i++) { 26 | const columns = rows.nth(i).locator("td"); 27 | const columnCount = await columns.count(); 28 | logInfo(`Number of columns in row ${i + 1}: ${columnCount}`); 29 | const rowData = []; 30 | for (let j = 0; j < columnCount; j++) { 31 | const cellText = await columns.nth(j).textContent(); 32 | rowData.push(cellText); 33 | } 34 | logInfo(`Row ${i + 1}: ${rowData.join('|')}`); 35 | } 36 | }) -------------------------------------------------------------------------------- /tests/web/example/launchEdge.spec.js: -------------------------------------------------------------------------------- 1 | import { chromium, test, firefox } from "@playwright/test"; 2 | 3 | test('Launch Edge', async () => { 4 | const browser = await chromium.launch({ 5 | channel: 'msedge' //Use system installed chrome 6 | , headless: false //Show browser 7 | }); 8 | 9 | const page = await browser.newPage(); 10 | await page.goto('https://www.google.com'); 11 | console.log(await page.title()); 12 | await browser.close(); 13 | }); 14 | 15 | 16 | test('Launch Firefox', async () => { 17 | // Launch the Firefox browser. 'headless: false' means we will see the browser UI. 18 | const firefoxBrowser = await firefox.launch({ 19 | headless: false 20 | }); 21 | 22 | // Create a new page (tab) in the Firefox browser 23 | const firefoxPage = await firefoxBrowser.newPage(); 24 | 25 | // Navigate the page to a specific URL 26 | await firefoxPage.goto('https://playwright.dev/'); 27 | console.log('Firefox launched successfully and navigated to playwright.dev'); 28 | 29 | try { 30 | // Launch the WebKit browser. 31 | const webkitBrowser = await webkit.launch({ 32 | headless: false 33 | }); 34 | 35 | // Create a new page in the WebKit browser 36 | const webkitPage = await webkitBrowser.newPage(); 37 | 38 | // Navigate the page to a specific URL 39 | await webkitPage.goto('https://webkit.org/'); 40 | console.log('WebKit launched successfully and navigated to webkit.org'); 41 | 42 | // To automatically close it, you would use: await webkitBrowser.close(); 43 | 44 | } catch (error) { 45 | console.error('Failed to launch WebKit:', error); 46 | } 47 | }); -------------------------------------------------------------------------------- /tests/resources/uiMap/moataeldebsy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Basic": { 3 | "Forms": { 4 | "Username": { 5 | "objType": "Label", 6 | "locatorType": "xpath", 7 | "locatorValue": "//label[@for='username']", 8 | "label": "Username" 9 | }, 10 | "UsernameText": { 11 | "objType": "textbox", 12 | "locatorType": "xpath", 13 | "locatorValue": "//input[@data-test='username-input']", 14 | "saveTestData": "A|B", 15 | "invalidTestData": "", 16 | "validTestData": "" 17 | }, 18 | "Email": { 19 | "objType": "Label", 20 | "locatorType": "xpath", 21 | "locatorValue": "//label[@for='email']", 22 | "label": "Email" 23 | }, 24 | "EmailText": { 25 | "objType": "textbox", 26 | "locatorType": "xpath", 27 | "locatorValue": "//input[@data-test='email-input']", 28 | "saveTestData": "", 29 | "invalidTestData": "", 30 | "validTestData": "" 31 | }, 32 | "Password": { 33 | "objType": "Label", 34 | "locatorType": "xpath", 35 | "locatorValue": "//label[@for='password']", 36 | "label": "Password" 37 | }, 38 | "PasswordText": { 39 | "objType": "textbox", 40 | "locatorType": "xpath", 41 | "locatorValue": "//input[@data-test='password-input']", 42 | "saveTestData": "", 43 | "invalidTestData": "", 44 | "validTestData": "" 45 | }, 46 | "SignIn": { 47 | "objType": "button", 48 | "locatorType": "xpath", 49 | "locatorValue": "//button[@data-test='submit-button']", 50 | "label": "Sign In" 51 | } 52 | }, 53 | "Table": {} 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tests/api/example/user.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | import { ApiHelper } from '@src/helper/api/apiHelper'; 3 | 4 | 5 | test.describe('Users API', () => { 6 | let apiHelper: ApiHelper; 7 | 8 | test.beforeEach(async ({ request,page }) => { 9 | apiHelper = new ApiHelper(page,request); 10 | }); 11 | 12 | test('should get all users', async () => { 13 | const response = await apiHelper.get('/users'); 14 | expect(response.status).toBe(200); 15 | expect(Array.isArray(response.data)).toBeTruthy(); 16 | expect(response.data.length).toBeGreaterThan(0); 17 | }); 18 | 19 | test('should get a single user', async () => { 20 | const response = await apiHelper.get('/users/1'); 21 | expect(response.status).toBe(200); 22 | expect(response.data.id).toBe(1); 23 | expect(response.data.name).toBeTruthy(); 24 | expect(response.data.email).toBeTruthy(); 25 | }); 26 | 27 | test('should create a new user', async () => { 28 | const userData = { 29 | name: 'John Doe', 30 | email: 'john@example.com', 31 | username: 'johndoe', 32 | website: 'example.com' 33 | }; 34 | const response = await apiHelper.post('/users', userData); 35 | expect(response.status).toBe(201); 36 | expect(response.data.name).toBe(userData.name); 37 | expect(response.data.email).toBe(userData.email); 38 | expect(response.data.id).toBeTruthy(); 39 | }); 40 | 41 | test('should update a user', async () => { 42 | const updateData = { 43 | name: 'Jane Doe', 44 | email: 'jane@example.com' 45 | }; 46 | const response = await apiHelper.patch('/users/1', updateData); 47 | expect(response.status).toBe(200); 48 | expect(response.data.name).toBe(updateData.name); 49 | expect(response.data.email).toBe(updateData.email); 50 | }); 51 | }); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "Nodenext" /* Specify what module code is generated. */, 5 | "lib": [ 6 | "ESNext", 7 | "DOM" 8 | ], 9 | "moduleResolution": "Nodenext" /* Specify what module code is generated. */, 10 | "baseUrl": "./" /* Specify the base directory to resolve non-relative module names. */, 11 | "paths": { 12 | "@src/*": [ 13 | "src/*" 14 | ], 15 | "@tests/*": [ 16 | "tests/*" 17 | ], 18 | "@pages/*": [ 19 | "tests/pages/*" 20 | ] 21 | }, 22 | "experimentalDecorators": true, 23 | "emitDecoratorMetadata": true, 24 | "rootDir": ".", 25 | "allowJs": true, 26 | "checkJs": false, 27 | "outDir": "./dist" /* Specify an output folder for all emitted files. */, 28 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, 29 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 30 | "strict": true /* Enable all strict type-checking options. */, 31 | "skipLibCheck": true /* Skip type checking all .d.ts files. */, 32 | "noEmit": true, 33 | "noImplicitAny": true, 34 | "resolveJsonModule": true, 35 | "types": [ 36 | "node", 37 | "playwright" 38 | ] 39 | }, 40 | "include": [ 41 | "src", 42 | "eslint.config.mjs", 43 | "./playwright.config.ts", 44 | "tests", 45 | "*.js", 46 | "*.ts", 47 | "*.mts", 48 | "tests/**/*.ts", 49 | "fixtures/**/*.ts", 50 | "pages/**/*.ts", 51 | "helpers/**/*.ts", 52 | "enums/**/*.ts" 53 | ], 54 | "exclude": [ 55 | "node_modules", 56 | "dist", 57 | "playwright-report", 58 | "test-results" 59 | ] 60 | } -------------------------------------------------------------------------------- /tests/web/example/launchBrowserUsingConfig.spec.ts: -------------------------------------------------------------------------------- 1 | // Import all browser types from the Playwright library 2 | import { chromium, firefox, webkit, test } from "@playwright/test"; 3 | 4 | let browserType = 'msedge'; // <-- CHANGE THIS VALUE 5 | 6 | test("Launch browser using config", async () => { 7 | let browser; 8 | // --- Browser Launch Logic --- 9 | // Use a switch statement to select and launch the correct browser 10 | switch (browserType) { 11 | case 'chrome': 12 | browser = await chromium.launch({ 13 | headless: false 14 | }); 15 | break; 16 | 17 | case 'msedge': 18 | browser = await chromium.launch({ 19 | channel: 'msedge', // This is specific to chromium 20 | headless: false 21 | }); 22 | break; 23 | case 'firefox': 24 | browser = await firefox.launch({ 25 | headless: false 26 | }); 27 | break; 28 | case 'webkit': 29 | browser = await webkit.launch({ 30 | headless: false 31 | }); 32 | break; 33 | 34 | default: 35 | // If the browserType is not recognized, throw an error 36 | throw new Error(`Invalid browser type specified: ${browserType}. Use 'chrome', 'msedge', 'firefox', or 'webkit'.`); 37 | } 38 | 39 | // The rest of the code works for any browser 40 | const context = await browser.newContext(); 41 | const page = await context.newPage(); 42 | 43 | // Navigate to a test page 44 | await page.goto('https://playwright.dev/'); 45 | console.log(`${browserType} launched successfully and navigated to playwright.dev`); 46 | 47 | // Add a delay of 5 seconds so you can see the browser before it closes 48 | await page.waitForTimeout(5000); 49 | }) 50 | 51 | 52 | -------------------------------------------------------------------------------- /tests/mobile/example/healthcheckiOS.test.ts: -------------------------------------------------------------------------------- 1 | import { AppiumHelper } from "../../../helper/mobile/appiumHelper"; 2 | 3 | const iOSVersion = "16.4"; 4 | 5 | const capabilities = { 6 | platformName: "iOS", 7 | "appium:platformVersion": iOSVersion, 8 | "appium:deviceName": "iPhone 13 Pro Max", 9 | "appium:automationName": "XCUITest", 10 | "appium:app": "com.apple.Preferences", 11 | "appium:locale": "US", 12 | "appium:language": "en", 13 | }; 14 | 15 | // describe("Healthcheck iOS Appium connection", function () { 16 | // let app: AppiumHelper; 17 | 18 | // before(async () => { 19 | // app = new AppiumHelper(); 20 | // await app.init(capabilities); 21 | // }); 22 | 23 | // after(async () => { 24 | // await app.quit(); 25 | // }); 26 | 27 | // it("checks iOS version number on Settings App", async () => { 28 | // // Go the the "General" section 29 | // const generalElement = await app.findElement( 30 | // '//XCUIElementTypeCell[@name="General"]' 31 | // ); 32 | // await generalElement.click(); 33 | 34 | // // Go the the "About" section 35 | // const aboutElement = await app.findElement( 36 | // '//XCUIElementTypeCell[@name="About"]' 37 | // ); 38 | // await aboutElement.click(); 39 | 40 | // // Go the the "iOS Version" section 41 | // const versionElement = await app.findElement( 42 | // '//XCUIElementTypeCell[@name="iOS Version"]' 43 | // ); 44 | // await versionElement.click(); 45 | 46 | // // Check the version is the on expected 47 | // const iosVersionElement = await app.findElement( 48 | // `//XCUIElementTypeButton[contains(@name, "iOS ${iOSVersion}")]` 49 | // ); 50 | // const isDisplayed = await iosVersionElement.isDisplayed(); 51 | 52 | // if (!isDisplayed) { 53 | // throw new Error( 54 | // `Could not find iOS version label ${iOSVersion} on the device` 55 | // ); 56 | // } 57 | // }); 58 | // }); 59 | -------------------------------------------------------------------------------- /tests/web/example/route.spec.ts: -------------------------------------------------------------------------------- 1 | import test from "@playwright/test"; 2 | import { Console, log } from "console"; 3 | 4 | let fakePayloadOrders = { data: [], message: "No Users" }; 5 | 6 | test("Intercept Network call and Fake/Mock API response", async ({ page }) => { 7 | await page.goto("http://xxxx.com"); 8 | await page.route("http://xxxx.com/abc/ird", async (route) => { 9 | //go the response 10 | const response = await page.request.fetch(route.request()); 11 | let body = JSON.stringify(fakePayloadOrders); 12 | 13 | //send response to browser and override respond body 14 | route.fulfill({ 15 | status: 200, 16 | body: body, 17 | }); 18 | }); 19 | }); 20 | 21 | test("Intercept Network request", async ({ page }) => { 22 | await page.goto("http://xxxx.com"); 23 | 24 | //intercept request send to server & respond by url value passed in route.continue 25 | await page.route("http://xxxx.com/abc/ird", async (route) => { 26 | route.continue({ 27 | url: "http://xxxx.com/abc/123455", 28 | }); 29 | }); 30 | }); 31 | 32 | test.only("Capture Request and Response", async ({ page }) => { 33 | page.on("request", (request) => { 34 | console.log(request.url(), request.method()); 35 | }); 36 | page.on("requestfinished", (data) => { 37 | console.log(data); 38 | }); 39 | 40 | page.on("websocket", (data) => { 41 | console.log(data.url()); 42 | }); 43 | 44 | page.on("console", async (msg) => { 45 | const values = []; 46 | for (const arg of msg.args()) values.push(await arg.jsonValue()); 47 | console.log(...values); 48 | }); 49 | 50 | page.on("response", (response) => { 51 | // console.log(response?.status(), response?.body()); 52 | console.log(response?.status()); 53 | }); 54 | 55 | page.on("requestfailed", (request) => { 56 | console.log(request.url() + " " + request.failure()?.errorText); 57 | }); 58 | await page.goto("https://www.flipkart.com"); 59 | }); 60 | -------------------------------------------------------------------------------- /tests/mobile/example/healthcheckAndroid.test.ts: -------------------------------------------------------------------------------- 1 | import { AppiumHelper } from "../../../helper/mobile/appiumHelper"; 2 | 3 | const androidCapabilities = { 4 | platformName: "Android", 5 | "appium:automationName": "UiAutomator2", 6 | "appium:deviceName": "Android", 7 | "appium:appPackage": "com.android.settings", 8 | "appium:appActivity": ".Settings", 9 | "appium:locale": "US", 10 | "appium:language": "en", 11 | }; 12 | 13 | // describe("Healthcheck Android Appium connection", function () { 14 | // let app: AppiumHelper; 15 | 16 | // before(async () => { 17 | // app = new AppiumHelper(); 18 | // await app.init(androidCapabilities); 19 | // }); 20 | 21 | // after(async () => { 22 | // await app.quit(); 23 | // }); 24 | 25 | // it("checks battery level on Settings App", async () => { 26 | // const imageButton = await app.findElement("android.widget.ImageButton"); 27 | // await imageButton.click(); 28 | // const editText = await app.findElement( 29 | // "android=new UiSelector().className(android.widget.EditText)" 30 | // ); 31 | // await editText.click(); 32 | // await editText.setValue("bat"); 33 | // const linearLayout = await app.findElement( 34 | // "/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout[2]/android.widget.ScrollView/android.widget.LinearLayout/android.widget.ScrollView/android.widget.LinearLayout/android.widget.FrameLayout/android.support.v7.widget.RecyclerView/android.widget.LinearLayout[3]/android.widget.LinearLayout" 35 | // ); 36 | // await linearLayout.click(); 37 | // const progressBar = await app.findElement( 38 | // "android=new UiSelector().className(android.widget.ProgressBar)" 39 | // ); 40 | // await progressBar.isDisplayed(); 41 | // }); 42 | // }); 43 | -------------------------------------------------------------------------------- /tests/api/example/post.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | import { ApiHelper } from '../../../helper/api/apiHelper'; 3 | 4 | test.describe('Posts API', () => { 5 | let apiHelper: ApiHelper; 6 | 7 | test.beforeEach(async ({ request }) => { 8 | apiHelper = new ApiHelper(request); 9 | }); 10 | 11 | test('should get all posts', async () => { 12 | const response = await apiHelper.get('/posts'); 13 | expect(response.status).toBe(200); 14 | expect(Array.isArray(response.data)).toBeTruthy(); 15 | expect(response.data.length).toBeGreaterThan(0); 16 | }); 17 | 18 | test('should get a single post', async () => { 19 | const response = await apiHelper.get('/posts/1'); 20 | expect(response.status).toBe(200); 21 | expect(response.data.id).toBe(1); 22 | expect(response.data.title).toBeTruthy(); 23 | expect(response.data.body).toBeTruthy(); 24 | }); 25 | 26 | test('should create a new post', async () => { 27 | const postData = { 28 | title: 'Test Post', 29 | body: 'This is a test post', 30 | userId: 1 31 | }; 32 | const response = await apiHelper.post('/posts', postData); 33 | expect(response.status).toBe(201); 34 | expect(response.data.title).toBe(postData.title); 35 | expect(response.data.body).toBe(postData.body); 36 | expect(response.data.userId).toBe(postData.userId); 37 | expect(response.data.id).toBeTruthy(); 38 | }); 39 | 40 | test('should update a post', async () => { 41 | const updateData = { 42 | title: 'Updated Post', 43 | body: 'This post has been updated', 44 | userId: 1 45 | }; 46 | const response = await apiHelper.put('/posts/1', updateData); 47 | expect(response.status).toBe(200); 48 | expect(response.data.title).toBe(updateData.title); 49 | expect(response.data.body).toBe(updateData.body); 50 | }); 51 | 52 | test('should delete a post', async () => { 53 | const response = await apiHelper.delete('/posts/1'); 54 | expect(response.status).toBe(200); 55 | }); 56 | }); -------------------------------------------------------------------------------- /tests/load/BrowerLoadTest.spec.js: -------------------------------------------------------------------------------- 1 | const withTransactionTimer = async (transactionName, events, userActions) => { 2 | const startedTime = Date.now(); 3 | await userActions(); 4 | const difference = Date.now() - startedTime; 5 | events.emit("histogram", transactionName, difference); 6 | }; 7 | 8 | async function browserloadTest(page, userContext, events) { 9 | await withTransactionTimer("login", events, async () => { 10 | await page.goto( 11 | "https://opensource-demo.orangehrmlive.com/web/index.php/auth/login" 12 | ); 13 | await page.getByPlaceholder("Username").click(); 14 | await page.getByPlaceholder("Username").fill("Admin"); 15 | await page.getByPlaceholder("Password").fill("admin123"); 16 | await page.getByRole("button", { name: "Login" }).click(); 17 | await page.waitForLoadState("domcontentloaded"); 18 | }); 19 | 20 | await withTransactionTimer("OpenAdminPage", events, async () => { 21 | await page.getByRole("link", { name: "Admin" }).click(); 22 | await page.waitForLoadState("domcontentloaded"); 23 | }); 24 | 25 | await withTransactionTimer("OpenPimPage", events, async () => { 26 | await page.getByRole("link", { name: "PIM" }).click(); 27 | await page.waitForLoadState("domcontentloaded"); 28 | }); 29 | 30 | await withTransactionTimer("OpenTimePage", events, async () => { 31 | await page.getByRole("link", { name: "Time" }).click(); 32 | await page.waitForLoadState("domcontentloaded"); 33 | }); 34 | 35 | await withTransactionTimer("OpenRecruitmentPage", events, async () => { 36 | await page.getByRole("link", { name: "Recruitment" }).click(); 37 | await page.waitForLoadState("domcontentloaded"); 38 | }); 39 | 40 | await withTransactionTimer("OpenMyInfoPage", events, async () => { 41 | await page.getByRole("link", { name: "My Info" }).click(); 42 | await page.waitForLoadState("domcontentloaded"); 43 | }); 44 | 45 | await withTransactionTimer("OpenPerformancePage", events, async () => { 46 | await page.getByRole("link", { name: "Performance" }).click(); 47 | await page.waitForLoadState("domcontentloaded"); 48 | }); 49 | } 50 | 51 | module.exports = { browserloadTest }; 52 | -------------------------------------------------------------------------------- /tests/web/example/authenticationUsingAuthJSON.spec.ts: -------------------------------------------------------------------------------- 1 | import { test } from '@playwright/test'; 2 | 3 | // Run tests in this describe block sequentially (serial). If one test fails, 4 | // the remaining tests in the block are skipped. 5 | test.describe.serial('Auth Flow', () => { 6 | // First test logs in and saves the authenticated browser context state 7 | // to a file (auth.json) for reuse in subsequent tests. 8 | test('Authenticate and save state', async ({ page }) => { 9 | // Navigate to the login page 10 | await page.goto('https://www.saucedemo.com/'); 11 | 12 | // (Optional UI interaction) Focus/double-click area showing login hints 13 | await page.locator('[data-test="login-credentials"]').dblclick(); 14 | 15 | // Enter username and password 16 | await page.locator('[data-test="username"]').click(); 17 | await page.locator('[data-test="username"]').fill('standard_user'); 18 | await page.locator('[data-test="username"]').press('Tab'); 19 | await page.locator('[data-test="password"]').fill('secret_sauce'); 20 | 21 | // Submit the form and wait for a post-login UI element to be visible 22 | await page.locator('[data-test="login-button"]').click(); 23 | await page.locator("//*[@id='shopping_cart_container']").waitFor({ state: 'visible' }); 24 | 25 | // Persist the authenticated state to a file so it can be reused 26 | // by other tests without logging in again. 27 | await page.context().storageState({ path: 'auth.json' }); 28 | 29 | // Close the page 30 | await page.close(); 31 | }); 32 | 33 | // Apply the saved storage state to tests defined below this line 34 | test.use({ storageState: 'auth.json' }); 35 | 36 | // Second test reuses the saved auth state to directly access an authenticated page 37 | test('Use saved State and login into saucedemo website', async ({ page }) => { 38 | // With storageState applied, user should already be logged in 39 | await page.goto('https://www.saucedemo.com/inventory.html'); 40 | 41 | // Verify a logged-in UI element is present and visible 42 | await page.locator("//*[@id='shopping_cart_container']").waitFor({ state: 'visible' }); 43 | }); 44 | }) -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import tseslint from '@typescript-eslint/eslint-plugin'; 2 | import tsParser from '@typescript-eslint/parser'; 3 | import prettierPlugin from 'eslint-plugin-prettier'; 4 | import playwright from 'eslint-plugin-playwright'; 5 | import { fileURLToPath } from 'url'; 6 | import { dirname } from 'path'; 7 | 8 | const prettierConfig = { 9 | semi: true, 10 | tabWidth: 4, 11 | useTabs: false, 12 | printWidth: 80, 13 | singleQuote: true, 14 | trailingComma: 'es5', 15 | bracketSpacing: true, 16 | arrowParens: 'always', 17 | proseWrap: 'preserve', 18 | }; 19 | 20 | const __dirname = dirname(fileURLToPath(import.meta.url)); 21 | 22 | const config = [ 23 | { 24 | ignores: ['node_modules', 'dist', 'playwright-report', 'test-results'], 25 | }, 26 | { 27 | files: ['**/*.ts', '**/*.tsx'], 28 | languageOptions: { 29 | parser: tsParser, 30 | parserOptions: { 31 | ecmaVersion: 2020, 32 | sourceType: 'module', 33 | project: ['./tsconfig.json'], 34 | tsconfigRootDir: __dirname, 35 | }, 36 | }, 37 | plugins: { 38 | '@typescript-eslint': tseslint, 39 | prettier: prettierPlugin, 40 | playwright, 41 | }, 42 | rules: { 43 | ...(tseslint.configs.recommended?.rules ?? {}), 44 | ...(playwright.configs['flat/recommended']?.rules ?? {}), 45 | 'prettier/prettier': ['error', prettierConfig], 46 | '@typescript-eslint/explicit-function-return-type': 'error', 47 | '@typescript-eslint/no-explicit-any': 'error', 48 | '@typescript-eslint/no-unused-vars': [ 49 | 'error', 50 | { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }, 51 | ], 52 | 'no-console': 'error', 53 | 'prefer-const': 'error', 54 | '@typescript-eslint/no-inferrable-types': 'error', 55 | '@typescript-eslint/no-empty-function': 'error', 56 | '@typescript-eslint/no-floating-promises': 'error', 57 | 'playwright/missing-playwright-await': 'error', 58 | 'playwright/no-page-pause': 'error', 59 | 'playwright/no-useless-await': 'error', 60 | 'playwright/no-skipped-test': 'error', 61 | }, 62 | }, 63 | ]; 64 | 65 | export default config; 66 | -------------------------------------------------------------------------------- /tests/web/example/mockApi.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@tests/fixtures/test-options" 2 | 3 | test.use({ baseURL: 'https://practicesoftwaretesting.com' }) 4 | test.describe('Mock the brand category', () => { 5 | 6 | test('Validate category with subcategoty', async ({ page, request }) => { 7 | // 1. Set up the Route: Intercept calls to the specific API endpoint 8 | await page.route('https://api.practicesoftwaretesting.com/categories/tree', async (route) => { 9 | const response = await route.fetch(); 10 | const mockres = await request.get('http://localhost:3001/brands') 11 | const mockJson = await mockres.json() 12 | // 2. Fulfill the Route: Provide a fake JSON response 13 | await route.fulfill({ response: response, json: mockJson }); 14 | }) 15 | 16 | // 3. Navigate to the page that makes the API call 17 | await page.goto('/') 18 | await page.waitForTimeout(2000) 19 | // 4. Run the Test: Assert that the UI displays the mocked data 20 | await expect(page.getByRole('textbox', { name: 'Search' })).toBeVisible() 21 | await expect(page.getByRole('checkbox', { name: 'Hand Tools' })).not.toBeVisible() 22 | }) 23 | 24 | test('Fetch Fake Product Price', async ({ page, request }) => { 25 | await page.route('**/api/products', async route => { 26 | // 1. Fetch the real response 27 | const response = await route.fetch(); 28 | // 2. Get the JSON body 29 | const json = await response.json(); 30 | 31 | // 3. Modify the data (e.g., add a new item or change a field) 32 | json.push({ id: 999, name: 'Fake Special Offer', price: 0.00 }); 33 | 34 | // 4. Fulfill the route with the modified data 35 | await route.fulfill({ 36 | // Use the original response object to keep status, headers, etc. 37 | response, 38 | // Override the body with the modified JSON 39 | json, 40 | }); 41 | }); 42 | 43 | await page.goto('/products'); 44 | await expect(page.getByText('Fake Special Offer')).toBeVisible(); 45 | }) 46 | }) 47 | 48 | -------------------------------------------------------------------------------- /tests/web/orangeHrm.spec.ts: -------------------------------------------------------------------------------- 1 | //test 1 -> Login successful - Verify welcome message on Dashboard 2 | //test 2 - Message Invalid credentials 3 | //test 3 -> Error message password can not be empty 4 | //test 4 -> Error message username can not be empty 5 | //test 5 -> Verify Access to HR Administration Panel 6 | //Navigate to HR Administration panel (login as HR admin) 7 | //test 6 -> Verify Employee addition (login as HR admin) 8 | // Navigate to Employee Management section 9 | // Add a new Employee with required details 10 | // Save the employee record 11 | //test 7 -> Verify Employee deletion (login as HR admin) 12 | // Navigate to Employee Management section 13 | //select an existing employee and initiate the deletion process 14 | // confirm the deletion action 15 | //test 8 -> Verify Leave Application Submission (login as Employee) 16 | // Navigate to the leave section 17 | // Apply for leave by providing required details 18 | // submit the leave application 19 | //test 9 -> Verify Leave Approval(login as HR admin) 20 | // Navigate to leave Approval section 21 | // Review pending leave application 22 | // Approve or reject the leave 23 | //test 10 -> Verify Time Tracking Record Creation 24 | //Navigate to Time Tracking module 25 | // Click on Add Time Record button 26 | // Fill the details & Save the record 27 | //test 11 -> Verify Time Tracking Record Deletion 28 | //Navigate to Time Tracking module 29 | // find an existing time tracking record 30 | // Click on delete button 31 | //confirm the deletion 32 | //test 12 -> Validate attendance marking for an employee 33 | //Navigate to attendance module 34 | //Select an employee from the employee list 35 | //Mark attendance for that employee for the current date 36 | //test 13 -> Verify Job posting Creation (login as HR admin) 37 | //Navigate to Recruitment section 38 | //Create a new job posting 39 | //test 14 -> Verify Application Submission(login as job applicant) 40 | //Navigate to career section 41 | //Apply for specific job posting by submitting the application 42 | 43 | //test 15 -> Verify New Employee Onboarding(login as HR admin) 44 | //Navigate to Onboarding section 45 | //Initiate the onboarding process for a newly added employee 46 | //test 16 -> Verify Training Program Creation (login as HR admin) 47 | //Navigate to training section 48 | //create a new training program with all details 49 | -------------------------------------------------------------------------------- /src/utils/config/azure/playwright.service.config.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * This file enables Playwright client to connect to remote browsers. 3 | * It should be placed in the same directory as playwright.config.ts. 4 | */ 5 | 6 | import { defineConfig } from "@playwright/test"; 7 | import config from "./playwright.config"; 8 | import dotenv from "dotenv"; 9 | 10 | // Define environment on the dev box in .env file: 11 | // .env: 12 | // PLAYWRIGHT_SERVICE_ACCESS_TOKEN=XXX 13 | // PLAYWRIGHT_SERVICE_URL=XXX 14 | 15 | // Define environment in your GitHub workflow spec. 16 | // env: 17 | // PLAYWRIGHT_SERVICE_ACCESS_TOKEN: ${{ secrets.PLAYWRIGHT_SERVICE_ACCESS_TOKEN }} 18 | // PLAYWRIGHT_SERVICE_URL: ${{ secrets.PLAYWRIGHT_SERVICE_URL }} 19 | // PLAYWRIGHT_SERVICE_RUN_ID: ${{ github.run_id }}-${{ github.run_attempt }}-${{ github.sha }} 20 | 21 | dotenv.config(); 22 | 23 | // Name the test run if it's not named yet. 24 | process.env.PLAYWRIGHT_SERVICE_RUN_ID = process.env.PLAYWRIGHT_SERVICE_RUN_ID || new Date().toISOString(); 25 | 26 | // Can be 'linux' or 'windows'. 27 | const os = process.env.PLAYWRIGHT_SERVICE_OS || "linux"; 28 | 29 | export default defineConfig(config, { 30 | // Define more generous timeout for the service operation if necessary. 31 | // timeout: 60000, 32 | // expect: { 33 | // timeout: 10000, 34 | // }, 35 | workers: 20, 36 | 37 | // Enable screenshot testing and configure directory with expectations. 38 | // https://learn.microsoft.com/azure/playwright-testing/how-to-configure-visual-comparisons 39 | ignoreSnapshots: false, 40 | snapshotPathTemplate: `{testDir}/__screenshots__/{testFilePath}/${os}/{arg}{ext}`, 41 | 42 | use: { 43 | // Specify the service endpoint. 44 | connectOptions: { 45 | wsEndpoint: `${process.env.PLAYWRIGHT_SERVICE_URL}?cap=${JSON.stringify({ 46 | // Can be 'linux' or 'windows'. 47 | os, 48 | runId: process.env.PLAYWRIGHT_SERVICE_RUN_ID, 49 | })}`, 50 | timeout: 30000, 51 | headers: { 52 | "x-mpt-access-key": process.env.PLAYWRIGHT_SERVICE_ACCESS_TOKEN!, 53 | }, 54 | // Allow service to access the localhost. 55 | exposeNetwork: "", 56 | }, 57 | }, 58 | // Tenmp workaround for config merge bug in OSS https://github.com/microsoft/playwright/pull/28224 59 | projects: config.projects ? config.projects : [{}], 60 | }); 61 | -------------------------------------------------------------------------------- /config.ts: -------------------------------------------------------------------------------- 1 | import * as dotenv from "dotenv" 2 | import Joi from "joi" 3 | import {z} from "zod" 4 | 5 | //Load enviornment variables from .env 6 | dotenv.config() 7 | 8 | 9 | //Define the schema for validation 10 | // const envSchema = Joi.object({ 11 | // //Enironment 12 | // BASE_URL : Joi.string().uri().required(), 13 | // USER_NAME: Joi.string().required(), 14 | // PASSWORD: Joi.string().required(), 15 | 16 | // //Playwright settings 17 | // WORKERS : Joi.number().integer().min(1).required(), 18 | // RETRY_FAILED : Joi.number().integer().min(0).required(), 19 | // MAX_TEST_RUNTIME : Joi.number().integer().min(1000).required(), 20 | // HEADLESS_BROWSER : Joi.boolean().required() 21 | // }).unknown(true) //Allow other unknow enviornment variables 22 | 23 | // Define the schema for validation using Zod 24 | // const envSchema = z.object({ 25 | // // Environment 26 | // BASE_URL: z.string().url().nonempty(), 27 | // USER_NAME: z.string().nonempty(), 28 | // PASSWORD: z.string().nonempty(), 29 | 30 | // // Playwright settings 31 | // WORKERS: z.number().int().min(1), 32 | // RETRY_FAILED: z.number().int().min(0), 33 | // MAX_TEST_RUNTIME: z.number().int().min(1000), 34 | // HEADLESS_BROWSER: z.boolean() 35 | // }).passthrough(); //Allow unknonn environment variables 36 | 37 | //.strict() //Allow only known environment variables 38 | 39 | //To Do - Convert above validation schema in zod 40 | 41 | 42 | //Validate environment variables 43 | // const envVars = envSchema.validate(process.env,{allowUnknown:true,abortEarly:false}) 44 | // const envVars = envSchema.safeParse(process.env) 45 | 46 | // if (envVars.error){ 47 | // throw new Error(`Enviornment validation error : ${envVars.error.message}`) 48 | // } 49 | 50 | //Config class with validate enviornment variables 51 | export class Config{ 52 | 53 | static readonly BASE_URL :string = process.env.BASE_URL || ''; 54 | static readonly USER_NAME :string = process.env.USER_NAME || ''; 55 | static readonly PASSWORD :string = process.env.PASSWORD || ''; 56 | 57 | static readonly WORKERS :number = parseInt(process.env.WORKERS || '1'); 58 | static readonly RETRY_FAILED :number = parseInt(process.env.RETRY_FAILED || '0'); 59 | static readonly MAX_TEST_RUNTIME :number = parseInt(process.env.MAX_TEST_RUNTIME || '1000'); 60 | static readonly HEADLESS_BROWSER :boolean = process.env.HEADLESS_BROWSER === 'true'; 61 | } 62 | 63 | export const MCP_CONFIG = { 64 | mcpServers: { 65 | playwright: { 66 | url: "http://localhost:8931/sse" 67 | } 68 | } 69 | }; -------------------------------------------------------------------------------- /tests/web/example/alert.spec.ts: -------------------------------------------------------------------------------- 1 | import { logInfo } from "@src/utils/report/Logger"; 2 | import { expect, test } from "@tests/fixtures/test-options"; 3 | 4 | const baseURL = 'https://test-edzotech.web.app/index.html'; 5 | 6 | 7 | test("Test 0 : Subscribe on dialog and call dismiss by clicking on Ok button", async ({ 8 | web,page 9 | }) => { 10 | 11 | page.on('dialog', dialog => { 12 | logInfo('Script Alert ' + dialog.message()); 13 | dialog.accept(); 14 | }) 15 | 16 | await web.navigateToUrl(baseURL) 17 | 18 | const element = await web.findElement('xpath', "//*[@id='show-alert-btn']"); 19 | 20 | expect(element).not.toBeNull(); 21 | 22 | await element!.click(); 23 | }); 24 | 25 | test("Test 1 : Subscribe on dialog and call dismiss by clicking on Ok button", async ({ 26 | web 27 | }) => { 28 | 29 | await web.navigateToUrl(baseURL) 30 | 31 | const element = await web.findElement('xpath', "//*[@id='show-alert-btn']"); 32 | 33 | expect(element).not.toBeNull(); 34 | 35 | await Promise.all([ 36 | web.waitAndHandleDialog('accept'), // or 'dismiss', with optional prompt text 37 | await element!.click(), // this triggers the dialog 38 | ]); 39 | }); 40 | 41 | test("Test 2 : Subscribe on dialog and call accept by clicking on Ok button", async ({ 42 | web, page 43 | }) => { 44 | 45 | page.on('dialog', dialog => { 46 | logInfo('Script Alert ' + dialog.message()); 47 | dialog.accept(); 48 | }) 49 | 50 | await web.navigateToUrl(baseURL) 51 | 52 | const element = await web.findElement('xpath', "//*[@id='show-confirm-btn']"); 53 | 54 | await element!.click(); 55 | 56 | }); 57 | 58 | test("Test 3 : Subscribe on dialog and dismiss by clicking on Cancel button", async ({ 59 | web, page 60 | }) => { 61 | 62 | page.on('dialog', dialog => { 63 | logInfo('Script Alert ' + dialog.message()); 64 | dialog.dismiss(); 65 | }) 66 | 67 | await web.navigateToUrl(baseURL) 68 | 69 | const element = await web.findElement('xpath', "//*[@id='show-confirm-btn']"); 70 | 71 | await element!.click(); 72 | 73 | }); 74 | 75 | test("Test 4 : Subscribe on Prompt, enter text in input box and call accept by clicking on Ok button", async ({ 76 | web, page 77 | }) => { 78 | 79 | let textVal = ""; 80 | page.on('dialog', dialog => { 81 | logInfo('Script Alert ' + dialog.message()); 82 | dialog.accept(textVal); 83 | }) 84 | 85 | await web.navigateToUrl(baseURL) 86 | 87 | const element = await web.findElement('xpath', "//*[@id='show-prompt-btn']"); 88 | 89 | await element?.click(); 90 | 91 | 92 | textVal = "Enter value in Prompt" 93 | }); 94 | -------------------------------------------------------------------------------- /tests/api/example/api1.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | import { api } from './utils/apiHelper'; 3 | import { handleError } from './utils/errorHandler'; 4 | 5 | interface Post { 6 | userId: number; 7 | id: number; 8 | title: string; 9 | body: string; 10 | } 11 | 12 | interface User { 13 | id: number; 14 | name: string; 15 | username: string; 16 | email: string; 17 | } 18 | 19 | test.describe('JSONPlaceholder API Tests', () => { 20 | test('should get all posts', async () => { 21 | try { 22 | const response = await api.get('/posts'); 23 | expect(response.status).toBe(200); 24 | expect(Array.isArray(response.data)).toBeTruthy(); 25 | expect(response.data.length).toBeGreaterThan(0); 26 | 27 | const firstPost = response.data[0]; 28 | expect(firstPost).toHaveProperty('id'); 29 | expect(firstPost).toHaveProperty('title'); 30 | expect(firstPost).toHaveProperty('body'); 31 | expect(firstPost).toHaveProperty('userId'); 32 | } catch (error) { 33 | handleError(error); 34 | } 35 | }); 36 | 37 | test('should create a new post', async () => { 38 | try { 39 | const newPost = { 40 | title: 'Test Post', 41 | body: 'This is a test post', 42 | userId: 1 43 | }; 44 | 45 | const response = await api.post('/posts', newPost); 46 | expect(response.status).toBe(201); 47 | expect(response.data).toHaveProperty('id'); 48 | expect(response.data.title).toBe(newPost.title); 49 | expect(response.data.body).toBe(newPost.body); 50 | } catch (error) { 51 | handleError(error); 52 | } 53 | }); 54 | 55 | test('should update a post', async () => { 56 | try { 57 | const updatedPost = { 58 | title: 'Updated Post', 59 | body: 'This post has been updated', 60 | userId: 1 61 | }; 62 | 63 | const response = await api.put('/posts/1', updatedPost); 64 | expect(response.status).toBe(200); 65 | expect(response.data.title).toBe(updatedPost.title); 66 | expect(response.data.body).toBe(updatedPost.body); 67 | } catch (error) { 68 | handleError(error); 69 | } 70 | }); 71 | 72 | test('should delete a post', async () => { 73 | try { 74 | const response = await api.delete<{}>('/posts/1'); 75 | expect(response.status).toBe(200); 76 | } catch (error) { 77 | handleError(error); 78 | } 79 | }); 80 | 81 | test('should handle 404 error gracefully', async () => { 82 | try { 83 | await api.get('/nonexistent-endpoint'); 84 | } catch (error) { 85 | expect(error.statusCode).toBe(404); 86 | } 87 | }); 88 | }); -------------------------------------------------------------------------------- /tests/web/example/launchMultipleBrowserInSingle.spec.js: -------------------------------------------------------------------------------- 1 | import { chromium, firefox, test } from '@playwright/test' 2 | 3 | test('Launch Multiple Browser In Single Spec', async () => { 4 | const browser1 = await chromium.launch({ 5 | channel: 'chrome' //Use system installed chrome 6 | , headless: false //Show browser 7 | }); 8 | 9 | const browser2 = await firefox.launch({ 10 | channel: 'firefox' //Use system installed chrome 11 | , headless: false //Show browser 12 | }); 13 | 14 | const browser3 = await chromium.launch({ 15 | channel: 'msedge' //Use system installed chrome 16 | , headless: false //Show browser 17 | }); 18 | 19 | 20 | const page1 = browser1.newPage(); 21 | const page2 = browser2.newPage(); 22 | const page3 = browser3.newPage(); 23 | 24 | await page1.goto('https://www.google.com'); 25 | await page2.goto('https://www.google.com'); 26 | await page3.goto('https://www.google.com'); 27 | 28 | console.log(`Page 1 Title: await ${await page1.title()}`); 29 | console.log(`Page 2 Title: await ${await page2.title()}`); 30 | console.log(`Page 3 Title: await ${await page3.title()}`); 31 | 32 | await browser1.close(); 33 | await browser2.close(); 34 | await browser3.close(); 35 | }); 36 | 37 | 38 | test('Run Different Navigate Method', async () => { 39 | const browser = await chromium.launch({ 40 | channel: 'chrome' //Use system installed chrome 41 | , headless: false //Show browser 42 | }); 43 | 44 | const page = await browser.newPage(); 45 | 46 | 47 | // 1. Navigate to a URL using page.goto() 48 | console.log('Navigating to first page: example.com'); 49 | await page.goto('https://example.com'); 50 | await page.waitForTimeout(2000); // Wait for 2 seconds to see the page 51 | 52 | // 2. Navigate to a second URL 53 | console.log('Navigating to second page: playwright.dev'); 54 | await page.goto('https://playwright.dev'); 55 | await page.waitForTimeout(2000); 56 | 57 | // 3. Go back in browser history using page.goBack() 58 | console.log('Going back to example.com'); 59 | await page.goBack(); 60 | await page.waitForTimeout(2000); 61 | 62 | // 4. Go forward in browser history using page.goForward() 63 | console.log('Going forward to playwright.dev'); 64 | await page.goForward(); 65 | await page.waitForTimeout(2000); 66 | 67 | // 5. Reload the current page using page.reload() 68 | console.log('Reloading the current page'); 69 | await page.reload(); 70 | await page.waitForTimeout(2000); 71 | 72 | // Close the browser 73 | await browser.close(); 74 | console.log('Browser closed.'); 75 | }); -------------------------------------------------------------------------------- /src/utils/config/azure/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from "@playwright/test"; 2 | import * as dotenv from "dotenv"; 3 | 4 | switch (process.env.NODE_ENV) { 5 | case "local": 6 | dotenv.config({ path: "./environments/local.env" }); 7 | break; 8 | case "dev": 9 | dotenv.config({ path: "./environments/local.env" }); 10 | break; 11 | case "qa": 12 | dotenv.config({ path: "./environments/qa.env" }); 13 | break; 14 | case "prod": 15 | dotenv.config({ path: "./environments/prod.env" }); 16 | 17 | break; 18 | 19 | default: 20 | break; 21 | } 22 | export default defineConfig({ 23 | testDir: "./src", 24 | /* Run tests in files in parallel */ 25 | fullyParallel: true, 26 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 27 | forbidOnly: !!process.env.CI, 28 | /* Retry on CI only */ 29 | retries: process.env.CI ? 2 : 0, 30 | /* Opt out of parallel tests on CI. */ 31 | workers: process.env.CI ? 1 : undefined, 32 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 33 | reporter: "html", 34 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 35 | use: { 36 | /* Base URL to use in actions like `await page.goto('/')`. */ 37 | // baseURL: 'http://127.0.0.1:3000', 38 | 39 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 40 | trace: "on-first-retry", 41 | }, 42 | 43 | /* Configure projects for major browsers */ 44 | projects: [ 45 | // { 46 | // name: "chromium", 47 | // use: { ...devices["Desktop Chrome"] }, 48 | // }, 49 | 50 | // { 51 | // name: "firefox", 52 | // use: { ...devices["Desktop Firefox"] }, 53 | // }, 54 | 55 | // { 56 | // name: "webkit", 57 | // use: { ...devices["Desktop Safari"] }, 58 | // }, 59 | 60 | /* Test against mobile viewports. */ 61 | // { 62 | // name: 'Mobile Chrome', 63 | // use: { ...devices['Pixel 5'] }, 64 | // }, 65 | // { 66 | // name: 'Mobile Safari', 67 | // use: { ...devices['iPhone 12'] }, 68 | // }, 69 | 70 | /* Test against branded browsers. */ 71 | { 72 | name: "Microsoft Edge", 73 | use: { ...devices["Desktop Edge"], channel: "msedge" }, 74 | }, 75 | // { 76 | // name: 'Google Chrome', 77 | // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, 78 | // }, 79 | ], 80 | 81 | /* Run your local dev server before starting the tests */ 82 | // webServer: { 83 | // command: 'npm run start', 84 | // url: 'http://127.0.0.1:3000', 85 | // reuseExistingServer: !process.env.CI, 86 | // }, 87 | }); 88 | -------------------------------------------------------------------------------- /tests/web/IntercpetFailingAPI.js: -------------------------------------------------------------------------------- 1 | //Create a setup.ts File (Global Hook) 2 | //Create a new file named setup.ts inside your tests/ folder (or any preferred directory). This file will extend Playwright’s test configuration. 3 | 4 | import { test as base } from '@playwright/test'; 5 | 6 | // Extend Playwright's test object to include API monitoring in all tests 7 | export const test = base.extend<{ page: any }>({ 8 | page: async ({ browser }, use) => { 9 | const context = await browser.newContext(); 10 | const page = await context.newPage(); 11 | 12 | // Listen to all network requests 13 | page.on('request', request => { 14 | console.log(`➡️ Request: ${request.method()} ${request.url()}`); 15 | }); 16 | 17 | // Intercept API responses and check for failures 18 | page.on('response', async response => { 19 | if (!response.ok()) { 20 | console.error(`❌ Failed API Call: ${response.url()}`); 21 | console.error(`Status Code: ${response.status()}`); 22 | 23 | const request = response.request(); 24 | const requestData = request.postData(); 25 | const responseBody = await response.text(); 26 | 27 | console.error(`📌 Request Data: ${requestData || 'No Body'}`); 28 | console.error(`📌 Response Body: ${responseBody || 'No Response Body'}`); 29 | } 30 | }); 31 | 32 | await use(page); // Makes `page` available for all tests 33 | await page.close(); 34 | } 35 | }); 36 | 37 | export { expect } from '@playwright/test'; 38 | 39 | 40 | // Use the Extended Test in Your Spec Files 41 | // Modify all your spec files (*.spec.ts) to import the extended test object from setup.ts instead of Playwright's default test. 42 | 43 | // Example: tests/example.spec.ts 44 | 45 | import { expect } from '../setup'; // Import from setup.ts 46 | 47 | test.describe('UI Test Suite with API Monitoring', () => { 48 | test('Test Case 1 - Navigate to Homepage', async ({ page }) => { 49 | await page.goto('https://example.com'); 50 | await page.click('button#submit'); 51 | await page.waitForTimeout(2000); 52 | }); 53 | 54 | test('Test Case 2 - Login Flow', async ({ page }) => { 55 | await page.goto('https://example.com/login'); 56 | await page.fill('input[name="username"]', 'testUser'); 57 | await page.fill('input[name="password"]', 'password123'); 58 | await page.click('button#login'); 59 | await page.waitForTimeout(2000); 60 | }); 61 | }); 62 | 63 | 64 | //Prompt 65 | //Write playwright script using typescript which should log all API requests failures across test files when UI test is running. -------------------------------------------------------------------------------- /tests/web/example/ConsoleAndPageError.spec.ts: -------------------------------------------------------------------------------- 1 | import { test} from "@tests/fixtures/test-options"; 2 | import { expect } from "@playwright/test"; 3 | 4 | // @ts-check // Enables type checking in JavaScript files, good practice for Playwright tests. 5 | test 6 | 7 | // Array to store console log messages of type 'error'. 8 | const logs: { 9 | message: string; 10 | type: string; 11 | }[] = [] 12 | 13 | // Array to store unhandled page errors (exceptions). 14 | const errorMsg: { 15 | name: string, 16 | message: string 17 | }[] = [] 18 | 19 | // Group tests related to error and console log checks. 20 | test.describe('error and console log check', () => { 21 | 22 | // This block runs before each test in this describe block. 23 | test.beforeEach(async ({ page }) => { 24 | 25 | // --- Console Log Listener --- 26 | // Attaches an event listener for 'console' events on the page. 27 | page.on('console', (msg) => { 28 | // Filter and store only console messages of type 'error'. 29 | if (msg.type() == 'error') { 30 | logs.push({ message: msg.text(), type: msg.type() }) 31 | } 32 | }) 33 | 34 | // --- Page Error Listener --- 35 | // Attaches an event listener for unhandled 'pageerror' exceptions in the page's execution context. 36 | page.on('pageerror', (error) => { 37 | // Store the name and message of the unhandled error. 38 | errorMsg.push({ name: error.name, message: error.message }) 39 | }) 40 | 41 | }) 42 | 43 | 44 | test('Check the exception and logs in console log', async ({ page }, testInfo) => { 45 | 46 | // Navigate to the target URL. This action will trigger the listeners 47 | // for any console errors or unhandled exceptions that occur during load. 48 | await page.goto('https://timesofindia.indiatimes.com/') 49 | 50 | 51 | // Format the collected console errors into a single string for attachment. 52 | const errorLogs = [...logs?.map(e => e.message)].join('\n') 53 | 54 | // If console errors were found, attach them to the test report for debugging. 55 | if (logs?.length > 0) { 56 | await testInfo.attach('console logs', { 57 | body: errorLogs.toString(), 58 | contentType: 'text/plain' 59 | }) 60 | } 61 | 62 | // --- Assertions --- 63 | 64 | // Soft assertion: Checks if no console errors were logged. 65 | // The test continues if this fails, but it's marked as a failure. 66 | expect.soft(logs.length).toBe(0) 67 | 68 | // Hard assertion: Checks if no unhandled page errors/exceptions occurred. 69 | // The test fails immediately if this condition is not met. 70 | expect(errorMsg.length).toBe(0) 71 | 72 | }) 73 | 74 | }) -------------------------------------------------------------------------------- /tests/web/example/locator.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "@playwright/test"; 2 | 3 | /* playwright built in locators 4 | example code is run on website -> https://opensource-demo.orangehrmlive.com/web/index.php/auth/login 5 | In case you want to read more about role refer -> https://www.w3.org/TR/wai-aria-1.2/#role_definitions*/ 6 | 7 | test("test locator getByRole() @locator", async ({ page: page }) => { 8 | /*supported role in playwright: 9 | "alert"|"alertdialog"|"application"|"article"|"banner"|"blockquote"|"button"| 10 | "caption"|"cell"|"checkbox"|"code"|"columnheader"|"combobox"|"complementary"|"contentinfo"|"definition"| 11 | "deletion"|"dialog"|"directory"|"document"|"emphasis"|"feed"|"figure"|"form"|"generic"|"grid"|"gridcell"| 12 | "group"|"heading"|"img"|"insertion"|"link"|"list"|"listbox"|"listitem"|"log"|"main"|"marquee"|"math"|"meter"| 13 | "menu"|"menubar"|"menuitem"|"menuitemcheckbox"|"menuitemradio"|"navigation"|"none"|"note"|"option"|"paragraph"| 14 | "presentation"|"progressbar"|"radio"|"radiogroup"|"region"|"row"|"rowgroup"|"rowheader"|"scrollbar"|"search"|" 15 | "searchbox"|"separator"|"slider"|"spinbutton"|"status"|"strong"|"subscript"|"superscript"|"switch"|"tab"|"table"| 16 | "tablist"|"tabpanel"|"term"|"textbox"|"time"|"timer"|"toolbar"|"tooltip"|"tree"|"treegrid"|"treeitem" 17 | */ 18 | 19 | await page.goto( 20 | "https://opensource-demo.orangehrmlive.com/web/index.php/auth/login" 21 | ); 22 | await page.getByRole("textbox", { name: "username" }).fill("Admin"); 23 | await page.getByRole("textbox", { name: "password" }).fill("admin123"); 24 | await page.getByRole("button", { name: "Login" }).click({ 25 | button: "left", 26 | }); 27 | }); 28 | 29 | test("test locator getByPlaceholder() @locator", async ({ page }) => { 30 | await page.goto( 31 | "https://opensource-demo.orangehrmlive.com/web/index.php/auth/login" 32 | ); 33 | await page.getByPlaceholder("Username").fill("Adminplaceholder"); 34 | await page.getByPlaceholder("Password").fill("admin123"); 35 | 36 | await page.getByPlaceholder(/Username/).fill("Adminregexp"); 37 | await page.getByPlaceholder(/Password/).fill("admin123"); 38 | 39 | await page.getByPlaceholder(/username/i).fill("Admin_reg_ex_ignorecase"); 40 | await page.getByPlaceholder(/Password/i).fill("admin123"); 41 | 42 | await page.getByRole("button", { name: "Login" }).click({ 43 | button: "left", 44 | }); 45 | 46 | await page.getByText("Invalid credentials").click(); 47 | expect(await page.getByText("Invalid credentials").count()).toBe(1); 48 | }); 49 | 50 | test("test locator getByText() @locator", async ({ page }) => {}); 51 | test("test locator getByLabel() @locator", async ({ page }) => {}); 52 | 53 | test("test locator getByAltText() @locator", async ({ page }) => {}); 54 | test("test locator getByTitle() @locator", async ({ page }) => {}); 55 | test("test locator getByTestId() @locator", async ({ page }) => {}); 56 | -------------------------------------------------------------------------------- /src/utils/reader/jsonReader.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | import { logInfo } from '../report/Logger'; 4 | 5 | export class JsonReader { 6 | private filePath: string; 7 | public jsonData: unknown; 8 | 9 | constructor(filePath: string) { 10 | // Resolve the path relative to the project root 11 | // Adjust the path resolution based on where the file is located 12 | this.filePath = path.resolve(__dirname, '..', '..', '..', 'tests', 'resources', 'uiMap', filePath); 13 | logInfo('Reading JSON from:', this.filePath); 14 | this.readJsonFile(); 15 | } 16 | 17 | /** 18 | * Reads a JSON file from the given path and returns its content as an object. 19 | * @returns Parsed JSON object or null if an error occurs. 20 | */ 21 | readJsonFile(): unknown { 22 | try { 23 | if (!fs.existsSync(this.filePath)) { 24 | console.error(`File not found: ${this.filePath}`); 25 | return false; 26 | } 27 | 28 | const fileContent = fs.readFileSync(this.filePath, 'utf-8'); 29 | this.jsonData = JSON.parse(fileContent); 30 | logInfo(this.jsonData); 31 | } catch (error) { 32 | console.error("Error reading or parsing JSON file:", error); 33 | this.jsonData = null; 34 | return false; 35 | } 36 | } 37 | 38 | /** 39 | * Retrieves a value from a nested JSON object using a JSON path hierarchy. 40 | * @param jsonObj - The JSON object. 41 | * @param path - The dot-separated JSON path (e.g., "user.address.city"). 42 | * @param key - The specific key to retrieve from the parent object (e.g., "city"). 43 | * @returns The value at the specified key in the parent object or undefined if not found. 44 | */ 45 | getJsonValue(path: string, key: string): string | undefined { 46 | if (!this.jsonData) { 47 | logInfo("No Json Data loaded") 48 | return undefined; 49 | } 50 | 51 | // First navigate to the parent object using the path 52 | const parentObject = path.split('.').reduce((acc, part) => { 53 | if (acc && acc[part] !== undefined) { 54 | return acc[part]; 55 | } 56 | return undefined; 57 | }, this.jsonData); 58 | 59 | // If we found the parent object, return the value for the specific key 60 | if (parentObject && parentObject[key] !== undefined) { 61 | return parentObject[key]; 62 | } 63 | logInfo(`Path or key not found: ${path}.${key}`); 64 | 65 | return undefined; 66 | } 67 | } 68 | 69 | 70 | // Example usage 71 | 72 | 73 | // function readData() { 74 | // const jsonReader = new JsonReader('./orangeHRM.json'); // Update with your actual JSON file path 75 | // // const jsonData = jsonReader.readJsonFile(); 76 | // if (jsonReader.jsonData) { 77 | // const value = jsonReader.getJsonValue("App.Login.OrangeHRMLogo", 'locatorValue'); // Example: Reading "city" from "user.address.city" 78 | // logInfo("Extracted Value:", value); 79 | // } 80 | // } 81 | 82 | // readData(); -------------------------------------------------------------------------------- /tests/web/example/inputFieldOperation.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "../../fixtures/customFixtures" 2 | import type { TestInfo } from "../../fixtures/customFixtures" 3 | import { logInfo } from '../../../src/utils/report/Logger'; 4 | 5 | test("Perform operation on Input field using fill()", async ({ web }, testInfo: TestInfo) => { 6 | //Navigate to URL 7 | await web.navigateToUrl("https://test-edzotech.web.app/index.html"); 8 | 9 | //Find locator 10 | const nameInputField = await web.webPage.locator("//input[@id='name']"); 11 | 12 | //Check if enabled 13 | const isInputFieldEnabled = await nameInputField.isEnabled(); 14 | logInfo("Is input field enabled?", isInputFieldEnabled); //returns true 15 | 16 | //Check if visible 17 | const isInputFieldVisible = await nameInputField.isVisible(); 18 | logInfo("Is input field visible?", isInputFieldVisible); //returns true 19 | 20 | //Fill data 21 | await nameInputField.fill("ABCD123"); 22 | 23 | //Read value using textContent() 24 | const textOne = await nameInputField.textContent(); 25 | 26 | //Read value using innerText() 27 | const textTwo = await nameInputField.innerText(); 28 | 29 | //Read value using innerHTML() 30 | const textThree = await nameInputField.innerHTML(); 31 | 32 | //Read value using inputValue() 33 | let textFour = await nameInputField.inputValue(); 34 | 35 | //Log value using textContent() 36 | logInfo("Read value using textContent()", textOne); //returns blank 37 | 38 | //Log value using innerText() 39 | logInfo("Read value using innerText()", textTwo); //returns blank 40 | 41 | //Log value using innerHTML() 42 | logInfo("Read value using innerHTML()", textThree); //returns blank 43 | 44 | //Log value using inputValue() 45 | logInfo("Read value using inputValue()", textFour); //returns ABCD123 46 | 47 | // Assertion to verify the value was set correctly 48 | await expect(nameInputField).toHaveValue('ABCD123'); 49 | 50 | //Clear input field 51 | await nameInputField.clear(); 52 | 53 | //Read value using inputValue() after clear() 54 | textFour = await nameInputField.inputValue(); 55 | 56 | //Log value using inputValue() after clear() 57 | logInfo("Read value using inputValue() after clear()", textFour); //returns '' 58 | }); 59 | 60 | 61 | test("Perform operation on Input field using pressSequentially()", async ({ web }, testInfo: TestInfo) => { 62 | //Navigate to URL 63 | await web.navigateToUrl("https://test-edzotech.web.app/index.html"); 64 | 65 | //Find locator 66 | const nameInputField = await web.webPage.locator("//input[@id='name']"); 67 | 68 | //Enter text on interval of 1 second 69 | await nameInputField.pressSequentially("ABCD123",{delay:1000}); 70 | 71 | //Assertion to verify the value was set correctly 72 | expect(nameInputField).toHaveValue('ABCD123'); 73 | }) 74 | 75 | 76 | test.only('Perofrm operation on Input field using press',async({web},testInfo:TestInfo)=>{ 77 | //Navigate to URL 78 | await web.navigateToUrl("https://test-edzotech.web.app/index.html"); 79 | 80 | //Find locator 81 | const nameInputField = await web.webPage.locator("//input[@id='name']"); 82 | 83 | //Press the Enter key 84 | await nameInputField.press("Enter"); 85 | }) -------------------------------------------------------------------------------- /tests/web/example/accessibilitytest.spec.ts: -------------------------------------------------------------------------------- 1 | import AxeBuilder from "@axe-core/playwright"; 2 | import fs from 'fs' 3 | import { createHtmlReport } from 'axe-html-reporter' 4 | 5 | import { test,expect } from "@tests/fixtures/test-options"; 6 | 7 | //the following WCAG are the default from lighthouse 8 | const lightHouseTags:string[] =[ 'wcag2a','wcag2aa'] 9 | 10 | //define rules for each page, you can disable specific rule by using option in AxeBuilder 11 | const rules:string[] =["accesskeys","aria-allowed-attr","aria-allowed-role","aria-command-name", 12 | "aria-conditional-attr","aria-deprecated-role","aria-dialog-name","aria-hidden-body", 13 | "aria-hidden-focus","aria-input-field-name","aria-meter-name","aria-progressbar-name", 14 | "aria-prohibited-attr","aria-required-attr","aria-required-children","aria-required-parent", 15 | "aria-roles","aria-text","aria-toggle-field-name","aria-tooltip-name","aria-treeitem-name", 16 | "aria-valid-attr-value","aria-valid-attr","button-name","bypass","color-contrast", 17 | "definition-list","dlitem","document-title","duplicate-id-aria","form-field-multiple-labels", 18 | "frame-title","heading-order","html-has-lang","html-lang-valid","html-xml-lang-mismatch", 19 | "image-alt","image-redundant-alt","input-button-name","input-image-alt","label", 20 | "link-in-text-block","link-name","list","listitem","meta-refresh","meta-viewport", 21 | "object-alt","select-name","skip-link","tabindex","table-duplicate-name","target-size", 22 | "td-headers-attr","th-has-data-cells","valid-lang","video-caption", 23 | //"focusable-controls", 24 | "interactive-element-affordance","logical-tab-order","visual-order-follows-dom", 25 | "focus-traps","managed-focus","use-landmarks","offscreen-content-hidden", 26 | "custom-controls-labels","custom-controls-roles","empty-heading","identical-links-same-purpose", 27 | "landmark-one-main","label-content-name-mismatch","table-fake-caption", 28 | "td-has-header" 29 | ] 30 | 31 | test('product listing page', async ({ web, page }, testInfo) => { 32 | await web.navigateToUrl('https://www.saucedemo.com/') 33 | await web.webPage.getByRole('textbox', { name: 'Username'}).fill('standard_user') 34 | await web.webPage.getByRole('textbox', { name: 'Password'}).fill('secret_sauce') 35 | await web.webPage.getByRole('button', { name: 'Login'}).click() 36 | await expect(web.webPage.getByText('Swag Labs')).toBeVisible() 37 | 38 | await test.step('Check the accessablity test', async () => { 39 | const report = await new AxeBuilder({page}) 40 | .withTags(lightHouseTags) 41 | .withRules(rules) 42 | .analyze() 43 | 44 | //attach the violation output to each test results 45 | testInfo.attach(testInfo.title,{ 46 | body: JSON.stringify(report.violations, null, 2), 47 | contentType: 'application/json' 48 | }) 49 | 50 | //create the axe-core html report 51 | const htmlReport = createHtmlReport({ 52 | results: report, 53 | options:{ 54 | projectKey: testInfo.title, 55 | doNotCreateReportFile: true 56 | } 57 | }); 58 | 59 | //write the html report for each page 60 | write_accesability_output(`${testInfo.title.replaceAll(' ','_')}`, htmlReport) 61 | 62 | expect(report.violations).toHaveLength(0) 63 | }) 64 | 65 | }) 66 | 67 | function write_accesability_output(path:string, htmlReport: string){ 68 | 69 | if (!fs.existsSync('test-results/reports')) { 70 | fs.mkdirSync('test-results/reports', { 71 | recursive: true, 72 | }); 73 | } 74 | fs.writeFileSync(`test-results/reports/${path}.html`, htmlReport); 75 | 76 | } 77 | -------------------------------------------------------------------------------- /tests/web/example/trackApiResponse.spec.js: -------------------------------------------------------------------------------- 1 | // example.spec.ts 2 | // We import our custom 'test' object from the fixtures file. 3 | // This is the key step to use the extended 'page' fixture. 4 | import { test } from './fixtures'; 5 | import { expect } from '@playwright/test'; 6 | import { chromium, test } from "@playwright/test"; 7 | 8 | test('test', async () => { 9 | const browser = await chromium.launch({ headless: false }); // Adjust as needed 10 | const page = await browser.newPage(); 11 | 12 | // Array to store failed API details 13 | const failedApis = []; 14 | 15 | // Add the response listener 16 | page.on('response', (response) => { 17 | const request = response.request(); 18 | // Filter for API calls (XHR or fetch requests) 19 | if (['xhr', 'fetch'].includes(request.resourceType()) && response.status() !== 200) { 20 | failedApis.push({ 21 | url: request.url(), 22 | method: request.method(), 23 | status: response.status(), 24 | // Optional: Add more details if needed, e.g., response.body() for content (but it's async) 25 | }); 26 | } 27 | }); 28 | 29 | // Your existing UI navigation and operations go here 30 | await page.goto('https://example.com'); // Example navigation 31 | await page.click('button#some-button'); // Example interaction 32 | // ... more actions ... 33 | 34 | // At the end of your script, handle the results (e.g., log or integrate into a report) 35 | if (failedApis.length > 0) { 36 | console.log('Failed APIs detected:', failedApis); 37 | // Optionally: Throw an error, write to a file, or add to a test result object 38 | // e.g., result.failedApis = failedApis; 39 | } else { 40 | console.log('All APIs succeeded (status 200).'); 41 | } 42 | 43 | await browser.close(); 44 | }); 45 | 46 | 47 | 48 | // Use 'test' just like you would with the default Playwright test runner. 49 | // The custom 'page' fixture is automatically provided. 50 | test('should successfully load a page and report no API failures', async ({ page }) => { 51 | console.log('--- Test 1: Successful API calls ---'); 52 | // Navigate to a test page. Any API calls will be monitored by the fixture. 53 | await page.goto('https://playwright.dev'); 54 | 55 | // Perform some actions that might trigger API calls. 56 | // The fixture will silently monitor for non-200 responses in the background. 57 | await expect(page.locator('text=Docs')).toBeVisible(); 58 | 59 | // At the end of the test, the logic in 'fixtures.ts' will run, 60 | // and since all API calls (on this page) were successful, 61 | // it will log 'All API calls for this test succeeded.'. 62 | }); 63 | 64 | // A second test to demonstrate how the fixture captures a failed API call. 65 | test('should capture and report a failed API response', async ({ page }) => { 66 | console.log('--- Test 2: Failed API calls ---'); 67 | 68 | // Intercept and mock a specific API route to return a 404 status. 69 | // This simulates a failed backend call. 70 | await page.route('**/api/users/123', (route) => { 71 | route.fulfill({ 72 | status: 404, 73 | contentType: 'application/json', 74 | body: JSON.stringify({ error: 'User not found' }), 75 | }); 76 | }); 77 | 78 | // Navigate to a page or perform an action that triggers the mocked API call. 79 | await page.goto('https://example.com'); 80 | 81 | // Trigger a fetch request that will be intercepted and fail. 82 | await page.evaluate(async () => { 83 | await fetch('/api/users/123'); 84 | }); 85 | 86 | // There is no explicit assertion here because the custom fixture 87 | // handles the reporting and potential assertion for us at the end of the test. 88 | // When this test finishes, the logic in 'fixtures.ts' will log the 404 error. 89 | }); 90 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from "@playwright/test"; 2 | import * as dotenv from "dotenv"; 3 | import { Config } from "./config"; 4 | import { on } from "events"; 5 | //import OrtoniReport from "ortoni-report"; 6 | 7 | switch (process.env.NODE_ENV) { 8 | case "local": 9 | dotenv.config({ path: "./environments/local.env" }); 10 | break; 11 | case "dev": 12 | dotenv.config({ path: "./environments/local.env" }); 13 | break; 14 | case "qa": 15 | dotenv.config({ path: "./environments/qa.env" }); 16 | break; 17 | case "prod": 18 | dotenv.config({ path: "./environments/prod.env" }); 19 | break; 20 | default: 21 | break; 22 | } 23 | export default defineConfig({ 24 | testDir: "./tests", 25 | testMatch: ["**/*.ts", "**/*.js"], 26 | timeout: 180 * 1000, 27 | expect: { timeout: 180 * 1000 }, 28 | /* Run tests in files in parallel */ 29 | fullyParallel: true, 30 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 31 | forbidOnly: !!process.env.CI, 32 | /* Retry on CI only */ 33 | retries: process.env.CI ? 1 : Config.RETRY_FAILED, 34 | /* Opt out of parallel tests on CI. */ 35 | workers: process.env.CI ? 1 : Config.WORKERS, 36 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 37 | reporter: [ 38 | [`./src/utils/report/CustomReporterConfig.ts`], 39 | ["html", { open: "always", host: "127.0.0.1", port: 5723 }], 40 | //["OrtoniReport", { outputFolder: "reports" }], 41 | ], 42 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 43 | use: { 44 | /* Base URL to use in actions like `await page.goto('/')`. */ 45 | // baseURL: 'http://127.0.0.1:3000', 46 | 47 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 48 | trace: "retain-on-failure", 49 | headless: Config.HEADLESS_BROWSER, 50 | baseURL: Config.BASE_URL, 51 | screenshot: "on", 52 | video: "on", 53 | storageState: "auth.json", 54 | }, 55 | 56 | /* Configure projects for major browsers */ 57 | projects: [ 58 | // { 59 | // name: "chromium", 60 | // use: { ...devices["Desktop Chrome"] }, 61 | // metadata:{ 62 | // video:"on" 63 | // } 64 | // }, 65 | 66 | // { 67 | // name: "firefox", 68 | // use: { ...devices["Desktop Firefox"] }, 69 | // }, 70 | 71 | // { 72 | // name: "webkit", 73 | // use: { ...devices["Desktop Safari"] }, 74 | // }, 75 | 76 | /* Test against mobile viewports. */ 77 | // { 78 | // name: 'Mobile Chrome', 79 | // use: { ...devices['Pixel 5'] }, 80 | // }, 81 | // { 82 | // name: 'Mobile Safari', 83 | // use: { ...devices['iPhone 12'] }, 84 | // }, 85 | 86 | /* Test against branded browsers. */ 87 | { 88 | name: "Microsoft Edge", 89 | use: { 90 | ...devices["Desktop Edge"], 91 | channel: "msedge", 92 | //viewport: { width: 1920, height: 1080 }, 93 | viewport: devices["Desktop Edge"].viewport, //set viewport dynamically 94 | // baseURL:'https://restful-booker.herokuapp.com', 95 | ignoreHTTPSErrors: true, 96 | acceptDownloads: true, 97 | }, 98 | }, 99 | // {viewport: devices["Desktop Edge"].viewport, 100 | // name: "Chrome", 101 | // use: { 102 | // ...devices["Desktop Chrome"], 103 | // channel: "chrome", 104 | // screenshot: "on", 105 | // trace: "on", 106 | // video: "on", 107 | // headless: false, 108 | // viewport: { width: 1920, height: 1080 }, 109 | // }, 110 | // }, 111 | ], 112 | 113 | 114 | /* Run your local dev server before starting the tests */ 115 | // webServer: { 116 | // command: 'npm run mock-data', 117 | // url: 'http://localhost:3001', 118 | // reuseExistingServer: !process.env.CI, 119 | // }, 120 | }); 121 | -------------------------------------------------------------------------------- /tests/api/example/api.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test, APIRequestContext } from "@playwright/test"; 2 | import { ApiHelper } from "@src/helper/api/apiHelper"; 3 | import { pwApi } from "pw-api-plugin"; 4 | 5 | 6 | let token: string; 7 | let bookingId: string; 8 | 9 | test.beforeAll(async ({ request }) => { 10 | //1. Hit /Auth Api and provide username/password as body 11 | //2. fetch token value from JSON response 12 | //3. save in token variable 13 | 14 | const apiHelper = await new ApiHelper(page,pwApi); 15 | const responseMsg = await apiHelper.invokePostApi("/auth", { 16 | username: "admin", 17 | password: "password123", 18 | }); 19 | 20 | expect(responseMsg.token); 21 | 22 | token = responseMsg.token; 23 | console.log(token); 24 | }); 25 | 26 | test("Get booking list and verify response -- No Authentication Required ", async ({ 27 | request, 28 | }) => { 29 | /* Test Flow 30 | 1. Hit API endpoint 31 | 2. Verify API status code 32 | 3. Verify JSON Response 33 | 4. Verify JSON Schema 34 | */ 35 | test.info().annotations.push({ 36 | type: "purpose", 37 | description: 38 | "This will make GET call to https://restful-booker.herokuapp.com/booking with no authentication", 39 | }); 40 | const apiHelper = await new ApiHelper(request); // 41 | const responseMsg = await apiHelper.invokeGetApi("/booking"); 42 | console.log(JSON.stringify(responseMsg)); 43 | for (let index = 0; index < responseMsg.length; index++) { 44 | test.info().annotations.push({ 45 | type: "value", 46 | description: `BookingId : ${responseMsg[index].bookingid}`, 47 | }); 48 | expect(responseMsg[index].bookingid).not.toBeNull(); 49 | } 50 | }); 51 | 52 | test("Get Booking Details using BookingID --> 1914", async ({ request }) => { 53 | /* Test Flow 54 | 1. Hit API endpoint 55 | 2. Verify API status code 56 | 3. Verify JSON Response 57 | 4. Verify JSON Schema 58 | */ 59 | test.info().annotations.push({ 60 | type: "purpose", 61 | description: 62 | "This will make GET call to https://restful-booker.herokuapp.com/booking/:id and verify keys/values in response", 63 | }); 64 | 65 | const apiHelper = await new ApiHelper(request); // 66 | const bookingDetails = await apiHelper.invokeGetApi("/booking/1914"); 67 | console.log(JSON.stringify(bookingDetails)); 68 | 69 | expect(bookingDetails.firstname).toBe("John"); 70 | expect(bookingDetails.lastname).toBe("Allen"); 71 | expect(bookingDetails.totalprice).toBe(111); 72 | expect(bookingDetails.depositpaid).toBeTruthy(); 73 | expect(apiHelper.isValidDate(bookingDetails.bookingdates.checkin)).toBe(true); 74 | expect(apiHelper.isValidDate(bookingDetails.bookingdates.checkout)).toBe( 75 | true 76 | ); 77 | expect(bookingDetails.additionalneeds).toBe("super bowls"); 78 | }); 79 | 80 | test("Get booking list, pass to booking/:id API and verify response -- No Authentication Required ", async ({ 81 | request, 82 | }) => { 83 | /* Test Flow 84 | 1. Hit API endpoint 85 | 2. Verify API status code 86 | 3. Verify JSON Response 87 | 4. Verify JSON Schema 88 | */ 89 | test.info().annotations.push({ 90 | type: "purpose", 91 | description: 92 | "This will make GET call to https://restful-booker.herokuapp.com/booking with no authentication", 93 | }); 94 | const apiHelper = await new ApiHelper(request); // 95 | const responseMsg = await apiHelper.invokeGetApi("/booking"); 96 | console.log(JSON.stringify(responseMsg)); 97 | for (let index = 0; index < responseMsg.length; index++) { 98 | test.info().annotations.push({ 99 | type: "value", 100 | description: `BookingId : ${responseMsg[index].bookingid}`, 101 | }); 102 | expect(responseMsg[index].bookingid).not.toBeNull(); 103 | let bookingDetail = await apiHelper.invokeGetApi( 104 | `/booking/${responseMsg[index].bookingid}` 105 | ); 106 | console.log(JSON.stringify(bookingDetail)); 107 | } 108 | }); 109 | //API used for writing test code - https://restful-booker.herokuapp.com/apidoc/index.html 110 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playwright-framework-template", 3 | "version": "1.0.0", 4 | "description": "Playwright Framework Template : Unified automation framework for Web, Mobile, API & Load testing", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo %NODE_ENV% && npx playwright test --headed --config=playwright.config.ts", 8 | "test:local": "cross-env NODE_ENV='local' npm run test", 9 | "test:dev": "cross-env NODE_ENV='dev' npm run test", 10 | "test:sit": "cross-env NODE_ENV='sit' npm run test", 11 | "test:headed": "npx playwright test --headed", 12 | "test:chromium": "npx playwright test --project=chromium", 13 | "test:firefox": "npx playwright test --project=firefox", 14 | "test:webkit": "npx playwright test --project=webkit", 15 | "test:mobile": "npx playwright test --project=mobile-chrome", 16 | "test:api": "npx playwright test tests/api", 17 | "test:debug": "npx playwright test --debug", 18 | "test:ui-mode": "npx playwright test --ui", 19 | "test:report": "npx playwright show-report", 20 | "install:deps": "npx playwright install --with-deps", 21 | "install:browsers": "npm install -D @playwright/test@latest --force", 22 | "codegen": "npx playwright codegen", 23 | "mcp": "ts-node mcp-server.ts", 24 | "test:mcp": "playwright test", 25 | "ci": "npx playwright test --project=chromium --workers=1", 26 | "flaky": "npx playwright test --project=chromium --repeat-each=20", 27 | "sanity": "npx playwright test --grep @Sanity --workers=1", 28 | "smoke": "npx playwright test --grep @Smoke --workers=1", 29 | "api": "npx playwright test --grep @Api --workers=1", 30 | "regresion": "npx playwright test --grep @Regressoin --workers=1", 31 | "lint": "eslint . --ext .js,.ts", 32 | "lint:fix": "npx eslint . --ext .js --fix", 33 | "check": "npm run lint && npm run build", 34 | "format": "prettier --write \"**/*.{js,ts,tsx}\"", 35 | "format:check": "prettier --check \"**/*.{js,ts,tsx}\"", 36 | "pretest": "tsc --noEmit && npx eslint tests/**/*.ts && npx prettier tests/**/*.ts --write", 37 | "prepare": "husky" 38 | }, 39 | "lint-staged": { 40 | "*.{ts,tsx,js,jsx}": [ 41 | "npx eslint --fix", 42 | "npx prettier --write" 43 | ] 44 | }, 45 | "keywords": [ 46 | "playwright", 47 | "typescript", 48 | "testing", 49 | "automation", 50 | "e2e", 51 | "cross-browser", 52 | "page-object-model" 53 | ], 54 | "author": "Abhay Bharti", 55 | "license": "MIT", 56 | "devDependencies": { 57 | "@commitlint/cli": "^19.8.1", 58 | "@commitlint/config-conventional": "^19.8.1", 59 | "@eslint/js": "^9.34.0", 60 | "@playwright/test": "^1.56.0", 61 | "@types/eslint__js": "^8.42.3", 62 | "@types/node": "^20.17.24", 63 | "@types/ssh2": "^1.15.4", 64 | "@typescript-eslint/eslint-plugin": "^8.46.2", 65 | "@typescript-eslint/parser": "^8.46.2", 66 | "eslint": "^9.38.0", 67 | "eslint-config-airbnb-base": "^15.0.0", 68 | "eslint-config-prettier": "^10.1.8", 69 | "eslint-define-config": "^2.1.0", 70 | "eslint-plugin-import": "^2.25.2", 71 | "eslint-plugin-jsdoc": "^51.3.1", 72 | "eslint-plugin-playwright": "^2.2.2", 73 | "eslint-plugin-prettier": "^5.5.4", 74 | "eslint-plugin-sonarjs": "^3.0.4", 75 | "eslint-plugin-unicorn": "^59.0.1", 76 | "jiti": "^2.6.1", 77 | "json-server": "^0.17.4", 78 | "lint-staged": "^16.2.6", 79 | "playwright-ajv-schema-validator": "^1.0.2", 80 | "prettier": "3.6.2", 81 | "pw-api-plugin": "^2.0.2", 82 | "ts-node": "^10.9.2", 83 | "typedoc": "^0.28.7", 84 | "typescript": "^5.9.2", 85 | "typescript-eslint": "^8.42.0" 86 | }, 87 | "dependencies": { 88 | "@axe-core/playwright": "^4.10.2", 89 | "@types/mocha": "^10.0.6", 90 | "@types/webdriverio": "^5.0.0", 91 | "ajv": "^8.12.0", 92 | "appium": "^2.4.1", 93 | "axe-html-reporter": "^2.2.11", 94 | "dotenv": "^16.5.0", 95 | "joi": "^17.13.3", 96 | "mocha": "^10.2.0", 97 | "moment": "^2.30.1", 98 | "nodejs-nodemailer-outlook": "^1.2.4", 99 | "ssh2": "^1.16.0", 100 | "webdriverio": "^8.28.0", 101 | "winston": "^3.11.0", 102 | "zod": "^3.24.2" 103 | } 104 | } -------------------------------------------------------------------------------- /src/helper/terminal/SSHHelper.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This module mimics SSHLibrary keywords using Node.js + SSH2 library 3 | * Install dependency: npm install ssh2 4 | */ 5 | 6 | import { Client, ConnectConfig } from 'ssh2'; 7 | import { step } from "@src/utils/report/ReportAction"; 8 | 9 | export class SshHelper { 10 | private conn: Client; 11 | private isConnected: boolean = false; 12 | private stdout: string = ''; 13 | private stderr: string = ''; 14 | 15 | constructor() { 16 | this.conn = new Client(); 17 | } 18 | 19 | /** 20 | * Establish SSH Connection 21 | * @param config - SSH connection details 22 | */ 23 | @step('openConnection') 24 | public async openConnection(config: ConnectConfig): Promise { 25 | return new Promise((resolve, reject) => { 26 | this.conn 27 | .on('ready', () => { 28 | console.log('SSH Connection Established'); 29 | this.isConnected = true; 30 | resolve(); 31 | }) 32 | .on('error', (err) => { 33 | console.error('SSH Connection Error:', err); 34 | reject(err); 35 | }) 36 | .connect(config); 37 | }); 38 | } 39 | 40 | /** 41 | * Run Command on Remote Linux Machine 42 | * @param command - Command to execute 43 | * @returns Command output as a string 44 | */ 45 | @step('runCommand') 46 | public async runCommand(command: string): Promise { 47 | return new Promise((resolve, reject) => { 48 | if (!this.isConnected) { 49 | return reject(new Error('SSH session is not connected')); 50 | } 51 | 52 | this.conn.exec(command, (err, stream) => { 53 | if (err) return reject(err); 54 | 55 | let output = ''; 56 | stream 57 | .on('close', (code: string, signal: string) => { 58 | console.log(`Command execution completed. Exit code: ${code}, Signal: ${signal}`); 59 | resolve(output); 60 | }) 61 | .on('data', (data: string) => { 62 | output += data.toString(); 63 | }) 64 | .stderr.on('data', (data) => { 65 | console.error('Error:', data.toString()); 66 | }); 67 | }); 68 | }); 69 | } 70 | 71 | /** 72 | * Close the SSH Connection 73 | */ 74 | @step('closeConnection') 75 | public closeConnection(): void { 76 | if (this.conn && this.isConnected) { 77 | this.conn.end(); 78 | this.isConnected = false; 79 | console.log('SSH Connection Closed'); 80 | } 81 | } 82 | 83 | /** 84 | * Get last command output 85 | */ 86 | @step('getStdout') 87 | getStdout(): string { 88 | return this.stdout; 89 | } 90 | 91 | /** 92 | * Verify output contains string 93 | */ 94 | @step('verifyOutputContains') 95 | verifyOutputContains(text: string): boolean { 96 | return this.stdout.includes(text); 97 | } 98 | 99 | } 100 | 101 | //Example code 102 | 103 | // import { SSHHelper } from './SSHHelper'; 104 | 105 | // async function main() { 106 | // const sshHelper = new SSHHelper(); 107 | 108 | // const config = { 109 | // host: '192.168.1.100', // Replace with your Linux device's IP 110 | // port: 22, // Default SSH port 111 | // username: 'your-username', 112 | // password: 'your-password', // Use SSH keys for better security 113 | // }; 114 | 115 | // try { 116 | // // Step 1: Establish SSH Connection 117 | // await sshHelper.setUpSshSession(config); 118 | 119 | // // Step 2: Run Commands 120 | // const output = await sshHelper.runCommand('ls -l'); 121 | // console.log('Command Output:', output); 122 | 123 | // } catch (error) { 124 | // console.error('Error:', error); 125 | // } finally { 126 | // // Step 3: Close Connection 127 | // sshHelper.closeConnection(); 128 | // } 129 | // } 130 | 131 | // main(); 132 | -------------------------------------------------------------------------------- /tests/fixtures/customFixtures.ts: -------------------------------------------------------------------------------- 1 | import { test as base, BrowserContext, Browser, Page, TestInfo, VideoMode, expect, request } from '@playwright/test' 2 | import { ApiHelper, SshHelper, WebHelper } from "../../src/helper"; 3 | import { pwApi } from 'pw-api-plugin'; 4 | import { analyzeHAR } from '@src/utils/harAnalyser'; 5 | import { JsonReader } from '@src/utils/reader/jsonReader'; 6 | 7 | type MyMixtures = { 8 | context: BrowserContext 9 | } 10 | 11 | // Initialize browser context with HAR recording 12 | const initHarRecording = async (browser: Browser, testInfo: TestInfo) => { 13 | const fileName = testInfo.title.replace(/[^a-zA-Z0-9]/g, '-') 14 | const harFilePath = `./test-results/${fileName}.har` 15 | const newContext = await browser.newContext({ 16 | recordHar: { 17 | path: harFilePath, 18 | mode: 'full', 19 | // urlFilter: /api.practicesoftwaretesting.com/, 20 | urlFilter: /.*/, 21 | }, 22 | ...(testInfo.project.metadata?.video ? { 23 | recordVideo: { 24 | dir: testInfo.outputPath('videos'), 25 | }, 26 | } : {}), 27 | }) 28 | return newContext 29 | } 30 | 31 | // Teardown and save HAR file 32 | const teardownHarRecording = async (page: Page, testInfo: TestInfo, videoMode: 'on' | 'off') => { 33 | const fileName = testInfo.title.replace(/[^a-zA-Z0-9]/g, '-') 34 | if (videoMode === 'on') { 35 | const videoPath = testInfo.outputPath('my-video.webm') 36 | await Promise.all([page.video()?.saveAs(videoPath), page.close()]) 37 | testInfo.attachments.push({ 38 | name: 'video', 39 | path: videoPath, 40 | contentType: 'video/webm', 41 | }) 42 | } 43 | await page.context().close({ reason: 'Saving HAR file' }) 44 | await analyzeHAR(`./test-results/${fileName}.har`, `./test-results/${fileName}.html`) 45 | await testInfo.attach('Network Console', { path: `./test-results/${fileName}.html`, contentType: 'text/html' }) 46 | } 47 | 48 | export const test = base.extend<{ 49 | api: ApiHelper; 50 | web: WebHelper; 51 | ssh: SshHelper; 52 | MyMixtures: any; 53 | json: JsonReader; 54 | config: { jsonPath: string }; 55 | }>({ 56 | config: async ({ }, use) => { 57 | // Get JSON path from environment variable or use default 58 | const jsonPath = process.env.JSON_PATH || 'moataeldebsy.json'; 59 | await use({ jsonPath }); 60 | }, 61 | api: async ({ request }, use) => { 62 | await use(new ApiHelper(request)) 63 | }, 64 | 65 | web: async ({ page, browser, config }, use) => { 66 | console.log(config.jsonPath) 67 | const context = await browser.newContext(); 68 | await use(new WebHelper(page, context, config.jsonPath)); 69 | }, 70 | ssh: async ({ }, use) => { 71 | await use(new SshHelper()) 72 | }, 73 | json: async ({ config }, use) => { 74 | console.log('Initializing JSON reader with:', config.jsonPath); 75 | await use(new JsonReader(config.jsonPath)) 76 | 77 | // npx playwright test --config:jsonPath=./alternative-config.json -- command to pass json filename 78 | 79 | //npx playwright test --config:jsonPath=./moataeldebsy.json --debug ./tests/e2e/moatazeldebsy/form.spec.ts 80 | }, 81 | page: async ({ browser }, use, testInfo) => { 82 | // Initialize HAR recording 83 | const context = await initHarRecording(browser, testInfo); 84 | const page = await context.newPage(); 85 | 86 | // Array to store failed API details for this test run 87 | const failedApis: any = []; 88 | 89 | // Add the response listener to the page 90 | page.on('response', (response) => { 91 | const request = response.request(); 92 | // Filter for API calls (XHR or fetch requests) and non-200 status codes 93 | if (['xhr', 'fetch'].includes(request.resourceType()) && response.status() !== 200) { 94 | failedApis.push({ 95 | url: request.url(), 96 | method: request.method(), 97 | status: response.status(), 98 | }); 99 | } 100 | }); 101 | 102 | // Proceed with the actual test execution 103 | await use(page); 104 | 105 | // Log any failed APIs 106 | if (failedApis.length > 0) { 107 | console.log('\n--- Failed APIs Detected ---'); 108 | console.log(failedApis); 109 | console.log('----------------------------\n'); 110 | } else { 111 | console.log('All API calls for this test succeeded.'); 112 | } 113 | 114 | // Teardown HAR recording 115 | await teardownHarRecording(page, testInfo, 'on'); 116 | }, 117 | 118 | }) 119 | 120 | export type { TestInfo } from "@playwright/test" 121 | export { expect } from "@playwright/test" 122 | 123 | -------------------------------------------------------------------------------- /tests/web/example/assertion.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect,test } from "@tests/fixtures/test-options"; 2 | 3 | 4 | /* 5 | These assertions will retry until the assertion passes, or the assertion timeout is reached. 6 | */ 7 | test("Using playwright Auto-Retrying Assertion", async ({ web,api }) => { 8 | await web.navigateToUrl("https://www.google.com"); 9 | // await expect(web.toHaveTitle("Google")); 10 | await expect(web.webPage.getByTestId("status")).toHaveText("PASS"); 11 | 12 | const response = api.invokeGetApi("url"); 13 | 14 | await expect(locator).toBeAttached(); //Element is attached 15 | await expect(locator).toBeChecked(); //Checkbox is checked 16 | await expect(locator).toBeDisabled(); //Element is disabled 17 | await expect(locator).toBeEditable(); //Element is editable 18 | await expect(locator).toBeEmpty(); //Container is empty 19 | await expect(locator).toBeEnabled(); //Element is enabled 20 | await expect(locator).toBeFocused(); //Element is focused 21 | await expect(locator).toBeHidden(); //Element is not visible 22 | await expect(locator).toBeInViewport(); //Element intersects viewport 23 | await expect(locator).toBeVisible(); //Element is visible 24 | await expect(locator).toContainText("xyz"); //Element contains text 25 | await expect(locator).toHaveAttribute("class"); //Element has a DOM attribute 26 | await expect(locator).toHaveClass("icon"); //Element has a class property 27 | await expect(locator).toHaveCount(1); //List has exact number of children 28 | // await expect(locator).toHaveCSS() //Element has CSS property 29 | await expect(locator).toHaveId("id"); //Element has an ID 30 | // await expect(locator).toHaveJSProperty() //Element has a JavaScript property 31 | await expect(locator).toHaveScreenshot(); //Element has a screenshot 32 | await expect(locator).toHaveText("ABC"); //Element matches text 33 | await expect(locator).toHaveValue("ABC"); //Input has a value 34 | //await expect(locator).toHaveValues("ABC") //Select has options selected 35 | await expect(page).toHaveScreenshot(); //Page has a screenshot 36 | await expect(page).toHaveTitle("ABC"); //Page has a title 37 | await expect(page).toHaveURL("ABC"); //Page has a URL 38 | //await expect(response).toBeOK() //Response has an OK status 39 | }); 40 | 41 | /* 42 | These assertions will test any condition but do not auto-retry. Using these assertions can lead to a flaky test 43 | 44 | */ 45 | test("Using playwright Non-Retrying Assertion", async ({ page }) => { 46 | await page.goto("https://www.google.com"); 47 | await expect(page).toHaveTitle("Google"); 48 | const value = ""; 49 | expect(value).toBe("ABC"); //Value is the same 50 | expect(value).toBeCloseTo(100); // Number is approximately equal 51 | expect(value).toBeDefined(); //Value is not undefined 52 | expect(value).toBeFalsy(); //Value is falsy, e.g. false, 0, null, etc. 53 | expect(value).toBeGreaterThan(5); // Number is more than 54 | expect(value).toBeGreaterThanOrEqual(5); // Number is more than or equal 55 | expect(value).toBeInstanceOf(Object); //Object is an instance of a class 56 | expect(value).toBeLessThan(19); //Number is less than 57 | expect(value).toBeLessThanOrEqual(19); // Number is less than or equal 58 | expect(value).toBeNaN(); //Value is NaN 59 | expect(value).toBeNull(); //Value is null 60 | expect(value).toBeTruthy(); // Value is truthy, i.e. not false, 0, null, etc. 61 | expect(value).toBeUndefined(); // Value is undefined 62 | expect(value).toContain("ABC"); //String contains a substring 63 | expect(value).toContain("ABC"); //Array or set contains an element 64 | expect(value).toContainEqual("ABC"); //Array or set contains a similar element 65 | expect(value).toEqual(5); //Value is similar - deep equality and pattern matching 66 | expect(value).toHaveLength(10); //Array or string has length 67 | expect(value).toHaveProperty("name"); // Object has a property 68 | expect(value).toMatch(/name/i); //String matches a regular expression 69 | // expect(value).toMatchObject(); // Object contains specified properties 70 | // expect(value).toStrictEqual(); //Value is similar, including property types 71 | expect(value).toThrow(); //Function throws an error 72 | // expect(value).any(); //Matches any instance of a class/primitive 73 | // expect(value).anything(); //Matches anything 74 | // expect(value).arrayContaining(); //Array contains specific elements 75 | // expect(value).closeTo(); //Number is approximately equal 76 | // expect(value).objectContaining(); // Object contains specific properties 77 | // expect(value).stringContaining(); //String contains a substring 78 | // expect(value).stringMatching(); //String matches a regular expression 79 | }); 80 | -------------------------------------------------------------------------------- /src/utils/report/Logger.ts: -------------------------------------------------------------------------------- 1 | import winston, { 2 | Logger as WinstonLogger, 3 | LoggerOptions, 4 | format, 5 | } from 'winston'; 6 | import { TransformableInfo } from 'logform'; 7 | import { inspect } from 'util'; 8 | import fs from 'fs'; 9 | // Types 10 | type LogLevel = 11 | | 'error' 12 | | 'warn' 13 | | 'info' 14 | | 'http' 15 | | 'verbose' 16 | | 'debug' 17 | | 'silly'; 18 | type LogMethod = (message: string, meta?: unknown) => void; 19 | 20 | // Constants 21 | const LOGS_DIRECTORY = 'logs'; 22 | const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB 23 | const MAX_FILES = 5; 24 | 25 | // Custom formatter for console output 26 | const consoleFormat = format.combine( 27 | format.timestamp({ format: 'DD-MM-YYYY HH:mm:ss' }), 28 | format.errors({ stack: true }), 29 | format.colorize({ all: true }), 30 | format.printf((info: TransformableInfo) => { 31 | const { timestamp, level, message, stack, ...meta } = info; 32 | let log = `${timestamp} [${level}]: ${message}`; 33 | 34 | if (Object.keys(meta).length > 0) { 35 | log += `\n${inspect(meta, { depth: null, colors: true })}`; 36 | } 37 | 38 | return stack ? `${log}\n${stack}` : log; 39 | }) 40 | ); 41 | 42 | // File format (JSON) 43 | const fileFormat = format.combine( 44 | format.timestamp(), 45 | format.json(), 46 | format.errors({ stack: true }) 47 | ); 48 | 49 | // Create logs directory if it doesn't exist 50 | 51 | if (!fs.existsSync(LOGS_DIRECTORY)) { 52 | fs.mkdirSync(LOGS_DIRECTORY); 53 | } 54 | 55 | // Logger configuration 56 | const loggerConfig: LoggerOptions = { 57 | level: process.env.NODE_ENV === 'production' ? 'info' : 'debug', 58 | format: fileFormat, 59 | defaultMeta: { service: 'job-posting-service' }, 60 | transports: [ 61 | // Error logs 62 | new winston.transports.File({ 63 | filename: `${LOGS_DIRECTORY}/error.log`, 64 | level: 'error', 65 | maxsize: MAX_FILE_SIZE, 66 | maxFiles: MAX_FILES, 67 | }), 68 | // Combined logs 69 | new winston.transports.File({ 70 | filename: `${LOGS_DIRECTORY}/combined.log`, 71 | maxsize: MAX_FILE_SIZE, 72 | maxFiles: MAX_FILES, 73 | }), 74 | // Console (only in development) 75 | ...(process.env.NODE_ENV !== 'production' 76 | ? [ 77 | new winston.transports.Console({ 78 | format: consoleFormat, 79 | }), 80 | ] 81 | : []), 82 | ], 83 | // Handle uncaught exceptions 84 | exceptionHandlers: [ 85 | new winston.transports.File({ 86 | filename: `${LOGS_DIRECTORY}/exceptions.log`, 87 | maxsize: MAX_FILE_SIZE, 88 | maxFiles: MAX_FILES, 89 | }), 90 | ], 91 | // Handle unhandled promise rejections 92 | rejectionHandlers: [ 93 | new winston.transports.File({ 94 | filename: `${LOGS_DIRECTORY}/rejections.log`, 95 | maxsize: MAX_FILE_SIZE, 96 | maxFiles: MAX_FILES, 97 | }), 98 | ], 99 | // Exit on error, set to false if using with express 100 | exitOnError: false, 101 | }; 102 | 103 | // Create logger instance 104 | const logger: WinstonLogger = winston.createLogger(loggerConfig); 105 | 106 | // Add console transport in production (without colors) 107 | if (process.env.NODE_ENV === 'production') { 108 | logger.add( 109 | new winston.transports.Console({ 110 | format: format.combine(format.timestamp(), format.json()), 111 | }) 112 | ); 113 | } 114 | 115 | // Log method with type safety 116 | const log = (level: LogLevel, message: string, meta?: unknown): void => { 117 | logger.log(level, message, { 118 | ...(meta && typeof meta === 'object' ? meta : { meta }), 119 | }); 120 | }; 121 | 122 | // Public API 123 | export const logInfo: LogMethod = (message, meta) => log('info', message, meta); 124 | export const logError: LogMethod = (message, error) => 125 | log('error', message, error); 126 | export const logWarn: LogMethod = (message, meta) => log('warn', message, meta); 127 | export const logDebug: LogMethod = (message, meta) => 128 | log('debug', message, meta); 129 | export const logHttp: LogMethod = (message, meta) => log('http', message, meta); 130 | 131 | export const logFunctionCall = ( 132 | funcName: string, 133 | args: Record 134 | ): void => { 135 | logInfo(`Function: ${funcName} called`, { 136 | parameters: args, 137 | timestamp: new Date().toISOString(), 138 | }); 139 | }; 140 | 141 | // Export the logger instance 142 | export default logger; 143 | 144 | //Example code to use logger 145 | // import { logInfo, logError } from './logger'; 146 | 147 | // Basic usage 148 | // logInfo('Application started'); 149 | // logError('Something went wrong', error); 150 | 151 | // With metadata 152 | // logInfo('User logged in', { userId: 123, ip: '192.168.1.1' }); 153 | 154 | // // Function call logging 155 | // logFunctionCall('processUser', { userId: 123, action: 'login' }); 156 | -------------------------------------------------------------------------------- /logs/combined.log: -------------------------------------------------------------------------------- 1 | {"level":"info","message":"info","meta":"Test Case Completed : Read all data from table Status : interrupted","service":"job-posting-service","timestamp":"2025-11-08T04:26:33.094Z"} 2 | {"level":"info","message":"playwright-framework-template undefined","service":"job-posting-service","timestamp":"2025-11-08T04:26:43.594Z"} 3 | {"level":"info","message":"info","meta":"Test Suite Started : , 1 tests","service":"job-posting-service","timestamp":"2025-11-08T04:26:43.704Z"} 4 | {"level":"info","message":"info","meta":"Test Case Started : Read all data from table","service":"job-posting-service","timestamp":"2025-11-08T04:26:48.470Z"} 5 | {"level":"info","message":"Column 1: Last Name","service":"job-posting-service","timestamp":"2025-11-08T04:27:24.813Z"} 6 | {"level":"info","message":"info","meta":"Test Case Completed : Read all data from table Status : interrupted","service":"job-posting-service","timestamp":"2025-11-08T04:27:58.705Z"} 7 | {"level":"info","message":"playwright-framework-template undefined","service":"job-posting-service","timestamp":"2025-11-08T04:28:29.217Z"} 8 | {"level":"info","message":"info","meta":"Test Suite Started : , 1 tests","service":"job-posting-service","timestamp":"2025-11-08T04:28:29.322Z"} 9 | {"level":"info","message":"info","meta":"Test Case Started : Read all data from table","service":"job-posting-service","timestamp":"2025-11-08T04:28:33.383Z"} 10 | {"level":"info","message":"Column 1: Last Name","service":"job-posting-service","timestamp":"2025-11-08T04:28:45.729Z"} 11 | {"level":"info","message":"Column 2: First Name","service":"job-posting-service","timestamp":"2025-11-08T04:28:48.188Z"} 12 | {"level":"info","message":"Column 3: Email","service":"job-posting-service","timestamp":"2025-11-08T04:28:54.058Z"} 13 | {"level":"info","message":"Column 4: Due","service":"job-posting-service","timestamp":"2025-11-08T04:28:56.196Z"} 14 | {"level":"info","message":"Column 5: Web Site","service":"job-posting-service","timestamp":"2025-11-08T04:28:56.226Z"} 15 | {"level":"info","message":"Column 6: Action","service":"job-posting-service","timestamp":"2025-11-08T04:28:56.255Z"} 16 | {"level":"info","message":"Number of rows in table: 4","service":"job-posting-service","timestamp":"2025-11-08T04:28:56.285Z"} 17 | {"level":"info","message":"Number of columns in row 1: 6","service":"job-posting-service","timestamp":"2025-11-08T04:28:58.933Z"} 18 | {"level":"info","message":"Row 1: Smith,John,jsmith@gmail.com,$50.00,http://www.jsmith.com,\n edit\n delete\n ","service":"job-posting-service","timestamp":"2025-11-08T04:28:59.240Z"} 19 | {"level":"info","message":"Number of columns in row 2: 6","service":"job-posting-service","timestamp":"2025-11-08T04:28:59.291Z"} 20 | {"level":"info","message":"Row 2: Bach,Frank,fbach@yahoo.com,$51.00,http://www.frank.com,\n edit\n delete\n ","service":"job-posting-service","timestamp":"2025-11-08T04:28:59.578Z"} 21 | {"level":"info","message":"Number of columns in row 3: 6","service":"job-posting-service","timestamp":"2025-11-08T04:28:59.666Z"} 22 | {"level":"info","message":"Row 3: Doe,Jason,jdoe@hotmail.com,$100.00,http://www.jdoe.com,\n edit\n delete\n ","service":"job-posting-service","timestamp":"2025-11-08T04:29:00.019Z"} 23 | {"level":"info","message":"Number of columns in row 4: 6","service":"job-posting-service","timestamp":"2025-11-08T04:29:00.067Z"} 24 | {"level":"info","message":"Row 4: Conway,Tim,tconway@earthlink.net,$50.00,http://www.timconway.com,\n edit\n delete\n ","service":"job-posting-service","timestamp":"2025-11-08T04:29:00.507Z"} 25 | {"level":"info","message":"info","meta":"Test Case Completed : Read all data from table Status : passed","service":"job-posting-service","timestamp":"2025-11-08T04:29:03.904Z"} 26 | {"level":"info","message":"playwright-framework-template undefined","service":"job-posting-service","timestamp":"2025-11-08T04:31:22.554Z"} 27 | {"level":"info","message":"info","meta":"Test Suite Started : , 1 tests","service":"job-posting-service","timestamp":"2025-11-08T04:31:22.674Z"} 28 | {"level":"info","message":"info","meta":"Test Case Started : Read all data from table","service":"job-posting-service","timestamp":"2025-11-08T04:31:29.080Z"} 29 | {"level":"info","message":"Header Text: Last Name|First Name|Email|Due|Web Site|Action","service":"job-posting-service","timestamp":"2025-11-08T04:32:02.776Z"} 30 | {"level":"info","message":"Number of rows in table: 4","service":"job-posting-service","timestamp":"2025-11-08T04:32:02.811Z"} 31 | {"level":"info","message":"Number of columns in row 1: 6","service":"job-posting-service","timestamp":"2025-11-08T04:32:17.177Z"} 32 | {"level":"info","message":"Row 1: Smith|John|jsmith@gmail.com|$50.00|http://www.jsmith.com|\n edit\n delete\n ","service":"job-posting-service","timestamp":"2025-11-08T04:32:17.323Z"} 33 | {"level":"info","message":"Number of columns in row 2: 6","service":"job-posting-service","timestamp":"2025-11-08T04:32:17.346Z"} 34 | {"level":"info","message":"Row 2: Bach|Frank|fbach@yahoo.com|$51.00|http://www.frank.com|\n edit\n delete\n ","service":"job-posting-service","timestamp":"2025-11-08T04:32:17.512Z"} 35 | {"level":"info","message":"Number of columns in row 3: 6","service":"job-posting-service","timestamp":"2025-11-08T04:32:17.538Z"} 36 | {"level":"info","message":"Row 3: Doe|Jason|jdoe@hotmail.com|$100.00|http://www.jdoe.com|\n edit\n delete\n ","service":"job-posting-service","timestamp":"2025-11-08T04:32:17.674Z"} 37 | {"level":"info","message":"Number of columns in row 4: 6","service":"job-posting-service","timestamp":"2025-11-08T04:32:17.697Z"} 38 | {"level":"info","message":"Row 4: Conway|Tim|tconway@earthlink.net|$50.00|http://www.timconway.com|\n edit\n delete\n ","service":"job-posting-service","timestamp":"2025-11-08T04:32:17.836Z"} 39 | {"level":"info","message":"info","meta":"Test Case Completed : Read all data from table Status : passed","service":"job-posting-service","timestamp":"2025-11-08T04:32:22.991Z"} 40 | -------------------------------------------------------------------------------- /tests/api/example/dashboard.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test, APIRequestContext } from "@playwright/test"; 2 | import moment from "moment"; 3 | import { ApiHelper } from "../../../helper/api/apiHelper"; 4 | 5 | let token: string; 6 | let bookingId: string; 7 | 8 | test.beforeAll(async ({ request }) => { 9 | //1. Hit /Auth Api and provide username/password as body 10 | //2. fetch token value from JSON response 11 | //3. save in token variable 12 | 13 | const apiHelper = await new ApiHelper(request); 14 | }); 15 | 16 | const testSuiteTestScriptObject = { 17 | Product_Buy_Flow: 18 | "Verify_that_on_the_product_page,_the_user_can_select_the_desired_attribute_of_the_product_eg_size_color_etc,Verify_that_the_user_can_add_to_the_cart_one_or_more_products,Verify_that_users_can_add_products_to_the_wishlist,Verify_that_the_user_can_see_the_previously_added_products_on_the_cart_page_after_signing_in_to_the_application,Verify_that_the_user_can_successfully_buy_more_than_one_products_that_were_added_to_his/her_cart,Verify_that_the_user_cannot_add_more_than_the_available_inventory_of_the_product,Verify_that_the_limit_to_the_number_of_products_a_user_can_buy_is_working_correctly_Also_an_error_message_gets_displayed_preventing_the_user_from_buying_more_than_the_limit,Verify_that_the_delivery_can_be_declined_during_checkout_for_the_places_where_shipping_is_not_available,Verify_that_the_Cash_on_Delivery_option_of_payment_is_working_fine,Verify_that_the_different_prepaid_methods_of_payments_are_working_fine,Verify_that_product_return_functionality_works_correctly", 19 | User_Registration: 20 | "Verify_that_all_the_required_fields__username,_email,_password,_confirm_password,_etc_are_present_on_the_registration_page,Verify_that_on_passing_valid_values,_a_user_should_get_registered_and_the_same_should_be_allowed_to_log_in_to_the_application,Verify_that_if_a_user_tries_to_register_an_existing_username_then_an_error_message_should_get_displayed,Verify_that_the_required/mandatory_fields_are_marked_with_the_‘*’_symbol,Verify_that_for_a_better_user_interface__dropdowns,_radio_buttons,_checkboxes,_etc_fields_are_displayed_wherever_possible_instead_of_just_text_boxes,Verify_the_page_has_both_submit_and_cancel/reset_buttons_at_the_end,Verify_that_clicking_submits_button_after_entering_all_the_required_fields,_submits_the_data_to_the_server,Verify_that_clicking_the_cancel/reset_button_after_entering_all_the_required_fields,_cancels_the_submit_request,_and_reset_all_the_fields,Verify_that_if_no_value_is_passed_to_the_mandatory_fields_and_submit_button_is_clicked_then_it_leads_to_a_validation_error,Verify_that_the_user_can_leave_the_optional_fields_blank_and_on_clicking_the_submit_button_no_validation_error_appears,Verify_that_whenever_possible_validation_should_take_place_on_the_client_side_For_example,_if_a_user_presses_submit_button_without_entering_a_username_and_password_then_this_validation_should_take_place_on_the_client_side_instead_of_sending_blank_entries_to_the_server,Check_the_upper_limit_of_the_different_textbox_fields,Verify_validation_on_the_date_and_email_fields_Only_valid_dates_and_valid_email_Ids_should_be_allowed,Check_validation_on_numeric_fields_by_entering_alphabets_and_special_characters,Check_that_leading_and_trailing_spaces_are_trimmed_ie_in_case,_the_user_appends_space_before_and_after_a_field,_then_the_same_should_get_trimmed_before_getting_stored_on_the_server", 21 | Portal_Validation: 22 | "Verify_that_the_company_logo_and_name_are_clearly_visible,Verify_that_the_user_is_able_to_navigate_through_all_the_products_across_different_categories,Verify_that_all_the_links_and_banners_are_redirecting_to_the_correct_product/category_pages_and_none_of_the_links_are_broken,Verify_that_all_the_information_displayed__product_name,_category_name,_price,_and_product_description_is_clearly_visible,Verify_that_all_the_images__product_and_banner_are_clearly_visible,Verify_that_category_pages_have_a_relevant_product_listed,_specific_to_the_category,Verify_that_the_correct_count_of_total_products_is_displayed_on_the_category_pages,Search__Verify_that_on_searching,_all_the_products_satisfying_the_search_criteria_are_visible_on_the_search_result_page,Search__Verify_that_on_searching,_products_get_displayed_on_the_basis_of_their_relevancy,Search__Verify_that_the_count_of_products_is_correctly_displayed_on_the_search_result_page_for_a_particular_search_term,Filtering__Verify_that_filtering_functionality_correctly_filters_products_based_on_the_filter_applied,Filtering__Verify_that_filtering_works_correctly_on_category_pages,Filtering__Verify_that_filtering_works_correctly_on_the_search_result_page,Filtering__Verify_that_the_correct_count_of_total_products_is_displayed_after_a_filter_is_applied,Filtering__Verify_that_the_correct_count_and_products_get_displayed_on_applying_multiple_filters,Sorting__Verify_that_all_the_sort_options_work_correctly_On_sorting_the_products_based_on_the_sort_option_chosen,Sorting__Verify_that_sorting_works_correctly_on_the_category_pages,Sorting__Verify_that_sorting_works_correctly_on_the_search_result_page,Sorting__Verify_that_sorting_works_correctly_on_the_pages_containing_the_filtered_result_after_applying_filters,Sorting__Verify_that_the_product_count_remains_the_same_irrespective_of_the_sorting_option_applied", 23 | }; 24 | 25 | // for (const suiteName in testSuiteTestScriptObject) { 26 | // let testCaseList = testSuiteTestScriptObject[suiteName].split(","); 27 | // for (let iLoop = 0; testCaseList[iLoop]; iLoop++) { 28 | // //console.log(testCaseList[iLoop]); 29 | // test(`${testCaseList[iLoop]}`, async ({ request }) => { 30 | // const apiHelper = await new ApiHelper(request); // 31 | // let todayDate = new Date(); 32 | // let env = "SIT"; 33 | // let runid = `ecom_run_${moment().format("YYYY-MM-DD")}`; //ecom_run_02_17_2024 34 | // let payLoad = { 35 | // runid: runid, 36 | // suite: suiteName, 37 | // testcasename: testCaseList[iLoop], 38 | // status: "PASS", 39 | // env: "SIT", 40 | // failurereason: "", 41 | // duration: 10, 42 | // browser: null, 43 | // timestamp: moment().format("YYYY-MM-DD HH:mm:ss"), //"2024-03-17 16:40:48", 44 | // reportpath: "abc.html", 45 | // subscriptionkey: "clttt86dt0000mxs8a9thes2h", 46 | // testid: `${runid}|${suiteName}|${testCaseList[iLoop]}|${env}`, 47 | // executiondate: `${moment().format("YYYY-MM-DD")}`, 48 | // }; 49 | // console.log("payload : ", payLoad); 50 | // const responseMsg = await apiHelper.invokePostApi( 51 | // "/api/testcase/updateTestCaseExecution", 52 | // payLoad 53 | // ); 54 | // }); 55 | // } 56 | // } 57 | -------------------------------------------------------------------------------- /data/db.json: -------------------------------------------------------------------------------- 1 | { 2 | "brands": [ 3 | { 4 | "id": "01J47796Q8XWWJ66Z4MX764YKS", 5 | "name": "Hand Tools", 6 | "slug": "hand-tools", 7 | "parent_id": null, 8 | "sub_categories": [ 9 | { 10 | "id": "01J47796QCFV3FS7SEKFF51VD1", 11 | "name": "Hammer", 12 | "slug": "hammer", 13 | "parent_id": "01J47796Q8XWWJ66Z4MX764YKS", 14 | "sub_categories": [] 15 | }, 16 | { 17 | "id": "01J47796QCFV3FS7SEKFF51VD2", 18 | "name": "Hand Saw", 19 | "slug": "hand-saw", 20 | "parent_id": "01J47796Q8XWWJ66Z4MX764YKS", 21 | "sub_categories": [] 22 | }, 23 | { 24 | "id": "01J47796QCFV3FS7SEKFF51VD3", 25 | "name": "Wrench", 26 | "slug": "wrench", 27 | "parent_id": "01J47796Q8XWWJ66Z4MX764YKS", 28 | "sub_categories": [] 29 | }, 30 | { 31 | "id": "01J47796QCFV3FS7SEKFF51VD4", 32 | "name": "Screwdriver", 33 | "slug": "screwdriver", 34 | "parent_id": "01J47796Q8XWWJ66Z4MX764YKS", 35 | "sub_categories": [] 36 | }, 37 | { 38 | "id": "01J47796QCFV3FS7SEKFF51VD5", 39 | "name": "Pliers", 40 | "slug": "pliers", 41 | "parent_id": "01J47796Q8XWWJ66Z4MX764YKS", 42 | "sub_categories": [] 43 | }, 44 | { 45 | "id": "01J47796QCFV3FS7SEKFF51VD6", 46 | "name": "Chisels", 47 | "slug": "chisels", 48 | "parent_id": "01J47796Q8XWWJ66Z4MX764YKS", 49 | "sub_categories": [] 50 | }, 51 | { 52 | "id": "01J47796QCFV3FS7SEKFF51VD7", 53 | "name": "Glass", 54 | "slug": "Glass", 55 | "parent_id": "01J47796Q8XWWJ66Z4MX764YKS", 56 | "sub_categories": [] 57 | } 58 | ] 59 | } 60 | ], 61 | "mobiles": [ 62 | { 63 | "id": 1, 64 | "name": "Google Pixel 6 Pro", 65 | "data": { 66 | "color": "Cloudy White", 67 | "capacity": "128 GB" 68 | }, 69 | "reviewId": 1 70 | }, 71 | { 72 | "id": 2, 73 | "name": "Apple iPhone 12 Mini, 256GB, Blue", 74 | "data": null, 75 | "reviewId": 2 76 | }, 77 | { 78 | "id": 3, 79 | "name": "Apple iPhone 12 Pro Max", 80 | "data": { 81 | "color": "Cloudy White", 82 | "capacity GB": 512 83 | }, 84 | "reviewId": 3 85 | }, 86 | { 87 | "id": 4, 88 | "name": "Apple iPhone 11, 64GB", 89 | "data": { 90 | "price": 389.99, 91 | "color": "Purple" 92 | } 93 | }, 94 | { 95 | "id": 5, 96 | "name": "Samsung Galaxy Z Fold2", 97 | "data": { 98 | "price": 689.99, 99 | "color": "Brown" 100 | } 101 | }, 102 | { 103 | "id": 6, 104 | "name": "Apple AirPods", 105 | "data": { 106 | "generation": "3rd", 107 | "price": 120 108 | } 109 | }, 110 | { 111 | "id": 7, 112 | "name": "Apple MacBook Pro 16", 113 | "data": { 114 | "year": 2019, 115 | "price": 1849.99, 116 | "CPU model": "Intel Core i9", 117 | "Hard disk size": "1 TB" 118 | } 119 | }, 120 | { 121 | "id": 8, 122 | "name": "Apple Watch Series 8", 123 | "data": { 124 | "Strap Colour": "Elderberry", 125 | "Case Size": "41mm" 126 | } 127 | }, 128 | { 129 | "id": 9, 130 | "name": "Beats Studio3 Wireless", 131 | "data": { 132 | "Color": "Red", 133 | "Description": "High-performance wireless noise cancelling headphones" 134 | } 135 | }, 136 | { 137 | "id": 10, 138 | "name": "Apple iPad Mini 5th Gen", 139 | "data": { 140 | "Capacity": "64 GB", 141 | "Screen size": 7.9 142 | } 143 | }, 144 | { 145 | "id": 11, 146 | "name": "Apple iPad Mini 5th Gen", 147 | "data": { 148 | "Capacity": "254 GB", 149 | "Screen size": 7.9 150 | } 151 | }, 152 | { 153 | "id": 12, 154 | "name": "Apple iPad Air", 155 | "data": { 156 | "Generation": "4th", 157 | "Price": "419.99", 158 | "Capacity": "64 GB" 159 | } 160 | }, 161 | { 162 | "id": 13, 163 | "name": "Apple iPad Air", 164 | "data": { 165 | "Generation": "4th", 166 | "Price": "519.99", 167 | "Capacity": "256 GB" 168 | } 169 | }, 170 | { 171 | "data": { 172 | "color": "Cloudy White", 173 | "capacity": "128 GB", 174 | "price": "500" 175 | }, 176 | "reviewId": 5, 177 | "id": 20 178 | } 179 | ], 180 | "reviews": [ 181 | { 182 | "id": 1, 183 | "rating": 3, 184 | "comment": "Mobile camera have good resolution", 185 | "mobileId": 1 186 | }, 187 | { 188 | "id": 2, 189 | "rating": 4, 190 | "comment": "Battery life is good", 191 | "mobileId": 1 192 | }, 193 | { 194 | "id": 3, 195 | "rating": 3, 196 | "comment": "Price is too high", 197 | "mobileId": 2 198 | }, 199 | { 200 | "id": 4, 201 | "rating": 5, 202 | "comment": "Lot of new apps....", 203 | "mobileId": 3 204 | } 205 | ] 206 | } -------------------------------------------------------------------------------- /tests/resources/uiMap/orangeHRM.json: -------------------------------------------------------------------------------- 1 | { 2 | "App": { 3 | "Login": { 4 | "objType": "page", 5 | "OrangeHRMLogo": { 6 | "objType": "img", 7 | "locatorType": "xpath", 8 | "locatorValue": "//img[@alt='company-branding']" 9 | }, 10 | "LoginInfo": {}, 11 | "UsernameText": { 12 | "objType": "text", 13 | "locatorType": "xpath", 14 | "locatorValue": "//label[normalize-space()='Username']", 15 | "label": "Username" 16 | }, 17 | "Username": { 18 | "objType": "input", 19 | "locatorType": "xpath", 20 | "locatorValue": "//input[@placeholder='Username']", 21 | "label": "", 22 | "validTestData": "a,abcde", 23 | "invalidTestData": ",abcdef,%%%%,$$$$", 24 | "saveTestData": "admin,test", 25 | "fieldErrorMessage": "Required,invalid error" 26 | }, 27 | "PasswordText": { 28 | "objType": "text", 29 | "locatorType": "xpath", 30 | "locatorValue": "//label[normalize-space()='Password']", 31 | "label": "Password" 32 | }, 33 | "Password": { 34 | "objType": "input", 35 | "locatorType": "xpath", 36 | "locatorValue": "//input[@placeholder='Password']", 37 | "label": "", 38 | "validTestData": "", 39 | "invalidTestData": "", 40 | "saveTestData": "", 41 | "fieldErrorMessage": "" 42 | }, 43 | "Login": { 44 | "elementType": "button", 45 | "locatorType": "xpath", 46 | "locatorValue": "//button[normalize-space()='Login']", 47 | "labelName": "Login" 48 | }, 49 | "Forgotyourpassword": { 50 | "elementType": "link", 51 | "locatorType": "xpath", 52 | "locatorValue": "//p[normalize-space()='Forgot your password?']", 53 | "labelName": "Forgot your password?" 54 | } 55 | }, 56 | "Admin": { 57 | "objType": "sidebar", 58 | "locatorType": "xpath", 59 | "locatorValue": "//a[@class='oxd-main-menu-item active']//span[1]", 60 | "label": "Admin", 61 | "UserManagement": { 62 | "objType": "tab", 63 | "locatorType": "xpath", 64 | "locatorValue": "//span[normalize-space()='User Management']", 65 | "label": "User Management", 66 | "Users": { 67 | "objType": "subtab", 68 | "locatorType": "xpath", 69 | "locatorValue": "//a[text()='Users']", 70 | "SystemUsers": { 71 | "objType": "section", 72 | "locatorType": "xpath", 73 | "locatorValue": "//*[text()='System Users']", 74 | "UsernameText": { 75 | "objType": "text", 76 | "locatorType": "xpath", 77 | "locatorValue": "//label[text()='Username']", 78 | "label": "Username", 79 | "Username": { 80 | "objType": "input", 81 | "locatorType": "xpath", 82 | "locatorValue": "//input[@class='oxd-input oxd-input--active']", 83 | "label": "", 84 | "validTestData": "", 85 | "invalidTestData": "", 86 | "saveTestData": "", 87 | "fieldErrorMessage": "", 88 | "UserRoleText": { 89 | "objType": "text", 90 | "locatorType": "xpath", 91 | "locatorValue": "//label[text()='User Role']", 92 | "label": "User Role", 93 | "UserRole": { 94 | "objType": "input", 95 | "locatorType": "xpath", 96 | "locatorValue": "//div[@class='oxd-select-text-input']", 97 | "label": "", 98 | "validTestData": "", 99 | "invalidTestData": "", 100 | "saveTestData": "", 101 | "fieldErrorMessage": "", 102 | "EmployeeNameText": { 103 | "objType": "text", 104 | "locatorType": "xpath", 105 | "locatorValue": "//label[text()='Employee Name']", 106 | "label": "Employee Name", 107 | "EmployeeName": { 108 | "objType": "input", 109 | "locatorType": "xpath", 110 | "locatorValue": "//input[@placeholder='Type for hints...']", 111 | "label": "", 112 | "validTestData": "", 113 | "invalidTestData": "", 114 | "saveTestData": "", 115 | "fieldErrorMessage": "", 116 | "StatusText": { 117 | "objType": "text", 118 | "locatorType": "xpath", 119 | "locatorValue": "//label[text()='Status']", 120 | "label": "User Role", 121 | "Status": { 122 | "objType": "input", 123 | "locatorType": "xpath", 124 | "locatorValue": "//div[@class='oxd-select-text-input']", 125 | "label": "", 126 | "validTestData": "", 127 | "invalidTestData": "", 128 | "saveTestData": "", 129 | "fieldErrorMessage": "", 130 | "Reset": { 131 | "elementType": "button", 132 | "locatorType": "xpath", 133 | "locatorValue": "//button[text()=' Reset ']", 134 | "labelName": "Reset", 135 | "Search": { 136 | "elementType": "button", 137 | "locatorType": "xpath", 138 | "locatorValue": "//button[text()=' Search ']", 139 | "labelName": "Search", 140 | "Add": { 141 | "elementType": "button", 142 | "locatorType": "xpath", 143 | "locatorValue": "//button[text()=' Add ']", 144 | "labelName": "Add", 145 | "RecordsTable": { 146 | "objType": "table/grid", 147 | "locatorType": "xpath", 148 | "locatorValue": "//div[@role='table']" 149 | } 150 | } 151 | } 152 | } 153 | } 154 | } 155 | } 156 | } 157 | } 158 | } 159 | } 160 | } 161 | } 162 | } 163 | } 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/utils/harAnalyser.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync, writeFileSync } from 'fs' 2 | // Types for findings 3 | type SlowRequest = Record<'url' | 'method' | 'timestamp', string> & Record<'waitTime', number> 4 | type LargePayload = Record<'url' | 'method' | 'totalSize' | 'timestamp', string> 5 | type ErrorResponse = Record<'url' | 'method' | 'status' | 'errorMessage' | 'timestamp', string> 6 | 7 | // Initialize findings 8 | const findings = { 9 | slowRequests: [] as SlowRequest[], 10 | largePayloads: [] as LargePayload[], 11 | errorResponses: [] as ErrorResponse[], 12 | }; 13 | 14 | // Analyze HAR entries 15 | export const analyzeHAR = async (harFilePath: string, htmlOututPath: string) => { 16 | const harData = JSON.parse(readFileSync(harFilePath, 'utf-8')) 17 | harData.log.entries.forEach((entry: { request: any; response: any; timings: any; startedDateTime: any }) => { 18 | const { request, response, timings, startedDateTime } = entry 19 | const { method, url } = request 20 | const { status, statusText, headersSize, bodySize } = response 21 | 22 | // Identify slow requests (e.g., > 1 second) 23 | if (timings.wait > 1000) { 24 | findings.slowRequests.push({ 25 | url, 26 | method, 27 | waitTime: timings.wait, 28 | timestamp: startedDateTime, 29 | }) 30 | } 31 | 32 | // Identify large payloads (e.g., > 1 MB) 33 | const totalSize = headersSize + bodySize 34 | if (totalSize > 1024 * 1024) { 35 | findings.largePayloads.push({ 36 | url, 37 | method, 38 | totalSize: `${(totalSize / 1024 / 1024).toFixed(2)} MB`, 39 | timestamp: startedDateTime, 40 | }) 41 | } 42 | 43 | // Identify error responses (status codes 4xx and 5xx) 44 | if (status !== 200) { 45 | findings.errorResponses.push({ 46 | url, 47 | method, 48 | status, 49 | errorMessage: statusText || 'N/A', 50 | timestamp: startedDateTime, 51 | }) 52 | } 53 | }) 54 | 55 | // Generate HTML report 56 | interface GenerateHTMLData { 57 | slowRequests: SlowRequest[] 58 | largePayloads: LargePayload[] 59 | errorResponses: ErrorResponse[] 60 | } 61 | 62 | interface GenerateHTML { 63 | (data: GenerateHTMLData): string 64 | } 65 | 66 | const generateHTML: GenerateHTML = (data) => ` 67 | 68 | 69 | 70 | 71 | 72 | Data Analysis Report 73 | 74 | 75 | 76 |
77 |

Comprehensive API Data Analysis Report

78 |
79 |
80 |
81 |

Summary

82 |

83 | This report provides a detailed analysis of the provided HAR file, highlighting slow requests, large payloads, and error responses. 84 |

85 |
86 |
87 |

Slow Requests

88 |
89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | ${data.slowRequests 100 | .map( 101 | (req: SlowRequest) => ` 102 | 103 | 104 | 105 | 106 | 107 | 108 | `, 109 | ) 110 | .join('')} 111 | 112 |
URLMethodWait Time (ms)Timestamp
${req.url}${req.method}${req.waitTime}${req.timestamp}
113 |
114 |
115 |
116 |

Large Payloads

117 |
118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | ${data.largePayloads 129 | .map( 130 | (payload: LargePayload) => ` 131 | 132 | 133 | 134 | 135 | 136 | 137 | `, 138 | ) 139 | .join('')} 140 | 141 |
URLMethodSizeTimestamp
${payload.url}${payload.method}${payload.totalSize}${payload.timestamp}
142 |
143 |
144 |
145 |

Error Responses

146 |
147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | ${data.errorResponses 159 | .map( 160 | (error: ErrorResponse) => ` 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | `, 169 | ) 170 | .join('')} 171 | 172 |
URLMethodStatusError MessageTimestamp
${error.url}${error.method}${error.status}${error.errorMessage}${error.timestamp}
173 |
174 |
175 |
176 |
177 |

© 2025 Comprehensive Data Analysis

178 |
179 | 180 | 181 | 182 | ` 183 | 184 | // Write the HTML report to a file 185 | const htmlContent = generateHTML(findings) 186 | writeFileSync(htmlOututPath, htmlContent, 'utf-8') 187 | console.log('Data analysis report generated successfully') 188 | } -------------------------------------------------------------------------------- /src/helper/api/apiHelper.ts: -------------------------------------------------------------------------------- 1 | import { Helper } from "@src/helper/Helper"; 2 | import type { APIRequestContext, APIResponse } from "@playwright/test"; 3 | import { ApiError } from "@src/utils/error/ErrorManager"; 4 | import { validateSchema } from 'playwright-ajv-schema-validator'; 5 | import { step } from "@src/utils/report/ReportAction"; 6 | import { logError, logInfo } from "@src/utils/report/Logger"; 7 | 8 | enum methodType { 9 | GET = "get", 10 | POST = "post", 11 | DELETE = "delete", 12 | PUT = "put", 13 | PATCH = "patch", 14 | HEAD = "head", 15 | } 16 | 17 | const BASE_URL = "https://restful-booker.herokuapp.com"; 18 | 19 | export class ApiHelper extends Helper { 20 | private readonly apiRequest: APIRequestContext; 21 | private readonly retries: number; 22 | private readonly timeout: number; 23 | 24 | 25 | /** 26 | * The constructor function initializes a new context for the API. 27 | * @param {any} apiContext - The `apiContext` parameter is an object that represents the context of an 28 | * API. It is used to store and manage information related to the API, such as authentication 29 | * credentials, request headers, and other configuration settings. 30 | */ 31 | constructor(apiRequest: APIRequestContext, config?: { timeout?: number; retries?: number }) { 32 | super(); 33 | this.apiRequest = apiRequest; 34 | this.timeout = config?.timeout || 30000; 35 | this.retries = config?.retries || 3; 36 | } 37 | 38 | 39 | /** 40 | * Simplified helper for making API requests and returning the status and JSON body. 41 | * This helper automatically performs the request based on the provided method, URL, body, and headers. 42 | * 43 | * @param {string} params.method - The HTTP method to use (POST, GET, PUT, DELETE). 44 | * @param {string} params.url - The URL to send the request to. 45 | * @param {string} [params.baseUrl] - The base URL to prepend to the request URL. 46 | * @param {Record | null} [params.body=null] - The body to send with the request (for POST and PUT requests). 47 | * @param {Record | undefined} [params.headers=undefined] - The headers to include with the request. 48 | * @returns {Promise<{ status: number; body: unknown }>} - An object containing the status code and the parsed response body. 49 | * - `status`: The HTTP status code returned by the server. 50 | * - `body`: The parsed JSON response body from the server. 51 | */ 52 | @step('hitApiEndPoint') 53 | async hitApiEndPoint({ 54 | method, 55 | endPoint, 56 | body = null, 57 | headers, 58 | statusCode 59 | }: { method: methodType, endPoint: string, body?: string | null, headers?: string, statusCode: number }): Promise<{ status: number, body: unknown }> { 60 | 61 | let response: APIResponse | null = null; 62 | 63 | const options: { 64 | data?: Record | null; 65 | headers?: Record; 66 | } = {}; 67 | 68 | if (body) { options.data = body; } 69 | 70 | if (headers) { 71 | options.headers = { 72 | Authorization: `Token ${headers}`, 73 | "Content-Type": "application/json", 74 | }; 75 | } else { 76 | options.headers = { 77 | "Content-Type": "application/json", 78 | }; 79 | } 80 | 81 | switch (method.toLowerCase()) { 82 | case methodType.GET: 83 | response = await this.invokeGetApi(endPoint, options); 84 | break; 85 | case methodType.POST: 86 | response = await this.invokePostApi(endPoint, body, options); 87 | break; 88 | case methodType.DELETE: 89 | response = await this.invokeDeleteApi(endPoint, options); 90 | break; 91 | case methodType.PUT: 92 | response = await this.invokePutApi(endPoint, body, options); 93 | break; 94 | default: 95 | logError(`Unsupported operation type: ${method}`); 96 | } 97 | 98 | if (!response) { 99 | throw new ApiError(`No response received for method ${method} on ${endPoint}`); 100 | } 101 | 102 | if (!response.ok()) { 103 | const text = await response.text(); 104 | new ApiError( 105 | `POST ${endPoint} failed: ${response.status()} ${response.statusText()} | body: ${text}` 106 | ); 107 | } 108 | const status = response.status(); 109 | 110 | let bodyData: unknown = null; 111 | const contentType = response.headers()["content-type"] || ""; 112 | 113 | try { 114 | if (contentType.includes("application/json")) { 115 | bodyData = await response.json(); 116 | } else if (contentType.includes("text/")) { 117 | bodyData = await response.text(); 118 | } 119 | } catch (err) { 120 | console.warn(`Failed to parse response body for status ${status}: ${err}`); 121 | } 122 | 123 | return { status, body: bodyData }; 124 | } 125 | 126 | @step('invokeGetApi') 127 | async invokeGetApi(endPoint: string, options: unknown): Promise { 128 | try { 129 | logInfo(`Making GET request to endPoint: ${BASE_URL}${endPoint} `); 130 | const response = await this.apiRequest.get(endPoint, options); 131 | return await response; 132 | } catch (error) { 133 | console.log(error); 134 | throw new ApiError("Get request failed"); 135 | } 136 | } 137 | 138 | @step('invokeDeleteApi') 139 | async invokeDeleteApi(endPoint: string, options: unknown): Promise { 140 | let response; 141 | try { 142 | logInfo( 143 | `Making DELETE request to endPoint: ${BASE_URL}${endPoint} ` 144 | ); 145 | response = await this.apiRequest.delete(endPoint, options); 146 | 147 | return await response; 148 | } catch (error) { 149 | throw new ApiError("Delete request failed"); 150 | } 151 | } 152 | 153 | /** 154 | * The function `invokePostApi` is an asynchronous function that sends a POST request to an API 155 | * endpoint with a payload and returns the response data. 156 | * @param {string} endPoint - The `endPoint` parameter is a string that represents the URL or endpoint 157 | * of the API you want to call. 158 | * @param {object} payload - The `payload` parameter is an object that contains the data to be sent in 159 | * the request body. It is typically used to send data to the server for processing or to update a 160 | * resource. 161 | * @returns the response data as a JSON object if the response status is 200. If there is an error, it 162 | * will return the error object. 163 | */ 164 | @step('invokePostApi') 165 | async invokePostApi( 166 | endPoint: string, 167 | body: unknown, 168 | options: unknown, 169 | ): Promise { 170 | let response; 171 | try { 172 | logInfo( 173 | `Making POST request to endPoint: ${BASE_URL}${endPoint} ` 174 | ); 175 | const reqOptions: any = { ...(options as any) }; 176 | if (body !== undefined && body !== null) { 177 | reqOptions.data = body; 178 | } 179 | response = await this.apiRequest.post(endPoint, reqOptions); 180 | return await response; 181 | } catch (error) { 182 | throw new ApiError("Post request failed"); 183 | } 184 | } 185 | 186 | @step('invokePutApi') 187 | async invokePutApi( 188 | endPoint: string, 189 | payload: unknown, 190 | options: unknown, 191 | ): Promise { 192 | let response; 193 | try { 194 | logInfo( 195 | `Making PUT request to endPoint: ${BASE_URL}${endPoint} ` 196 | ); 197 | const reqOptions: any = { ...(options as any) }; 198 | if (payload !== undefined && payload !== null) { 199 | reqOptions.data = payload; 200 | } 201 | response = await this.apiRequest.put(endPoint, reqOptions); 202 | return await response; 203 | } catch (error) { 204 | throw new ApiError("Post request failed"); 205 | } 206 | } 207 | 208 | getResponseHeader(response:APIResponse){ 209 | return response.headers() 210 | } 211 | 212 | validateSchema(response:APIResponse,schema:object){ 213 | 214 | } 215 | } 216 | 217 | 218 | -------------------------------------------------------------------------------- /logs/rejections.log: -------------------------------------------------------------------------------- 1 | {"date":"Sat Nov 08 2025 09:19:40 GMT+0530 (India Standard Time)","error":{},"level":"error","message":"unhandledRejection: columnHeaderText.textextContent is not a function\nTypeError: columnHeaderText.textextContent is not a function\n at eval (eval-31c3f682.repl:2:33)\n at textContent (eval-31c3f682.repl:3:3)\n at C:\\fastcode\\playwright-framework-template\\tests\\web\\example\\webTable.spec.ts:18:62\n at C:\\fastcode\\playwright-framework-template\\node_modules\\playwright\\lib\\worker\\workerMain.js:310:9\n at C:\\fastcode\\playwright-framework-template\\node_modules\\playwright\\lib\\worker\\testInfo.js:305:11\n at TimeoutManager.withRunnable (C:\\fastcode\\playwright-framework-template\\node_modules\\playwright\\lib\\worker\\timeoutManager.js:67:14)\n at TestInfoImpl._runWithTimeout (C:\\fastcode\\playwright-framework-template\\node_modules\\playwright\\lib\\worker\\testInfo.js:303:7)\n at C:\\fastcode\\playwright-framework-template\\node_modules\\playwright\\lib\\worker\\workerMain.js:308:7\n at WorkerMain._runTest (C:\\fastcode\\playwright-framework-template\\node_modules\\playwright\\lib\\worker\\workerMain.js:283:5)\n at WorkerMain.runTestGroup (C:\\fastcode\\playwright-framework-template\\node_modules\\playwright\\lib\\worker\\workerMain.js:198:11)\n at process. (C:\\fastcode\\playwright-framework-template\\node_modules\\playwright\\lib\\common\\process.js:65:22)","os":{"loadavg":[0,0,0],"uptime":782977.14},"process":{"argv":["C:\\Program Files\\nodejs\\node.exe","C:\\fastcode\\playwright-framework-template\\node_modules\\playwright\\lib\\common\\process.js"],"cwd":"C:\\fastcode\\playwright-framework-template","execPath":"C:\\Program Files\\nodejs\\node.exe","gid":null,"memoryUsage":{"arrayBuffers":1296009,"external":5094883,"heapTotal":70443008,"heapUsed":42648656,"rss":140890112},"pid":21108,"uid":null,"version":"v22.18.0"},"rejection":true,"service":"job-posting-service","stack":"TypeError: columnHeaderText.textextContent is not a function\n at eval (eval-31c3f682.repl:2:33)\n at textContent (eval-31c3f682.repl:3:3)\n at C:\\fastcode\\playwright-framework-template\\tests\\web\\example\\webTable.spec.ts:18:62\n at C:\\fastcode\\playwright-framework-template\\node_modules\\playwright\\lib\\worker\\workerMain.js:310:9\n at C:\\fastcode\\playwright-framework-template\\node_modules\\playwright\\lib\\worker\\testInfo.js:305:11\n at TimeoutManager.withRunnable (C:\\fastcode\\playwright-framework-template\\node_modules\\playwright\\lib\\worker\\timeoutManager.js:67:14)\n at TestInfoImpl._runWithTimeout (C:\\fastcode\\playwright-framework-template\\node_modules\\playwright\\lib\\worker\\testInfo.js:303:7)\n at C:\\fastcode\\playwright-framework-template\\node_modules\\playwright\\lib\\worker\\workerMain.js:308:7\n at WorkerMain._runTest (C:\\fastcode\\playwright-framework-template\\node_modules\\playwright\\lib\\worker\\workerMain.js:283:5)\n at WorkerMain.runTestGroup (C:\\fastcode\\playwright-framework-template\\node_modules\\playwright\\lib\\worker\\workerMain.js:198:11)\n at process. (C:\\fastcode\\playwright-framework-template\\node_modules\\playwright\\lib\\common\\process.js:65:22)","timestamp":"2025-11-08T03:49:40.835Z","trace":[{"column":33,"file":"eval-31c3f682.repl","function":"eval","line":2,"method":null,"native":false},{"column":3,"file":"eval-31c3f682.repl","function":"textContent","line":3,"method":null,"native":false},{"column":62,"file":"C:\\fastcode\\playwright-framework-template\\tests\\web\\example\\webTable.spec.ts","function":null,"line":18,"method":null,"native":false},{"column":9,"file":"C:\\fastcode\\playwright-framework-template\\node_modules\\playwright\\lib\\worker\\workerMain.js","function":null,"line":310,"method":null,"native":false},{"column":11,"file":"C:\\fastcode\\playwright-framework-template\\node_modules\\playwright\\lib\\worker\\testInfo.js","function":null,"line":305,"method":null,"native":false},{"column":14,"file":"C:\\fastcode\\playwright-framework-template\\node_modules\\playwright\\lib\\worker\\timeoutManager.js","function":"TimeoutManager.withRunnable","line":67,"method":"withRunnable","native":false},{"column":7,"file":"C:\\fastcode\\playwright-framework-template\\node_modules\\playwright\\lib\\worker\\testInfo.js","function":"TestInfoImpl._runWithTimeout","line":303,"method":"_runWithTimeout","native":false},{"column":7,"file":"C:\\fastcode\\playwright-framework-template\\node_modules\\playwright\\lib\\worker\\workerMain.js","function":null,"line":308,"method":null,"native":false},{"column":5,"file":"C:\\fastcode\\playwright-framework-template\\node_modules\\playwright\\lib\\worker\\workerMain.js","function":"WorkerMain._runTest","line":283,"method":"_runTest","native":false},{"column":11,"file":"C:\\fastcode\\playwright-framework-template\\node_modules\\playwright\\lib\\worker\\workerMain.js","function":"WorkerMain.runTestGroup","line":198,"method":"runTestGroup","native":false},{"column":22,"file":"C:\\fastcode\\playwright-framework-template\\node_modules\\playwright\\lib\\common\\process.js","function":null,"line":65,"method":null,"native":false}]} 2 | {"date":"Sat Nov 08 2025 09:22:59 GMT+0530 (India Standard Time)","error":{"name":"Error"},"level":"error","message":"unhandledRejection: locator.textContent: Error: strict mode violation: locator('//*[@id=\\'table1\\']').locator('thead').locator('tr').first().locator('th').locator('span') resolved to 6 elements:\n 1) Last Name aka locator('#table1').getByText('Last Name')\n 2) First Name aka locator('#table1').getByText('First Name')\n 3) Email aka locator('#table1').getByText('Email')\n 4) Due aka locator('#table1').getByText('Due')\n 5) Web Site aka locator('#table1').getByText('Web Site')\n 6) Action aka locator('#table1').getByText('Action')\n\nCall log:\n\u001b[2m - waiting for locator('//*[@id=\\'table1\\']').locator('thead').locator('tr').first().locator('th').locator('span')\u001b[22m\n\nlocator.textContent: Error: strict mode violation: locator('//*[@id=\\'table1\\']').locator('thead').locator('tr').first().locator('th').locator('span') resolved to 6 elements:\n 1) Last Name aka locator('#table1').getByText('Last Name')\n 2) First Name aka locator('#table1').getByText('First Name')\n 3) Email aka locator('#table1').getByText('Email')\n 4) Due aka locator('#table1').getByText('Due')\n 5) Web Site aka locator('#table1').getByText('Web Site')\n 6) Action aka locator('#table1').getByText('Action')\n\nCall log:\n\u001b[2m - waiting for locator('//*[@id=\\'table1\\']').locator('thead').locator('tr').first().locator('th').locator('span')\u001b[22m\n\n at eval (eval-b5eed3dc.repl:2:66)\n at nth (eval-b5eed3dc.repl:3:3)\n at C:\\fastcode\\playwright-framework-template\\tests\\web\\example\\webTable.spec.ts:17:47","os":{"loadavg":[0,0,0],"uptime":783175.89},"process":{"argv":["C:\\Program Files\\nodejs\\node.exe","C:\\fastcode\\playwright-framework-template\\node_modules\\playwright\\lib\\common\\process.js"],"cwd":"C:\\fastcode\\playwright-framework-template","execPath":"C:\\Program Files\\nodejs\\node.exe","gid":null,"memoryUsage":{"arrayBuffers":1313693,"external":5068800,"heapTotal":34476032,"heapUsed":32383224,"rss":107753472},"pid":8916,"uid":null,"version":"v22.18.0"},"rejection":true,"service":"job-posting-service","stack":"locator.textContent: Error: strict mode violation: locator('//*[@id=\\'table1\\']').locator('thead').locator('tr').first().locator('th').locator('span') resolved to 6 elements:\n 1) Last Name aka locator('#table1').getByText('Last Name')\n 2) First Name aka locator('#table1').getByText('First Name')\n 3) Email aka locator('#table1').getByText('Email')\n 4) Due aka locator('#table1').getByText('Due')\n 5) Web Site aka locator('#table1').getByText('Web Site')\n 6) Action aka locator('#table1').getByText('Action')\n\nCall log:\n\u001b[2m - waiting for locator('//*[@id=\\'table1\\']').locator('thead').locator('tr').first().locator('th').locator('span')\u001b[22m\n\n at eval (eval-b5eed3dc.repl:2:66)\n at nth (eval-b5eed3dc.repl:3:3)\n at C:\\fastcode\\playwright-framework-template\\tests\\web\\example\\webTable.spec.ts:17:47","timestamp":"2025-11-08T03:52:59.577Z","trace":[{"column":66,"file":"eval-b5eed3dc.repl","function":"eval","line":2,"method":null,"native":false},{"column":3,"file":"eval-b5eed3dc.repl","function":"nth","line":3,"method":null,"native":false},{"column":47,"file":"C:\\fastcode\\playwright-framework-template\\tests\\web\\example\\webTable.spec.ts","function":null,"line":17,"method":null,"native":false}]} 3 | {"date":"Sat Nov 08 2025 09:23:12 GMT+0530 (India Standard Time)","error":{},"level":"error","message":"unhandledRejection: locator.textContent: Test ended.\nCall log:\n\u001b[2m - waiting for locator('//*[@id=\\'table1\\']').locator('thead').locator('tr').first().locator('th').locator('span')\u001b[22m\n\nlocator.textContent: Test ended.\nCall log:\n\u001b[2m - waiting for locator('//*[@id=\\'table1\\']').locator('thead').locator('tr').first().locator('th').locator('span')\u001b[22m\n\n at C:\\fastcode\\playwright-framework-template\\tests\\web\\example\\webTable.spec.ts:17:84","os":{"loadavg":[0,0,0],"uptime":783188.687},"process":{"argv":["C:\\Program Files\\nodejs\\node.exe","C:\\fastcode\\playwright-framework-template\\node_modules\\playwright\\lib\\common\\process.js"],"cwd":"C:\\fastcode\\playwright-framework-template","execPath":"C:\\Program Files\\nodejs\\node.exe","gid":null,"memoryUsage":{"arrayBuffers":2182349,"external":5944843,"heapTotal":69562368,"heapUsed":38869160,"rss":139264000},"pid":17000,"uid":null,"version":"v22.18.0"},"rejection":true,"service":"job-posting-service","stack":"locator.textContent: Test ended.\nCall log:\n\u001b[2m - waiting for locator('//*[@id=\\'table1\\']').locator('thead').locator('tr').first().locator('th').locator('span')\u001b[22m\n\n at C:\\fastcode\\playwright-framework-template\\tests\\web\\example\\webTable.spec.ts:17:84","timestamp":"2025-11-08T03:53:12.381Z","trace":[{"column":84,"file":"C:\\fastcode\\playwright-framework-template\\tests\\web\\example\\webTable.spec.ts","function":null,"line":17,"method":null,"native":false}]} 4 | -------------------------------------------------------------------------------- /Notes: -------------------------------------------------------------------------------- 1 | import { linkSync } from 'fs'; 2 | 3 | /** 4 | * Extends Playwright locator with a helper function to find, store, and manage elements using different locator strategies 5 | * @param {import('@playwright/test').Page} page - Playwright page instance 6 | * @returns {Object} Helper object with locator methods 7 | */ 8 | function createLocatorHelper(page) { 9 | // Internal storage for locators 10 | const locatorStorage = new Map(); 11 | 12 | return { 13 | /** 14 | * Finds a single element using the specified locator strategy 15 | * Checks if element is displayed and enabled 16 | * Returns null if element is not found 17 | * @param {string} type - Locator type ('css', 'xpath', 'text', 'testid', 'role', 'label') 18 | * @param {string} value - Locator value 19 | * @param {Object} [options] - Optional locator options 20 | * @returns {Promise<{locator: import('@playwright/test').Locator, isDisplayed: boolean, isEnabled: boolean} | null>} Playwright locator with status or null 21 | */ 22 | async findElement(type, value, options = {}) { 23 | try { 24 | let locator; 25 | switch (type.toLowerCase()) { 26 | case 'css': 27 | locator = page.locator(value, options); 28 | break; 29 | case 'xpath': 30 | locator = page.locator(`xpath=${value}`, options); 31 | break; 32 | case 'text': 33 | locator = page.getByText(value, options); 34 | break; 35 | case 'testid': 36 | locator = page.getByTestId(value, options); 37 | break; 38 | case 'role': 39 | locator = page.getByRole(value, options); 40 | break; 41 | case 'label': 42 | locator = page.getByLabel(value, options); 43 | break; 44 | default: 45 | console.warn(`Unsupported locator type: ${type}`); 46 | return null; 47 | } 48 | 49 | // Check if element exists 50 | const count = await locator.count(); 51 | if (count === 0) { 52 | console.warn(`Element not found with ${type} locator: ${value}`); 53 | return null; 54 | } 55 | 56 | // Check visibility and enabled status 57 | const isDisplayed = await locator.isVisible(); 58 | const isEnabled = await locator.isEnabled(); 59 | 60 | return { 61 | locator, 62 | isDisplayed, 63 | isEnabled 64 | }; 65 | } catch (error) { 66 | console.warn(`Error while finding element with ${type} locator: ${value}. Error: ${error.message}`); 67 | return null; 68 | } 69 | } 70 | 71 | 72 | 73 | /** 74 | * Waits for an element to be visible using the specified locator strategy 75 | * @param {string} type - Locator type 76 | * @param {string} value - Locator value 77 | * @param {Object} [options] - Optional locator options with timeout 78 | * @returns {Promise} Playwright locator 79 | */ 80 | async waitForElement(type, value, options = { timeout: 5000 }) { 81 | try { 82 | const result = await this.findElement(type, value); 83 | if (!result) { 84 | throw new Error(`Element not found with ${type} locator: ${value}`); 85 | } 86 | await result.locator.waitFor({ state: 'visible', ...options }); 87 | return result.locator; 88 | } catch (error) { 89 | throw new Error(`Failed to wait for element with ${type} locator: ${value}. Error: ${error.message}`); 90 | } 91 | } 92 | 93 | }; 94 | } 95 | 96 | // Example usage: 97 | /* 98 | const { test, expect } = require('@playwright/test'); 99 | const { createLocatorHelper } = require('./playwright_locator_helper'); 100 | 101 | test('example test with enhanced findElement', async ({ page }) => { 102 | const locatorHelper = createLocatorHelper(page); 103 | await page.goto('https://example.com'); 104 | 105 | // Example with CSS locator 106 | const buttonResult = await locatorHelper.findElement('css', 'button.submit'); 107 | if (buttonResult) { 108 | console.log(`Button is displayed: ${buttonResult.isDisplayed}`); 109 | console.log(`Button is enabled: ${buttonResult.isEnabled}`); 110 | if (buttonResult.isDisplayed && buttonResult.isEnabled) { 111 | await buttonResult.locator.click(); 112 | } 113 | } else { 114 | console.log('Button not found'); 115 | } 116 | }); 117 | */ 118 | 119 | module.exports = { createLocatorHelper }; 120 | 121 | // Example usage: 122 | /* 123 | const { test, expect } = require('@playwright/test'); 124 | const { createLocatorHelper } = require('./playwright_locator_helper'); 125 | 126 | test('example test with stored locators', async ({ page }) => { 127 | const locatorHelper = createLocatorHelper(page); 128 | 129 | await page.goto('https://example.com'); 130 | 131 | // Store locators 132 | await locatorHelper.storeLocator('submitButton', 'css', 'button[type="submit"]'); 133 | await locatorHelper.storeLocator('usernameInput', 'css', '#username'); 134 | 135 | // Use stored locators 136 | const usernameInput = await locatorHelper.getStoredLocator('usernameInput'); 137 | await usernameInput.fill('testuser'); 138 | 139 | const submitButton = await locatorHelper.getStoredLocator('submitButton'); 140 | await submitButton.click(); 141 | 142 | // Clean up 143 | locatorHelper.clearStoredLocators(); 144 | }); 145 | */ 146 | 147 | module.exports = { createLocatorHelper }; 148 | 149 | Usage Example 150 | 151 | /** 152 | * Example usage of findElement function for different locator types 153 | * Assumes a webpage with common elements (e.g., a login form) 154 | */ 155 | const { test, expect } = require('@playwright/test'); 156 | const { createLocatorHelper } = require('./playwright_locator_helper'); 157 | 158 | test.describe('Locator Examples', () => { 159 | test.beforeEach(async ({ page }) => { 160 | // Navigate to a sample page (replace with your test URL) 161 | await page.goto('https://example.com/login'); 162 | }); 163 | 164 | test('Use CSS locator', async ({ page }) => { 165 | const locatorHelper = createLocatorHelper(page); 166 | 167 | // Find an input field by CSS selector 168 | const usernameInput = await locatorHelper.findElement('css', '#username'); 169 | await usernameInput.fill('testuser'); 170 | 171 | // Verify the input value 172 | await expect(usernameInput).toHaveValue('testuser'); 173 | }); 174 | 175 | test('Use XPath locator', async ({ page }) => { 176 | const locatorHelper = createLocatorHelper(page); 177 | 178 | // Find a submit button using XPath 179 | const submitButton = await locatorHelper.findElement('xpath', '//button[@type="submit"]'); 180 | await submitButton.click(); 181 | 182 | // Verify button is clickable (enabled) 183 | await expect(submitButton).toBeEnabled(); 184 | }); 185 | 186 | test('Use Text locator', async ({ page }) => { 187 | const locatorHelper = createLocatorHelper(page); 188 | 189 | // Find a link by its text content 190 | const forgotPasswordLink = await locatorHelper.findElement('text', 'Forgot Password'); 191 | await forgotPasswordLink.click(); 192 | 193 | // Verify navigation or element visibility 194 | await expect(page).toHaveURL(/forgot-password/); 195 | }); 196 | 197 | test('Use TestID locator', async ({ page }) => { 198 | const locatorHelper = createLocatorHelper(page); 199 | 200 | // Find an element by data-testid attribute 201 | const loginForm = await locatorHelper.findElement('testid', 'login-form'); 202 | 203 | // Verify the element is visible 204 | await expect(loginForm).toBeVisible(); 205 | }); 206 | 207 | test('Use Role locator', async ({ page }) => { 208 | const locatorHelper = createLocatorHelper(page); 209 | 210 | // Find a button by its role and name 211 | const loginButton = await locatorHelper.findElement('role', 'button', { name: 'Log In' }); 212 | await loginButton.click(); 213 | 214 | // Verify button interaction 215 | await expect(loginButton).toHaveAttribute('aria-pressed', 'true'); 216 | }); 217 | 218 | test('Use Label locator', async ({ page }) => { 219 | const locatorHelper = createLocatorHelper(page); 220 | 221 | // Find an input by its associated label 222 | const passwordInput = await locatorHelper.findElement('label', 'Password'); 223 | await passwordInput.fill('secure123'); 224 | 225 | // Verify input value 226 | await expect(passwordInput).toHaveValue('secure123'); 227 | }); 228 | }); 229 | 230 | ## phaatak package 231 | https://www.npmjs.com/package/phaatak Try this npm package for 232 | - managing multiple tabs 233 | - handling new windows 234 | - Saving and loading profiles 235 | - Stop the browser 236 | - close All tabs 237 | 238 | 239 | Example code - https://github.com/ghostinlinux/phaatak-playwright-demo/ 240 | 241 | ## Visual testing 242 | https://medium.com/@adam_pajda/playwright-visual-tests-with-git-lfs-and-docker-d537ddd6e86a 243 | 244 | Verify all broken links 245 | https://medium.com/@thananjayan1988/automatic-broken-link-detection-with-playwright-a241a6f41973 246 | 247 | How to link existing chrome browser in playwright run 248 | https://dev.to/mxschmitt/running-playwright-codegen-with-existing-chromium-profiles-5g7k 249 | 250 | Mobile App testing 251 | https://medium.com/@testerstalk/mobile-app-testing-with-playwright-a-step-by-step-guide-in-detail-cabf5def30a9 252 | 253 | Excel data comparision using Playwright 254 | https://medium.com/@testerstalk/how-to-download-validate-excel-file-in-playwright-b8acbb19a4e8 255 | 256 | https://idavidov.eu/never-commit-broken-code-again-a-guide-to-eslint-and-husky-in-playwright?source=more_series_bottom_blogs 257 | 258 | App to run UI test - 259 | https://moatazeldebsy.github.io/test-automation-practices/#/ 260 | 261 | https://github.com/AtulKrSharma/PlaywrightInjectTagsWithCaching/blob/main/playwright.config.ts 262 | 263 | https://www.saucedemo.com/ 264 | https://the-internet.herokuapp.com/ 265 | https://selectorshub.com/xpath-practice-page/ 266 | https://test-edzotech.web.app/doms.html 267 | https://github.com/gauravkhuraana/PlaywrightTypeScriptWithAgenticAI 268 | https://idavidov.eu/my-first-live-session-developing-playwright-framework-for-rest-api-testin 269 | 270 | Try this prompt 271 | https://gauravkhurana.in/docs/AI/prompt-library/#playwright-typescript-framework-creation 272 | 273 | A must follow: 274 | https://github.com/seontechnologies/playwright-utils -------------------------------------------------------------------------------- /.windsurf/rules/rules.md: -------------------------------------------------------------------------------- 1 | --- 2 | trigger: always_on 3 | --- 4 | 5 | ## Primary Goal for AI Agent 6 | 7 | Your primary goal is to assist in the development and maintenance of a robust and maintainable automated testing framework. This includes generating high-quality TypeScript code for Playwright tests (UI, API, E2E), page objects, fixtures, and utility functions, as well as providing expert advice on testing strategies and best practices, all while adhering to the instructions provided in specific prompts. 8 | 9 | ## Tech Stack 10 | 11 | - TypeScript (Strict Mode, ESNext) 12 | - Playwright (latest stable version) 13 | - Zod (for schema validation) 14 | 15 | ## AI Persona & Role 16 | 17 | You are an expert Senior Test Automation Engineer with deep specialization in: 18 | - TypeScript for test automation. 19 | - Playwright for UI, API, and end-to-end testing. 20 | - Designing and implementing scalable and maintainable Page Object Models (POM). 21 | - API testing best practices, including request/response validation using libraries like Zod. 22 | - Frontend and Backend testing considerations relevant to a complex platforms. 23 | - Adherence to strict coding standards and best practices. 24 | 25 | You are expected to: 26 | - Write concise, idiomatic, and technically accurate TypeScript code. 27 | - Provide type-safe examples and ensure all generated code includes correct type annotations. 28 | - Proactively identify potential issues and suggest improvements in test design and implementation *when asked or if it directly relates to the prompt`s request*. 29 | - Explain complex concepts clearly and provide rationale for your suggestions when asked. 30 | - Perform tasks like refactoring, debugging, suggesting improvements, or generating test cases *only when explicitly instructed to do so in a prompt*. 31 | 32 | ## Project Structure & Contextroot/ 33 | | 34 | ├── env/ # Environment configuration files 35 | │ ├── .env.dev 36 | │ └── .env.example 37 | │ 38 | ├── fixture/ # Playwright test fixtures 39 | │ ├── api/ # API specific fixtures 40 | │ │ ├── api-request-fixture.ts # Base fixture for API requests 41 | │ │ ├── plain-function.ts # Utility functions for API interactions 42 | │ │ ├── schemas.ts # Zod schemas for API validation 43 | │ │ └── types-guards.ts # Type guards for API responses 44 | │ └── pom/ # Page Object Model fixtures 45 | │ ├── page-object-fixtures.ts # Fixtures for instantiating page objects 46 | │ └── test-options.ts # Merges various test fixtures (e.g., page object and API fixtures) 47 | | 48 | ├── pages/ # Page Object Model classes 49 | │ ├── adminPanel/ # Page objects for the Admin Panel 50 | │ └── clientSite/ # Page objects for the Client Site 51 | | 52 | ├── tests-data/ # Test data (e.g., JSON, CSV files) - To be populated later. 53 | | 54 | ├── tests/ # Test scripts 55 | │ ├── auth.setup.ts # Global authentication setup 56 | │ ├── AdminPanel/ # E2E tests for the Admin Panel 57 | │ ├── API/ # API tests 58 | │ └── ClientSite/ # E2E tests for the Client Site 59 | | 60 | ├── .gitignore 61 | ├── .prettierrc # Prettier configuration for code formatting 62 | ├── package-lock.json 63 | ├── package.json 64 | ├── playwright.config.ts # Main Playwright configuration 65 | ├── README.md 66 | └── TEST-PLAN.md # Overall test plan document 67 | 68 | **Key Files & Their Purpose:** 69 | - `playwright.config.ts`: Defines global settings, projects, reporters (standard HTML reporter is sufficient), and base URLs. 70 | - `fixture/pom/test-options.ts`: Responsible for merging different sets of fixtures. 71 | - `fixture/api/schemas.ts`: Contains Zod schemas used for validating API request payloads and response bodies. 72 | - `pages/**/*.ts`: Contain Page Object classes, structured as per the guidelines below. 73 | - Any new utility functions or classes should be created in appropriate files within this structure. 74 | 75 | ## Code Style & Best Practices 76 | 77 | **General:** 78 | - Adhere strictly to the settings defined in `.prettierrc`. 79 | - All code must be type-safe. Leverage TypeScript`s features to ensure this. 80 | - Avoid `any` type unless absolutely necessary and provide a justification. 81 | - Use modern JavaScript features (ESNext) where appropriate (e.g., optional chaining, nullish coalescing). 82 | - No commented-out code in the final output. Explanatory comments are acceptable if they clarify complex logic. 83 | 84 | **Playwright Specific:** 85 | 1. **Page Object Model (POM) (`pages/**/*.ts`):** 86 | * Page classes should encapsulate locators (as getters) and methods representing user interactions or assertions for that page/component. 87 | * **Constructor:** Must accept `page: Page` (from Playwright) as its first argument, using the `private` shorthand. 88 | ``` 89 | 90 | typescript 91 | constructor(private page: Page) {} 92 | 93 | 94 | ``` 95 | * **Locators:** 96 | * Define all locators as public `get` accessors. 97 | * **Prioritize user-facing locators (Playwright`s recommendations):** 98 | 1. `this.page.getByRole()` 99 | 2. `this.page.getByText()` 100 | 3. `this.page.getByLabel()` 101 | 4. `this.page.getByPlaceholder()` 102 | 5. `this.page.getByAltText()` 103 | 6. `this.page.getByTitle()` 104 | * Use `this.page.frameLocator()` for elements within iframes. 105 | * Only use `this.page.locator()` (CSS or XPath) as a last resort when semantic locators are not feasible or stable. If using `this.page.locator()`, prefer `data-testid` attributes for robustness (e.g., `this.page.locator('[data-testid="error-message"]')`). 106 | * Ensure locators are specific enough to avoid ambiguity (e.g., using `.first()` if multiple elements match and it`s intended). 107 | * **Methods:** 108 | * Methods should represent complete user actions or flows (e.g., `publishArticle(...)`, `navigateToEditArticlePage()`) or retrieve page state (e.g., `getPublishErrorMessageText()`). 109 | * Methods performing actions **must include validation** to confirm the action`s success. This can involve: 110 | * Waiting for network responses: `await this.page.waitForResponse(response => ...)` 111 | * Asserting element visibility/state: `await expect(this.page.getByRole('heading', { name: title })).toBeVisible();` 112 | * Checking URL changes or other relevant side effects. 113 | * Provide comprehensive JSDoc comments for all public methods, including: 114 | * A clear description of what the method does. 115 | * `@param` for each parameter with its type and description. 116 | * `@returns {Promise}` for action methods or `Promise` for methods returning data. 117 | * Mention any important pre/post-conditions or potential errors if applicable. 118 | * Avoid methods that perform only a single Playwright action (e.g., a method solely for `await someButton.click()`). Incorporate such actions into larger, more meaningful user flow methods. 119 | 120 | * **Example Page Object (`ArticlePage`):** 121 | ``` 122 | 123 | 1. **Tests (`tests/**/*.spec.ts`):** 124 | * Use the merged fixtures from `fixture/pom/test-options.ts` (e.g., `test('should display user dashboard', async ({ articlePage, otherPage }) => { ... })`). 125 | * Tests should be independent and focused on a single piece of functionality or user story. 126 | * Employ web-first assertions (`expect(locator).toBeVisible()`, `expect(page).toHaveURL()`). 127 | * Use built-in configuration objects like `devices` from `playwright.config.ts` when appropriate (e.g., for responsive testing). 128 | * Avoid hardcoded timeouts. Rely on Playwright`s auto-waiting mechanisms and web-first assertions. If a specific timeout is needed for an assertion, pass it as an option: `expect(locator).toBeVisible({ timeout: 5000 });`. 129 | * Group related tests using `test.describe()`. 130 | * Use `test.beforeEach` and `test.afterEach` for setup and teardown logic specific to a group of tests. 131 | * For global setup (like authentication), use the `auth.setup.ts` pattern. 132 | 133 | 2. **API Tests (`tests/API/**/*.spec.ts`):** 134 | * Utilize `apiRequest` context (from fixtures) for making API calls. 135 | * Use Zod schemas from `fixture/api/schemas.ts` to validate request payloads and response structures. 136 | * Verify status codes, headers, and response bodies thoroughly. 137 | 138 | 3. **Fixtures (`fixture/**/*.ts`):** 139 | * Clearly define the purpose and usage of each fixture with JSDoc. 140 | * Ensure fixtures are scoped correctly (worker vs. test) based on their purpose. 141 | * `fixture/pom/page-object-fixture.ts` should define how page objects are instantiated and provided to tests. 142 | * `fixture/api/api-request-fixture.ts` should configure the `APIRequestContext` (e.g., base URL from environment variables). 143 | 144 | ## Output Format & Expectations 145 | 146 | - **Code Generation:** Provide complete, runnable TypeScript code blocks that strictly follow these rules. 147 | - **Explanations:** When asked for explanations or advice, be clear, concise, and provide actionable recommendations aligned with these guidelines. 148 | - **Error Handling in Generated Code:** Generated code should include robust error handling where appropriate (e.g., try-catch for API calls if not handled by Playwright`s `toPass` or similar, checks for element states if complex interactions are involved). Standard Playwright assertions usually handle waits and implicit erroring. 149 | - **Clarity on Ambiguity:** If a user`s prompt is ambiguous or lacks necessary detail to follow these rules, ask clarifying questions before generating code. 150 | 151 | ## Things to Avoid 152 | 153 | - Generating overly complex or monolithic functions/classes. Prefer smaller, focused units that adhere to Single Responsibility Principle. 154 | - Using `page.waitForTimeout()` for arbitrary waits. Rely on web-first assertions and Playwright`s actionability checks. 155 | - Directly manipulating the DOM (e.g., `page.evaluate()` to change styles or content) unless it`s for a specific, justified testing scenario (like mocking complex browser APIs not supported by Playwright). 156 | - Hardcoding sensitive data (credentials, API keys) or environment-specific URLs directly in tests or page objects. Use environment variables (`process.env`) accessed via `playwright.config.ts` or fixtures. 157 | - Suggesting outdated practices or libraries not listed in the Tech Stack. 158 | - Including commented-out code in the final output, unless it`s a JSDoc or a brief, necessary clarification for exceptionally complex logic that cannot be made self-evident through code structure. 159 | - Deviating from the specified POM structure and locator strategies. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Playwright-Framework-Template

2 | 3 | ## Introduction 4 | 5 | Playwright-Framework-Template - This project is based on Microsoft Playwright, Appium, Artillery which enables reliable end-to-end testing, Web testing, API testing, Mobile testing, load testing. It is a single unified for your entire testing suite : UI, API, Mobile, Load testing. 6 | 7 | _☝ If you liked the project, please give a ⭐ on [GitHub](https://github.com/abhaybharti/playwright-framework-template). It will motivate me to add more such project._ 8 | 9 | \_☝ If you want new feature to be added or you believe you've encountered a bug [Open an issue] (https://github.com/abhaybharti/playwright-framework-template/issues) . 10 | 11 | ## Core capabilites 12 | - **Cross-Browser Testing**: Chromium, Firefox,WebKit, Edge 13 | - **Multipel Test Types**: UI, API, Mobile, Load Testing, Visual Testing 14 | - **Page Object Model**: Scalable & maintainable test architecture 15 | - **JSON Config**: Build script 16 | - **Dependency Injection**: Custom Fixtures, TypeScript Decorators for clean test & function code 17 | - **Comprehenisve Reporting**: HTML, Log file, Video, Trace, Screenshot, 18 | - **Capture API**: HTML, Log file, Video, Trace, Screenshot, 19 | 20 | 21 | 22 | ## Features 23 | 24 | - Easy to Configure 25 | - Auto wait for all elements & checks 26 | - Generate HTML report 27 | - Generate detailed trace file which helps to debug 28 | - Generate snapshot for each step 29 | - Record video for test case execution 30 | - Support Web automation with support for chrome, Edge, Firefox and Safari 31 | - Support Mobile automation with support for Android native, web and hybrid apps automation 32 | - Support iOS native, web and hybrid apps automation 33 | - Support execution of Web tests on Microsoft Azure (can scale on demand) 34 | - Support API testing (GET, POST, PUT, DELETE HTTP methods) 35 | - Dynamic data handling using external JSON files 36 | - Support taking screenshots 37 | - Run functional test for load testing using Artillery 38 | - Support Serial and Parallel execution 39 | - Environment configuration using .env files 40 | 41 | ## Tech Stack/Libraries Used 42 | 43 | - [PlayWright](https://playwright.dev/) - for web/API automation/Mobile Device Emulation Testing 44 | - [Artillery](https://www.artillery.io/) - for load testing 45 | - [Appium](https://appium.io/docs/en/2.4/) - for mobile app automation 46 | - [ESLint](https://eslint.org/) - pinpoint issues and guide you in rectifying potential problems in both JavaScript and TypeScript. 47 | - [Prettier](https://prettier.io/) - for formatting code & maintain consistent style throughout codebase 48 | - [Dotenv](https://www.dotenv.org/) - to load environment variables from .env file 49 | - [Secrets](https://github.com/du5rte/secrets) - to load secrets from AWS Secrets Manager 50 | - [TypeScript](https://www.typescriptlang.org/) - for type safety 51 | - [Joi](https://github.com/sideway/joi) - for data validation 52 | - [Moment](https://momentjs.com/) - for date & time handling 53 | - [Winston](https://github.com/winstonjs/winston) - for logging 54 | - [Mocha](https://mochajs.org/) - for test runner 55 | - [Chai](https://www.chaijs.com/) - for assertion 56 | 57 | 58 | 59 | ## Getting Started 60 | 61 | ## Project Structure 62 | ```text 63 | playwright-framework-template/ 64 | ├── .github/workflows/ # GitHub Actions CI/CD 65 | ├── src/ 66 | | └── helper/ # Helpers 67 | | | ├── api/apiHelper.ts # API Helper 68 | | | ├── load/loadHelper.ts # Load testing Helper 69 | | | ├── mobile/mobileHelper.ts # Mobile action Helper 70 | | | └── web/webHelper.ts # Web action Helper 71 | | └── utils/ # Utilities 72 | | | ├── config/ # config files 73 | | | ├── error/ # Centralized error manager 74 | | | ├── reader/ # JSON/Excel reader Helper 75 | | | ├── report/ # Custom report manager 76 | | | └── other/ # Har Analyzer 77 | | └── globals/ # Global setup/teardown/healthcheck scripts 78 | ├── tests/ 79 | | ├── fixtures/ # Custom test fixtures 80 | | ├── api/ # API Integration tests 81 | | | └── example/ # example api tests 82 | | ├── e2e/ # E2E tets 83 | | | └── example/ # example e2e tests 84 | | ├── load/ # Load testing 85 | | | └── example/ # example load tests 86 | | ├── resources/ # resource files 87 | | ├── web/ # browser tests 88 | | | └── example/ # example web tests 89 | | └── mobile/ # Mobile tests 90 | | └── example/ # example mobile tests 91 | ├── Dockerfile # Dockerfile 92 | ├── docker-compose.yml # docker-compose.yml 93 | ├── .env # environment variables 94 | ├── .env.example # environment variables example 95 | ├── .gitignore # git ignore 96 | ├── .prettierrc # prettier config 97 | ├── .eslintrc.json # eslint config 98 | ├── package.json # package.json 99 | ├── playwright.config.ts # playwright config 100 | ├── README.md # README.md 101 | └── tsconfig.json # tsconfig.json 102 | 103 | 104 | ### Prerequisite 105 | 106 | - `nodejs`: Download and install Node JS from 107 | > `https://nodejs.org/en/download` 108 | - `Visual Studio Code/WindSurf/Cursor`: Download and install code editor 109 | - `Git` 110 | 111 | ### Installation 112 | 113 | - clone the repo using below URL 114 | 115 | > `https://github.com/abhaybharti/playwright-framework-template.git` 116 | 117 | - Navigate to folder and install dependencies 118 | 119 | > `npm install` 120 | 121 | - For first time installation use below command to download & install required browsers: 122 | 123 | > `npx playwright install` 124 | 125 | - In case you want to do fresh setup of playwright 126 | - Create a folder & run command `npm init playwright@latest` 127 | - select `TypeScript` & select default for other options 128 | 129 | 130 | ## Usage 131 | 132 | - For browser configuration, change required parameters in playwright.config.ts 133 | - To run your suite on MS Azure, copy the below code in `azure-pipeline.yml` and `playwright.service.config.ts` to root folder, update following key & run your suite 134 | - PLAYWRIGHT_SERVICE_ACCESS_TOKEN 135 | - PLAYWRIGHT_SERVICE_URL=XXX 136 | 137 | ## How to generate Playwright code (Playwright Test Generator) 138 | 139 | - run command `npx playwright codegen` 140 | - Browser gets opened & navigate to web app & perform test actions 141 | 142 | Playwright test generator generates tests and pick locator for you. It uses role,text and test ID locators. 143 | 144 | To pick a locator, run the `codegen` command followed by URL, `npx playwright codegen https://opensource-demo.orangehrmlive.com/web/index.php/auth/login` 145 | 146 | ## Writing Tests 147 | 148 | - Create test files in `src/tests` folder 149 | 150 | ## Sample Test 151 | 152 | ### Unit/Integration Testing 153 | 154 | ### Sample Web Test 155 | 156 | > Note: Refer to [sample-web-test](https://github.com/abhaybharti/playwright-framework-template/tree/master/src/tests/web/example) 157 | 158 | Pls go through different `\*.ts` file, which has tests example for different purpose. 159 | 160 | #### Locator Example 161 | 162 | > Note: Refer to [sample-web-test](https://github.com/abhaybharti/playwright-framework-template/tree/master/src/tests/web/example/locator.spec.ts) 163 | 164 | ### Sample Web Load Test 165 | 166 | ### Sample Api Test 167 | 168 | > Note: Refer to [sample-api-test](https://github.com/abhaybharti/playwright-framework-template/tree/master/src/tests/api/example) 169 | 170 | Pls go through different `\*.ts` files, which has tests example for different api tests. 171 | 172 | ### Sample API Load Test 173 | 174 | ### Sample Mobile Test 175 | 176 | ## Run Test 177 | 178 | ### To Run Web Test 179 | 180 | - `npx playwright test (name-of-file.spec.ts) --headed` to run test in ui mode 181 | - `npx playwright test (name-of-file.spec.ts) --headed --config=playwright.config.chrome.ts` to run test in ui mode on chrome browser 182 | - `npx playwright test (name-of-file.spec.ts) --headed --config=playwright.config.firefox.ts` to run test in ui mode on firefox browser 183 | - `npx playwright test (name-of-file.spec.ts) --headed --config=playwright.config.edge.ts` to run test in ui mode on edge browser 184 | 185 | ### To Run Api Test 186 | 187 | ### To Run Mobile Test 188 | 189 | ### To Run Test Multiple Times in Parallel 190 | 191 | `npx playwright test --workers=5 --headed --repeat-each=5` 192 | 193 | - This will run test 5 times, at a time 5 instance will run. `--workers=5` will run 5 instances 194 | 195 | ### To Run Test Multiple Times in Sequence 196 | 197 | `npx playwright test --workers=1 --headed --repeat-each=5` 198 | 199 | - This will run test 5 times, at a time single instance will run, `--repeat-each=5` will run 5 times 200 | 201 | ### To Run Load Test using Artillery & PlayWright Suite 202 | 203 | `artillery run artillery-script.yml --output report.json` 204 | 205 | ### Grouping and Organizing Test Suite in PlayWright 206 | 207 | `npx playwright test --grep @smoke` This will run only test tagged as @smoke 208 | 209 | ## Debug And Analyze 210 | 211 | ### View Trace Result of PlayWright Execution 212 | 213 | - Open `https://trace.playwright.dev` 214 | - Upload `trace.zip` file to above site, it will show trace details 215 | 216 | ### Run test in debug mode 217 | 218 | `npx playwright test UIBasictest.spec.js --debug` 219 | 220 | This will start running script in debug mode & open PlayWright inspector 221 | 222 | ### How to generate load test report using artillery + Playwright 223 | 224 | `artillery report report.json --output report.html` 225 | 226 | #### Run & Generate Report 227 | 228 | ### Best Practices in Test Authoring: 229 | 230 | - `Create isolated test cases`: Each test case should be independent. 231 | - `Write Meaningful Test Case Titles`: Make your test case titles descriptive and meaningful. 232 | - `Follow the AAA (Arrange-Act-Assert) Pattern`: Align your Test-Driven Development (TDD) approach with the clarity of Arrange, Act, and Assert. 233 | - `Maintain Cleanliness`: Separate additional logic from tests for a tidy and focused codebase. 234 | 235 | ## GitHub Actions - created workflow to run test 236 | 237 | ## Contributing 238 | 239 | We love our contributors! Here's how you can contribute: 240 | 241 | - [Open an issue](https://github.com/abhaybharti/playwright-framework-template/issues) if you believe you've encountered a bug. 242 | - Make a [pull request](https://github.com/abhaybharti/playwright-framework-template/pulls) to add new features/make quality-of-life improvements/fix bugs. 243 | 244 | 245 | 246 | 247 | 248 | 249 | g --------------------------------------------------------------------------------