diff --git a/cucumber.js b/cucumber.js index e8192d6..cef709b 100644 --- a/cucumber.js +++ b/cucumber.js @@ -1,3 +1,8 @@ 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 hooks/**/*.ts', + '--require steps/**/*.ts', + '--publish-quiet' + ].join(' ') }; \ No newline at end of file diff --git a/features/login.feature b/features/login.feature index fb9f1fa..38cddca 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 + Then I should see the login 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 8a7ceab..3ef4272 100644 --- a/features/product.feature +++ b/features/product.feature @@ -6,8 +6,9 @@ Feature: Product Feature # 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 sort products by "" + Then products should be sorted by price "" Examples: - # TODO: extend the datatable to paramterize this test - | sort | \ No newline at end of file + | sortOption | 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 2863478..77f6925 100644 --- a/features/purchase.feature +++ b/features/purchase.feature @@ -6,9 +6,9 @@ Feature: Purchase Feature 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 + Then cart count should be "1" + Then I open the cart + Then I checkout the product + Then I enter checkout information "Rajesh" "Sathuri" "500081" + Then I finish the purchase + Then I should see the purchase confirmation text "Thank you for your order!" \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index b90d7c6..b26590b 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": { @@ -189,6 +189,7 @@ "resolved": "https://registry.npmjs.org/@cucumber/cucumber/-/cucumber-10.0.1.tgz", "integrity": "sha512-g7W7SQnNMSNnMRQVGubjefCxdgNFyq4P3qxT2Ve7Xhh8ZLoNkoRDcWsyfKQVWnxNfgW3aGJmxbucWRoTi+ZUqg==", "dev": true, + "peer": true, "dependencies": { "@cucumber/ci-environment": "9.2.0", "@cucumber/cucumber-expressions": "16.1.2", @@ -253,6 +254,7 @@ "resolved": "https://registry.npmjs.org/@cucumber/gherkin/-/gherkin-26.2.0.tgz", "integrity": "sha512-iRSiK8YAIHAmLrn/mUfpAx7OXZ7LyNlh1zT89RoziSVCbqSVDxJS6ckEzW8loxs+EEXl0dKPQOXiDmbHV+C/fA==", "dev": true, + "peer": true, "dependencies": { "@cucumber/messages": ">=19.1.4 <=22" } @@ -350,6 +352,7 @@ "resolved": "https://registry.npmjs.org/@cucumber/message-streams/-/message-streams-4.0.1.tgz", "integrity": "sha512-Kxap9uP5jD8tHUZVjTWgzxemi/0uOsbGjd4LBOSxcJoOCRbESFwemUzilJuzNTB8pcTQUh8D5oudUyxfkJOKmA==", "dev": true, + "peer": true, "peerDependencies": { "@cucumber/messages": ">=17.1.1" } @@ -359,6 +362,7 @@ "resolved": "https://registry.npmjs.org/@cucumber/messages/-/messages-22.0.0.tgz", "integrity": "sha512-EuaUtYte9ilkxcKmfqGF9pJsHRUU0jwie5ukuZ/1NPTuHS1LxHPsGEODK17RPRbZHOFhqybNzG2rHAwThxEymg==", "dev": true, + "peer": true, "dependencies": { "@types/uuid": "9.0.1", "class-transformer": "0.5.1", @@ -549,6 +553,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.3.tgz", "integrity": "sha512-XJavIpZqiXID5Yxnxv3RUDKTN5b81ddNC3ecsA0SoFXz/QU8OGBwZGMomiq0zw+uuqbL/krztv/DINAQ/EV4gg==", "dev": true, + "peer": true, "dependencies": { "undici-types": "~5.26.4" } @@ -974,6 +979,7 @@ "resolved": "https://registry.npmjs.org/@cucumber/messages/-/messages-21.0.1.tgz", "integrity": "sha512-pGR7iURM4SF9Qp1IIpNiVQ77J9kfxMkPOEbyy+zRmGABnWWCsqMpJdfHeh9Mb3VskemVw85++e15JT0PYdcR3g==", "dev": true, + "peer": true, "dependencies": { "@types/uuid": "8.3.4", "class-transformer": "0.5.1", @@ -2473,6 +2479,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz", "integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==", "dev": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/pages/checkout.page.ts b/pages/checkout.page.ts new file mode 100644 index 0000000..7765e7e --- /dev/null +++ b/pages/checkout.page.ts @@ -0,0 +1,31 @@ +import { Page } from "@playwright/test"; + +export class Checkout { + constructor(private page: Page) {} + + async openCart() { + await this.page.locator('.shopping_cart_link').click(); + } + + async checkout() { + await this.page.locator('#checkout').click(); + } + + async enterUserInfo(first: string, last: string, zip: string) { + await this.page.locator('#first-name').fill(first); + await this.page.locator('#last-name').fill(last); + await this.page.locator('#postal-code').fill(zip); + await this.page.locator('#continue').click(); + } + + async finishPurchase() { + await this.page.locator('#finish').click(); + } + + async validateConfirmation(expected: string) { + const actual = await this.page.locator('.complete-header').innerText(); + if (actual.trim() !== expected) { + throw new Error(`Expected "${expected}" but got "${actual}"`); + } + } +} \ No newline at end of file diff --git a/pages/login.page.ts b/pages/login.page.ts index 5a01614..5c468d3 100644 --- a/pages/login.page.ts +++ b/pages/login.page.ts @@ -23,4 +23,16 @@ export class Login { await this.page.locator(this.passwordField).fill(this.password) await this.page.locator(this.loginButton).click() } -} \ No newline at end of file + public async validateLoginError(expected: string) { + const errorMessage = await this.page + .locator('h3[data-test="error"]') + .innerText(); + + if (errorMessage.trim() !== expected) { + throw new Error( + `Expected error message to be "${expected}" but found "${errorMessage}"` + ); + } + } +} + diff --git a/pages/product.page.ts b/pages/product.page.ts index 14bedb1..e19f89c 100644 --- a/pages/product.page.ts +++ b/pages/product.page.ts @@ -1,14 +1,61 @@ -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; - constructor(page: Page) { - this.page = page; + private readonly addToCart = + 'button[id="add-to-cart-sauce-labs-backpack"]'; + private readonly sortDropdown = + 'select.product_sort_container'; + private readonly prices = + '.inventory_item_price'; + private readonly cartBadge = + '.shopping_cart_badge'; + + constructor(page: Page) { + this.page = page; + } + + public async addBackPackToCart() { + await this.page.locator(this.addToCart).click(); + } + + public async sortProducts(option: string) { + const dropdown = this.page.locator(this.sortDropdown); + await dropdown.waitFor({ state: 'visible' }); + await dropdown.selectOption({ label: option }); +} + + private async getAllPrices(): Promise { + const priceTexts = await this.page + .locator(this.prices) + .allInnerTexts(); + + return priceTexts.map(p => Number(p.replace('$', '').trim())); + } + + public async validatePriceSorting(order: string) { + const prices = await this.getAllPrices(); + const sorted = [...prices].sort((a, b) => + order === 'asc' ? a - b : b - a + ); + + if (JSON.stringify(prices) !== JSON.stringify(sorted)) { + throw new Error( + `Prices are not sorted in ${order} order. Actual: ${prices}` + ); } + } + + // EXTRA COVERAGE + + public async validateCartCount(expected: string) { + const count = await this.page.locator(this.cartBadge).innerText(); - public async addBackPackToCart() { - await this.page.locator(this.addToCart).click() + if (count !== expected) { + throw new Error( + `Expected cart count ${expected} but found ${count}` + ); } + } } \ No newline at end of file diff --git a/steps/login.steps.ts b/steps/login.steps.ts index c2aa0d8..f4f4bc3 100644 --- a/steps/login.steps.ts +++ b/steps/login.steps.ts @@ -8,4 +8,8 @@ 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 message {string}', async (expectedMessage) => { + await new Login(getPage()).validateLoginError(expectedMessage); }); \ No newline at end of file diff --git a/steps/product.steps.ts b/steps/product.steps.ts index bb52fb9..81ce17f 100644 --- a/steps/product.steps.ts +++ b/steps/product.steps.ts @@ -4,4 +4,16 @@ import { Product } from '../pages/product.page'; Then('I will add the backpack to the cart', async () => { await new Product(getPage()).addBackPackToCart(); +}); + +Then('I sort products by {string}', async (option: string) => { + await new Product(getPage()).sortProducts(option); +}); + +Then('products should be sorted by price {string}', async (order: string) => { + await new Product(getPage()).validatePriceSorting(order); +}); + +Then('cart count should be {string}', async (count: string) => { + await new Product(getPage()).validateCartCount(count); }); \ No newline at end of file diff --git a/steps/purchase.steps.ts b/steps/purchase.steps.ts new file mode 100644 index 0000000..150be1f --- /dev/null +++ b/steps/purchase.steps.ts @@ -0,0 +1,29 @@ +import { Then } from '@cucumber/cucumber'; +import { getPage } from '../playwrightUtilities'; +import { Checkout } from '../pages/checkout.page'; + +Then('I open the cart', async () => { + await new Checkout(getPage()).openCart(); +}); + +Then('I checkout the product', async () => { + await new Checkout(getPage()).checkout(); +}); + +Then( + 'I enter checkout information {string} {string} {string}', + async (first: string, last: string, zip: string) => { + await new Checkout(getPage()).enterUserInfo(first, last, zip); + } +); + +Then('I finish the purchase', async () => { + await new Checkout(getPage()).finishPurchase(); +}); + +Then( + 'I should see the purchase confirmation text {string}', + async (expected: string) => { + await new Checkout(getPage()).validateConfirmation(expected); + } +); \ No newline at end of file