From 32f6b5547f4252cae5b371de046d1d6428fcc7bd Mon Sep 17 00:00:00 2001 From: Ruslan Petrov Date: Thu, 2 Apr 2026 14:02:34 -0400 Subject: [PATCH] Done --- .DS_Store | Bin 0 -> 8196 bytes cucumber.js | 2 +- features/login.feature | 9 ++++++--- features/product.feature | 15 ++++++++------- features/purchase.feature | 17 ++++++++--------- package-lock.json | 2 +- pages/login.page.ts | 27 ++++++++++++++++++++++++++ pages/product.page.ts | 39 ++++++++++++++++++++++++++++++++++++++ pages/purchase.page.ts | 36 +++++++++++++++++++++++++++++++++++ playwrightUtilities.ts | 7 +++++-- steps/login.steps.ts | 8 ++++++++ steps/product.steps.ts | 32 ++++++++++++++++++++++++++++++- steps/purchase.steps.ts | 25 ++++++++++++++++++++++++ 13 files changed, 195 insertions(+), 24 deletions(-) create mode 100644 .DS_Store create mode 100644 pages/purchase.page.ts create mode 100644 steps/purchase.steps.ts diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..0e9e6a4ad7144a7a2b34448b569677496c530480 GIT binary patch literal 8196 zcmeHMO>YuG7=DL>T|#T9LBqvl6R#9$Yc-7*iuB;YOY!5N2HAq`mSu(cu|HYF&@6490v&+SUv2muEd1hzd=bd+-8Cc#a5sAjMyGK+eA_tYRSU{6f_&(P} z$wVVNKmk6{fIRBah^lm%u(ktM0jq#jz$#!BunPPO3gDg1#aZ*-*LQ7e6|f5YmkRLr zgNw>ok8r6`eRZHwDF85wVO7va9-ugGg!KrQ8Wj~jb@m`4s)$kyq3O8qvN^CG;Zmcf zlhAY$(aa*sP=uNtJXe{MsB5&XRlq8cRDg5$0-e!R)p-6sej3O#FOVKA_(Sy-wfa*P z^oU9cRvVdrkA@_H6Py8DpdCmB!%r&BtiV`|bfU=^Bof7eMV9cL7htDml}xEWJ7hj@7J z49%zGeZJ1pn~jIjC*qqo4E*t=R{Jh88`<2AO=r_7IB#1Qa@Lx3Cev}FGkVQe&!r5! ztA59O>Gx*sV(EztCmlcRjTFJ}_F(enl^=HHtRbgicdWQAeZ$E+`F629pYIr|o_)2<0c}i6WxcByH>KRm*D^d-e~lQ5eXOcP}%x zd9{u8Pu;D&m*Ohf_y0;^+p!8<4+S=Km}SoYPu8FRUyp~eQCbD80)M1{$TnNe2B=p* z8cZ*oYrClLP`R+(Qlp}vQRz5P{SW}I{$Yr|3sdIQBV2042#P%jP%_xYD)3ho_yq`R BE;axF literal 0 HcmV?d00001 diff --git a/cucumber.js b/cucumber.js index e8192d6..61ce55a 100644 --- a/cucumber.js +++ b/cucumber.js @@ -1,3 +1,3 @@ module.exports = { - default: `--require-module ts-node/register --require './steps/**/*.ts' --require './hooks/**/*.ts --format @cucumber/pretty-formatter` + default: `--require-module ts-node/register --require './steps/**/*.ts' --require './hooks/**/*.ts' --format @cucumber/pretty-formatter` }; \ No newline at end of file diff --git a/features/login.feature b/features/login.feature index fb9f1fa..30bcf94 100644 --- a/features/login.feature +++ b/features/login.feature @@ -4,9 +4,12 @@ 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 "Epic sadface: Sorry, this user has been locked out." + + Scenario: Validate standard_user can login successfully + Then I will login as 'standard_user' + Then I should see the products page header "Products" \ No newline at end of file diff --git a/features/product.feature b/features/product.feature index 8a7ceab..c502aff 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' + Then I sort products by price "" + Then I should see the products sorted by price "" + Examples: - # TODO: extend the datatable to paramterize this test - | sort | \ No newline at end of file + | sort | + | Price (low to high)| + | Price (high to low)| \ No newline at end of file diff --git a/features/purchase.feature b/features/purchase.feature index 2863478..c90f8b6 100644 --- a/features/purchase.feature +++ b/features/purchase.feature @@ -3,12 +3,11 @@ 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 go to the cart + Then I checkout with first name "John" last name "Doe" postal code "12345" + Then I continue the checkout + Then I finish the checkout + Then I should see the order confirmation "Thank you for your order!" \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index b90d7c6..b5c47aa 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 5a01614..fb30ae5 100644 --- a/pages/login.page.ts +++ b/pages/login.page.ts @@ -22,5 +22,32 @@ export class Login { await this.page.locator(this.userNameField).fill(userName) await this.page.locator(this.passwordField).fill(this.password) await this.page.locator(this.loginButton).click() + + // Wait for navigation to inventory page (or error message). + await Promise.race([ + this.page.waitForURL('**/inventory.html', { timeout: 10000 }), + this.page.locator('[data-test="error"]').waitFor({ state: 'visible', timeout: 10000 }) + ]).catch(() => null); + + // If login succeeded, ensure inventory content is visible. + if (this.page.url().includes('/inventory.html')) { + await this.page.waitForSelector('.inventory_list', { timeout: 10000 }); + } + } + + public async validateProductsPage(expectedHeader: string) { + const actualHeader = (await this.page.locator('.title').textContent())?.trim() || ''; + if (actualHeader !== expectedHeader) { + throw new Error(`Expected products page header to be "${expectedHeader}" but found "${actualHeader}"`); + } + } + + public async validateErrorMessage(expectedError: string) { + const errorLocator = this.page.locator('[data-test="error"]'); + const actualError = (await errorLocator.textContent())?.trim() || ''; + + if (actualError !== expectedError) { + throw new Error(`Expected login error to be "${expectedError}" but found "${actualError}"`); + } } } \ No newline at end of file diff --git a/pages/product.page.ts b/pages/product.page.ts index 14bedb1..559bef3 100644 --- a/pages/product.page.ts +++ b/pages/product.page.ts @@ -3,6 +3,9 @@ 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 cartButton: string = '.shopping_cart_link' + private readonly sortSelect: string = 'select.product_sort_container' + private readonly itemPrices: string = '.inventory_item_price' constructor(page: Page) { this.page = page; @@ -11,4 +14,40 @@ export class Product { public async addBackPackToCart() { await this.page.locator(this.addToCart).click() } + + public async sortByPrice(option: string) { + const value = option === 'Price (low to high)' ? 'lohi' : 'hilo' + const sortLocator = this.page.locator(this.sortSelect) + await sortLocator.waitFor({ state: 'visible', timeout: 10000 }) + + const selected = await sortLocator.selectOption(value) + if (!selected || selected.length === 0) { + throw new Error(`Cannot set sort option '${option}' (value: '${value}')`) + } + + await this.page.waitForTimeout(500) + } + + public async getPrices(): Promise { + const priceElements = this.page.locator(this.itemPrices) + const count = await priceElements.count() + const prices: number[] = [] + + for (let i = 0; i < count; i++) { + const text = (await priceElements.nth(i).textContent())?.trim() || '' + const price = Number(text.replace('$', '')) + prices.push(price) + } + + return prices + } + + public async addBackPackAndGoToCart() { + await this.addBackPackToCart() + await this.page.locator(this.cartButton).click() + } + + public async goToCart() { + await this.page.locator(this.cartButton).click() + } } \ No newline at end of file diff --git a/pages/purchase.page.ts b/pages/purchase.page.ts new file mode 100644 index 0000000..3afcc13 --- /dev/null +++ b/pages/purchase.page.ts @@ -0,0 +1,36 @@ +import { Page } from "@playwright/test"; + +export class Purchase { + private readonly page: Page; + + constructor(page: Page) { + this.page = page; + } + + public async goToCart() { + await this.page.locator('.shopping_cart_link').click(); + } + + public async clickCheckout() { + await this.page.locator('[data-test="checkout"]').click(); + } + + public async fillCheckoutInfo(firstName: string, lastName: string, postalCode: string) { + await this.page.locator('[data-test="firstName"]').fill(firstName); + await this.page.locator('[data-test="lastName"]').fill(lastName); + await this.page.locator('[data-test="postalCode"]').fill(postalCode); + } + + public async continueCheckout() { + await this.page.locator('[data-test="continue"]').click(); + } + + public async finishCheckout() { + await this.page.locator('[data-test="finish"]').click(); + } + + public async getConfirmationText(): Promise { + const text = await this.page.locator('.complete-header').textContent(); + return text ? text.trim() : ''; + } +} diff --git a/playwrightUtilities.ts b/playwrightUtilities.ts index 93244a2..92307a7 100644 --- a/playwrightUtilities.ts +++ b/playwrightUtilities.ts @@ -1,4 +1,4 @@ -import { Browser, chromium, Page } from 'playwright'; +import { Browser, chromium, Page } from '@playwright/test'; let browser: Browser | null = null; let page: Page | null = null; @@ -6,7 +6,10 @@ const DEFAULT_TIMEOUT = 30000; export const initializeBrowser = async () => { if (!browser) { - browser = await chromium.launch({ headless: false }); + browser = await chromium.launch({ + headless: true, + args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage'], + }); } }; diff --git a/steps/login.steps.ts b/steps/login.steps.ts index c2aa0d8..088abbb 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 login error {string}', async (expectedError) => { + await new Login(getPage()).validateErrorMessage(expectedError); +}); + +Then('I should see the products page header {string}', async (expectedHeader) => { + await new Login(getPage()).validateProductsPage(expectedHeader); }); \ No newline at end of file diff --git a/steps/product.steps.ts b/steps/product.steps.ts index bb52fb9..f5f3d06 100644 --- a/steps/product.steps.ts +++ b/steps/product.steps.ts @@ -4,4 +4,34 @@ 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 go to the cart', async () => { + await new Product(getPage()).goToCart(); +}); + +Then('I sort products by price {string}', async (sortOption) => { + await new Product(getPage()).sortByPrice(sortOption); +}); + +Then('I should see the products sorted by price {string}', async (sortOption) => { + const product = new Product(getPage()); + const prices = await product.getPrices(); + + if (prices.length !== 6) { + throw new Error(`Expected 6 products, but found ${prices.length}`); + } + + const sorted = [...prices]; + sorted.sort((a, b) => a - b); + + if (sortOption === 'Price (high to low)') { + sorted.reverse(); + } + + const match = sorted.every((value, index) => value === prices[index]); + + if (!match) { + throw new Error(`Expected products to be sorted by ${sortOption}, but were ${prices}`); + } +}); diff --git a/steps/purchase.steps.ts b/steps/purchase.steps.ts new file mode 100644 index 0000000..2a99d7f --- /dev/null +++ b/steps/purchase.steps.ts @@ -0,0 +1,25 @@ +import { Then } from '@cucumber/cucumber'; +import { getPage } from '../playwrightUtilities'; +import { Purchase } from '../pages/purchase.page'; + +Then('I checkout with first name {string} last name {string} postal code {string}', async (firstName, lastName, postalCode) => { + const purchase = new Purchase(getPage()); + await purchase.clickCheckout(); + await purchase.fillCheckoutInfo(firstName, lastName, postalCode); +}); + +Then('I continue the checkout', async () => { + await new Purchase(getPage()).continueCheckout(); +}); + +Then('I finish the checkout', async () => { + await new Purchase(getPage()).finishCheckout(); +}); + +Then('I should see the order confirmation {string}', async (expectedText) => { + const text = await new Purchase(getPage()).getConfirmationText(); + + if (text !== expectedText) { + throw new Error(`Expected order confirmation to be "${expectedText}" but found "${text}"`); + } +});