From e91a804fbf7a70daa9c07964603b1ebba05bf330 Mon Sep 17 00:00:00 2001 From: tomnguyen2604 Date: Thu, 19 Mar 2026 23:28:09 -0400 Subject: [PATCH 1/3] Hoa_Nguyen_Playright_Test_Complete --- .DS_Store | Bin 0 -> 6148 bytes features/login.feature | 11 +++++--- features/product.feature | 15 ++++++----- features/purchase.feature | 18 ++++++------- package-lock.json | 2 +- pages/checkout.page.ts | 55 ++++++++++++++++++++++++++++++++++++++ pages/login.page.ts | 39 ++++++++++++++++++--------- pages/product.page.ts | 50 ++++++++++++++++++++++++++++++---- playwrightUtilities.ts | 48 ++++++++++++++++++++------------- steps/login.steps.ts | 26 ++++++++++++------ steps/product.steps.ts | 8 ++++++ steps/purchase.steps.ts | 37 +++++++++++++++++++++++++ 12 files changed, 245 insertions(+), 64 deletions(-) create mode 100644 .DS_Store create mode 100644 pages/checkout.page.ts create mode 100644 steps/purchase.steps.ts diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..65ebcabf7ab92b947d646f4a97ce3df65805659e GIT binary patch literal 6148 zcmeHKJ5Iwu5S;}FBA}o^h=RgB5(-E(R0hHUAU&Uu5F!#B0ii@b02PPe0!VNH5Dll` z1hf=LyxCoxS=))I5JEfA?px2ydY+%{-I$2fX1}pQG)qKfG{#^GU4e0&Th7L;XA!9Q z9G%um?I`Z{a+%)Y74QoDMFsfX4QQEK)TT@4{Z-RjZ@M63;CdWo3)u1hD>r@B*}f?J$DlOZ|G&A;_6hg{Zx$?zpBTT>EF-n$MkSY}^^}X4-;D<>iZk5$ zyf4rUouPUIitJNYkNVWf>QmZ#T^p4vxjlV6UfX*&^xTr^Px2-2iO+ZDU7{ZDW-H4S z0qj;XD!D!H2;I{8l1*`jtWOl0`WJYXnqZN6lEznc{WL1;x0|BA$QazW;taRGer}k< zC}S01CgyaK5Lygr0zjFH8}pk6xA=}!P)0Jk=b^_~F?*#V3!CI;bwDKiyl zrYif1q0DsnLz@>_ObnXoq;zGR$5vMM3q|Sb@P|5`RAkWiUIDLwtH8K9oaOz0d-(Ui zJINn;1-t_PN&yvAYt>C$lHFT#7sq>Th<1R+#(9ZBJq4ZJj`e`I;srEqh=n`=Mivu; R@WA{Z0WE{?yaK - Then I will login as 'standard_user' - # TODO: Sort the items by - # TODO: Validate all 6 items are sorted correctly by price + Scenario Outline: Validate product sort by price + Then I will login as 'standard_user' + Then I sort the products by "" + Then I should see 6 products sorted by price in "" order + Examples: - # TODO: extend the datatable to paramterize this test - | sort | \ No newline at end of file + | sort | order | + | Price (low to high) | ascending | + | Price (high to low) | descending | \ No newline at end of file diff --git a/features/purchase.feature b/features/purchase.feature index 2863478..4aeaf72 100644 --- a/features/purchase.feature +++ b/features/purchase.feature @@ -3,12 +3,12 @@ Feature: Purchase Feature Background: Given I open the "https://www.saucedemo.com/" page - Scenario: Validate successful purchase text - Then I will login as 'standard_user' - Then I will add the backpack to the cart - # TODO: Select the cart (top-right) - # TODO: Select Checkout - # TODO: Fill in the First Name, Last Name, and Zip/Postal Code - # TODO: Select Continue - # TODO: Select Finish - # TODO: Validate the text 'Thank you for your order!' \ No newline at end of file + Scenario: Validate successful purchase text + Then I will login as 'standard_user' + Then I will add the backpack to the cart + Then I open the cart + Then I proceed to checkout + Then I enter checkout details with first name "John", last name "Doe", and postal code "12345" + Then I continue checkout + Then I finish the purchase + Then I should see the purchase confirmation text "Thank you for your order!" \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index b90d7c6..b5c47aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "Playwright-Project", + "name": "Playwright-Cucumber-Exercise", "lockfileVersion": 3, "requires": true, "packages": { diff --git a/pages/checkout.page.ts b/pages/checkout.page.ts new file mode 100644 index 0000000..b062809 --- /dev/null +++ b/pages/checkout.page.ts @@ -0,0 +1,55 @@ +import { Page } from "@playwright/test"; + +export class Checkout { + private readonly page: Page; + private readonly cartLink: string = ".shopping_cart_link"; + private readonly checkoutButton: string = 'button[id="checkout"]'; + private readonly firstNameField: string = 'input[id="first-name"]'; + private readonly lastNameField: string = 'input[id="last-name"]'; + private readonly postalCodeField: string = 'input[id="postal-code"]'; + private readonly continueButton: string = 'input[id="continue"]'; + private readonly finishButton: string = 'button[id="finish"]'; + private readonly purchaseConfirmation: string = ".complete-header"; + + constructor(page: Page) { + this.page = page; + } + + public async openCart() { + await this.page.locator(this.cartLink).click(); + } + + public async checkout() { + await this.page.locator(this.checkoutButton).click(); + } + + public async enterCheckoutDetails( + firstName: string, + lastName: string, + postalCode: string, + ) { + await this.page.locator(this.firstNameField).fill(firstName); + await this.page.locator(this.lastNameField).fill(lastName); + await this.page.locator(this.postalCodeField).fill(postalCode); + } + + public async continueCheckout() { + await this.page.locator(this.continueButton).click(); + } + + public async finishPurchase() { + await this.page.locator(this.finishButton).click(); + } + + public async validatePurchaseConfirmationText(expectedText: string) { + const confirmationText = + ( + await this.page.locator(this.purchaseConfirmation).textContent() + )?.trim() ?? ""; + if (confirmationText !== expectedText) { + throw new Error( + `Expected confirmation text to be "${expectedText}" but found "${confirmationText}"`, + ); + } + } +} diff --git a/pages/login.page.ts b/pages/login.page.ts index 5a01614..570354f 100644 --- a/pages/login.page.ts +++ b/pages/login.page.ts @@ -1,11 +1,12 @@ -import { Page } from "@playwright/test" +import { Page } from "@playwright/test"; export class Login { - private readonly page: Page - private readonly password: string = 'secret_sauce' - private readonly passwordField: string = 'input[id="password"]' - private readonly userNameField: string = 'input[id="user-name"]' - private readonly loginButton: string = 'input[id="login-button"]' + private readonly page: Page; + private readonly password: string = "secret_sauce"; + private readonly passwordField: string = 'input[id="password"]'; + private readonly userNameField: string = 'input[id="user-name"]'; + private readonly loginButton: string = 'input[id="login-button"]'; + private readonly loginError: string = 'h3[data-test="error"]'; constructor(page: Page) { this.page = page; @@ -14,13 +15,27 @@ export class Login { public async validateTitle(expectedTitle: string) { const pageTitle = await this.page.title(); if (pageTitle !== expectedTitle) { - throw new Error(`Expected title to be ${expectedTitle} but found ${pageTitle}`); + throw new Error( + `Expected title to be ${expectedTitle} but found ${pageTitle}`, + ); } } - public async loginAsUser(userName: string) { - await this.page.locator(this.userNameField).fill(userName) - await this.page.locator(this.passwordField).fill(this.password) - await this.page.locator(this.loginButton).click() + await this.page.locator(this.userNameField).fill(userName); + await this.page.locator(this.passwordField).fill(this.password); + await this.page.locator(this.loginButton).click(); + } + public async validateLoginErrorMessage(expectedMessage: string) { + const actualMessage = + (await this.page.locator(this.loginError).textContent())?.trim() ?? + ""; + if (actualMessage !== expectedMessage) { + throw new Error( + `Expected error message to be "${expectedMessage}" but found "${actualMessage}"`, + ); + } + } + public async validateInventoryPageNavigation() { + await this.page.waitForURL("**/inventory.html"); } -} \ No newline at end of file +} diff --git a/pages/product.page.ts b/pages/product.page.ts index 14bedb1..3a15c2d 100644 --- a/pages/product.page.ts +++ b/pages/product.page.ts @@ -1,14 +1,54 @@ -import { Page } from "@playwright/test" +import { Page } from "@playwright/test"; export class Product { - private readonly page: Page - private readonly addToCart: string = 'button[id="add-to-cart-sauce-labs-backpack"]' + private readonly page: Page; + private readonly addToCart: string = + 'button[id="add-to-cart-sauce-labs-backpack"]'; + private readonly sortDropdown: string = ".product_sort_container"; + private readonly itemPrices: string = ".inventory_item_price"; constructor(page: Page) { this.page = page; } public async addBackPackToCart() { - await this.page.locator(this.addToCart).click() + await this.page.locator(this.addToCart).click(); } -} \ No newline at end of file + public async sortProductsBy(sort: string) { + await this.page + .locator(this.sortDropdown) + .selectOption({ label: sort }); + } + public async validateProductPricesSorted( + order: string, + expectedCount: number, + ) { + const rawPrices = await this.page + .locator(this.itemPrices) + .allTextContents(); + const parsedPrices = rawPrices.map((price) => + Number(price.replace("$", "").trim()), + ); + + if (parsedPrices.length !== expectedCount) { + throw new Error( + `Expected ${expectedCount} products but found ${parsedPrices.length}`, + ); + } + + const sortedPrices = [...parsedPrices].sort((a, b) => a - b); + const expectedPrices = + order.toLowerCase() === "descending" + ? sortedPrices.reverse() + : sortedPrices; + const isSorted = parsedPrices.every( + (price, index) => price === expectedPrices[index], + ); + + if (!isSorted) { + throw new Error( + `Expected prices to be sorted in ${order} order, but found [${parsedPrices.join(", ")}]`, + ); + } + } +} diff --git a/playwrightUtilities.ts b/playwrightUtilities.ts index 93244a2..834a4c9 100644 --- a/playwrightUtilities.ts +++ b/playwrightUtilities.ts @@ -1,33 +1,45 @@ -import { Browser, chromium, Page } from 'playwright'; +import { Browser, chromium, Page, firefox, webkit } from "playwright"; let browser: Browser | null = null; let page: Page | null = null; const DEFAULT_TIMEOUT = 30000; export const initializeBrowser = async () => { - if (!browser) { - browser = await chromium.launch({ headless: false }); - } + if (!browser) { + const browserName = + process.env.BROWSER ?? + (process.platform === "darwin" ? "webkit" : "chromium"); + const browserType = + browserName === "firefox" + ? firefox + : browserName === "webkit" + ? webkit + : chromium; + const isHeadless = process.env.HEADLESS !== "false"; + browser = await browserType.launch({ headless: isHeadless }); + } }; export const initializePage = async () => { - if (browser && !page) { - page = await browser.newPage(); - page.setDefaultTimeout(DEFAULT_TIMEOUT); - } + if (browser && !page) { + page = await browser.newPage(); + page.setDefaultTimeout(DEFAULT_TIMEOUT); + } }; export const getPage = (): Page => { - if (!page) { - throw new Error('Page has not been initialized. Please call initializePage first.'); - } - return page; + if (!page) { + throw new Error( + "Page has not been initialized. Please call initializePage first.", + ); + } + return page; }; export const closeBrowser = async () => { - if (browser) { - await browser.close(); - browser = null; - page = null; - } -}; \ No newline at end of file + if (browser) { + await browser.close(); + browser = null; + page = null; + } +}; diff --git a/steps/login.steps.ts b/steps/login.steps.ts index c2aa0d8..6bef08c 100644 --- a/steps/login.steps.ts +++ b/steps/login.steps.ts @@ -1,11 +1,21 @@ -import { Then } from '@cucumber/cucumber'; -import { getPage } from '../playwrightUtilities'; -import { Login } from '../pages/login.page'; +import { Then } from "@cucumber/cucumber"; +import { getPage } from "../playwrightUtilities"; +import { Login } from "../pages/login.page"; -Then('I should see the title {string}', async (expectedTitle) => { - await new Login(getPage()).validateTitle(expectedTitle); +Then("I should see the title {string}", async (expectedTitle) => { + await new Login(getPage()).validateTitle(expectedTitle); }); -Then('I will login as {string}', async (userName) => { - await new Login(getPage()).loginAsUser(userName); -}); \ No newline at end of file +Then("I will login as {string}", async (userName) => { + await new Login(getPage()).loginAsUser(userName); +}); +Then( + "I should see the login error message {string}", + async (expectedMessage) => { + await new Login(getPage()).validateLoginErrorMessage(expectedMessage); + }, +); + +Then("I should be redirected to the inventory page", async () => { + await new Login(getPage()).validateInventoryPageNavigation(); +}); diff --git a/steps/product.steps.ts b/steps/product.steps.ts index bb52fb9..2582393 100644 --- a/steps/product.steps.ts +++ b/steps/product.steps.ts @@ -4,4 +4,12 @@ import { Product } from '../pages/product.page'; Then('I will add the backpack to the cart', async () => { await new Product(getPage()).addBackPackToCart(); +}); + +Then('I sort the products by {string}', async (sortOption) => { + await new Product(getPage()).sortProductsBy(sortOption); +}); + +Then('I should see {int} products sorted by price in {string} order', async (expectedCount, order) => { + await new Product(getPage()).validateProductPricesSorted(order, expectedCount); }); \ No newline at end of file diff --git a/steps/purchase.steps.ts b/steps/purchase.steps.ts new file mode 100644 index 0000000..e0187aa --- /dev/null +++ b/steps/purchase.steps.ts @@ -0,0 +1,37 @@ +import { Then } from "@cucumber/cucumber"; +import { getPage } from "../playwrightUtilities"; +import { Checkout } from "../pages/checkout.page"; + +Then("I open the cart", async () => { + await new Checkout(getPage()).openCart(); +}); +Then("I proceed to checkout", async () => { + await new Checkout(getPage()).checkout(); +}); + +Then( + "I enter checkout details with first name {string}, last name {string}, and postal code {string}", + async (firstName, lastName, postalCode) => { + await new Checkout(getPage()).enterCheckoutDetails( + firstName, + lastName, + postalCode, + ); + }, +); +Then("I continue checkout", async () => { + await new Checkout(getPage()).continueCheckout(); +}); + +Then("I finish the purchase", async () => { + await new Checkout(getPage()).finishPurchase(); +}); + +Then( + "I should see the purchase confirmation text {string}", + async (expectedText) => { + await new Checkout(getPage()).validatePurchaseConfirmationText( + expectedText, + ); + }, +); From 228b809d5e4194cae24ad183443d707ca08c2119 Mon Sep 17 00:00:00 2001 From: tomnguyen2604 Date: Thu, 19 Mar 2026 23:28:25 -0400 Subject: [PATCH 2/3] Small updates --- steps/product.steps.ts | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/steps/product.steps.ts b/steps/product.steps.ts index 2582393..a833de2 100644 --- a/steps/product.steps.ts +++ b/steps/product.steps.ts @@ -1,15 +1,21 @@ -import { Then } from '@cucumber/cucumber'; -import { getPage } from '../playwrightUtilities'; -import { Product } from '../pages/product.page'; +import { Then } from "@cucumber/cucumber"; +import { getPage } from "../playwrightUtilities"; +import { Product } from "../pages/product.page"; -Then('I will add the backpack to the cart', async () => { - await new Product(getPage()).addBackPackToCart(); +Then("I will add the backpack to the cart", async () => { + await new Product(getPage()).addBackPackToCart(); }); -Then('I sort the products by {string}', async (sortOption) => { - await new Product(getPage()).sortProductsBy(sortOption); +Then("I sort the products by {string}", async (sortOption) => { + await new Product(getPage()).sortProductsBy(sortOption); }); -Then('I should see {int} products sorted by price in {string} order', async (expectedCount, order) => { - await new Product(getPage()).validateProductPricesSorted(order, expectedCount); -}); \ No newline at end of file +Then( + "I should see {int} products sorted by price in {string} order", + async (expectedCount, order) => { + await new Product(getPage()).validateProductPricesSorted( + order, + expectedCount, + ); + }, +); From 5453923fcc671ae1dae553b2adad787b00d4b05b Mon Sep 17 00:00:00 2001 From: tomnguyen2604 Date: Thu, 19 Mar 2026 23:29:27 -0400 Subject: [PATCH 3/3] README updates --- README.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index b911105..9a684e2 100644 --- a/README.md +++ b/README.md @@ -6,19 +6,16 @@ node >= v18.5.x npm >= v7 - ## Setup // Install Visual Studio Code (or any editor) https://code.visualstudio.com/download - // Install Node.js https://nodejs.org/en/download - ```bash git clone https://github.com/automationExamples/Playwright-Cucumber-Exercise.git npm install @@ -31,28 +28,31 @@ Cucumber v1.7.0 Cucumber (Gherkin) Support enhanced for Behat - ## Instructions + To run the test + ```bash npm run test ``` After running, to generate the cucumber report (cucumber_report.html) + ```bash npm run report ``` -It is not expected that you complete every task, however, please give your best effort +It is not expected that you complete every task, however, please give your best effort You will be scored based on your ability to complete the following tasks: -- [ ] Install and setup this repository on your personal computer -- [ ] Complete the automation tasks listed below +- [Y] Install and setup this repository on your personal computer +- [Y] Complete the automation tasks listed below ### Tasks -- [ ] Modify the scenario 'Validate the login page title' from [login.feature](features/login.feature#8) which runs but fails. Determine the cause of the failure and update the scenario to pass in the test -- [ ] Extend the scenario 'Validate login error message' from [login.feature](features/login.feature#10) which runs and passes but is missing a step. Extend the scenario to validate the error message received. -- [ ] Modify and extend the 'Validate successful purchase text' from [purchase.feature](features/purchase.feature#6) with steps for each comment listed. Consider writing a new steps.ts file along with an appropriate page.ts -- [ ] Modify and extend the 'Validate product sort by price sort' from [product.feature](features/product.feature#6) with steps for each comment listed. Utilize the Scenario Outline and Examples table to parameterize the test -- [ ] Extend the testing coverage with anything you believe would be beneficial + +- [Y] Modify the scenario 'Validate the login page title' from [login.feature](features/login.feature#8) which runs but fails. Determine the cause of the failure and update the scenario to pass in the test +- [Y] Extend the scenario 'Validate login error message' from [login.feature](features/login.feature#10) which runs and passes but is missing a step. Extend the scenario to validate the error message received. +- [Y] Modify and extend the 'Validate successful purchase text' from [purchase.feature](features/purchase.feature#6) with steps for each comment listed. Consider writing a new steps.ts file along with an appropriate page.ts +- [Y] Modify and extend the 'Validate product sort by price sort' from [product.feature](features/product.feature#6) with steps for each comment listed. Utilize the Scenario Outline and Examples table to parameterize the test +- [Y] Extend the testing coverage with anything you believe would be beneficial