Playwright framework implementation - Part 2

Playwright framework implementation - Part 2

Hoping the you have completed part 1 of the post, let’s move on to part 2!! In this post, let's tryin make use of the files that we haven't used in the part 1. We are gonna create a new test for creating a new user and checking whether the user is landed in the My Account page after sign up First things first. Creating the files that we need for the next test. Run the below command from the project directory cd src/apps/automation-practice && touch data/create-account.data.json && touch data/my-account.data.json && touch locators/create-account.locator.ts && touch locators/my-account.locator.ts && touch pages/create-account.page.ts && touch pages/my-account.page.ts && touch tests/create-account.spec.ts && touch tests/my-account.spec.tsLet’s take a moment to plan on how to tackle the sign-up functionality. We know that we need a unique email and password everytime we need to test this. Considering this, let's use an existing package named faker-js. faker-js is a package that is used to provide dummy but valid data for testing. Fortunately, this package is already part of the package.json file and no action needed for this πŸ˜‡ Now that we know that we need to create unique test data for every run, we also need to persist the existing data, so that we can use it later. Well, good news! We have that as well covered in the frameworkStep # 1Starting with adding test data to the *.data.json files As we discussed, the test data for new account creation is gonna use the faker-js, all we have to do is add an empty array to src/apps/automation-practice/data/create-account.data.json as below []And for the My Account page validation, we are going to validate whether the user is navigated to the My Account page after sign up. All we are doing here is to verify the title of the page matches with the My Account page title. Add the below to src/apps/automation-practice/data/my-account.data.json file { "title": "My Account Magento Commerce - website to practice selenium | demo website for automation testing | selenium practice sites" }Step # 2Now that we have the test data task out of our way, let's close the creation of page objects task by updating the *.locator.json files. Add the below code to src/apps/automation-practice/locators/create-account.locator.ts export const firstNameEdit = 'input[name="firstname"]'; export const lastNameEdit = 'input[name="lastname"]'; export const emailEdit = 'input[name="email"]'; export const passwordEdit = 'input[name="password"]'; export const passwordConfirm = 'input[name="password_confirmation"]'; export const createAccountButton = 'button:has-text("Create an Account")';Since, we are only validating the title in the My Account page and not any elements within the page, we can ignore the src/apps/automation-practice/locators/my-account.locator.ts file for nowStep # 3Start creating the necessary steps for the test. Add the below code to src/apps/automation-practice/pages/create-account.page.ts import path from "path"; import fs from "fs"; import type { Page, TestInfo } from "@playwright/test"; import { test } from "../fixtures"; import * as locators from "../locators/create-account.locator"; import * as actions from "@utils/base/web/actions"; import { appendFile } from "@home/src/utils/functions/file"; import { faker } from "@faker-js/faker";export default class CreateAccountPage { constructor(public page: Page, public workerInfo: TestInfo) {} async createAccount() { const dataFilePath = path.join( __dirname, "..", "data", "create-account.data.json" ); const firstName = faker.name.firstName(); const lastName = faker.name.lastName(); const email = faker.internet.email(); const password = faker.internet.password(); const account = { firstName, lastName, email, password, confirmPassword: password, }; await appendFile(dataFilePath, account); const data = JSON.parse(fs.readFileSync(dataFilePath, "utf-8")); await test.step( this.workerInfo.project.name + ": Enter first name: " + data[data.length - 1].firstName, async () => await actions.fill( this.page, locators.firstNameEdit, data[data.length - 1].firstName, this.workerInfo ) ); await test.step( this.workerInfo.project.name + ": Enter last name: " + data[data.length - 1].lastName, async () => await actions.fill( this.page, locators.lastNameEdit, data[data.length - 1].lastName, this.workerInfo ) ); await test.step( this.workerInfo.project.name + ": Enter email: " + data[data.length - 1].email, async () => await actions.fill( this.page, locators.emailEdit, data[data.length - 1].email, this.workerInfo ) ); await test.step( this.workerInfo.project.name + ": Enter password: " + data[data.length - 1].password, async () => await actions.fill( this.page, locators.passwordEdit, data[data.length - 1].password, this.workerInfo ) ); await test.step( this.workerInfo.project.name + ": Enter confirm password: " + data[data.length - 1].confirmPassword, async () => await actions.fill( this.page, locators.passwordConfirm, data[data.length - 1].confirmPassword, this.workerInfo ) ); await test.step( this.workerInfo.project.name + ": Click create account button ", async () => await actions.clickElement( this.page, locators.createAccountButton, this.workerInfo ) ); } }Add the below code to src/apps/automation-practice/pages/my-account.page.ts import type { Page, TestInfo } from "@playwright/test"; import * as data from "../data/my-account.data.json"; import * as actions from "@utils/base/web/actions";export default class MyAccountPage { constructor(public page: Page, public workerInfo: TestInfo) {} async verifyPageTitle() { await actions.verifyPageTitle(this.page, data.title, this.workerInfo); } }Step # 4Updating fixtures. Once we have added the steps that needs to be performed for the test, it's time for us to update the fixtures. Your src/apps/automation-practice/fixtures/index.ts file should looks like below import { test as baseTest } from "@playwright/test"; import CommonPage from "@common/pages/common.page"; import HomePage from "../pages/home.page"; import CreateAccountPage from "../pages/create-account.page"; import MyAccountPage from "../pages/my-account.page";type pages = { commonPage: CommonPage; homePage: HomePage; createAccountPage: CreateAccountPage; myAccountPage: MyAccountPage; };const testPages = baseTest.extend<pages>({ commonPage: async ({ page }, use, workerInfo) => await use(new CommonPage(page, workerInfo)), homePage: async ({ page }, use, workerInfo) => await use(new HomePage(page, workerInfo)), createAccountPage: async ({ page }, use, workerInfo) => await use(new CreateAccountPage(page, workerInfo)), myAccountPage: async ({ page }, use, workerInfo) => await use(new MyAccountPage(page, workerInfo)), });export const test = testPages; export const expect = testPages.expect; export const describe = testPages.describe;Step # 5Time to create our test. Before we add the test code to the *.spec.ts files, we need to add the locators in src/apps/automation-practice/locators/home.locator.ts to click on the Create Account link in home page Add the below code to src/apps/automation-practice/locators/home.locator.ts export const createAccountLink = "text=Create an Account";Add the below code to src/apps/automation-practice/tests/create-account.spec.ts import { test, describe } from "../fixtures"; import * as homePageLocators from "../locators/home.locator"; import * as createAccountLocators from "../locators/create-account.locator";test.beforeEach(async ({ homePage }) => { await homePage.navigateToAutomationPractice(); });describe("New Account", () => { test("Create new account", async ({ homePage, commonPage, createAccountPage, myAccountPage, }) => { await homePage.clickLink(homePageLocators.createAccountLink); await commonPage.waitForAnimationEnd( createAccountLocators.createAccountButton ); await createAccountPage.createAccount(); await myAccountPage.verifyPageTitle(); }); });Add the below code to src/apps/automation-practice/tests/my-account.spec.ts import { test, describe } from "../fixtures";describe("My Account", () => { test("Verify account page url and title", async ({ myAccountPage }) => { await myAccountPage.verifyPageTitle(); }); });Time to test 🍹Step # 6Change the testMatch option in src/apps/automation-practice/playwright.config.ts as below testMatch: "tests/create-account.spec.ts",and run yarn test:automation-practice Hoping that you have done everything right until now, you would now be able to see something like belowAnd you should be able to see the newly created users test data in src/apps/automation-practice/data/create-account.data.json That's all for today. Happie coding πŸ“£

