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

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 

    //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");
    });

    • Introduce page objects for easy maintenance
      • project structure
      • all page objects & final feature file
      • //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;
        view raw BasePage.ts hosted with ❤ by GitHub
        //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();
        }
        );
        });
        //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 + '"]');
        }
        }
        view raw HomePage.ts hosted with ❤ by GitHub
        //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)"
        );
        }
        }
        //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- 

    Comments

    1. Thanks for writeup on the approach, and github code link :)

      ReplyDelete
    2. similar 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 -

      Winner'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

      ReplyDelete

    Post a Comment