diff --git a/features/login.feature b/features/login.feature index fb9f1fa5..afa344ff 100644 --- a/features/login.feature +++ b/features/login.feature @@ -5,8 +5,13 @@ 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 + # TODO: Add a step to validate the error message received + Then I should see the error message "Epic sadface: Sorry, this user has been locked out." + + Scenario: Validate successful login + Then I will login as 'standard_user' + Then I should be redirected to the products page \ No newline at end of file diff --git a/features/product.feature b/features/product.feature index 8a7ceab9..442b1ca5 100644 --- a/features/product.feature +++ b/features/product.feature @@ -7,7 +7,12 @@ Feature: Product Feature Scenario Outline: Validate product sort by price Then I will login as 'standard_user' # TODO: Sort the items by + + Then I will sort the items by "" # TODO: Validate all 6 items are sorted correctly by price + Then I will 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 + | 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 28634789..0d47290d 100644 --- a/features/purchase.feature +++ b/features/purchase.feature @@ -6,9 +6,16 @@ Feature: Purchase Feature Scenario: Validate successful purchase text Then I will login as 'standard_user' Then I will add the backpack to the cart + Then I will validate Cart Badge shows 1 item # TODO: Select the cart (top-right) + Then I will click on the cart icon # TODO: Select Checkout + Then I will click on the checkout button # TODO: Fill in the First Name, Last Name, and Zip/Postal Code + Then I will Fill in the First Name, Last Name, and Zip Code # TODO: Select Continue + Then I will click on 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 on the finish button + # TODO: Validate the text 'Thank you for your order!' + Then I should see the text 'Thank you for your order!' \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index b90d7c6c..43fbbd8d 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": { @@ -7,9 +7,10 @@ "devDependencies": { "@cucumber/cucumber": "^10.0.1", "@cucumber/pretty-formatter": "^1.0.0", - "@playwright/test": "^1.40.1", + "@playwright/test": "^1.62.1", "@types/node": "^20.10.3", "cucumber-html-reporter": "^7.1.1", + "playwright": "^1.62.1", "ts-node": "^10.9.1", "typescript": "^5.3.2" } @@ -491,18 +492,19 @@ } }, "node_modules/@playwright/test": { - "version": "1.40.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.40.1.tgz", - "integrity": "sha512-EaaawMTOeEItCRvfmkI9v6rBkF1svM8wjl/YPRrg2N2Wmp+4qJYkWtJsbew1szfKKDm6fPLy4YAanBhIlf9dWw==", + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.62.1.tgz", + "integrity": "sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "playwright": "1.40.1" + "playwright": "1.62.1" }, "bin": { "playwright": "cli.js" }, "engines": { - "node": ">=16" + "node": ">=20" } }, "node_modules/@teppeis/multimaps": { @@ -1900,33 +1902,35 @@ } }, "node_modules/playwright": { - "version": "1.40.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.40.1.tgz", - "integrity": "sha512-2eHI7IioIpQ0bS1Ovg/HszsN/XKNwEG1kbzSDDmADpclKc7CyqkHw7Mg2JCz/bbCxg25QUPcjksoMW7JcIFQmw==", + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.1.tgz", + "integrity": "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.40.1" + "playwright-core": "1.62.1" }, "bin": { "playwright": "cli.js" }, "engines": { - "node": ">=16" + "node": ">=20" }, "optionalDependencies": { "fsevents": "2.3.2" } }, "node_modules/playwright-core": { - "version": "1.40.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.40.1.tgz", - "integrity": "sha512-+hkOycxPiV534c4HhpfX6yrlawqVUzITRKwHAmYfmsVreltEl6fAZJ3DPfLMOODw0H3s1Itd6MDCWmP1fl/QvQ==", + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz", + "integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==", "dev": true, + "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" }, "engines": { - "node": ">=16" + "node": ">=20" } }, "node_modules/progress": { diff --git a/package.json b/package.json index ed38f6f5..c0d0cadc 100644 --- a/package.json +++ b/package.json @@ -6,9 +6,10 @@ "devDependencies": { "@cucumber/cucumber": "^10.0.1", "@cucumber/pretty-formatter": "^1.0.0", - "@playwright/test": "^1.40.1", + "@playwright/test": "^1.62.1", "@types/node": "^20.10.3", "cucumber-html-reporter": "^7.1.1", + "playwright": "^1.62.1", "ts-node": "^10.9.1", "typescript": "^5.3.2" } diff --git a/pages/login.page.ts b/pages/login.page.ts index 5a01614b..c76a4a0f 100644 --- a/pages/login.page.ts +++ b/pages/login.page.ts @@ -1,26 +1,48 @@ -import { Page } from "@playwright/test" +import { Page } 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 loginButton: string = 'input[id="login-button"]' + 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 loginButton: string = 'input[id="login-button"]'; + private readonly errorMessage: string = 'h3[data-test="error"]'; - constructor(page: Page) { - this.page = page; + 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}`, + ); } + } + + 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(); + } - 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}`); - } + public async validateErrorMessage(expectedErrorMessage: string) { + const errorMessageLocator = this.page.locator(this.errorMessage); + const actualErrorMessage = await errorMessageLocator.textContent(); + if (actualErrorMessage?.trim() !== expectedErrorMessage) { + throw new Error( + `Expected error message to be "${expectedErrorMessage}" but found "${actualErrorMessage?.trim()}"`, + ); } + } - 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() + public async validateSuccessfulLogin() { + const url = this.page.url(); + if (!url.includes("/inventory.html")) { + throw new Error( + `Expected to be redirected to the products page, but current URL is ${url}`, + ); } -} \ No newline at end of file + } +} diff --git a/pages/product.page.ts b/pages/product.page.ts index 14bedb1b..c7a69a80 100644 --- a/pages/product.page.ts +++ b/pages/product.page.ts @@ -1,14 +1,54 @@ -import { Page } from "@playwright/test" +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 page: Page; + private readonly addToCart: string = + 'button[id="add-to-cart-sauce-labs-backpack"]'; + private readonly sortBy: string = + 'select[data-test="product-sort-container"]'; - constructor(page: Page) { - this.page = page; + constructor(page: Page) { + this.page = page; + } + + public async addBackPackToCart() { + await this.page.locator(this.addToCart).click(); + } + + public async sortItemsBy(sort: string) { + let sortValue: string; + if (sort === "Price (low to high)") { + sortValue = "lohi"; + } else if (sort === "Price (high to low)") { + sortValue = "hilo"; + } else { + throw new Error(`Invalid sort option: ${sort}`); + } + await this.page.selectOption(this.sortBy, sortValue); + } + + public async validateItemsAreSortedByPrice(sort: string) { + const prices = await this.page.$$eval( + 'div[class="inventory_item_price"]', + (elements) => + elements.map((el) => + parseFloat(el.textContent?.replace("$", "") || "0"), + ), + ); + + let sortedPrices; + if (sort === "Price (low to high)") { + sortedPrices = [...prices].sort((a, b) => a - b); + } else if (sort === "Price (high to low)") { + sortedPrices = [...prices].sort((a, b) => b - a); + } else { + throw new Error(`Invalid sort option: ${sort}`); } - public async addBackPackToCart() { - await this.page.locator(this.addToCart).click() + if (JSON.stringify(prices) !== JSON.stringify(sortedPrices)) { + throw new Error( + `Expected items to be sorted by price, but got ${JSON.stringify(prices)}`, + ); } -} \ No newline at end of file + } +} diff --git a/pages/purchase.page.ts b/pages/purchase.page.ts new file mode 100644 index 00000000..3eb5b859 --- /dev/null +++ b/pages/purchase.page.ts @@ -0,0 +1,64 @@ +import { Page } from "@playwright/test"; + +export class Purchase { + private readonly page: Page; + private readonly cartIcon: string = 'a[class="shopping_cart_link"]'; + 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 thankYouMessage: string = 'h2[class="complete-header"]'; + private readonly cartBadge: string = 'span[class="shopping_cart_badge"]'; + + constructor(page: Page) { + this.page = page; + } + + public async validateCartBadgeShowsItems(expectedItems: number) { + const cartBadge = await this.page.locator(this.cartBadge).textContent(); + if (cartBadge !== expectedItems.toString()) { + throw new Error( + `Expected cart badge to show "${expectedItems}" items, but got "${cartBadge}"`, + ); + } + } + + public async clickOnCartIcon() { + await this.page.locator(this.cartIcon).click(); + } + + public async clickOnCheckoutButton() { + await this.page.locator(this.checkoutButton).click(); + } + + public async fillInCheckoutInformation( + firstName: string, + lastName: string, + postalCode: string, + ) { + await this.page.fill(this.firstNameField, firstName); + await this.page.fill(this.lastNameField, lastName); + await this.page.fill(this.postalCodeField, postalCode); + } + + public async clickOnContinueButton() { + await this.page.locator(this.continueButton).click(); + } + + public async clickOnFinishButton() { + await this.page.locator(this.finishButton).click(); + } + + public async validateThankYouMessage() { + const thankYouMessage = await this.page + .locator(this.thankYouMessage) + .textContent(); + if (thankYouMessage !== "Thank you for your order!") { + throw new Error( + `Expected thank you message to be "Thank you for your order!", but got "${thankYouMessage}"`, + ); + } + } +} diff --git a/steps/login.steps.ts b/steps/login.steps.ts index c2aa0d80..48686469 100644 --- a/steps/login.steps.ts +++ b/steps/login.steps.ts @@ -1,11 +1,22 @@ -import { Then } from '@cucumber/cucumber'; -import { getPage } from '../playwrightUtilities'; -import { Login } from '../pages/login.page'; +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) => { await new Login(getPage()).validateTitle(expectedTitle); }); -Then('I will login as {string}', async (userName) => { +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 error message {string}", + async (expectedErrorMessage) => { + await new Login(getPage()).validateErrorMessage(expectedErrorMessage); + }, +); + +Then("I should be redirected to the products page", async () => { + await new Login(getPage()).validateSuccessfulLogin(); +}); diff --git a/steps/product.steps.ts b/steps/product.steps.ts index bb52fb98..d244a3a8 100644 --- a/steps/product.steps.ts +++ b/steps/product.steps.ts @@ -1,7 +1,18 @@ -import { Then } from '@cucumber/cucumber'; -import { getPage } from '../playwrightUtilities'; -import { Product } from '../pages/product.page'; +import { Then } from "@cucumber/cucumber"; +import { getPage } from "../playwrightUtilities"; +import { Product } from "../pages/product.page"; -Then('I will add the backpack to the cart', async () => { +Then("I will add the backpack to the cart", async () => { await new Product(getPage()).addBackPackToCart(); -}); \ No newline at end of file +}); + +Then("I will sort the items by {string}", async (sort: string) => { + await new Product(getPage()).sortItemsBy(sort); +}); + +Then( + "I will validate all 6 items are sorted correctly by price {string}", + async (sort: string) => { + await new Product(getPage()).validateItemsAreSortedByPrice(sort); + }, +); diff --git a/steps/purchase.step.ts b/steps/purchase.step.ts new file mode 100644 index 00000000..45a3ef50 --- /dev/null +++ b/steps/purchase.step.ts @@ -0,0 +1,38 @@ +import { Then } from "@cucumber/cucumber"; +import { getPage } from "../playwrightUtilities"; +import { Purchase } from "../pages/purchase.page"; + +Then( + "I will validate Cart Badge shows {int} item", + async (itemCount: number) => { + await new Purchase(getPage()).validateCartBadgeShowsItems(itemCount); + }, +); + +Then("I will click on the cart icon", async () => { + await new Purchase(getPage()).clickOnCartIcon(); +}); + +Then("I will click on the checkout button", async () => { + await new Purchase(getPage()).clickOnCheckoutButton(); +}); + +Then("I will Fill in the First Name, Last Name, and Zip Code", async () => { + await new Purchase(getPage()).fillInCheckoutInformation( + "John", + "Doe", + "12345", + ); +}); + +Then("I will click on the continue button", async () => { + await new Purchase(getPage()).clickOnContinueButton(); +}); + +Then("I will click on the finish button", async () => { + await new Purchase(getPage()).clickOnFinishButton(); +}); + +Then("I should see the text 'Thank you for your order!'", async () => { + await new Purchase(getPage()).validateThankYouMessage(); +});