diff --git a/features/login.feature b/features/login.feature index fb9f1fa5..0519b120 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 the login error message "Epic sadface: Sorry, this user has been locked out." diff --git a/features/product.feature b/features/product.feature index 8a7ceab9..0f140cf6 100644 --- a/features/product.feature +++ b/features/product.feature @@ -3,11 +3,17 @@ 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 + Scenario Outline: Validate product sort by price + Then I will login as 'standard_user' + Then I will sort the products by "" + Then all 6 product prices should be sorted "" + Examples: - # TODO: extend the datatable to paramterize this test - | sort | \ No newline at end of file + | sort | direction | + | Price (low to high) | ascending | + | Price (high to low) | descending | + + Scenario: Validate cart reflects the selected item + Then I will login as 'standard_user' + Then I will add the backpack to the cart + Then the cart item count should be 1 diff --git a/features/purchase.feature b/features/purchase.feature index 28634789..03a8d7d7 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 will open the cart + Then I will checkout + Then I will provide checkout information with first name "Ada", last name "Lovelace", and postal code "10001" + Then I will continue checkout + Then I will finish checkout + Then I should see the order confirmation "Thank you for your order!" diff --git a/pages/login.page.ts b/pages/login.page.ts index 5a01614b..ae7e5477 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,11 @@ export class Login { await this.page.locator(this.passwordField).fill(this.password) await this.page.locator(this.loginButton).click() } -} \ No newline at end of file + + public async validateErrorMessage(expectedMessage: string) { + const actualMessage = await this.page.locator(this.errorMessage).textContent() + if (actualMessage?.trim() !== expectedMessage) { + throw new Error(`Expected login error message to be "${expectedMessage}" but found "${actualMessage?.trim() ?? ''}"`) + } + } +} diff --git a/pages/product.page.ts b/pages/product.page.ts index 14bedb1b..7e577f99 100644 --- a/pages/product.page.ts +++ b/pages/product.page.ts @@ -3,6 +3,17 @@ 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 cartLink: string = '.shopping_cart_link' + private readonly cartBadge: string = '.shopping_cart_badge' + 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 orderConfirmation: string = '[data-test="complete-header"]' + private readonly sortSelect: string = '[data-test="product-sort-container"]' + private readonly productPrice: string = '.inventory_item_price' constructor(page: Page) { this.page = page; @@ -11,4 +22,76 @@ export class Product { public async addBackPackToCart() { await this.page.locator(this.addToCart).click() } -} \ No newline at end of file + + public async openCart() { + await this.page.locator(this.cartLink).click() + } + + public async checkout() { + await this.page.locator(this.checkoutButton).click() + } + + public async fillCheckoutInformation(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 finishCheckout() { + await this.page.locator(this.finishButton).click() + } + + public async validateOrderConfirmation(expectedText: string) { + const actualText = await this.page.locator(this.orderConfirmation).textContent() + if (actualText?.trim() !== expectedText) { + throw new Error(`Expected order confirmation to be "${expectedText}" but found "${actualText?.trim() ?? ''}"`) + } + } + + public async sortBy(sortLabel: string) { + const sortValues: Record = { + 'Price (low to high)': 'lohi', + 'Price (high to low)': 'hilo', + } + const sortValue = sortValues[sortLabel] + if (!sortValue) { + throw new Error(`Unsupported product sort option: ${sortLabel}`) + } + await this.page.locator(this.sortSelect).selectOption(sortValue) + } + + public async validatePricesAreSorted(direction: string, expectedCount: number) { + const prices = await this.page.locator(this.productPrice).allTextContents() + const numericPrices = prices.map((price) => Number.parseFloat(price.replace('$', ''))) + + if (numericPrices.length !== expectedCount) { + throw new Error(`Expected ${expectedCount} product prices but found ${numericPrices.length}`) + } + + const isAscending = direction === 'ascending' + const isDescending = direction === 'descending' + if (!isAscending && !isDescending) { + throw new Error(`Unsupported sort direction: ${direction}`) + } + + for (let index = 1; index < numericPrices.length; index += 1) { + const previous = numericPrices[index - 1] + const current = numericPrices[index] + const isCorrectOrder = isAscending ? previous <= current : previous >= current + if (!isCorrectOrder) { + throw new Error(`Expected prices to be sorted ${direction}, but found ${numericPrices.join(', ')}`) + } + } + } + + public async validateCartItemCount(expectedCount: number) { + const actualCount = Number.parseInt((await this.page.locator(this.cartBadge).textContent())?.trim() ?? '0', 10) + if (actualCount !== expectedCount) { + throw new Error(`Expected cart item count to be ${expectedCount} but found ${actualCount}`) + } + } +} diff --git a/steps/login.steps.ts b/steps/login.steps.ts index c2aa0d80..e1e99ba8 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); -}); \ No newline at end of file +}); + +Then('I should see the login error message {string}', async (expectedMessage) => { + await new Login(getPage()).validateErrorMessage(expectedMessage); +}); diff --git a/steps/product.steps.ts b/steps/product.steps.ts index bb52fb98..98290c04 100644 --- a/steps/product.steps.ts +++ b/steps/product.steps.ts @@ -4,4 +4,40 @@ 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 +}); + +Then('I will open the cart', async () => { + await new Product(getPage()).openCart(); +}); + +Then('I will checkout', async () => { + await new Product(getPage()).checkout(); +}); + +Then('I will provide checkout information with first name {string}, last name {string}, and postal code {string}', async (firstName, lastName, postalCode) => { + await new Product(getPage()).fillCheckoutInformation(firstName, lastName, postalCode); +}); + +Then('I will continue checkout', async () => { + await new Product(getPage()).continueCheckout(); +}); + +Then('I will finish checkout', async () => { + await new Product(getPage()).finishCheckout(); +}); + +Then('I should see the order confirmation {string}', async (expectedText) => { + await new Product(getPage()).validateOrderConfirmation(expectedText); +}); + +Then('I will sort the products by {string}', async (sortLabel) => { + await new Product(getPage()).sortBy(sortLabel); +}); + +Then('all {int} product prices should be sorted {string}', async (expectedCount, direction) => { + await new Product(getPage()).validatePricesAreSorted(direction, expectedCount); +}); + +Then('the cart item count should be {int}', async (expectedCount) => { + await new Product(getPage()).validateCartItemCount(expectedCount); +});