From e843bf16544c04f4f93f6d6f734850381c736e36 Mon Sep 17 00:00:00 2001 From: Jonathan Date: Thu, 16 Jul 2026 15:27:40 -0400 Subject: [PATCH] Complete interview tasks: fix login, add purchase flow, product sort outline Co-authored-by: Cursor --- .vscode/settings.json | 13 ++++++++++ features/login.feature | 5 ++-- features/product.feature | 15 +++++------ features/purchase.feature | 18 +++++++------- package-lock.json | 2 +- pages/login.page.ts | 12 +++++++++ pages/product.page.ts | 32 ++++++++++++++++++++++++ pages/purchase.page.ts | 52 +++++++++++++++++++++++++++++++++++++++ steps/common.steps.ts | 2 +- steps/login.steps.ts | 10 +++++--- steps/product.steps.ts | 9 +++++++ steps/purchase.steps.ts | 27 ++++++++++++++++++++ 12 files changed, 173 insertions(+), 24 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 pages/purchase.page.ts create mode 100644 steps/purchase.steps.ts diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..97608524 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,13 @@ +{ + "cucumberautocomplete.steps": [ + "steps/**/*.ts" + ], + "cucumberautocomplete.syncfeatures": "features/**/*.feature", + "cucumberautocomplete.strictGherkinCompletion": true, + "cucumberautocomplete.smartSnippets": true, + "editor.quickSuggestions": { + "comments": false, + "strings": true, + "other": true + } +} diff --git a/features/login.feature b/features/login.feature index fb9f1fa5..6f253a1b 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 + And I should see the error message "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..e5b39ddc 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 + Scenario Outline: Validate product sort by price + Then I will login as 'standard_user' + And I will sort products by "" + Then all products should be sorted by price "" + Examples: - # TODO: extend the datatable to paramterize this test - | sort | \ No newline at end of file + | sort | order | + | Price (low to high) | asc | + | Price (high to low) | desc | \ No newline at end of file diff --git a/features/purchase.feature b/features/purchase.feature index 28634789..8f0b8274 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' + And I will add the backpack to the cart + And I go to the cart + And I click checkout + And I fill in my details with first name "John" last name "Doe" and zip "12345" + And I click continue + And I click finish + Then I should see the confirmation message "Thank you for your order!" \ No newline at end of file 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..313880a9 100644 --- a/pages/login.page.ts +++ b/pages/login.page.ts @@ -6,6 +6,9 @@ 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 lockOutMessage: string = '[data-test="error"]' + private readonly lockedOutMessage: string = 'This user has been locked out.' + constructor(page: Page) { this.page = page; @@ -23,4 +26,13 @@ export class Login { await this.page.locator(this.passwordField).fill(this.password) await this.page.locator(this.loginButton).click() } + + // Waits for the error message to appear, then checks it matches the expected text + public async validateErrorMessage(expectedMessage: string) { + await this.page.locator(this.lockOutMessage).waitFor({ state: 'visible' }) + const actualMessage = await this.page.locator(this.lockOutMessage).innerText() + if (actualMessage !== expectedMessage) { + throw new Error(`Expected error message "${expectedMessage}" but found "${actualMessage}"`) + } + } } \ No newline at end of file diff --git a/pages/product.page.ts b/pages/product.page.ts index 14bedb1b..b6ff69db 100644 --- a/pages/product.page.ts +++ b/pages/product.page.ts @@ -3,12 +3,44 @@ 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 sortDropdown: string = 'select[data-test="product-sort-container"]' + private readonly itemPrice: string = '.inventory_item_price' constructor(page: Page) { this.page = page; } + // Clicks the "Add to cart" button for the Sauce Labs Backpack item public async addBackPackToCart() { await this.page.locator(this.addToCart).click() } + + // Selects an option from the sort dropdown (e.g. "Price (low to high)") + public async sortBy(sortLabel: string) { + await this.page.locator(this.sortDropdown).selectOption({ label: sortLabel }) + } + + // Reads every product price on the page and returns them as numbers (e.g. [9.99, 15.99]) + public async getPrices(): Promise { + const priceTexts = await this.page.locator(this.itemPrice).allTextContents() + return priceTexts.map(text => parseFloat(text.replace('$', ''))) + } + + // Checks that the product prices are in the expected order ('asc' or 'desc'), throws if not + public async validateSortedByPrice(order: string) { + const prices = await this.getPrices() + + for (let i = 1; i < prices.length; i++) { + const previousPrice = prices[i - 1] + const currentPrice = prices[i] + + if (order === 'asc' && currentPrice < previousPrice) { + throw new Error(`Expected prices to be sorted low to high: [${prices.join(', ')}]`) + } + + if (order === 'desc' && currentPrice > previousPrice) { + throw new Error(`Expected prices to be sorted high to low: [${prices.join(', ')}]`) + } + } + } } \ No newline at end of file diff --git a/pages/purchase.page.ts b/pages/purchase.page.ts new file mode 100644 index 00000000..20dbedab --- /dev/null +++ b/pages/purchase.page.ts @@ -0,0 +1,52 @@ +import { Page } 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 confirmationMessage: string = '[data-test="complete-header"]' + + constructor(page: Page) { + this.page = page; + } + + // Clicks the cart icon in the top-right to open the cart + public async goToCart() { + await this.page.locator(this.cartIcon).click() + } + + // Clicks the Checkout button inside the cart + public async clickCheckout() { + await this.page.locator(this.checkoutButton).click() + } + + // Fills in the customer information form fields + public async fillInDetails(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) + } + + // Clicks Continue to proceed to the order summary + public async clickContinue() { + await this.page.locator(this.continueButton).click() + } + + // Clicks Finish to complete the purchase + public async clickFinish() { + await this.page.locator(this.finishButton).click() + } + + // Reads the confirmation message and checks it matches the expected text + public async validateConfirmationMessage(expectedMessage: string) { + const actualMessage = await this.page.locator(this.confirmationMessage).innerText() + if (actualMessage !== expectedMessage) { + throw new Error(`Expected "${expectedMessage}" but found "${actualMessage}"`) + } + } +} diff --git a/steps/common.steps.ts b/steps/common.steps.ts index c4bee79b..f642bf49 100644 --- a/steps/common.steps.ts +++ b/steps/common.steps.ts @@ -1,6 +1,6 @@ import { Given } from "@cucumber/cucumber"; import { getPage } from "../playwrightUtilities"; -Given('I open the {string} page', async (url) => { +Given('I open the {string} page', async (url: string) => { await getPage().goto(url); }); \ No newline at end of file diff --git a/steps/login.steps.ts b/steps/login.steps.ts index c2aa0d80..df527c4e 100644 --- a/steps/login.steps.ts +++ b/steps/login.steps.ts @@ -2,10 +2,14 @@ import { Then } from '@cucumber/cucumber'; import { getPage } from '../playwrightUtilities'; import { Login } from '../pages/login.page'; -Then('I should see the title {string}', async (expectedTitle) => { +Then('I should see the title {string}', async (expectedTitle: string) => { await new Login(getPage()).validateTitle(expectedTitle); }); -Then('I will login as {string}', async (userName) => { +Then('I will login as {string}', async (userName: string) => { await new Login(getPage()).loginAsUser(userName); -}); \ No newline at end of file +}); + +Then('I should see the error message {string}', async (expectedMessage: string) => { + await new Login(getPage()).validateErrorMessage(expectedMessage); +}); diff --git a/steps/product.steps.ts b/steps/product.steps.ts index bb52fb98..f6a23405 100644 --- a/steps/product.steps.ts +++ b/steps/product.steps.ts @@ -4,4 +4,13 @@ import { Product } from '../pages/product.page'; Then('I will add the backpack to the cart', async () => { await new Product(getPage()).addBackPackToCart(); +}); + +Then('I will sort products by {string}', async (sortLabel: string) => { + await new Product(getPage()).sortBy(sortLabel); +}); + + +Then('all products should be sorted by price {string}', async (order: string) => { + await new Product(getPage()).validateSortedByPrice(order); }); \ No newline at end of file diff --git a/steps/purchase.steps.ts b/steps/purchase.steps.ts new file mode 100644 index 00000000..8e015036 --- /dev/null +++ b/steps/purchase.steps.ts @@ -0,0 +1,27 @@ +import { Then } from '@cucumber/cucumber'; +import { getPage } from '../playwrightUtilities'; +import { Purchase } from '../pages/purchase.page'; + +Then('I go to the cart', async () => { + await new Purchase(getPage()).goToCart(); +}); + +Then('I click checkout', async () => { + await new Purchase(getPage()).clickCheckout(); +}); + +Then('I fill in my details with first name {string} last name {string} and zip {string}', async (firstName: string, lastName: string, zip: string) => { + await new Purchase(getPage()).fillInDetails(firstName, lastName, zip); +}); + +Then('I click continue', async () => { + await new Purchase(getPage()).clickContinue(); +}); + +Then('I click finish', async () => { + await new Purchase(getPage()).clickFinish(); +}); + +Then('I should see the confirmation message {string}', async (expectedMessage: string) => { + await new Purchase(getPage()).validateConfirmationMessage(expectedMessage); +});