From 2cb3b42ff7f566cd95c51a26adc73c0572feafe4 Mon Sep 17 00:00:00 2001 From: "cardauc.victor" Date: Wed, 29 Jul 2026 16:04:07 -0500 Subject: [PATCH] Complete Playwright Cucumber automation exercise --- features/login.feature | 5 +- features/product.feature | 17 +++--- features/purchase.feature | 18 +++--- pages/login.page.ts | 9 +++ pages/product.page.ts | 115 ++++++++++++++++++++++++++++++++++++-- steps/login.steps.ts | 4 ++ steps/product.steps.ts | 54 +++++++++++++++++- 7 files changed, 196 insertions(+), 26 deletions(-) diff --git a/features/login.feature b/features/login.feature index fb9f1fa5..de6b0d09 100644 --- a/features/login.feature +++ b/features/login.feature @@ -4,9 +4,8 @@ Feature: Login Feature Given I open the "https://www.saucedemo.com/" page Scenario: Validate the login page title - # TODO: Fix this failing scenario - Then I should see the title "Labs Swag" + Then I should see the title "Swag Labs" Scenario: Validate login error message Then I will login as 'locked_out_user' - # TODO: Add a step to validate the error message received \ No newline at end of file + Then I should see login error message "Epic sadface: Sorry, this user has been locked out." diff --git a/features/product.feature b/features/product.feature index 8a7ceab9..c77e359d 100644 --- a/features/product.feature +++ b/features/product.feature @@ -3,11 +3,12 @@ Feature: Product Feature Background: Given I open the "https://www.saucedemo.com/" page - # Create a datatable to validate the Price (high to low) and Price (low to high) sort options (top-right) using a Scenario Outline - Scenario Outline: Validate product sort by price - Then I will login as 'standard_user' - # TODO: Sort the items by - # TODO: Validate all 6 items are sorted correctly by price - Examples: - # TODO: extend the datatable to paramterize this test - | sort | \ No newline at end of file + Scenario Outline: Validate product sort by price + Then I will login as 'standard_user' + Then I sort the products by "" + Then the product prices should be sorted "" + + Examples: + | sort | order | + | Price (low to high) | ascending | + | Price (high to low) | descending | diff --git a/features/purchase.feature b/features/purchase.feature index 28634789..10824d9f 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 select the cart + Then I select checkout + Then I enter first name "Victor", last name "Cardauc", and postal code "32541" + Then I select continue + Then I select finish + Then I should see the purchase confirmation "Thank you for your order!" diff --git a/pages/login.page.ts b/pages/login.page.ts index 5a01614b..ec4eb1a2 100644 --- a/pages/login.page.ts +++ b/pages/login.page.ts @@ -6,6 +6,7 @@ export class Login { 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 errorMessage: string = '[data-test="error"]' constructor(page: Page) { this.page = page; @@ -23,4 +24,12 @@ export class Login { await this.page.locator(this.passwordField).fill(this.password) await this.page.locator(this.loginButton).click() } + + public async validateErrorMessage(expectedMessage: string) { + const actualMessage = await this.page.locator(this.errorMessage).textContent() + if (actualMessage?.trim() !== expectedMessage) { + throw new Error(`Expected error message to be "${expectedMessage}" but found "${actualMessage?.trim()}"`); + } + +} } \ No newline at end of file diff --git a/pages/product.page.ts b/pages/product.page.ts index 14bedb1b..d9b0c35f 100644 --- a/pages/product.page.ts +++ b/pages/product.page.ts @@ -1,14 +1,121 @@ -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 cart: string = + '[data-test="shopping-cart-link"]'; + + private readonly checkoutButton: string = + '[data-test="checkout"]'; + + private readonly firstNameField: string = + '[data-test="firstName"]'; + + private readonly lastNameField: string = + '[data-test="lastName"]'; + + private readonly postalCodeField: string = + '[data-test="postalCode"]'; + + private readonly continueButton: string = + '[data-test="continue"]'; + + private readonly finishButton: string = + '[data-test="finish"]'; + + private readonly purchaseConfirmation: string = + '[data-test="complete-header"]'; + + private readonly sortDropdown: string = + '[data-test="product-sort-container"]'; + + private readonly productPrices: string = + '[data-test="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(); + } + + public async selectCart() { + await this.page.locator(this.cart).click(); + } + + public async selectCheckout() { + await this.page.locator(this.checkoutButton).click(); + } + + public async enterCustomerInformation( + 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 selectContinue() { + await this.page.locator(this.continueButton).click(); + } + + public async selectFinish() { + await this.page.locator(this.finishButton).click(); + } + + public async validatePurchaseConfirmation(expectedMessage: string) { + const actualMessage = await this.page + .locator(this.purchaseConfirmation) + .textContent(); + + if (actualMessage?.trim() !== expectedMessage) { + throw new Error( + `Expected purchase confirmation to be "${expectedMessage}" but found "${actualMessage?.trim()}"` + ); + } + } + public async sortProducts(sortOption: string) { + if (sortOption === 'Price (low to high)') { + await this.page.locator(this.sortDropdown).selectOption('lohi'); + } else if (sortOption === 'Price (high to low)') { + await this.page.locator(this.sortDropdown).selectOption('hilo'); + } else { + throw new Error(`Unsupported sort option: ${sortOption}`); + } +} + + public async validateProductPricesSorted(order: string) { + const priceTexts = await this.page + .locator(this.productPrices) + .allTextContents(); + + const actualPrices = priceTexts.map((price) => + Number(price.replace('$', '').trim()) + ); + + const expectedPrices = [...actualPrices]; + + if (order === 'ascending') { + expectedPrices.sort((a, b) => a - b); + } else if (order === 'descending') { + expectedPrices.sort((a, b) => b - a); + } else { + throw new Error(`Unsupported sort order: ${order}`); + } + + if (JSON.stringify(actualPrices) !== JSON.stringify(expectedPrices)) { + throw new Error( + `Prices are not sorted ${order}. + Actual: ${actualPrices.join(', ')} + Expected: ${expectedPrices.join(', ')}` + ); } +} } \ No newline at end of file diff --git a/steps/login.steps.ts b/steps/login.steps.ts index c2aa0d80..2c5b188c 100644 --- a/steps/login.steps.ts +++ b/steps/login.steps.ts @@ -8,4 +8,8 @@ Then('I should see the title {string}', async (expectedTitle) => { Then('I will login as {string}', async (userName) => { await new Login(getPage()).loginAsUser(userName); +}); + +Then('I should see login error message {string}', async (expectedMessage: string) => { + await new Login(getPage()).validateErrorMessage(expectedMessage); }); \ No newline at end of file diff --git a/steps/product.steps.ts b/steps/product.steps.ts index bb52fb98..0fdee5a4 100644 --- a/steps/product.steps.ts +++ b/steps/product.steps.ts @@ -3,5 +3,55 @@ 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(); -}); \ No newline at end of file + await new Product(getPage()).addBackPackToCart(); +}); + +Then('I select the cart', async () => { + await new Product(getPage()).selectCart(); +}); + +Then('I select checkout', async () => { + await new Product(getPage()).selectCheckout(); +}); + +Then( + 'I enter first name {string}, last name {string}, and postal code {string}', + async ( + firstName: string, + lastName: string, + postalCode: string + ) => { + await new Product(getPage()).enterCustomerInformation( + firstName, + lastName, + postalCode + ); + } +); + +Then('I select continue', async () => { + await new Product(getPage()).selectContinue(); +}); + +Then('I select finish', async () => { + await new Product(getPage()).selectFinish(); +}); + +Then( + 'I should see the purchase confirmation {string}', + async (expectedMessage: string) => { + await new Product(getPage()).validatePurchaseConfirmation( + expectedMessage + ); + }); + +Then('I sort the products by {string}', async (sortOption: string) => { + await new Product(getPage()).sortProducts(sortOption); +}); + +Then( + 'the product prices should be sorted {string}', + async (order: string) => { + await new Product(getPage()).validateProductPricesSorted(order); + } +); \ No newline at end of file