diff --git a/features/login.feature b/features/login.feature index fb9f1fa..0c3b0c7 100644 --- a/features/login.feature +++ b/features/login.feature @@ -4,9 +4,16 @@ 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 + Scenario Outline: Login validation as different users + Then I will login as '' with this password '' + Then I should see this result '' + + + Examples: + | username | password | result | + | standard_user | secret_sauce | inventory page | + | locked_out_user | secret_sauce | error message | + | invalid_user | wrong_pass | error message | + | | | username required | \ No newline at end of file diff --git a/features/logout.feature b/features/logout.feature new file mode 100644 index 0000000..d1a1dcc --- /dev/null +++ b/features/logout.feature @@ -0,0 +1,10 @@ +Feature: Logout Feature + + Background: + Given I open the "https://www.saucedemo.com/" page + Then I will login + + Scenario: Validate that a user is able to logout of the application + Then I logged out of the application + Then I should see the title "Swag Labs" + diff --git a/features/product.feature b/features/product.feature index 8a7ceab..7c9b96d 100644 --- a/features/product.feature +++ b/features/product.feature @@ -3,11 +3,10 @@ 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 + Then I will login + Then I will sort by '', and the results should be '' Examples: - # TODO: extend the datatable to paramterize this test - | sort | \ No newline at end of file + | sort | prices | + | Price (high to low) | 49.99, 29.99, 15.99, 15.99, 9.99, 7.99 | + | Price (low to high) | 7.99 ,9.99, 15.99, 15.99, 29.99, 49.99 | \ No newline at end of file diff --git a/features/purchase.feature b/features/purchase.feature index 2863478..463ea27 100644 --- a/features/purchase.feature +++ b/features/purchase.feature @@ -4,11 +4,9 @@ Feature: Purchase Feature Given I open the "https://www.saucedemo.com/" page Scenario: Validate successful purchase text - Then I will login as 'standard_user' + Then I will login 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 + Then I will go to my shopping cart + Then I will start to checkout + Then I will fill in my information + Then I will checkout \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index b90d7c6..906e85f 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,6 +7,7 @@ "devDependencies": { "@cucumber/cucumber": "^10.0.1", "@cucumber/pretty-formatter": "^1.0.0", + "@faker-js/faker": "^10.3.0", "@playwright/test": "^1.40.1", "@types/node": "^20.10.3", "cucumber-html-reporter": "^7.1.1", @@ -388,6 +389,23 @@ "integrity": "sha512-N43uWud8ZXuVjza423T9ZCIJsaZhFekmakt7S9bvogTxqdVGbRobjR663s0+uW0Rz9e+Pa8I6jUuWtoBLQD2Mw==", "dev": true }, + "node_modules/@faker-js/faker": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-10.3.0.tgz", + "integrity": "sha512-It0Sne6P3szg7JIi6CgKbvTZoMjxBZhcv91ZrqrNuaZQfB5WoqYYbzCUOq89YR+VY8juY9M1vDWmDDa2TzfXCw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/fakerjs" + } + ], + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || ^23.5.0 || >=24.0.0", + "npm": ">=10" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", diff --git a/package.json b/package.json index ed38f6f..134967e 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "devDependencies": { "@cucumber/cucumber": "^10.0.1", "@cucumber/pretty-formatter": "^1.0.0", + "@faker-js/faker": "^10.3.0", "@playwright/test": "^1.40.1", "@types/node": "^20.10.3", "cucumber-html-reporter": "^7.1.1", diff --git a/pages/checkout.page.ts b/pages/checkout.page.ts new file mode 100644 index 0000000..f4b8bb3 --- /dev/null +++ b/pages/checkout.page.ts @@ -0,0 +1,32 @@ +import {expect, Page} from "@playwright/test" +import { faker } from '@faker-js/faker' + +export class Checkout { + private readonly page: Page + private readonly checkoutButton: string = 'button[data-test="checkout"]' + private readonly checkoutFirstName: string = 'input[data-test="firstName"]' + private readonly checkoutLastName: string = 'input[data-test="lastName"]' + private readonly checkoutZipCode: string = 'input[data-test="postalCode"]' + private readonly continueCheckout: string = 'input[data-test="continue"]' + private readonly finishCheckout: string = 'button[data-test="finish"]' + private readonly thankYouMessage: string = 'h2[data-test="complete-header"]' + + constructor(page: Page) { + this.page = page; + } + + public async startCheckout(): Promise { + await this.page.locator(this.checkoutButton).click(); + } + + public async fillInCheckout(): Promise { + await this.page.locator(this.checkoutFirstName).fill(faker.person.firstName()); + await this.page.locator(this.checkoutLastName).fill(faker.person.lastName()); + await this.page.locator(this.checkoutZipCode).fill(faker.location.zipCode()); + } + public async checkoutItems(): Promise { + await this.page.locator(this.continueCheckout).click(); + await this.page.locator(this.finishCheckout).click(); + await expect(this.page.locator(this.thankYouMessage)).toHaveText('Thank you for your order!') + } +} \ No newline at end of file diff --git a/pages/login.page.ts b/pages/login.page.ts index 5a01614..8daa3fa 100644 --- a/pages/login.page.ts +++ b/pages/login.page.ts @@ -1,26 +1,36 @@ -import { Page } from "@playwright/test" +import {expect, Page} from "@playwright/test" +import { Product } from "./product.page"; +import {getPage} from "../playwrightUtilities"; 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 errorField: string = 'h3[data-test="error"]' constructor(page: Page) { this.page = page; } - public async validateTitle(expectedTitle: string) { + public async validateTitle(expectedTitle: string): Promise { 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 loginAsUser(username: string, password: string): Promise { + await this.page.locator(this.userNameField).fill(username); + await this.page.locator(this.passwordField).fill(password); + await this.page.locator(this.loginButton).click(); + } + + public async validateErrorMessage(result: string): Promise { + if (result === 'inventory page') { + await new Product(getPage()).validateTitle(); + } else if (result === 'error message' || result === 'username required') { + await expect(this.page.locator(this.errorField)).toBeVisible(); + } } } \ No newline at end of file diff --git a/pages/logout.page.ts b/pages/logout.page.ts new file mode 100644 index 0000000..af8436c --- /dev/null +++ b/pages/logout.page.ts @@ -0,0 +1,15 @@ +import {Page} from "@playwright/test" + +export class Logout { + private readonly page: Page + private readonly hamburgerMenu: string = 'button[id="react-burger-menu-btn"]' + private readonly logoutLink: string = 'a[data-test="logout-sidebar-link"]' + constructor(page: Page) { + this.page = page; + } + + public async logout(): Promise { + await this.page.locator(this.hamburgerMenu).click(); + await this.page.locator(this.logoutLink).click(); + } +} \ No newline at end of file diff --git a/pages/product.page.ts b/pages/product.page.ts index 14bedb1..6e9793b 100644 --- a/pages/product.page.ts +++ b/pages/product.page.ts @@ -1,14 +1,40 @@ -import { Page } from "@playwright/test" +import {expect, Page} from "@playwright/test" export class Product { private readonly page: Page + private readonly productTitle: string = 'span[data-test="title"]' private readonly addToCart: string = 'button[id="add-to-cart-sauce-labs-backpack"]' + private readonly shoppingCart: string = 'a[data-test="shopping-cart-link"]' + private readonly productSort: string = 'select[data-test="product-sort-container"]' + private readonly products: string = 'div[data-test="inventory-item-price"]' constructor(page: Page) { this.page = page; } - public async addBackPackToCart() { + public async validateTitle(): Promise { + await expect(this.page.locator(this.productTitle)).toBeVisible(); + } + public async addBackPackToCart(): Promise { await this.page.locator(this.addToCart).click() } + + public async selectCart(): Promise { + await this.page.locator(this.shoppingCart).click(); + } + + public async selectDropdown(sortOrder: string): Promise { + await this.page.locator(this.productSort).selectOption(sortOrder); + } + + public async sortItems(prices: string): Promise { + const priceElements :string[] = await this.page.locator(this.products).allTextContents(); + const actualPrices: number[] = priceElements.map((p: string) :number => parseFloat(p.replace('$', ''))); + const expectedPrices: number[] = prices.split(',').map(Number); + + expect(actualPrices.length).toBe(6); + expectedPrices.forEach((expectedPrice: number, index: number): void => { + expect(actualPrices[index]).toBe(expectedPrice); + }); + } } \ No newline at end of file diff --git a/steps/checkout.steps.ts b/steps/checkout.steps.ts new file mode 100644 index 0000000..05cac58 --- /dev/null +++ b/steps/checkout.steps.ts @@ -0,0 +1,15 @@ +import { Then } from '@cucumber/cucumber'; +import { getPage } from '../playwrightUtilities'; +import { Checkout } from '../pages/checkout.page'; + +Then('I will start to checkout', async () => { + await new Checkout(getPage()).startCheckout(); +}) + +Then('I will fill in my information', async () => { + await new Checkout(getPage()).fillInCheckout(); +}) + +Then('I will checkout', async () => { + await new Checkout(getPage()).checkoutItems(); +}) \ No newline at end of file diff --git a/steps/login.steps.ts b/steps/login.steps.ts index c2aa0d8..fb864b7 100644 --- a/steps/login.steps.ts +++ b/steps/login.steps.ts @@ -2,10 +2,19 @@ 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): Promise => { 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 +Then('I will login as {string} with this password {string}', async (username: string, password: string): Promise => { + await new Login(getPage()).loginAsUser(username, password); +}); + +// Without credentials - defaults to standard_user +Then('I will login', async (): Promise => { + await new Login(getPage()).loginAsUser('standard_user', 'secret_sauce'); +}); + +Then('I should see this result {string}', async (expectedErrorMessage: string): Promise => { + await new Login(getPage()).validateErrorMessage(expectedErrorMessage); +}) \ No newline at end of file diff --git a/steps/logout.steps.ts b/steps/logout.steps.ts new file mode 100644 index 0000000..18cd3e1 --- /dev/null +++ b/steps/logout.steps.ts @@ -0,0 +1,7 @@ +import { Then } from '@cucumber/cucumber'; +import { getPage } from '../playwrightUtilities'; +import { Logout } from '../pages/logout.page'; + +Then('I logged out of the application', async () => { + await new Logout(getPage()).logout(); +}); \ No newline at end of file diff --git a/steps/product.steps.ts b/steps/product.steps.ts index bb52fb9..d9b1218 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(); -}); \ No newline at end of file +}); + +Then('I will go to my shopping cart', async () => { + await new Product(getPage()).selectCart(); +}); + +Then('I will sort by {string}, and the results should be {string}', async (sort: string, prices: string) => { + await new Product(getPage()).selectDropdown(sort); + await new Product(getPage()).sortItems(prices) +})