diff --git a/features/login.feature b/features/login.feature index fb9f1fa5..4edbc0a2 100644 --- a/features/login.feature +++ b/features/login.feature @@ -5,8 +5,9 @@ Feature: Login Feature 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 error message container + Then The error message container should contain the text "Epic sadface: Sorry, this user has been locked out." \ No newline at end of file diff --git a/features/product.feature b/features/product.feature index 8a7ceab9..1028e69b 100644 --- a/features/product.feature +++ b/features/product.feature @@ -2,12 +2,22 @@ Feature: Product Feature Background: Given I open the "https://www.saucedemo.com/" page + Scenario Outline: Validate product sort by price + Then I will login as 'standard_user' + And I will sort items by '' + Then The items should be sorted by price '' - # 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 + Examples: + | price sort | + | Price (low to high) | + | Price (high to low) | + + Scenario Outline: Validate product sort by name + Then I will login as 'standard_user' + And I will sort items by '' + Then The items should be sorted by name '' + + Examples: + | name sort | + | Name (A to Z) | + | Name (Z to A) | diff --git a/features/purchase.feature b/features/purchase.feature index 28634789..fd54a527 100644 --- a/features/purchase.feature +++ b/features/purchase.feature @@ -3,12 +3,41 @@ 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 + 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) + Then I will click the cart button # TODO: Select Checkout + Then I will click the checkout button # TODO: Fill in the First Name, Last Name, and Zip/Postal Code + Then I will fill the checkout form as "Test" "User", "12345" # TODO: Select Continue + Then I will click the continue button # TODO: Select Finish - # TODO: Validate the text 'Thank you for your order!' \ No newline at end of file + Then I will click the finish button + # TODO: Validate the text 'Thank you for your order!' + Then The checkout success container should be visible + And The checkout success header should read 'Thank you for your order!' + And The checkout success text should read 'Your order has been dispatched, and will arrive just as fast as the pony can get there!' + + Scenario: Add item to cart + Then I will login as 'standard_user' + Then I will add "Sauce Labs Backpack" to the cart + Then The shopping cart badge count should be 1 + And I will add "Sauce Labs Fleece Jacket" to the cart + Then The shopping cart badge count should be 2 + Then I will click the cart button + Then The cart should contain "Sauce Labs Backpack" + And The cart should contain "Sauce Labs Fleece Jacket" + + Scenario: Remove item from cart + Then I will login as 'standard_user' + Then I will add "Sauce Labs Backpack" to the cart + And I will add "Sauce Labs Fleece Jacket" to the cart + Then The shopping cart badge count should be 2 + Then I will remove "Sauce Labs Backpack" from the cart + Then The shopping cart badge count should be 1 + Then I will click the cart button + Then The cart should contain "Sauce Labs Fleece Jacket" + And The cart should not contain "Sauce Labs Backpack" diff --git a/package-lock.json b/package-lock.json index b90d7c6c..b5c47aa5 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/login.page.ts b/pages/login.page.ts index 5a01614b..5209ef2e 100644 --- a/pages/login.page.ts +++ b/pages/login.page.ts @@ -1,4 +1,4 @@ -import { Page } from "@playwright/test" +import { expect, Page } 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 errorContainer: string = 'div.error-message-container.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 validateErrorExists() { + await expect(this.page.locator(this.errorContainer)).toBeVisible(); + } + + public async validateErrorMessage(expectedMessage: string) { + await expect(this.page.locator(this.errorContainer)).toContainText(expectedMessage); + } } \ No newline at end of file diff --git a/pages/product.page.ts b/pages/product.page.ts index 14bedb1b..4543810a 100644 --- a/pages/product.page.ts +++ b/pages/product.page.ts @@ -1,14 +1,70 @@ -import { Page } from "@playwright/test" +import { expect, 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 sortDropdown: string = + '[data-test="product-sort-container"]'; + private readonly itemContainer: string = '[data-test="inventory-list"] > div'; + private readonly itemPrice: string = `[data-test="inventory-item-price"]`; + private readonly itemName: string = `[data-test^="item-"][data-test$="-title-link"]`; - constructor(page: Page) { - this.page = page; + constructor(page: Page) { + this.page = page; + } + + public async selectSortOption(sort: string) { + await this.page.locator(this.sortDropdown).selectOption({ label: sort }); + } + + public async assertPricesSorted(sort: string) { + // Get all prices + const prices: number[] = []; + for ( + let i = 0; + i < + (await this.page + .locator(`${this.itemContainer} ${this.itemPrice}`) + .count()); + i++ + ) { + const priceFormatted = parseFloat( + ( + await this.page + .locator(`${this.itemContainer} ${this.itemPrice}`) + .nth(i) + .innerText() + ).replace("$", ""), + ); + prices.push(priceFormatted); } + console.log(`prices:`, prices); + const pricesSorted = [...prices].sort((a, b) => + sort === "Price (low to high)" ? a - b : b - a, + ); + expect(prices).toEqual(pricesSorted); + } - public async addBackPackToCart() { - await this.page.locator(this.addToCart).click() + public async assertNamesSorted(sort: string) { + // Get all prices + const names: string[] = []; + for ( + let i = 0; + i < + (await this.page + .locator(`${this.itemContainer} ${this.itemName}`) + .count()); + i++ + ) { + const name = await this.page + .locator(`${this.itemContainer} ${this.itemName}`) + .nth(i) + .innerText(); + names.push(name); } -} \ No newline at end of file + console.log(`names:`, names); + const namesSorted = [...names].sort((a, b) => + sort === "Name (A to Z)" ? a.localeCompare(b) : b.localeCompare(a), + ); + expect(names).toEqual(namesSorted); + } +} diff --git a/pages/purchase.page.ts b/pages/purchase.page.ts new file mode 100644 index 00000000..24e94b12 --- /dev/null +++ b/pages/purchase.page.ts @@ -0,0 +1,114 @@ +import { expect, Page } from "@playwright/test"; + +export class Purchase { + private readonly page: Page; + private readonly addToCart: string = + 'button[id="add-to-cart-sauce-labs-backpack"]'; + private readonly cartButton: string = '[data-test="shopping-cart-link"]'; + private readonly cartButtonBadge: string = + '[data-test="shopping-cart-badge"]'; + private readonly checkoutButton: string = '[data-test="checkout"]'; + private readonly checkoutForm = { + firstNameInput: '[data-test="firstName"]', + lastNameInput: '[data-test="lastName"]', + zipCodeInput: '[data-test="postalCode"]', + }; + private readonly continueButton: string = '[data-test="continue"]'; + private readonly finishButton: string = '[data-test="finish"]'; + private readonly checkoutCompleteContainer: string = + '[data-test="checkout-complete-container"]'; + private readonly checkoutCompleteHeader: string = + '[data-test="complete-header"]'; + private readonly checkoutCompleteText: string = '[data-test="complete-text"]'; + private readonly itemContainer: string = '[data-test="inventory-list"] > div'; + private readonly addToCartGeneric: string = 'button[id^="add-to-cart-"]'; + private readonly removeFromCartGeneric: string = 'button[id^="remove-"]'; + private readonly cartItem: string = + '[data-test="cart-list"] [data-test="inventory-item"]'; + + constructor(page: Page) { + this.page = page; + } + + public async addBackPackToCart() { + await this.page.locator(this.addToCart).click(); + } + + public async clickCartButton() { + await this.page.locator(this.cartButton).click(); + } + + public async clickCheckoutButton() { + await this.page.locator(this.checkoutButton).click(); + } + + public async fillCheckoutForm( + firstName: string, + lastName: string, + zipCode: string, + ) { + await this.page.locator(this.checkoutForm.firstNameInput).fill(firstName); + await this.page.locator(this.checkoutForm.lastNameInput).fill(lastName); + await this.page.locator(this.checkoutForm.zipCodeInput).fill(zipCode); + } + + public async clickContinueButton() { + await this.page.locator(this.continueButton).click(); + } + + public async clickFinishButton() { + await this.page.locator(this.finishButton).click(); + } + + public async assertSuccessContainerVisible() { + await expect( + this.page.locator(this.checkoutCompleteContainer), + ).toBeVisible(); + } + + public async assertSuccessHeaderCorrect(expectedText: string) { + await expect(this.page.locator(this.checkoutCompleteHeader)).toHaveText( + expectedText, + ); + } + + public async assertSuccessTextCorrect(expectedText: string) { + await expect(this.page.locator(this.checkoutCompleteText)).toHaveText( + expectedText, + ); + } + + public async addItemToShoppingCart(itemName: string) { + await this.page + .locator(this.itemContainer) + .filter({ hasText: itemName }) + .locator(this.addToCartGeneric) + .click(); + } + + public async assertShoppingCartBadgeCount(expectedCount: number) { + await expect( + this.page.locator(`${this.cartButton} ${this.cartButtonBadge}`), + ).toHaveText(`${expectedCount}`); + } + + public async assertItemIsVisibleInCart(itemName: string) { + await expect( + this.page.locator(this.cartItem).filter({ hasText: itemName }), + ).toBeVisible(); + } + + public async removeItemFromCart(itemName: string) { + await this.page + .locator(this.itemContainer) + .filter({ hasText: itemName }) + .locator(this.removeFromCartGeneric) + .click(); + } + + public async assertItemIsNotVisibleInCart(itemName: string) { + await expect( + this.page.locator(this.cartItem).filter({ hasText: itemName }), + ).toHaveCount(0); + } +} diff --git a/steps/login.steps.ts b/steps/login.steps.ts index c2aa0d80..baf5347a 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); +}); + +Then('I should see the error message container', async () => { + await new Login(getPage()).validateErrorExists(); +}); + +Then('The error message container should contain the text {string}', async (expectedMessage) => { + 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..0b5c28b7 100644 --- a/steps/product.steps.ts +++ b/steps/product.steps.ts @@ -1,7 +1,16 @@ -import { Then } from '@cucumber/cucumber'; -import { getPage } from '../playwrightUtilities'; -import { Product } from '../pages/product.page'; +import { Product } from './../pages/product.page'; +import { Then } from "@cucumber/cucumber"; +import { getPage } from "../playwrightUtilities"; + +Then("I will sort items by {string}", async (sortOption: string) => { + await new Product(getPage()).selectSortOption(sortOption); +}) + +Then("The items should be sorted by price {string}", async (sortOption: string) => { + await new Product(getPage()).assertPricesSorted(sortOption); +}) + +Then("The items should be sorted by name {string}", async (sortOption: string) => { + await new Product(getPage()).assertNamesSorted(sortOption); +}) -Then('I will add the backpack to the cart', async () => { - await new Product(getPage()).addBackPackToCart(); -}); \ No newline at end of file diff --git a/steps/purchase.steps.ts b/steps/purchase.steps.ts new file mode 100644 index 00000000..e6834b06 --- /dev/null +++ b/steps/purchase.steps.ts @@ -0,0 +1,62 @@ +import { Then } from "@cucumber/cucumber"; +import { getPage } from "../playwrightUtilities"; +import { Purchase } from "../pages/purchase.page"; + +Then("I will add the backpack to the cart", async () => { + await new Purchase(getPage()).addBackPackToCart(); +}); + +Then("I will click the cart button", async () => { + await new Purchase(getPage()).clickCartButton(); +}); + +Then("I will click the checkout button", async () => { + await new Purchase(getPage()).clickCheckoutButton(); +}); + +Then( + "I will fill the checkout form as {string} {string}, {string}", + async (firstName, lastName, zipCode) => { + await new Purchase(getPage()).fillCheckoutForm(firstName, lastName, `${zipCode}`); + }, +); + +Then("I will click the continue button", async () => { + await new Purchase(getPage()).clickContinueButton(); +}); + +Then("I will click the finish button", async () => { + await new Purchase(getPage()).clickFinishButton(); +}); + +Then("The checkout success container should be visible", async () => { + await new Purchase(getPage()).assertSuccessContainerVisible(); +}); + +Then("The checkout success header should read {string}", async (expectedText) => { + await new Purchase(getPage()).assertSuccessHeaderCorrect(expectedText); +}); + +Then("The checkout success text should read {string}", async (expectedText) => { + await new Purchase(getPage()).assertSuccessTextCorrect(expectedText); +}); + +Then("I will add {string} to the cart", async (itemName) => { + await new Purchase(getPage()).addItemToShoppingCart(itemName); +}); + +Then("The shopping cart badge count should be {int}", async (count) => { + await new Purchase(getPage()).assertShoppingCartBadgeCount(count); +}); + +Then("The cart should contain {string}", async (itemName) => { + await new Purchase(getPage()).assertItemIsVisibleInCart(itemName); +}); + +Then("I will remove {string} from the cart", async (itemName) => { + await new Purchase(getPage()).removeItemFromCart(itemName); +}); + +Then("The cart should not contain {string}", async (itemName) => { + await new Purchase(getPage()).assertItemIsNotVisibleInCart(itemName); +});