Playwright framework implementation - Part 1

Playwright framework implementation - Part 1

Let’s see the implementation of the playwright framework from the github repo which I created as part of my hands-on effort. Now before I begin, I just wanted to give a little heads-up. This could be a long post and you might have to spend some quality time in order to make the framework up and running. That being said, I will try to reduce the words as much as possible and explain things clear. However, just keep an open mind for now πŸ˜‰ Couple of things before we start. Let’s quickly spend some time in understanding how the framework is structured, so that you can have a mental picture of how things work when you start making changes. Framework structure . β”œβ”€β”€ πŸ“ src β”‚Β Β  β”œβ”€β”€ πŸ“ apps β”‚Β Β  β”‚Β Β  └── πŸ“ common β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ pages β”‚Β Β  β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ƒ common.page.ts # common page shared b/w apps β”‚Β Β  β”‚Β Β  └── πŸ“ app 01 β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ data β”‚Β Β  β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ƒ page_01.data.json # data for app 01, page 01 β”‚Β Β  β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ƒ page_02.data.json # data for app 01, page 02 β”‚Β Β  β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ƒ ... β”‚Β Β  β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ƒ page_n.data.json # data for app 01, page n β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ fixtures β”‚Β Β  β”‚Β Β  β”‚Β Β  β”‚Β Β  └── πŸ“ƒ index.ts # app 01 specific fixtures β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ locators β”‚Β Β  β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ƒ page_01.locator.ts # locators for app 01, page 01 β”‚Β Β  β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ƒ page_02.locator.ts # locators for app 01, page 02 β”‚Β Β  β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ƒ ... β”‚Β Β  β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ƒ page_n.locator.ts # locators for app 01, page n β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ pages β”‚Β Β  β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ƒ page_01.page.ts # functions for app 01, page 01 β”‚Β Β  β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ƒ page_02.page.ts # functions for app 01, page 02 β”‚Β Β  β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ƒ ... β”‚Β Β  β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ƒ page_n.page.ts # functions for app 01, page n β”‚Β Β  β”‚Β Β  β”‚Β Β  └── πŸ“ tests β”‚Β Β  β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ƒ page_01.spec.ts # tests for app 01, page 01 β”‚Β Β  β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ƒ page_02.spec.ts # tests for app 01, page 02 β”‚Β Β  β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ƒ ... β”‚Β Β  β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ƒ page_n.spec.ts # tests for app 01, page n β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ƒ config.json # config data for app 01 β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ƒ package.json # scripts for app 01 β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ƒ playwright.config.ts # config details for app 01 β”‚Β Β  β”‚Β Β  └── πŸ“ app 02 β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ data β”‚Β Β  β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ƒ page_01.data.json # data for app 02, page 01 β”‚Β Β  β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ƒ page_02.data.json # data for app 02, page 02 β”‚Β Β  β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ƒ ... β”‚Β Β  β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ƒ page_n.data.json # data for app 02, page n β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ fixtures β”‚Β Β  β”‚Β Β  β”‚Β Β  β”‚Β Β  └── πŸ“ƒ index.ts # app 02 specific fixtures β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ locators β”‚Β Β  β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ƒ page_01.locator.ts # locators for app 02, page 01 β”‚Β Β  β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ƒ page_02.locator.ts # locators for app 02, page 02 β”‚Β Β  β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ƒ ... β”‚Β Β  β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ƒ page_n.locator.ts # locators for app 02, page n β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ pages β”‚Β Β  β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ƒ page_01.page.ts # functions for app 02, page 01 β”‚Β Β  β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ƒ page_02.page.ts # functions for app 02, page 02 β”‚Β Β  β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ƒ ... β”‚Β Β  β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ƒ page_n.page.ts # functions for app 02, page n β”‚Β Β  β”‚Β Β  β”‚Β Β  └── πŸ“ tests β”‚Β Β  β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ƒ page_01.spec.ts # tests for app 02, page 01 β”‚Β Β  β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ƒ page_02.spec.ts # tests for app 02, page 02 β”‚Β Β  β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ƒ ... β”‚Β Β  β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ƒ page_n.spec.ts # tests for app 02, page n β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ƒ config.json # config data for app 02 β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ƒ package.json # scripts for app 02 β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ƒ playwright.config.ts # config details for app 02 β”‚Β Β  β”‚Β Β  └── πŸ“ ... β”‚Β Β  β”‚Β Β  └── πŸ“ app n β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ data β”‚Β Β  β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ƒ page_01.data.json # data for app n, page 01 β”‚Β Β  β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ƒ page_02.data.json # data for app n, page 02 β”‚Β Β  β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ƒ ... β”‚Β Β  β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ƒ page_n.data.json # data for app n, page n β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ fixtures β”‚Β Β  β”‚Β Β  β”‚Β Β  β”‚Β Β  └── πŸ“ƒ index.ts # app n specific fixtures β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ locators β”‚Β Β  β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ƒ page_01.locator.ts # locators for app n, page 01 β”‚Β Β  β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ƒ page_02.locator.ts # locators for app n, page 02 β”‚Β Β  β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ƒ ... β”‚Β Β  β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ƒ page_n.locator.ts # locators for app n, page n β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ pages β”‚Β Β  β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ƒ page_01.page.ts # functions for app n, page 01 β”‚Β Β  β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ƒ page_02.page.ts # functions for app n, page 02 β”‚Β Β  β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ƒ ... β”‚Β Β  β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ƒ page_n.page.ts # functions for app n, page n β”‚Β Β  β”‚Β Β  β”‚Β Β  └── πŸ“ tests β”‚Β Β  β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ƒ page_01.spec.ts # tests for app n, page 01 β”‚Β Β  β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ƒ page_02.spec.ts # tests for app n, page 02 β”‚Β Β  β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ƒ ... β”‚Β Β  β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ƒ page_n.spec.ts # tests for app n, page n β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ƒ config.json # config data for app n β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ƒ package.json # scripts for app n β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ƒ playwright.config.ts # config details for app n β”‚Β Β  └── πŸ“ utils β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ base β”‚Β Β  β”‚Β Β  β”‚Β Β  └── πŸ“ web β”‚Β Β  β”‚Β Β  β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ƒ actions.ts # actions in web applications β”‚Β Β  β”‚Β Β  β”‚Β Β  β”‚Β Β  └── πŸ“ƒ screenshots.ts # screenshot functions β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ functions (utility functions) β”‚Β Β  β”‚Β Β  β”œβ”€β”€ πŸ“ packages (other reusable packages) β”‚Β Β  β”‚Β Β  └── πŸ“ reports β”‚Β Β  β”‚Β Β  β”‚Β Β  └── πŸ“ƒ custom-reporter.ts # custom reporter to pretty print in console β”œβ”€β”€ πŸ“ƒ config.init.ts # configuration initializer β”œβ”€β”€ πŸ“ƒ global-setup.ts β”œβ”€β”€ πŸ“ƒ global-teardown.ts β”œβ”€β”€ πŸ“ƒ package.json β”œβ”€β”€ πŸ“ƒ playwright.config.ts # global configSo, why is the structure important πŸ€” Coz' it gives you the view of the things that you need to add for using this framework for a brand new application. The only problem is to know how to do it sequentially. For example, as soon as you look at the framework structure, you might already understand that you need app_name folder within the src/apps folder within which you would need to create all the sub-folders as shown.The question is which one should I create first and how are these folders linked?Moving on.. First things first. Listed below are the necessary pre-requisites for the framework implementation.Node JS - v18.1.0 or above (what I used while developing) IDE of your choice (VS Code recommended)and... that's it! πŸ’β€β™‚οΈ - If you want to use different versions of node in different projects, use nvmAlright, now that we have completed the pre-requisites, let’s start with the rest of the implementation πŸ”°Step # 1Clone the git repo using the below command git clone git@github.com:eric-stanley/playwright-framework.gitand cd into the cloned folderStep # 2Let’s assume that the application under test is named automation-practice Start creating the necessary πŸ“ and πŸ“ƒ using the below commands. mkdir src/apps/automation-practice && cd src/apps/automation-practice && mkdir data && mkdir fixtures && mkdir locators && mkdir pages && mkdir tests && touch config.json && touch package.json && touch playwright.config.ts && touch data/home.data.json && touch fixtures/index.ts && touch locators/home.locator.ts && touch pages/home.page.ts && touch tests/home.spec.tsCopy the below code and paste it in src/apps/automation-practice/config.json. This file has the environment specific url's for the AUT (Application Under Test) { "env": { "dev": { "url": "https://magento.softwaretestingboard.com/" }, "test": { "url": "https://magento.softwaretestingboard.com/" }, "uat": { "url": "https://magento.softwaretestingboard.com/" } } }πŸ”” Assuming that we have the same url for all environments 😏 Copy the below code and paste it in src/apps/automation-practice/package.json. Here we define the commands that we use to trigger the necessary run Note: This is a dependent file and will not run standalone. All dependencies for the framework is part of the main package.json which resides in the parent folder { "name": "automation-practice", "version": "1.0.0", "private": true, "author": "Eric Stanley", "license": "MIT", "scripts": { "test": "APP_NAME=automation-practice NODE_ENV=dev playwright test", "test:debug": "APP_NAME=automation-practice NODE_ENV=dev PWDEBUG=1 playwright test", "report": "npx playwright show-report reports/playwright-report", "allure": "npx allure generate reports/allure/allure-result -o reports/allure/allure-report --clean && npx allure open reports/allure/allure-report" } }πŸ”” Change the author to your name Copy the below code and paste it in src/apps/automation-practice/playwright.config.ts. This file has the app specific playwright config. import { PlaywrightTestConfig, devices } from "@playwright/test";const config: PlaywrightTestConfig = { testDir: "tests", testMatch: "tests/*.spec.ts", timeout: 30 * 1000, retries: 3, workers: 3, globalSetup: require.resolve("@home/global-setup"), globalTeardown: require.resolve("@home/global-teardown"), expect: { timeout: 20000, }, use: { headless: true, actionTimeout: 0, trace: "retain-on-failure", ignoreHTTPSErrors: true, video: "on-first-retry", screenshot: "only-on-failure", acceptDownloads: true, colorScheme: "dark", launchOptions: { slowMo: 500, }, }, reporter: [ ["list"], [ "json", { outputFile: "reports/json-reports/json-report.json", }, ], [ "html", { outputFolder: "reports/playwright-report/", open: "never", }, ], [ "allure-playwright", { outputFolder: "reports/allure/allure-result/", open: "never", }, ], ], projects: [ { name: "chromium", use: { ...devices["Desktop Chrome"] }, }, { name: "firefox", use: { ...devices["Desktop Firefox"] }, }, { name: "webkit", use: { ...devices["Desktop Safari"] }, }, ], };export default config;Now that we have completed the basic setup, let’s move on to step # 3Step # 3Let’s start with writing a basic test for verifying if the home page url contains a specific text and title equals a specific text Starting with the data first. Add the below data in src/apps/automation-practice/data/home.data.json { "urlContains": "softwaretestingboard", "title": "Home Page - Magento eCommerce - website to practice selenium | demo website for automation testing | selenium practice sites | selenium demo sites | best website to practice selenium automation | automation practice sites Magento Commerce - website to practice selenium | demo website for automation testing | selenium practice sites" }Now that we have the data in place, lets start writing the functions for the test. Add the below code to src/apps/automation-practice/pages/home.page.ts import type { Page, TestInfo } from "@playwright/test"; import { test, expect } from "../fixtures"; import * as data from "../data/home.data.json"; import * as actions from "@utils/base/web/actions";export default class HomePage { constructor(public page: Page, public workerInfo: TestInfo) {} async navigateToAutomationPractice() { await actions.navigateTo(this.page, process.env.URL, this.workerInfo); const url = this.page.url(); await test.step( this.workerInfo.project.name + ": Check if URL contains " + data.urlContains, async () => expect(url).toContain(data.urlContains) ); } async verifyPageTitle() { await actions.verifyPageTitle(this.page, data.title, this.workerInfo); } }At this point, you will get an error with the fixtures import line stating that, fixtures/index.ts is not a module. This is fine. We are going to tackle this next πŸ˜‰ But before that, let's take a moment to look at the code in home.page.ts. Basically, I have added two functions in this class; one for navigating to the home page navigateToAutomationPractice() and the other one to verify the title of the page verifyPageTitle(). The actions is a consolidated list of all actions (or atleast most of the actions) that you could possibly perform in a web page, since we will be using these actions in almost all of our pages, it's only logical to have this as a separate utility! Let’s tackle the fixtures error now. Add the below code in src/apps/automation-practice/fixtures/index.ts import { test as baseTest } from "@playwright/test"; import CommonPage from "@common/pages/common.page"; import HomePage from "../pages/home.page";type pages = { commonPage: CommonPage; homePage: HomePage; };const testPages = baseTest.extend<pages>({ commonPage: async ({ page }, use, workerInfo) => { await use(new CommonPage(page, workerInfo)); }, homePage: async ({ page }, use, workerInfo) => { await use(new HomePage(page, workerInfo)); }, });export const test = testPages; export const expect = testPages.expect; export const describe = testPages.describe;What we do here is basically extending the test object in @playwright/test to add all pages in our application, so that when we start writing our tests, we will be able to easily pull the page objects and its associated functions without creating a new object everytime when we need to access the home.page.ts which might inturn cause a lot of duplication in code. In other words, every time we access the test object from @playwright/test, it automatically injects all the app specific pages in it. Let’s see how it happens next Add the below code in src/apps/automation-practice/tests/home.spec.ts import { test, describe } from "../fixtures";describe("Home", () => { test("Verify home page url and title", async ({ homePage }) => { await homePage.navigateToAutomationPractice(); await homePage.verifyPageTitle(); }); });As you can see the second argument in the test object takes in a function and since we have already extended the homePage object within the test object, we are able to access it with in the function And..... that's it ⏰ Now, before we run the test, there is one last place where we need to update few things which is basically the script object in package.json which is present in the parent directory.Remember, this is not the package.json that we edited in step # 2. This is the one that is present in your parent (root) directoryAdd the below code in the scripts section in package.json file "test:automation-practice": "yarn workspace automation-practice test", "test:debug:automation-practice": "yarn workspace automation-practice test:debug", "report:automation-practice": "yarn workspace automation-practice report", "allure:automation-practice": "yarn workspace automation-practice allure"Once you are done with editing the package.json file in your root directory, you package.json file would look something like below { "name": "playwright-hands-on", "version": "1.0.0", "private": true, "description": "", "workspaces": ["src/apps/*"], "scripts": { "test": "yarn workspace ui-testing-playground test", "test:debug": "yarn workspace ui-testing-playground test:debug", "report": "yarn workspace ui-testing-playground report", "allure": "yarn workspace ui-testing-playground allure", "test:ui-testing-playground": "yarn workspace ui-testing-playground test", "test:debug:ui-testing-playground": "yarn workspace ui-testing-playground test:debug", "report:ui-testing-playground": "yarn workspace ui-testing-playground report", "allure:ui-testing-playground": "yarn workspace ui-testing-playground allure", "test:automation-practice": "yarn workspace automation-practice test", "test:debug:automation-practice": "yarn workspace automation-practice test:debug", "report:automation-practice": "yarn workspace automation-practice report", "allure:automation-practice": "yarn workspace automation-practice allure" }, "author": "Eric Stanley", "license": "MIT", "devDependencies": { "@playwright/test": "^1.25.2", "@types/adm-zip": "^0.5.0", "adm-zip": "^0.5.9", "allure-commandline": "^2.18.1", "allure-playwright": "^2.0.0-beta.19", "colors": "^1.4.0", "playwright": "^1.25.2", "ts-node": "^10.9.1", "typescript": "^4.8.3" } }Step # 4Time to test If you are running the project for the first time run the yarn command from your parent folder to install all dependencies. Once you are done with installing the dependencies, run the below command yarn test:automation-practiceIf you have done everything right, you might probably see something like belowYou might or might not get the Slow test file warning. You can play around with it by changing the timeout value in playwright.config.ts in src/apps/automation-practice folder πŸ™‹ Now, does that mean that we are done?Yep!I told you that this post would be a long one and we have covered a lotta ground until now, but why do we have the home.locator.ts file in the locators folder. And how do we compare screenshots πŸ€” Like I said, this could take some time, but at the same time, a lot has been already done. See you in the next post with implementing more features πŸ₯ Feel free to add your thoughts and comments in the comments section and I will try to address those in time ⏰

