From a83247fc8dafed70f644738a814f72af44290e61 Mon Sep 17 00:00:00 2001 From: Tetiana Date: Tue, 17 Mar 2026 21:28:38 -0400 Subject: [PATCH] Completed Playwright + Cucumber tasks: login, product, purchase --- features/login.feature | 28 +++++++++++++++++++---- features/product.feature | 24 ++++++++++++------- features/purchase.feature | 16 ++++++++++--- package-lock.json | 9 +++++++- pages/cart.page.ts | 35 ++++++++++++++++++++++++++++ pages/checkout.page.ts | 21 +++++++++++++++++ pages/checkoutComplete.page.ts | 13 +++++++++++ pages/checkoutOverview.page.ts | 12 ++++++++++ pages/login.page.ts | 14 +++++++++++- pages/product.page.ts | 42 +++++++++++++++++++++++++++++++++- steps/login.steps.ts | 10 +++++++- steps/product.steps.ts | 16 +++++++++++-- steps/purchase.steps.ts | 37 ++++++++++++++++++++++++++++++ 13 files changed, 256 insertions(+), 21 deletions(-) create mode 100644 pages/cart.page.ts create mode 100644 pages/checkout.page.ts create mode 100644 pages/checkoutComplete.page.ts create mode 100644 pages/checkoutOverview.page.ts create mode 100644 steps/purchase.steps.ts diff --git a/features/login.feature b/features/login.feature index fb9f1fa..7d9bab6 100644 --- a/features/login.feature +++ b/features/login.feature @@ -4,9 +4,29 @@ 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 will login as "locked_out_user" + Then I should see the error message "Epic sadface: Sorry, this user has been locked out." + + Scenario: Login with invalid username + Then I will login with username "invalid_user_name" and password "secret_sauce" + Then I should see the error message "Epic sadface: Username and password do not match any user in this service" + + Scenario: Login with invalid password + Then I will login with username "standard_user" and password "wrong_password" + Then I should see the error message "Epic sadface: Username and password do not match any user in this service" + + Scenario: Login with empty username + Then I will login with username "" and password "secret_sauce" + Then I should see the error message "Epic sadface: Username is required" + + Scenario: Login with empty password + Then I will login with username "standard_user" and password "" + Then I should see the error message "Epic sadface: Password is required" + + Scenario: Login with empty username and password + Then I will login with username "" and password "" + Then I should see the error message "Epic sadface: Username is required" + diff --git a/features/product.feature b/features/product.feature index 8a7ceab..22e2fe9 100644 --- a/features/product.feature +++ b/features/product.feature @@ -1,13 +1,21 @@ 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 sorting + + Then I will login as "standard_user" + When I sort products by "" + Then all products should be sorted by "" + + Examples: + | sort | + | Price (low to high) | + | Price (high to low) | + | Name (A to Z) | + | Name (Z to A) | + + + diff --git a/features/purchase.feature b/features/purchase.feature index 2863478..59e4649 100644 --- a/features/purchase.feature +++ b/features/purchase.feature @@ -4,11 +4,21 @@ Feature: Purchase Feature 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 + + Then I will login as 'standard_user' + Then I will add the backpack to the cart + + Then I open the cart + Then the cart should contain "Sauce Labs Backpack" with price "$29.99" and quantity "1" + Then I proceed to checkout + Then I fill in checkout information + Then I continue checkout + Then I finish the purchase + Then I should see the success message + # 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 + # TODO: Validate the text 'Thank you for your order!' diff --git a/package-lock.json b/package-lock.json index b90d7c6..b26590b 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": { @@ -189,6 +189,7 @@ "resolved": "https://registry.npmjs.org/@cucumber/cucumber/-/cucumber-10.0.1.tgz", "integrity": "sha512-g7W7SQnNMSNnMRQVGubjefCxdgNFyq4P3qxT2Ve7Xhh8ZLoNkoRDcWsyfKQVWnxNfgW3aGJmxbucWRoTi+ZUqg==", "dev": true, + "peer": true, "dependencies": { "@cucumber/ci-environment": "9.2.0", "@cucumber/cucumber-expressions": "16.1.2", @@ -253,6 +254,7 @@ "resolved": "https://registry.npmjs.org/@cucumber/gherkin/-/gherkin-26.2.0.tgz", "integrity": "sha512-iRSiK8YAIHAmLrn/mUfpAx7OXZ7LyNlh1zT89RoziSVCbqSVDxJS6ckEzW8loxs+EEXl0dKPQOXiDmbHV+C/fA==", "dev": true, + "peer": true, "dependencies": { "@cucumber/messages": ">=19.1.4 <=22" } @@ -350,6 +352,7 @@ "resolved": "https://registry.npmjs.org/@cucumber/message-streams/-/message-streams-4.0.1.tgz", "integrity": "sha512-Kxap9uP5jD8tHUZVjTWgzxemi/0uOsbGjd4LBOSxcJoOCRbESFwemUzilJuzNTB8pcTQUh8D5oudUyxfkJOKmA==", "dev": true, + "peer": true, "peerDependencies": { "@cucumber/messages": ">=17.1.1" } @@ -359,6 +362,7 @@ "resolved": "https://registry.npmjs.org/@cucumber/messages/-/messages-22.0.0.tgz", "integrity": "sha512-EuaUtYte9ilkxcKmfqGF9pJsHRUU0jwie5ukuZ/1NPTuHS1LxHPsGEODK17RPRbZHOFhqybNzG2rHAwThxEymg==", "dev": true, + "peer": true, "dependencies": { "@types/uuid": "9.0.1", "class-transformer": "0.5.1", @@ -549,6 +553,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.3.tgz", "integrity": "sha512-XJavIpZqiXID5Yxnxv3RUDKTN5b81ddNC3ecsA0SoFXz/QU8OGBwZGMomiq0zw+uuqbL/krztv/DINAQ/EV4gg==", "dev": true, + "peer": true, "dependencies": { "undici-types": "~5.26.4" } @@ -974,6 +979,7 @@ "resolved": "https://registry.npmjs.org/@cucumber/messages/-/messages-21.0.1.tgz", "integrity": "sha512-pGR7iURM4SF9Qp1IIpNiVQ77J9kfxMkPOEbyy+zRmGABnWWCsqMpJdfHeh9Mb3VskemVw85++e15JT0PYdcR3g==", "dev": true, + "peer": true, "dependencies": { "@types/uuid": "8.3.4", "class-transformer": "0.5.1", @@ -2473,6 +2479,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz", "integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==", "dev": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/pages/cart.page.ts b/pages/cart.page.ts new file mode 100644 index 0000000..42dc972 --- /dev/null +++ b/pages/cart.page.ts @@ -0,0 +1,35 @@ +import { Page, expect } from '@playwright/test'; + +export class Cart { + + constructor(private page: Page) {} + + private cartIcon = '.shopping_cart_link'; + private checkoutButton = '[data-test="checkout"]'; + private cartItemName = '.inventory_item_name'; + private cartItemPrice = '.inventory_item_price'; + private cartItemQuantity = '.cart_quantity'; + + + public async openCart() { + await this.page.click(this.cartIcon); + } + + public async goToCheckout() { + await this.page.click(this.checkoutButton); + } + +public async validateCartItem(expectedName: string, expectedPrice: string, expectedQty: string) { + const name = await this.page.locator(this.cartItemName).textContent(); + const price = await this.page.locator(this.cartItemPrice).textContent(); + const qty = await this.page.locator(this.cartItemQuantity).textContent(); + + expect(name?.trim()).toBe(expectedName); + expect(price?.trim()).toBe(expectedPrice); + expect(qty?.trim()).toBe(expectedQty); +} + +} + + + diff --git a/pages/checkout.page.ts b/pages/checkout.page.ts new file mode 100644 index 0000000..29d9d5c --- /dev/null +++ b/pages/checkout.page.ts @@ -0,0 +1,21 @@ +import { Page } from '@playwright/test'; + +export class Checkout { + + constructor(private page: Page) {} + + private firstName = '[data-test="firstName"]'; + private lastName = '[data-test="lastName"]'; + private postalCode = '[data-test="postalCode"]'; + private continueButton = '[data-test="continue"]'; + + public async fillInformation(first: string, last: string, zip: string) { + await this.page.fill(this.firstName, first); + await this.page.fill(this.lastName, last); + await this.page.fill(this.postalCode, zip); + } + + public async continue() { + await this.page.click(this.continueButton); + } +} diff --git a/pages/checkoutComplete.page.ts b/pages/checkoutComplete.page.ts new file mode 100644 index 0000000..2e576b9 --- /dev/null +++ b/pages/checkoutComplete.page.ts @@ -0,0 +1,13 @@ +import { Page, expect } from '@playwright/test'; + +export class CheckoutComplete { + + constructor(private page: Page) {} + + private successHeader = '.complete-header'; + + public async validateSuccessMessage() { + await expect(this.page.locator(this.successHeader)).toHaveText('Thank you for your order!'); + } +} + diff --git a/pages/checkoutOverview.page.ts b/pages/checkoutOverview.page.ts new file mode 100644 index 0000000..7ad7ef3 --- /dev/null +++ b/pages/checkoutOverview.page.ts @@ -0,0 +1,12 @@ +import { Page } from '@playwright/test'; + +export class CheckoutOverview { + + constructor(private page: Page) {} + + private finishButton = '[data-test="finish"]'; + + public async finish() { + await this.page.click(this.finishButton); + } +} diff --git a/pages/login.page.ts b/pages/login.page.ts index 5a01614..9203b1e 100644 --- a/pages/login.page.ts +++ b/pages/login.page.ts @@ -1,4 +1,4 @@ -import { Page } from "@playwright/test" +import { Page, expect } from "@playwright/test" export class Login { private readonly page: Page @@ -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 errorMessageLocator: string = '[data-test="error"]' constructor(page: Page) { this.page = page; @@ -23,4 +24,15 @@ export class Login { await this.page.locator(this.passwordField).fill(this.password) await this.page.locator(this.loginButton).click() } + + public async validateErrorMessage(expected: string) { + await expect(this.page.locator(this.errorMessageLocator)).toHaveText(expected); + } + + public async login(username: string, password: string) { + await this.page.locator(this.userNameField).fill(username); + await this.page.locator(this.passwordField).fill(password); + await this.page.locator(this.loginButton).click(); +} + } \ No newline at end of file diff --git a/pages/product.page.ts b/pages/product.page.ts index 14bedb1..c503f13 100644 --- a/pages/product.page.ts +++ b/pages/product.page.ts @@ -1,8 +1,12 @@ -import { Page } from "@playwright/test" +import { Page, expect } from "@playwright/test" export class Product { private readonly page: Page private readonly addToCart: string = 'button[id="add-to-cart-sauce-labs-backpack"]' + private readonly sortDropdown: string = '[data-test="product-sort-container"]' + private readonly priceLabels:string = '.inventory_item_price' + private readonly inventoryItemName: string = '.inventory_item_name' + constructor(page: Page) { this.page = page; @@ -11,4 +15,40 @@ export class Product { public async addBackPackToCart() { await this.page.locator(this.addToCart).click() } + + public async sortBy(option: string) { + await this.page.locator(this.sortDropdown).selectOption({ label: option }) + } + + private async validatePriceSorting(option: string) { + const prices = await this.page.locator(this.priceLabels).allTextContents(); + const numPrices = prices.map(p => Number(p.replace('$', ''))); + + const sortedPrices = [...numPrices].sort((a, b) => + option.includes('low to high') ? a - b : b - a + ); + + expect(numPrices).toEqual(sortedPrices); + } + + private async validateNameSorting(option: string) { + const names = await this.page.locator(this.inventoryItemName).allTextContents(); + const sortedNames = [...names].sort((a, b) => + option.includes("A to Z") + ? a.localeCompare(b) + : b.localeCompare(a) + ); + + expect(names).toEqual(sortedNames); + } + + public async validateSorting(option: string) { + if (option.includes("low to high") || option.includes("high to low")) { + await this.validatePriceSorting(option); + } else { + await this.validateNameSorting(option); + } + +} + } \ No newline at end of file diff --git a/steps/login.steps.ts b/steps/login.steps.ts index c2aa0d8..57b6c6f 100644 --- a/steps/login.steps.ts +++ b/steps/login.steps.ts @@ -8,4 +8,12 @@ Then('I should see the title {string}', async (expectedTitle) => { Then('I will login as {string}', async (userName) => { await new Login(getPage()).loginAsUser(userName); -}); \ No newline at end of file +}); + +Then('I should see the error message {string}', async (expectedMessage) => { + await new Login(getPage()).validateErrorMessage(expectedMessage); +}); + +Then('I will login with username {string} and password {string}', async (username, password) => { + await new Login(getPage()).login(username, password); +}); diff --git a/steps/product.steps.ts b/steps/product.steps.ts index bb52fb9..e60c7c6 100644 --- a/steps/product.steps.ts +++ b/steps/product.steps.ts @@ -1,7 +1,19 @@ -import { Then } from '@cucumber/cucumber'; +import { Then, When } 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(); -}); \ No newline at end of file +}); + + +When('I sort products by {string}', async (sortOption) => { + await new Product(getPage()).sortBy(sortOption); +}); + + +Then('all products should be sorted by {string}', async (sortOption) => { + await new Product(getPage()).validateSorting(sortOption); +}); + + diff --git a/steps/purchase.steps.ts b/steps/purchase.steps.ts new file mode 100644 index 0000000..edc4736 --- /dev/null +++ b/steps/purchase.steps.ts @@ -0,0 +1,37 @@ +import { Then } from '@cucumber/cucumber'; +import { getPage } from '../playwrightUtilities'; +import { Cart } from '../pages/cart.page'; +import { Checkout } from '../pages/checkout.page'; +import { CheckoutOverview } from '../pages/checkoutOverview.page'; +import { CheckoutComplete } from '../pages/checkoutComplete.page'; + + +Then('I open the cart', async () => { + await new Cart(getPage()).openCart(); +}); + +Then('the cart should contain {string} with price {string} and quantity {string}', + async (name, price, qty) => { + await new Cart(getPage()).validateCartItem(name, price, qty); +}); + +Then('I proceed to checkout', async () => { + await new Cart(getPage()).goToCheckout(); +}); + +Then('I fill in checkout information', async () => { + await new Checkout(getPage()).fillInformation("John", "Green", "10101"); +}); + +Then('I continue checkout', async () => { + await new Checkout(getPage()).continue(); +}); + +Then('I finish the purchase', async () => { + await new CheckoutOverview(getPage()).finish(); +}); + +Then('I should see the success message', async () => { + await new CheckoutComplete(getPage()).validateSuccessMessage(); +}); +