diff --git a/features/login.feature b/features/login.feature index fb9f1fa5..f39bd08f 100644 --- a/features/login.feature +++ b/features/login.feature @@ -1,12 +1,23 @@ -Feature: Login Feature +Feature: Login Background: 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" + # Verifies the correct site loaded. + Scenario: Login page displays the correct title + Then the page title should be "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 + # Tests both ways a login can fail using a single outline. + Scenario Outline: Login failure displays an appropriate error + When I login as '' + Then I should see an error containing '' + + Examples: + | username | error | + | locked_out_user | Sorry, this user has been locked out | + | invalid_user | Username and password do not match | + + # Confirms the user was actually redirected after a successful login. + Scenario: Valid credentials navigate to the inventory page + When I login as 'standard_user' + Then I should be on the "inventory" page diff --git a/features/product.feature b/features/product.feature index 8a7ceab9..ffbd625f 100644 --- a/features/product.feature +++ b/features/product.feature @@ -1,13 +1,25 @@ -Feature: Product Feature +Feature: Product + # All product tests require a logged-in user. Background: Given I open the "https://www.saucedemo.com/" page + When I login as 'standard_user' - # 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 + # Makes sure all products loaded on the page. + Scenario: Inventory page displays all products + Then the inventory should show 6 products + + # Tests both sort options since either one can break on its own. + Scenario Outline: Products can be sorted by price + When I sort products by price "" + Then all products should be sorted by price "" + + Examples: + | sort | + | low to high | + | high to low | + + # Checks the cart icon updates when an item is added. + Scenario: Adding a product updates the cart badge count + When I add the backpack to the cart + Then the cart badge should show "1" diff --git a/features/purchase.feature b/features/purchase.feature index 28634789..e68b1ac9 100644 --- a/features/purchase.feature +++ b/features/purchase.feature @@ -1,14 +1,16 @@ -Feature: Purchase Feature +Feature: Purchase + # All purchase tests require a logged-in user. Background: Given I open the "https://www.saucedemo.com/" page + When I login as 'standard_user' - 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 + # Walks through the full purchase flow from cart to confirmation. + Scenario: A user can complete a purchase successfully + When I add the backpack to the cart + And I go to the cart + And I proceed to checkout + And I enter checkout details "John" "Doe" "12345" + And I continue to the order summary + And I place the order + Then I should see the order confirmation "Thank you for your order!" 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..9c27e639 100644 --- a/pages/login.page.ts +++ b/pages/login.page.ts @@ -1,26 +1,34 @@ -import { Page } from "@playwright/test" +import { Page, expect } 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 passwordField: string = 'input[id="password"]' private readonly loginButton: string = 'input[id="login-button"]' + private readonly errorBanner: string = '[data-test="error"]' constructor(page: Page) { this.page = page; } 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}`); - } + await expect(this.page).toHaveTitle(expectedTitle); + } + + // toContainText checks for a partial match so minor wording changes don't break the test. + public async validateErrorContains(fragment: string) { + await expect(this.page.locator(this.errorBanner)).toContainText(fragment); + } + + // Checks the URL to confirm the page actually navigated, not just that something appeared. + public async validateCurrentUrl(urlFragment: string) { + await expect(this.page).toHaveURL(new RegExp(urlFragment)); } 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(); } -} \ No newline at end of file +} diff --git a/pages/product.page.ts b/pages/product.page.ts index 14bedb1b..2e03c108 100644 --- a/pages/product.page.ts +++ b/pages/product.page.ts @@ -1,14 +1,51 @@ -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 = '.product_sort_container' + private readonly inventoryItems: string = '.inventory_item' + private readonly itemPrices: string = '.inventory_item_price' + private readonly cartBadge: string = '.shopping_cart_badge' 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 validateProductCount(expectedCount: number) { + await expect(this.page.locator(this.inventoryItems)).toHaveCount(expectedCount); + } + + public async sortByPrice(sort: string) { + const value = sort === 'low to high' ? 'lohi' : 'hilo'; + await this.page.locator(this.sortDropdown).selectOption(value); + } + + public async validatePriceSortOrder(sort: string) { + const priceElements = await this.page.locator(this.itemPrices).all(); + const prices: number[] = []; + + for (const el of priceElements) { + const text = await el.textContent(); + prices.push(parseFloat(text!.replace('$', ''))); + } + + // Compare each price to the next one to verify the order is correct. + for (let i = 0; i < prices.length - 1; i++) { + if (sort === 'low to high') { + expect(prices[i]).toBeLessThanOrEqual(prices[i + 1]); + } else { + expect(prices[i]).toBeGreaterThanOrEqual(prices[i + 1]); + } + } + } + + // Checks the cart icon shows the right item count. + public async validateCartCount(expectedCount: string) { + await expect(this.page.locator(this.cartBadge)).toHaveText(expectedCount); + } +} diff --git a/pages/purchase.page.ts b/pages/purchase.page.ts new file mode 100644 index 00000000..2852f5ab --- /dev/null +++ b/pages/purchase.page.ts @@ -0,0 +1,44 @@ +import { Page, expect } from "@playwright/test" + +export class Purchase { + private readonly page: Page + private readonly cartIcon: string = '.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 zipField: string = '[data-test="postalCode"]' + private readonly continueButton: string = '[data-test="continue"]' + private readonly finishButton: string = '[data-test="finish"]' + private readonly successHeader: string = '.complete-header' + + constructor(page: Page) { + this.page = page; + } + + public async goToCart() { + await this.page.locator(this.cartIcon).click(); + } + + public async goToCheckout() { + await this.page.locator(this.checkoutButton).click(); + } + + // Fills each field separately so it's clear which one failed. + public async fillCheckoutForm(firstName: string, lastName: string, zip: string) { + await this.page.locator(this.firstNameField).fill(firstName); + await this.page.locator(this.lastNameField).fill(lastName); + await this.page.locator(this.zipField).fill(zip); + } + + public async continueCheckout() { + await this.page.locator(this.continueButton).click(); + } + + public async finishCheckout() { + await this.page.locator(this.finishButton).click(); + } + + public async validateSuccessMessage(expectedMessage: string) { + await expect(this.page.locator(this.successHeader)).toHaveText(expectedMessage); + } +} diff --git a/steps/login.steps.ts b/steps/login.steps.ts index c2aa0d80..210849eb 100644 --- a/steps/login.steps.ts +++ b/steps/login.steps.ts @@ -1,11 +1,19 @@ -import { Then } from '@cucumber/cucumber'; +import { When, 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('the page title should be {string}', async (expectedTitle: string) => { + 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 +When('I login as {string}', async (userName: string) => { + await new Login(getPage()).loginAsUser(userName); +}); + +Then('I should see an error containing {string}', async (errorFragment: string) => { + await new Login(getPage()).validateErrorContains(errorFragment); +}); + +Then('I should be on the {string} page', async (urlFragment: string) => { + await new Login(getPage()).validateCurrentUrl(urlFragment); +}); diff --git a/steps/product.steps.ts b/steps/product.steps.ts index bb52fb98..79a334ea 100644 --- a/steps/product.steps.ts +++ b/steps/product.steps.ts @@ -1,7 +1,24 @@ -import { Then } from '@cucumber/cucumber'; +import { When, 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(); -}); \ No newline at end of file +// {int} automatically converts the value to a number. +Then('the inventory should show {int} products', async (expectedCount: number) => { + await new Product(getPage()).validateProductCount(expectedCount); +}); + +When('I add the backpack to the cart', async () => { + await new Product(getPage()).addBackPackToCart(); +}); + +When('I sort products by price {string}', async (sort: string) => { + await new Product(getPage()).sortByPrice(sort); +}); + +Then('all products should be sorted by price {string}', async (sort: string) => { + await new Product(getPage()).validatePriceSortOrder(sort); +}); + +Then('the cart badge should show {string}', async (count: string) => { + await new Product(getPage()).validateCartCount(count); +}); diff --git a/steps/purchase.steps.ts b/steps/purchase.steps.ts new file mode 100644 index 00000000..b99edb93 --- /dev/null +++ b/steps/purchase.steps.ts @@ -0,0 +1,27 @@ +import { When, Then } from '@cucumber/cucumber'; +import { getPage } from '../playwrightUtilities'; +import { Purchase } from '../pages/purchase.page'; + +When('I go to the cart', async () => { + await new Purchase(getPage()).goToCart(); +}); + +When('I proceed to checkout', async () => { + await new Purchase(getPage()).goToCheckout(); +}); + +When('I enter checkout details {string} {string} {string}', async (firstName: string, lastName: string, zip: string) => { + await new Purchase(getPage()).fillCheckoutForm(firstName, lastName, zip); +}); + +When('I continue to the order summary', async () => { + await new Purchase(getPage()).continueCheckout(); +}); + +When('I place the order', async () => { + await new Purchase(getPage()).finishCheckout(); +}); + +Then('I should see the order confirmation {string}', async (expectedMessage: string) => { + await new Purchase(getPage()).validateSuccessMessage(expectedMessage); +});