Short circuit in javascript

Short circuit in javascript

Short circuit is an interesting concept in JavaScript where we make JavaScript to evaluate a typically unorthodox condition to human eye but make total sense to JavaScript typing engine So what do I mean my unorthodox anyway? Let’s take an if statement for example if (condition) { // do something if true } else { // do something else }If you look at the above statement from a layman perspective, he/she could still understand what the code does just by knowing a little english!! What that also means is that the code you see is as per the books and that’s how anyone teaches to code The above code can also be written using ternary operators like below (condition) ? // do something if true : // do something if falseHowever, the reason why ternary operator code is not traditional is because it does not have any keywords in it. Anyone could still understand the above syntax, but when it’s implemented in actual code like (a === b) ? x = y : x = zIt’s kinda hard for a newbie to understand what the code actually means or how it works. More about ternary operators hereNow, why am I taking about if statements and ternary operators when the heading says Short circuit in JavaScript!It’s because though there is no performance improvement by using ternary operators instead of traditional if else, it’s a little less to write. And if that makes you insane, get ready for obsession Short circuiting in JavaScript is all about how you play around logical operators with falsy and truthy values Before we move any further, let’s understand what falsy and truthy values areAny value that is considered false when expressed in boolean context is falsy. The value that I’m referring here could be anything. It could be a variable, array, object, condition, etc.Ok, so a boolean variable can be assigned a value of false and it becomes falsy. But how can I assign a value of false to an array. An array can be empty or undefined but definitely not false. That would make no sense! Good news is, JavaScript has some predefined values that are considered falsy The falsy values in JavaScript arethe number 0 the BigInt 0n the keyword null the keyword undefined the boolean false the number NaN the empty string "" (equivalent to '')The above values evaluate to false when coerced by JavaScript’s typing engine into a boolean value, but they are not necessarily equal to each other. And what are truthy values then? Everything else except falsy is truthy. Simple ❇️ Fun time!! Let’s start with an example again. Consider the following if statement if (condition) { // do something if true }If we use the ternary expression, the above if statement can be written as condition ? // do something if true : nullIf you notice closely, you might see that there is an unnecessary else condition in the ternary expression which basically does nothing. The problem is you can’t neglect it. A ternary expression should always have something to do for both true and false conditions which makes ternary expression not an optimal choice when there’s only if and no else Now, how can we rewrite this to make it smaller Time to get obsessed (condition) && // do something if trueLike I said, it’s basically how we play around with logical operators Before we dive into how the code works, it’s necessary understand how logical operators work. It works from left to right and short circuits What do I mean by short circuit here? When JavaScript evaluates an AND (&&) expression, if the first operand is false, JavaScript will short circuit and will not even look at what’s next in lineRemember, the logical AND returns true only if all conditions match. In other words, in a given sequence of conditions with logical AND operators will complete its execution successfully only if all conditions are true. The moment any of the condition is false, it just doesn’t matter whether or not the remaining conditions are true. The end result is gonna be falseSimply put, all conditions that comes after the false condition is not gonna affect the result. And if the result is clear even before evaluating all the conditions, it short circuits!It’s the same principle, JavaScript just short circuits once it encounters a false value and just ignores the remaining conditions and returns to the next line of code Before I close let’s just see a real example with logical AND (&&) and logical OR (||) let a, b, xa = 1 // b = undefined x = 'I am the default value'console.log(a && x) // prints 'I am the default value', since a is true console.log(b && x) // prints undefined, since b is false (short circuits and anything after false is not executed)console.log(a || x) // prints 1, since a is true (short circuits and anything after true is not executed) console.log(b || x) // prints 'I am the default value', since b is falseThat’s my best explanation of what short circuit is in JavaScript After all, no one said a bread board logic would NOT be helpful someday in the programming world πŸ™‚

Password Management

Password Management

Managing passwords is often a challenging task unless you have a eidetic memory or lazy enough to click on forget username/password. I believe the neither of those is gonna end up reading this post and trust me, you are not one of β€˜em either! So, when it comes to managing passwords, the first thing that comes to our mind is the best password management app in the app store or play store. Apparently, when we search it, we will end up in getting to know about the app that is listed in most of the sites and most reviews etc. So obviously, any person would not be needing to ask some security expert to decide on what app to choose. Now, am I a security expert? Hell no! I am not against the apps that are listed as the best in any point in time, however I am not someone who believes that the news is always true. Infact, the truth is what the media say it is. And this applies to websites as well. Now, before we get into the actual password management details, lets split things up into discrete pieces, so that we could better understand the need for password management.Why do we need to protect our passwords? Coz’ if someone else gets access to our password, they might tamper with our identityLet’s call this someone β€˜X’Does X really need your password? Unless, you are a friend/enemy of X, X wouldn’tDo I know X personally? Well, most probably no, coz’ X is a company most of the times and X being a person seldom happensAs you see, you cannot be a possible victim unless its X’s personal vendetta or you are on your way to becoming a celebrity/millionaire Therefore, its unlikely for X to target an individual and lot likely to target a company which has strong user base (One reason why big companies are falling victim to attacks by X). Also, as all of your credentials is stored in one single infrastructure, though the data is encrypted, you still are a victim as you have given all your data to X. Its just a matter of time before X finds a way to decrypt Well, if that’s the case, what else can we do. Do I mean that there is no actual protection? YES!But we can definitely work on minimizing the risk of saving all our details under one single infrastructure and maybe placing partial data in a place which is secure and the remaining in an unsecured location. The reason I say unsecured is coz’ this is where the protection actually happens. The last thing they would search is their own backyard. No wants to steal data that is open to everyone. That ain’t stealing anyways! The phrase That is where they least expect to hit β€˜em from just doesn’t apply for X just coz’ there are lot of other better places for them to hit and they would never know if that’s worth it If this theory sounds good to you, let's start practicals now. There is an existing password manager which already fits the above theory. You can find the website here Considering the above theory, you can safely assume that the app needs some basic understanding of the belowHow to create a github repository (private repo recommended) How to generate a PGP key pair (online generator here)*** Recommended optionsNote: The longer the passphrase, the more secure you password is and its ok to have the expires option set to NeverOnce you are done with the above 2 steps, it's time to install the app from the Compatible Clients section in https://www.passwordstore.org/ site After installation you would be required to enter the git repo and PGP key pair details before using the app Once repo and key details are entered you are good to go!Just to clear things about how this app ties to our theory, we need to understand a little more about how this app works. The repo you create is basically the database where you store your encrypted passwords. That would mean, each user has separate database (git repo) and it is in github (The world’s largest host of source code). Now, the password that you encrypt needs the key and the passphrase in order to be decoded; which again you have it in your local file system. In other words, you have the keys and passphrase in your app data folder and not in any server. As you see, you have now separated the key (PGP file) and the lock (git repo) and the only possible way to get access to your passwords is to get access to your key, passphrase and github repo! That would mean, hacking any one of these has no impact whatsoever!As a side note, its better to have a password hint in the password section instead of the actual password in the appThat said, this obviously is not an easy password management practice, but once setup its worth more than the money you spend on any subscription based or paid password management service. And the best part; you can have as many fields you want for any website/app by adding new keys as key value pairs Now, how do I know if this is the best/right way to manage passwords?Well, I don’t. It’s just my way πŸ™‚

Verify record in HTML table

Verify record in HTML table

Though not very often encountered, this again is an interesting problem to solve in automation testing. It’s quite obvious to come across table validation as an automation tester and there are quite a lot of options to do that. The most common verifications includeVerifying a unique identifier in a columnVerifying a specific text (cell) in a table based on a unique identifierVerifying a specific row (multiple values) in a table based on multiple identifiers (whether row exists)Hosted table for testing here and HTML Table source hereThe basic solution, as discussed here, solves 80% of all the listed problems. However, that only works when the table has a unique identifier. The solution for points 1 and 2 are already part of the previous post. So, let’s see how to solve the next complex problem # 3 This is again not a complex one to understand, however quite complex to validate with just xpath. There is still an option to validate this with some basic xpath’s and handling the validation part in the script/code but we are not gonna see that here for the same reason mentioned in my previous post. And it’s way too costly in this case. To make things clear, the problem that we intend to solve here is We have a table with multiple columns and rows; and our task is to validate whether a record exist with 3 or more values in a specific order in a row, or in other words, under specific headers. And we intend to solve this using only xpath i.e., without iteration in the code. Let’s take on one of the complex records to find in the table. In our example, let’s plan to find the value CodePen under the header Sites and 2002 under the header Views and 4135 under the header Clicks For our initial test, let’s first assume that we already know the index of the header column for each of these values that we need to verify in which case, the xpath would be something like //tr[td[.='CodePen' and position()=1] and td[.='2002' and position()=2] and td[.='4135' and position()=3]]As you can see, the position() keyword comes in handy in this case. So, the actual code to validate these values would be String expectedValue1 = "CodePen"; String expectedValue2 = "2002"; String expectedValue3 = "4135";String derivedXpath = "//tr[td[.='" + expectedValue1 + "' and position()=1] and td[.='" + expectedValue2 + "' and position()=2] and td[.='" + expectedValue3 + "' and position()=3]]";boolean isElementPresent = driver.findElements(By.xpath(derivedXpath)).size() > 0;if (isElementPresent) { // Do something after test step is passed } else { // Do something after test step is failed }Moving on to the next part where you do not know the position of each of the headers from which you need to validate the value. If you have already read my previous post on verifying text in HTML table, you might probably have guessed that we can use the count() operator to find the index! And guess what, that is correct πŸ™‚ However, this time instead of verifying the existence of the header, we will be using a different approach. We will still use the count() operator, but in conjunction with number() and boolean() operators to validate whether the header exists. The xpath to get the position of a particular header would be count(//th[.='Clicks']/preceding-sibling::th)+number(boolean(//th[.='Clicks']))The above xpath will get the count of preceding siblings with tag name β€˜th’ and then adds the boolean value for the existence of the object. Basically, the number() operator returns 1 (true)/0 (false) based on the input passed into it and the boolean() operator returns true/false based on the object existence. In simple termsboolean(Object) – true // Object is any matching tag from the given xpathboolean(null) – false // null is no matching tag from the given xpathnumber(true) – 1number(false) – 0The new xpath would be //tr[td[.='CodePen' and position()=count(//th[.='Sites']/preceding-sibling::th)+number(boolean(//th[.='Sites']))] and td[.='2002' and position()=count(//th[.='Views']/preceding-sibling::th)+number(boolean(//th[.='Views']))] and td[.='4135' and position()=count(//th[.='Clicks']/preceding-sibling::th)+number(boolean(//th[.='Clicks']))]]Hope you get the idea. If so, all you gotta do is to parameterize the position values in the above code. The new code would now be String expectedHeader1 = "Sites"; String expectedHeader2 = "Views"; String expectedHeader3 = "Clicks";String expectedValue1 = "CodePen"; String expectedValue2 = "2002"; String expectedValue3 = "4135";String derivedXpath = "//tr[td[.='" + expectedValue1 + "' and position()=count(//th[.='" + expectedHeader1 + "']/preceding-sibling::th)+number(boolean(//th[.='" + expectedHeader1 + "']))] and td[.='" + expectedValue2 + "' and position()=count(//th[.='" + expectedHeader2 + "']/preceding-sibling::th)+number(boolean(//th[.='" + expectedHeader2 + "']))] and td[.='" + expectedValue3 + "' and position()=count(//th[.='" + expectedHeader3 + "']/preceding-sibling::th)+number(boolean(//th[.='" + expectedHeader3 + "']))]]";boolean isElementPresent = driver.findElements(By.xpath(derivedXpath)).size() > 0;if (isElementPresent) { // Do something after test step is passed } else { // Do something after test step is failed }It’s a quite complex xpath to derive when it comes to validating dynamic values with multiple headers and values, but considering the amount of time this xpath saves in execution, I would say β€œworth it!”