How to approach for automating an End 2 End scenario with playwright especially when you are not good with selectors.
My attempt to a problem posted by LetCode with Koushik channel on Youtube dated 22nd October.
Problem statement -
- Automate scenario mentioned in this video, Link <<HERE>>
Solution approach -
- Generate code using Playwright Codegen
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const { test, expect } = require("@playwright/test"); | |
test("test", async ({ page }) => { | |
// Go to https://www.flipkart.com/ | |
await page.goto("https://www.flipkart.com/"); | |
// Click text=✕ | |
await page.click("text=✕"); | |
// Click img[alt="Travel"] | |
await page.click('img[alt="Travel"]'); | |
await expect(page).toHaveURL( | |
"https://www.flipkart.com/travel/flights?param=DTNavIcon&fm=neo%2Fmerchandising&iid=M_9a975f25-b80f-4215-9c0b-1ba91c293c0a_1_372UD5BXDFYS_MC.V4ZPKTOAO321&otracker=hp_rich_navigation_8_1.navigationCard.RICH_NAVIGATION_Travel_V4ZPKTOAO321&otracker1=hp_rich_navigation_PINNED_neo%2Fmerchandising_NA_NAV_EXPANDABLE_navigationCard_cc_8_L0_view-all&cid=V4ZPKTOAO321" | |
); | |
// Click label:nth-child(2) ._1XFPmK | |
await page.click("label:nth-child(2) ._1XFPmK"); | |
// Click input[name="0-departcity"] | |
await page.click('input[name="0-departcity"]'); | |
// Fill input[name="0-departcity"] | |
await page.fill('input[name="0-departcity"]', "kolka"); | |
// Click text=Kolkata, IndiaCCU | |
await page.click("text=Kolkata, IndiaCCU"); | |
// Fill input[name="0-arrivalcity"] | |
await page.fill('input[name="0-arrivalcity"]', "chenna"); | |
// Click text=Chennai, IndiaMAA | |
await page.click("text=Chennai, IndiaMAA"); | |
// Click text=1₹ 64872₹ 64873₹ 64874₹ 64875₹ 64876₹ 6487 >> button | |
await page.click("text=1₹ 64872₹ 64873₹ 64874₹ 64875₹ 64876₹ 6487 >> button"); | |
// Click text=30₹ 2730 | |
await page.click("text=30₹ 2730"); | |
// Click input[name="0-travellerclasscount"] | |
await page.click('input[name="0-travellerclasscount"]'); | |
// Click text=AdultsAbove 12 years1 >> :nth-match(button, 2) | |
await page.click("text=AdultsAbove 12 years1 >> :nth-match(button, 2)"); | |
// Click text=ChildrenBetween 2-12 years0 >> :nth-match(button, 2) | |
await page.click("text=ChildrenBetween 2-12 years0 >> :nth-match(button, 2)"); | |
// Click text=Done | |
await page.click("text=Done"); | |
// Click button:has-text("SEARCH") | |
await page.click('button:has-text("SEARCH")'); | |
await expect(page).toHaveURL( | |
"https://www.flipkart.com/travel/search/result/flight/CCU/MAA/01112021/30112021/2/1/0/e?source=Search%20Form" | |
); | |
// Click .switch-handle | |
await page.click(".switch-handle"); | |
await expect(page).toHaveURL( | |
"https://www.flipkart.com/travel/search/result/flight/CCU/MAA/01112021/30112021/2/1/0/e?stops=0&source=Search%20Form" | |
); | |
// Click text=15:00 2hr 20min non-stop 17:209613 | |
await page.click("text=15:00 2hr 20min non-stop 17:209613"); | |
// Click button:has-text("Book") | |
await page.click('button:has-text("Book")'); | |
await expect(page).toHaveURL( | |
"https://www.flipkart.com/travel/flights/review?searchToken=bbjhlrrhpdddtdhspddtdhthdhphqh8ojqh96ohtvdptptdtdtsppnvs&listingKeys=CCU-MAA-6E417%2CMAA-CCU-I5557" | |
); | |
// Click .gQr1xE._2sB7Xu | |
await page.click(".gQr1xE._2sB7Xu"); | |
// Click text=REVIEW ITINERARY | |
await page.click("text=REVIEW ITINERARY"); |
-
Go through the generated code and start refactoring
- Tools that I used to refine selectors -
- Output - Refined code
- Introduce page objects for easy maintenance
- project structure
- all page objects & final feature file
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//refined code using tools
const { test, expect } = require("@playwright/test");
let btnBook = 'button:has-text("Book")';
test.only("test", async ({ page }) => {
await page.goto("https://www.flipkart.com/");
await page.click("text=✕");
await page.click('img[alt="Travel"]');
await page.click("text=One WayRound Trip >> div");
await page.click("label:nth-child(2) ._1XFPmK");
await page.click('input[name="0-departcity"]');
await page.fill('input[name="0-departcity"]', "Kolkata");
await page.click("text=Kolkata, IndiaCCU");
await page.click('input[name="0-arrivalcity"]');
await page.fill('input[name="0-arrivalcity"]', "Chennai");
await page.click("text=Chennai, IndiaMAA");
await page.click('input[name="0-datefrom"]');
await page.click("table:nth-child(2) tbody tr:nth-child(1) td:nth-child(2)"); // 1st Nov
await page.click("table:nth-child(2) tbody tr:nth-child(5) td:nth-child(3)"); // 30th Nov
await page.click('input[name="0-travellerclasscount"]');
await page.click("text=AdultsAbove 12 years1 >> :nth-match(button, 2)");
await page.click("text=ChildrenBetween 2-12 years0 >> :nth-match(button, 2)");
await page.click('button:has-text("SEARCH")');
await page.waitForSelector(
"//div[@class='non-stop']//span[@class='u-ib u-rfloat']/*"
);
const locator = await page.locator(
"//div[@class='non-stop']//span[@class='u-ib u-rfloat']/*"
);
await expect(locator).toHaveClass("c-switch switch-off");
await page.click(".switch-handle");
await expect(locator).toHaveClass("c-switch switch-on");
const locatorAllPriceList =
"//div[@class='result-col outr']//div[@class='result-col-inner']//div[contains(@class,'price-group')]";
await page.hover(btnBook);
await page.waitForTimeout(1000);
const allFlightsPriceOnPage = await page.$$(locatorAllPriceList);
let lastRowForFlight;
for await (const flightPriceOnPage of allFlightsPriceOnPage) {
console.log(await flightPriceOnPage.innerText());
lastRowForFlight = flightPriceOnPage;
}
await lastRowForFlight.click();
await page.click(btnBook);
await page.click("text=REVIEW ITINERARY");
});
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//refined code using tools | |
const { test, expect } = require("@playwright/test"); | |
let btnBook = 'button:has-text("Book")'; | |
test.only("test", async ({ page }) => { | |
await page.goto("https://www.flipkart.com/"); | |
await page.click("text=✕"); | |
await page.click('img[alt="Travel"]'); | |
await page.click("text=One WayRound Trip >> div"); | |
await page.click("label:nth-child(2) ._1XFPmK"); | |
await page.click('input[name="0-departcity"]'); | |
await page.fill('input[name="0-departcity"]', "Kolkata"); | |
await page.click("text=Kolkata, IndiaCCU"); | |
await page.click('input[name="0-arrivalcity"]'); | |
await page.fill('input[name="0-arrivalcity"]', "Chennai"); | |
await page.click("text=Chennai, IndiaMAA"); | |
await page.click('input[name="0-datefrom"]'); | |
await page.click("table:nth-child(2) tbody tr:nth-child(1) td:nth-child(2)"); // 1st Nov | |
await page.click("table:nth-child(2) tbody tr:nth-child(5) td:nth-child(3)"); // 30th Nov | |
await page.click('input[name="0-travellerclasscount"]'); | |
await page.click("text=AdultsAbove 12 years1 >> :nth-match(button, 2)"); | |
await page.click("text=ChildrenBetween 2-12 years0 >> :nth-match(button, 2)"); | |
await page.click('button:has-text("SEARCH")'); | |
await page.waitForSelector( | |
"//div[@class='non-stop']//span[@class='u-ib u-rfloat']/*" | |
); | |
const locator = await page.locator( | |
"//div[@class='non-stop']//span[@class='u-ib u-rfloat']/*" | |
); | |
await expect(locator).toHaveClass("c-switch switch-off"); | |
await page.click(".switch-handle"); | |
await expect(locator).toHaveClass("c-switch switch-on"); | |
const locatorAllPriceList = | |
"//div[@class='result-col outr']//div[@class='result-col-inner']//div[contains(@class,'price-group')]"; | |
await page.hover(btnBook); | |
await page.waitForTimeout(1000); | |
const allFlightsPriceOnPage = await page.$$(locatorAllPriceList); | |
let lastRowForFlight; | |
for await (const flightPriceOnPage of allFlightsPriceOnPage) { | |
console.log(await flightPriceOnPage.innerText()); | |
lastRowForFlight = flightPriceOnPage; | |
} | |
await lastRowForFlight.click(); | |
await page.click(btnBook); | |
await page.click("text=REVIEW ITINERARY"); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//BasePage.ts | |
import { test as baseTest } from "@playwright/test"; | |
import { HomePage } from "../page/HomePage"; | |
import { TravelFlightsPage } from "../page/TravelFlightsPage"; | |
import { TravelSearchPage } from "../page/TravelSearchPage"; | |
const test = baseTest.extend<{ | |
homePage: HomePage; | |
travelFlightsPage: TravelFlightsPage; | |
travelSearchPage: TravelSearchPage; | |
}>({ | |
homePage: async ({ page }, use) => { | |
await use(new HomePage(page)); | |
}, | |
travelFlightsPage: async ({ page }, use) => { | |
await use(new TravelFlightsPage(page)); | |
}, | |
travelSearchPage: async ({ page }, use) => { | |
await use(new TravelSearchPage(page)); | |
}, | |
}); | |
export default test; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//flipkart.spec.ts | |
import test from "../helper/BasePage"; | |
test("Book Flight from Flipkart.com", async ({ | |
homePage, | |
travelFlightsPage, | |
travelSearchPage, | |
}) => { | |
await test.step("Goto flipkart.com", async () => { | |
await homePage.navigateTo("/"); | |
}); | |
await test.step("Skip login", async () => { | |
await homePage.closeLoginPopup(); | |
}); | |
await test.step("Goto Travel", async () => { | |
await homePage.navigateToPage("Travel"); | |
}); | |
await test.step("Verify that one way is selected by default", async () => { | |
await travelFlightsPage.verifyOneWayIsSelected(); | |
}); | |
await test.step("Click on round trip", async () => { | |
await travelFlightsPage.clickRoundTrip(); | |
}); | |
await test.step("From - Kolkata", async () => { | |
await travelFlightsPage.selectFromAs("Kolkata"); | |
}); | |
await test.step("To - Chennai", async () => { | |
await travelFlightsPage.selectToAs("Chennai"); | |
}); | |
await test.step("Depart on Nov 1", async () => { | |
await travelFlightsPage.selectDepartDate(); | |
}); | |
await test.step("Return on Nov 30", async () => { | |
await travelFlightsPage.selectReturnDate(); | |
}); | |
await test.step("Adult 2", async () => { | |
await travelFlightsPage.selectAdult(); | |
}); | |
await test.step("Child 1", async () => { | |
await travelFlightsPage.selectChild(); | |
}); | |
await test.step("Economy should be selected", async () => { | |
await travelFlightsPage.verifyEconomyIsSelected(); | |
}); | |
await test.step("Click on the search", async () => { | |
await travelFlightsPage.navigateToPage("SEARCH"); | |
}); | |
await test.step("Verify non-stop is not selected", async () => { | |
await travelSearchPage.verifyNonStopIsSelected(); | |
}); | |
await test.step("Click on the non-stop", async () => { | |
await travelSearchPage.selectNonStop(); | |
}); | |
await test.step("print all the prices", async () => { | |
await travelSearchPage.printAllOutboundFlights(); | |
}); | |
await test.step("Select the last flight", async () => { | |
await travelSearchPage.selectLastOutboundFlight(); | |
}); | |
await test.step("Click on the book button", async () => { | |
await travelSearchPage.navigateToReviewOrder(); | |
}); | |
await test.step( | |
"Verify the page navigates to the review store online", | |
async () => { | |
await travelSearchPage.reviewItenerary(); | |
} | |
); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//HomePage.ts | |
import { Page } from "@playwright/test"; | |
var settings = require("../settings.json"); | |
let btnCross; | |
export class HomePage { | |
readonly page: Page; | |
constructor(page: Page) { | |
this.page = page; | |
//locators | |
btnCross = "text=✕"; | |
} | |
async navigateTo(path) { | |
await this.page.goto(settings.baseURL + path); | |
} | |
async closeLoginPopup() { | |
await this.page.click(btnCross); | |
} | |
async navigateToPage(PageName) { | |
await this.page.click('img[alt="' + PageName + '"]'); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//TravelFlightsPage.ts | |
import { Page, expect } from "@playwright/test"; | |
let rdbOneWay, rdbRoundTrip; | |
let txtBoxDepartCity, txtBoxArrivalCity, txtBoxDepartDate; | |
let day, month, year; | |
export class TravelFlightsPage { | |
readonly page: Page; | |
constructor(page: Page) { | |
this.page = page; | |
//locators | |
rdbOneWay = "//input[@id='ONE_WAY']"; | |
rdbRoundTrip = "//div[@data-checked='false']"; | |
txtBoxDepartCity = 'input[name="0-departcity"]'; | |
txtBoxArrivalCity = 'input[name="0-arrivalcity"]'; | |
txtBoxDepartDate = 'input[name="0-datefrom"]'; | |
} | |
async navigateToPage(PageName) { | |
await this.page.click('button:has-text("' + PageName + '")'); | |
} | |
async verifyOneWayIsSelected() { | |
expect(await this.page.isChecked(rdbOneWay)).toBeTruthy(); | |
} | |
async verifyEconomyIsSelected() { | |
expect( | |
await this.page.isChecked( | |
"//div[@data-checked='true'][normalize-space()='Economy']" | |
) | |
).toBeTruthy(); | |
} | |
async clickRoundTrip() { | |
await this.page.check(rdbRoundTrip); | |
} | |
async selectFromAs(departCityName) { | |
await this.page.click(txtBoxDepartCity); | |
await this.page.fill(txtBoxDepartCity, departCityName); | |
await this.page.click("text=Kolkata, IndiaCCU"); | |
} | |
async selectToAs(arrivalCityName) { | |
await this.page.click(txtBoxArrivalCity); | |
await this.page.fill(txtBoxArrivalCity, arrivalCityName); | |
await this.page.click("text=Chennai, IndiaMAA"); | |
} | |
async selectDepartDate() { | |
await this.page.click(txtBoxDepartDate); | |
await this.selectTodayDate(2); | |
} | |
async selectTodayDate(addDays = 0) { | |
const monthNames = [ | |
"January", | |
"February", | |
"March", | |
"April", | |
"May", | |
"June", | |
"July", | |
"August", | |
"September", | |
"October", | |
"November", | |
"December", | |
]; | |
const date = new Date(); | |
date.setDate(date.getDate() + addDays); | |
month = await monthNames[date.getMonth()]; | |
year = date.getFullYear(); | |
await this.selectCalendarDate(date.getDate(), month, year); | |
} | |
async selectCalendarDate(day, month, year) { | |
let locator = await this.page.locator( | |
"//div[text()='" + | |
month + | |
" " + | |
year + | |
"']/ancestor::table//button[text()='" + | |
day + | |
"']" | |
); | |
await locator.click(); | |
} | |
async selectReturnDate() { | |
await this.page.click("//input[@name='0-dateto']"); | |
await this.selectTodayDate(15); | |
} | |
async selectAdult() { | |
await this.page.click('input[name="0-travellerclasscount"]'); | |
await this.page.click( | |
"text=AdultsAbove 12 years1 >> :nth-match(button, 2)" | |
); | |
} | |
async selectChild() { | |
await this.page.click( | |
"text=ChildrenBetween 2-12 years0 >> :nth-match(button, 2)" | |
); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//TravelSearchPage.ts | |
import { Page, expect } from "@playwright/test"; | |
let locatorSwitchNonStopOnOff, allFlightsPriceOnPage, lastRowForFlight; | |
let btnBook = 'button:has-text("Book")'; | |
let switchNonStopOnOff, switchHandle, switchOn, switchOff; | |
let allPriceList; | |
export class TravelSearchPage { | |
readonly page: Page; | |
constructor(page: Page) { | |
this.page = page; | |
//locators | |
switchNonStopOnOff = | |
"//div[@class='non-stop']//span[@class='u-ib u-rfloat']/*"; | |
switchHandle = ".switch-handle"; | |
switchOn = "c-switch switch-on"; | |
switchOff = "c-switch switch-off"; | |
allPriceList = | |
"//div[@class='result-col outr']//div[@class='result-col-inner']//div[contains(@class,'price-group')]"; | |
} | |
async verifyNonStopIsSelected() { | |
await this.page.waitForSelector(switchNonStopOnOff); | |
locatorSwitchNonStopOnOff = await this.page.locator(switchNonStopOnOff); | |
await expect(locatorSwitchNonStopOnOff).toHaveClass(switchOff); | |
} | |
async selectNonStop() { | |
await this.page.click(switchHandle); | |
await expect(locatorSwitchNonStopOnOff).toHaveClass(switchOn); | |
} | |
async printAllOutboundFlights() { | |
await this.page.waitForLoadState("networkidle"); // This resolves after 'networkidle' | |
allFlightsPriceOnPage = await this.page.$$(allPriceList); | |
for await (const flightPriceOnPage of allFlightsPriceOnPage) { | |
console.log(await flightPriceOnPage.innerText()); | |
lastRowForFlight = flightPriceOnPage; | |
} | |
} | |
async selectLastOutboundFlight() { | |
await lastRowForFlight.click(); | |
} | |
async navigateToReviewOrder() { | |
await this.page.click(btnBook); | |
} | |
async reviewItenerary() { | |
await this.page.click("text=REVIEW ITINERARY"); | |
} | |
} |
- Download complete code from - Github link 🔗
- Install NodeJS and Git from below links befor proceeding with below commands-
- Install NodeJS and Git from below links befor proceeding with below commands-
- Step by Step Guide
git clone https://github.com/777abhi/playwright-typescript.git
npm install
"npm test" or "npx playwright test --config=playwright.config.ts"
"npm run test:debug" or "npx playwright test --config=playwright.config.ts --debug"
"npm run test:report:html" or "npx playwright test --reporter=html"
Thanks for writeup on the approach, and github code link :)
ReplyDeleteThanks 😀
DeleteInsightful
ReplyDelete🔆
Deletesimilar to my attempt, some other people have made an attempt to this challenge with different languages and framework, below are the links to their GitHub repos -
ReplyDeleteWinner's for this challenge Ahamed and Abhinav - https://www.youtube.com/watch?v=I2AcfZ88rJ4&ab_channel=LetCodewithKoushik
Github code repo link:
Balaji Harish - https://github.com/BalajiHarish/Flipkart
Ahamed - https://github.com/Ahameds89/flipTask
Saravanan Seenivasan - https://github.com/sseenivasan89/FlipkartFlightSearch
Aravind Ram - https://github.com/arvindram95/FlipkartFlightBooking