From 58693d659b619621003271d105d773e2c0e086fd Mon Sep 17 00:00:00 2001 From: Senia Chap Date: Mon, 11 Aug 2025 22:25:10 -0400 Subject: [PATCH 1/2] Completed Playwright + Cucumber assessment --- features/login.feature | 3 +- features/product.feature | 11 +- features/purchase.feature | 9 +- hooks/globalHooks.ts | 2 +- package-lock.json | 260 +++++++++++++++++++++++--------------- package.json | 2 +- pages/login.page.ts | 32 ++++- pages/product.page.ts | 114 ++++++++++++++++- steps/login.steps.ts | 14 +- steps/product.steps.ts | 38 ++++++ 10 files changed, 368 insertions(+), 117 deletions(-) diff --git a/features/login.feature b/features/login.feature index fb9f1fa..62f4189 100644 --- a/features/login.feature +++ b/features/login.feature @@ -5,8 +5,9 @@ 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' + Then I should see the login error "Epic sadface: Sorry, this user has been locked out." # TODO: Add a step to validate the error message received \ No newline at end of file diff --git a/features/product.feature b/features/product.feature index 8a7ceab..6e27743 100644 --- a/features/product.feature +++ b/features/product.feature @@ -8,6 +8,15 @@ Feature: Product Feature 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 the product list by price option "" + Then the product prices should be sorted in "" order + Examples: # TODO: extend the datatable to paramterize this test - | sort | \ No newline at end of file + #| sort | + + Examples: + | 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 2863478..e83815a 100644 --- a/features/purchase.feature +++ b/features/purchase.feature @@ -11,4 +11,11 @@ Feature: Purchase Feature # 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 + # TODO: Validate the text 'Thank you for your order!' + + Then I open the shopping cart + Then I proceed to checkout + Then I enter checkout information: first name "Senia", last name "Chap", postal code "28212" + Then I continue to the checkout overview + Then I complete the purchase + Then I should see the order confirmation message "Thank you for your order!" \ No newline at end of file diff --git a/hooks/globalHooks.ts b/hooks/globalHooks.ts index 5743ca8..0dec42b 100644 --- a/hooks/globalHooks.ts +++ b/hooks/globalHooks.ts @@ -1,7 +1,7 @@ import { After, Before, setDefaultTimeout } from "@cucumber/cucumber"; import { closeBrowser, initializeBrowser, initializePage } from "../playwrightUtilities"; -setDefaultTimeout(15000); +setDefaultTimeout(30_000); Before( async () => { await initializeBrowser(); diff --git a/package-lock.json b/package-lock.json index b90d7c6..657b1ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "Playwright-Project", + "name": "Playwright-Cucumber-Exercise", "lockfileVersion": 3, "requires": true, "packages": { "": { "devDependencies": { - "@cucumber/cucumber": "^10.0.1", + "@cucumber/cucumber": "^10.9.0", "@cucumber/pretty-formatter": "^1.0.0", "@playwright/test": "^1.40.1", "@types/node": "^20.10.3", @@ -179,26 +179,28 @@ } }, "node_modules/@cucumber/ci-environment": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/@cucumber/ci-environment/-/ci-environment-9.2.0.tgz", - "integrity": "sha512-jLzRtVwdtNt+uAmTwvXwW9iGYLEOJFpDSmnx/dgoMGKXUWRx1UHT86Q696CLdgXO8kyTwsgJY0c6n5SW9VitAA==", - "dev": true + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@cucumber/ci-environment/-/ci-environment-10.0.1.tgz", + "integrity": "sha512-/+ooDMPtKSmvcPMDYnMZt4LuoipfFfHaYspStI4shqw8FyKcfQAmekz6G+QKWjQQrvM+7Hkljwx58MEwPCwwzg==", + "dev": true, + "license": "MIT" }, "node_modules/@cucumber/cucumber": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/@cucumber/cucumber/-/cucumber-10.0.1.tgz", - "integrity": "sha512-g7W7SQnNMSNnMRQVGubjefCxdgNFyq4P3qxT2Ve7Xhh8ZLoNkoRDcWsyfKQVWnxNfgW3aGJmxbucWRoTi+ZUqg==", + "version": "10.9.0", + "resolved": "https://registry.npmjs.org/@cucumber/cucumber/-/cucumber-10.9.0.tgz", + "integrity": "sha512-7XHJ6nmr9IkIag0nv6or82HfelbSInrEe3H4aT6dMHyTehwFLUifG6eQQ+uE4LZIOXAnzLPH37YmqygEO67vCA==", "dev": true, + "license": "MIT", "dependencies": { - "@cucumber/ci-environment": "9.2.0", - "@cucumber/cucumber-expressions": "16.1.2", - "@cucumber/gherkin": "26.2.0", + "@cucumber/ci-environment": "10.0.1", + "@cucumber/cucumber-expressions": "17.1.0", + "@cucumber/gherkin": "28.0.0", "@cucumber/gherkin-streams": "5.0.1", - "@cucumber/gherkin-utils": "8.0.2", - "@cucumber/html-formatter": "20.4.0", + "@cucumber/gherkin-utils": "9.0.0", + "@cucumber/html-formatter": "21.6.0", "@cucumber/message-streams": "4.0.1", - "@cucumber/messages": "22.0.0", - "@cucumber/tag-expressions": "5.0.1", + "@cucumber/messages": "24.1.0", + "@cucumber/tag-expressions": "6.1.0", "assertion-error-formatter": "^3.0.0", "capital-case": "^1.0.4", "chalk": "^4.1.2", @@ -216,18 +218,19 @@ "lodash.merge": "^4.6.2", "lodash.mergewith": "^4.6.2", "luxon": "3.2.1", + "mime": "^3.0.0", "mkdirp": "^2.1.5", "mz": "^2.7.0", "progress": "^2.0.3", "read-pkg-up": "^7.0.1", "resolve-pkg": "^2.0.0", "semver": "7.5.3", - "string-argv": "^0.3.1", + "string-argv": "0.3.1", "strip-ansi": "6.0.1", "supports-color": "^8.1.1", - "tmp": "^0.2.1", + "tmp": "0.2.3", + "type-fest": "^4.8.3", "util-arity": "^1.1.0", - "verror": "^1.10.0", "xmlbuilder": "^15.1.1", "yaml": "^2.2.2", "yup": "1.2.0" @@ -237,24 +240,96 @@ }, "engines": { "node": "18 || >=20" + }, + "funding": { + "url": "https://opencollective.com/cucumber" } }, "node_modules/@cucumber/cucumber-expressions": { - "version": "16.1.2", - "resolved": "https://registry.npmjs.org/@cucumber/cucumber-expressions/-/cucumber-expressions-16.1.2.tgz", - "integrity": "sha512-CfHEbxJ5FqBwF6mJyLLz4B353gyHkoi6cCL4J0lfDZ+GorpcWw4n2OUAdxJmP7ZlREANWoTFlp4FhmkLKrCfUA==", + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/@cucumber/cucumber-expressions/-/cucumber-expressions-17.1.0.tgz", + "integrity": "sha512-PCv/ppsPynniKPWJr5v566daCVe+pbxQpHGrIu/Ev57cCH9Rv+X0F6lio4Id3Z64TaG7btCRLUGewIgLwmrwOA==", "dev": true, + "license": "MIT", "dependencies": { "regexp-match-indices": "1.0.2" } }, + "node_modules/@cucumber/cucumber/node_modules/@cucumber/gherkin-utils": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@cucumber/gherkin-utils/-/gherkin-utils-9.0.0.tgz", + "integrity": "sha512-clk4q39uj7pztZuZtyI54V8lRsCUz0Y/p8XRjIeHh7ExeEztpWkp4ca9q1FjUOPfQQ8E7OgqFbqoQQXZ1Bx7fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cucumber/gherkin": "^28.0.0", + "@cucumber/messages": "^24.0.0", + "@teppeis/multimaps": "3.0.0", + "commander": "12.0.0", + "source-map-support": "^0.5.21" + }, + "bin": { + "gherkin-utils": "bin/gherkin-utils" + } + }, + "node_modules/@cucumber/cucumber/node_modules/@cucumber/gherkin-utils/node_modules/commander": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.0.0.tgz", + "integrity": "sha512-MwVNWlYjDTtOjX5PiD7o5pK0UrFU/OYgcJfjjK4RaHZETNtjJqrZa9Y9ds88+A+f+d5lv+561eZ+yCKoS3gbAA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@cucumber/cucumber/node_modules/@cucumber/tag-expressions": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@cucumber/tag-expressions/-/tag-expressions-6.1.0.tgz", + "integrity": "sha512-+3DwRumrCJG27AtzCIL37A/X+A/gSfxOPLg8pZaruh5SLumsTmpvilwroVWBT2fPzmno/tGXypeK5a7NHU4RzA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@cucumber/cucumber/node_modules/@teppeis/multimaps": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@teppeis/multimaps/-/multimaps-3.0.0.tgz", + "integrity": "sha512-ID7fosbc50TbT0MK0EG12O+gAP3W3Aa/Pz4DaTtQtEvlc9Odaqi0de+xuZ7Li2GtK4HzEX7IuRWS/JmZLksR3Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@cucumber/cucumber/node_modules/string-argv": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz", + "integrity": "sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, + "node_modules/@cucumber/cucumber/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@cucumber/gherkin": { - "version": "26.2.0", - "resolved": "https://registry.npmjs.org/@cucumber/gherkin/-/gherkin-26.2.0.tgz", - "integrity": "sha512-iRSiK8YAIHAmLrn/mUfpAx7OXZ7LyNlh1zT89RoziSVCbqSVDxJS6ckEzW8loxs+EEXl0dKPQOXiDmbHV+C/fA==", + "version": "28.0.0", + "resolved": "https://registry.npmjs.org/@cucumber/gherkin/-/gherkin-28.0.0.tgz", + "integrity": "sha512-Ee6zJQq0OmIUPdW0mSnsCsrWA2PZAELNDPICD2pLfs0Oz7RAPgj80UsD2UCtqyAhw2qAR62aqlktKUlai5zl/A==", "dev": true, + "license": "MIT", "dependencies": { - "@cucumber/messages": ">=19.1.4 <=22" + "@cucumber/messages": ">=19.1.4 <=24" } }, "node_modules/@cucumber/gherkin-streams": { @@ -337,10 +412,11 @@ } }, "node_modules/@cucumber/html-formatter": { - "version": "20.4.0", - "resolved": "https://registry.npmjs.org/@cucumber/html-formatter/-/html-formatter-20.4.0.tgz", - "integrity": "sha512-TnLSXC5eJd8AXHENo69f5z+SixEVtQIf7Q2dZuTpT/Y8AOkilGpGl1MQR1Vp59JIw+fF3EQSUKdf+DAThCxUNg==", + "version": "21.6.0", + "resolved": "https://registry.npmjs.org/@cucumber/html-formatter/-/html-formatter-21.6.0.tgz", + "integrity": "sha512-Qw1tdObBJrgXgXwVjKVjB3hFhFPI8WhIFb+ULy8g5lDl5AdnKDiyDXAMvAWRX+pphnRMMNdkPCt6ZXEfWvUuAA==", "dev": true, + "license": "MIT", "peerDependencies": { "@cucumber/messages": ">=18" } @@ -355,15 +431,38 @@ } }, "node_modules/@cucumber/messages": { - "version": "22.0.0", - "resolved": "https://registry.npmjs.org/@cucumber/messages/-/messages-22.0.0.tgz", - "integrity": "sha512-EuaUtYte9ilkxcKmfqGF9pJsHRUU0jwie5ukuZ/1NPTuHS1LxHPsGEODK17RPRbZHOFhqybNzG2rHAwThxEymg==", + "version": "24.1.0", + "resolved": "https://registry.npmjs.org/@cucumber/messages/-/messages-24.1.0.tgz", + "integrity": "sha512-hxVHiBurORcobhVk80I9+JkaKaNXkW6YwGOEFIh/2aO+apAN+5XJgUUWjng9NwqaQrW1sCFuawLB1AuzmBaNdQ==", "dev": true, + "license": "MIT", "dependencies": { - "@types/uuid": "9.0.1", + "@types/uuid": "9.0.8", "class-transformer": "0.5.1", - "reflect-metadata": "0.1.13", - "uuid": "9.0.0" + "reflect-metadata": "0.2.1", + "uuid": "9.0.1" + } + }, + "node_modules/@cucumber/messages/node_modules/reflect-metadata": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.1.tgz", + "integrity": "sha512-i5lLI6iw9AU3Uu4szRNPPEkomnkjRTaVt9hy/bn5g/oSzekBSMeLZblcjP74AW0vBabqERLLIrz+gR8QYR54Tw==", + "deprecated": "This version has a critical bug in fallback handling. Please upgrade to reflect-metadata@0.2.2 or newer.", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@cucumber/messages/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" } }, "node_modules/@cucumber/pretty-formatter": { @@ -560,10 +659,11 @@ "dev": true }, "node_modules/@types/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-rFT3ak0/2trgvp4yYZo5iKFEPsET7vKydKF+VRCxlQ9bpheehyAJH89dAkaLEq/j/RZXJIqcgsmPJKUP1Z28HA==", - "dev": true + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", + "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", + "dev": true, + "license": "MIT" }, "node_modules/acorn": { "version": "8.11.2", @@ -1652,6 +1752,19 @@ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "dev": true }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/minimatch": { "version": "9.0.3", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", @@ -2062,63 +2175,6 @@ "node": ">=8" } }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/rimraf/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/seed-random": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/seed-random/-/seed-random-2.2.0.tgz", @@ -2378,15 +2434,13 @@ "dev": true }, "node_modules/tmp": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", - "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", "dev": true, - "dependencies": { - "rimraf": "^3.0.0" - }, + "license": "MIT", "engines": { - "node": ">=8.17.0" + "node": ">=14.14" } }, "node_modules/toposort": { diff --git a/package.json b/package.json index ed38f6f..91bcf6b 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "report": "node generate-report.js" }, "devDependencies": { - "@cucumber/cucumber": "^10.0.1", + "@cucumber/cucumber": "^10.9.0", "@cucumber/pretty-formatter": "^1.0.0", "@playwright/test": "^1.40.1", "@types/node": "^20.10.3", diff --git a/pages/login.page.ts b/pages/login.page.ts index 5a01614..33f3db6 100644 --- a/pages/login.page.ts +++ b/pages/login.page.ts @@ -1,4 +1,6 @@ import { Page } from "@playwright/test" +import { expect } from "@playwright/test" + export class Login { private readonly page: Page @@ -11,11 +13,33 @@ export class Login { 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 validateTitle(expectedTitle: string) { - const pageTitle = await this.page.title(); - if (pageTitle !== expectedTitle) { - throw new Error(`Expected title to be ${expectedTitle} but found ${pageTitle}`); - } + //ignore spaces and capitalization + const actualTitle = (await this.page.title()).trim().toLowerCase(); + const normalizedExpected = expectedTitle.trim().toLowerCase(); + + expect(actualTitle, + `Expected page title to be "${expectedTitle}", but found "${actualTitle}"` + ).toBe(normalizedExpected); + } + + public async validateErrorMessage(expectedMessage: string) { + //get the error message in login form + const actualMessage = (await this.page.locator('[data-test="error"]').textContent()) || ''; + + const normalizedActual = actualMessage.trim().toLowerCase(); + const normalizedExpected = expectedMessage.trim().toLowerCase(); + + expect(normalizedActual, + `Expected error message to be "${expectedMessage}", but found "${actualMessage.trim()}"` + ).toBe(normalizedExpected); } public async loginAsUser(userName: string) { diff --git a/pages/product.page.ts b/pages/product.page.ts index 14bedb1..c2de0ab 100644 --- a/pages/product.page.ts +++ b/pages/product.page.ts @@ -1,14 +1,120 @@ -import { Page } from "@playwright/test" +import { Page, expect } 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; + + //product selectors + private readonly addToCart: string = 'button[id="add-to-cart-sauce-labs-backpack"]'; + + //sorting selectors + private readonly sortSelect = '[data-test="product_sort_container"]'; + private readonly priceCells = '.inventory_item_price'; + private readonly sortSelectFallback = '#header_container select'; + + //checkout flow + private readonly cartIcon = '.shopping_cart_link'; + private readonly checkoutBtn = '#checkout'; + private readonly firstName = '#first-name'; + private readonly lastName = '#last-name'; + private readonly postalCode = '#postal-code'; + private readonly continueBtn = '#continue'; + private readonly finishBtn = '#finish'; + private readonly completeHeader = '.complete-header'; + constructor(page: Page) { this.page = page; } + //product actions public async addBackPackToCart() { - await this.page.locator(this.addToCart).click() + await this.page.locator(this.addToCart).click(); + } + + //sorting actions + public async selectSort(sortText: string) { + // Ensure we are on the inventory page after login + await this.page.waitForURL(/\/inventory\.html$/); + + const select = this.page.locator(`${this.sortSelect}, ${this.sortSelectFallback}`).first(); + await select.waitFor({ state: 'visible' }); + + //selecting by label first + try { + await select.selectOption({ label: sortText }); + } catch { + //fallback + const normalized = sortText.trim().toLowerCase(); + const value = + normalized.includes('low to high') ? 'lohi' : + normalized.includes('high to low') ? 'hilo' : + undefined; + + if (!value) { + const available = await select.locator('option').allTextContents(); + throw new Error( + `Cannot select sort option "${sortText}". ` + + `Available options: ${available.map(s => `"${s.trim()}"`).join(', ')}` + ); + } + + await select.selectOption({ value }); + } + + //wait for sort completing + await this.page.waitForLoadState('networkidle'); + } + + + private async getAllPrices(): Promise { + await this.page.locator(this.priceCells).first().waitFor({ state: 'visible', timeout: 10000 }); + + const texts = await this.page.locator(this.priceCells).allTextContents(); + return texts.map(t => parseFloat(t.replace('$', '').trim())); + } + + public async validatePricesSorted(order: 'asc' | 'desc') { + const prices = await this.getAllPrices(); + if (prices.length !== 6) { + throw new Error(`Expected 6 items, found ${prices.length}. Prices: [${prices.join(', ')}]`); + } + + const expected = [...prices].sort((a, b) => a - b); + if (order === 'desc') expected.reverse(); + + expect(prices, `Prices not sorted ${order}. Actual: [${prices.join(', ')}]`) + .toEqual(expected); + } + + + + //purchase flow + public async openCart() { + await this.page.locator(this.cartIcon).click(); + } + + public async checkout() { + await this.page.locator(this.checkoutBtn).click(); + } + + public async fillCheckoutInfo(first: string, last: string, zip: string) { + await this.page.locator(this.firstName).fill(first); + await this.page.locator(this.lastName).fill(last); + await this.page.locator(this.postalCode).fill(zip); + } + + public async continueCheckout() { + await this.page.locator(this.continueBtn).click(); + } + + public async finishCheckout() { + await this.page.locator(this.finishBtn).click(); + } + + public async validateConfirmationMessage(expected: string) { + const actual = + (await this.page.locator(this.completeHeader).textContent())?.trim() || ""; + expect(actual).toBe(expected.trim()); } } \ No newline at end of file diff --git a/steps/login.steps.ts b/steps/login.steps.ts index c2aa0d8..2a9209a 100644 --- a/steps/login.steps.ts +++ b/steps/login.steps.ts @@ -1,6 +1,7 @@ import { Then } from '@cucumber/cucumber'; import { getPage } from '../playwrightUtilities'; import { Login } from '../pages/login.page'; +import { expect } from '@playwright/test'; Then('I should see the title {string}', async (expectedTitle) => { await new Login(getPage()).validateTitle(expectedTitle); @@ -8,4 +9,15 @@ Then('I should see the title {string}', async (expectedTitle) => { Then('I will login as {string}', async (userName) => { await new Login(getPage()).loginAsUser(userName); -}); \ No newline at end of file +}); + + +//added by Senia + +Then('I should see the login error {string}', async (expectedMessage) => { + const errorText = (await getPage().locator('[data-test="error"]').textContent()) || ''; + + expect(errorText.trim().toLowerCase()) + .toBe(expectedMessage.trim().toLowerCase()); +}); + diff --git a/steps/product.steps.ts b/steps/product.steps.ts index bb52fb9..e021160 100644 --- a/steps/product.steps.ts +++ b/steps/product.steps.ts @@ -2,6 +2,44 @@ import { Then } from '@cucumber/cucumber'; import { getPage } from '../playwrightUtilities'; import { Product } from '../pages/product.page'; +//product sorting +Then('I sort the product list by price option {string}', async (sortText: string) => { + await new Product(getPage()).selectSort(sortText); +}); + +Then('the product prices should be sorted in {string} order', async (order: 'asc' | 'desc') => { + await new Product(getPage()).validatePricesSorted(order); +}); + +//cart & checkout Then('I will add the backpack to the cart', async () => { await new Product(getPage()).addBackPackToCart(); +}); + + +//added by Senia + +Then('I open the shopping cart', async () => { + await new Product(getPage()).openCart(); +}); + +Then('I proceed to checkout', async () => { + await new Product(getPage()).checkout(); +}); + +Then('I enter checkout information: first name {string}, last name {string}, postal code {string}', + async (firstName: string, lastName: string, postalCode: string) => { + await new Product(getPage()).fillCheckoutInfo(firstName, lastName, postalCode); +}); + +Then('I continue to the checkout overview', async () => { + await new Product(getPage()).continueCheckout(); +}); + +Then('I complete the purchase', async () => { + await new Product(getPage()).finishCheckout(); +}); + +Then('I should see the order confirmation message {string}', async (message: string) => { + await new Product(getPage()).validateConfirmationMessage(message); }); \ No newline at end of file From ed9abb3bf9aadec4de3a2be006465ee9f5866919 Mon Sep 17 00:00:00 2001 From: Senia Chap Date: Fri, 20 Mar 2026 09:45:02 -0400 Subject: [PATCH 2/2] Completed Playwright Cucumber assessment and extended test coverage --- features/login.feature | 9 +++-- features/product.feature | 14 ++++---- features/purchase.feature | 16 +++++++-- pages/cart.page.ts | 21 ++++++++++++ pages/checkout.page.ts | 42 ++++++++++++++++++++++++ pages/login.page.ts | 7 ++-- pages/product.page.ts | 69 +++++++++------------------------------ steps/common.steps.ts | 10 ++++-- steps/login.steps.ts | 11 +++---- steps/product.steps.ts | 32 +++++++++++------- 10 files changed, 142 insertions(+), 89 deletions(-) create mode 100644 pages/cart.page.ts create mode 100644 pages/checkout.page.ts diff --git a/features/login.feature b/features/login.feature index 62f4189..5bde0c7 100644 --- a/features/login.feature +++ b/features/login.feature @@ -4,10 +4,15 @@ Feature: Login Feature Given I open the "https://www.saucedemo.com/" page Scenario: Validate the login page title - # TODO: Fix this failing scenario + # TODO: Fix this failing scenario - fixed 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 - fixed Then I should see the login error "Epic sadface: Sorry, this user has been locked out." - # TODO: Add a step to validate the error message received \ No newline at end of file + +#Extend the testing coverage + Scenario: Validate successful login redirects to inventory page + Then I will login as 'standard_user' + Then I should be on the inventory page \ No newline at end of file diff --git a/features/product.feature b/features/product.feature index 6e27743..0b7faad 100644 --- a/features/product.feature +++ b/features/product.feature @@ -9,14 +9,14 @@ Feature: Product Feature # TODO: Sort the items by # TODO: Validate all 6 items are sorted correctly by price - Then I sort the product list by price option "" - Then the product prices should be sorted in "" order + Then I sort the product list by price option "" + Then the product prices should be sorted in "" order + - Examples: # TODO: extend the datatable to paramterize this test #| sort | - Examples: - | sort | order | - | Price (low to high) | asc | - | Price (high to low) | desc | \ No newline at end of file + Examples: + | 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 e83815a..c569722 100644 --- a/features/purchase.feature +++ b/features/purchase.feature @@ -13,9 +13,19 @@ Feature: Purchase Feature # TODO: Select Finish # TODO: Validate the text 'Thank you for your order!' + Then I open the shopping cart + Then I proceed to checkout + Then I enter checkout information: first name "Senia", last name "Chap", postal code "28212" + Then I continue to the checkout overview + Then I complete the purchase + Then I should see the order confirmation message "Thank you for your order!" + + +#Extend the testing coverage + Scenario: Validate checkout error when customer info is missing + Then I will login as 'standard_user' + Then I will add the backpack to the cart Then I open the shopping cart Then I proceed to checkout - Then I enter checkout information: first name "Senia", last name "Chap", postal code "28212" Then I continue to the checkout overview - Then I complete the purchase - Then I should see the order confirmation message "Thank you for your order!" \ No newline at end of file + Then I should see the checkout error message "Error: First Name is required" \ No newline at end of file diff --git a/pages/cart.page.ts b/pages/cart.page.ts new file mode 100644 index 0000000..35b32b2 --- /dev/null +++ b/pages/cart.page.ts @@ -0,0 +1,21 @@ +import { Page } from "@playwright/test"; + +export class CartPage { + private readonly page: Page; + + private readonly cartIcon = '.shopping_cart_link'; + private readonly checkoutBtn = '#checkout'; + + constructor(page: Page) { + this.page = page; + } + + //purchase flow + async open() { + await this.page.locator(this.cartIcon).click(); + } + + async startCheckout() { + await this.page.locator(this.checkoutBtn).click(); + } +} \ No newline at end of file diff --git a/pages/checkout.page.ts b/pages/checkout.page.ts new file mode 100644 index 0000000..77528cc --- /dev/null +++ b/pages/checkout.page.ts @@ -0,0 +1,42 @@ +import { Page, expect } from "@playwright/test"; + +export class CheckoutPage { + private readonly page: Page; + + private readonly firstName = '#first-name'; + private readonly lastName = '#last-name'; + private readonly postalCode = '#postal-code'; + private readonly continueBtn = '#continue'; + private readonly finishBtn = '#finish'; + private readonly confirmation = '.complete-header'; + private readonly errorBanner = '[data-test="error"]'; + + constructor(page: Page) { + this.page = page; + } + + async fillCustomerInfo(first: string, last: string, zip: string) { + await this.page.locator(this.firstName).fill(first); + await this.page.locator(this.lastName).fill(last); + await this.page.locator(this.postalCode).fill(zip); + } + + async continue() { + await this.page.locator(this.continueBtn).click(); + } + + async placeOrder() { + await this.page.locator(this.finishBtn).click(); + } + + async assertOrderCompleted(expected: string) { + const actual = + (await this.page.locator(this.confirmation).textContent())?.trim() || ""; + expect(actual).toBe(expected.trim()); + } + + async assertCheckoutError(expected: string) { + await expect(this.page.locator(this.errorBanner)).toHaveText(expected); + } + +} \ No newline at end of file diff --git a/pages/login.page.ts b/pages/login.page.ts index 33f3db6..839b731 100644 --- a/pages/login.page.ts +++ b/pages/login.page.ts @@ -5,6 +5,7 @@ import { expect } 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"]' @@ -30,7 +31,7 @@ export class Login { ).toBe(normalizedExpected); } - public async validateErrorMessage(expectedMessage: string) { + public async assertErrorMessage(expectedMessage: string) { //get the error message in login form const actualMessage = (await this.page.locator('[data-test="error"]').textContent()) || ''; @@ -42,9 +43,9 @@ export class Login { ).toBe(normalizedExpected); } - public async loginAsUser(userName: string) { + public async login(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() } -} \ No newline at end of file +} diff --git a/pages/product.page.ts b/pages/product.page.ts index c2de0ab..8f65d2c 100644 --- a/pages/product.page.ts +++ b/pages/product.page.ts @@ -1,39 +1,29 @@ import { Page, expect } from "@playwright/test"; + export class Product { private readonly page: Page; //product selectors - private readonly addToCart: string = 'button[id="add-to-cart-sauce-labs-backpack"]'; + private readonly addBackpackBtn: string = 'button[id="add-to-cart-sauce-labs-backpack"]'; //sorting selectors - private readonly sortSelect = '[data-test="product_sort_container"]'; + private readonly sortSelect = '[data-test="product-sort-container"]'; private readonly priceCells = '.inventory_item_price'; private readonly sortSelectFallback = '#header_container select'; - - - //checkout flow - private readonly cartIcon = '.shopping_cart_link'; - private readonly checkoutBtn = '#checkout'; - private readonly firstName = '#first-name'; - private readonly lastName = '#last-name'; - private readonly postalCode = '#postal-code'; - private readonly continueBtn = '#continue'; - private readonly finishBtn = '#finish'; - private readonly completeHeader = '.complete-header'; constructor(page: Page) { this.page = page; } //product actions - public async addBackPackToCart() { - await this.page.locator(this.addToCart).click(); + async addBackpack() { + await this.page.locator(this.addBackpackBtn).click(); } //sorting actions - public async selectSort(sortText: string) { + public async applySort(sortText: string) { // Ensure we are on the inventory page after login await this.page.waitForURL(/\/inventory\.html$/); @@ -44,7 +34,7 @@ export class Product { try { await select.selectOption({ label: sortText }); } catch { - //fallback + //fallback logic const normalized = sortText.trim().toLowerCase(); const value = normalized.includes('low to high') ? 'lohi' : @@ -67,54 +57,27 @@ export class Product { } - private async getAllPrices(): Promise { + private async getDisplayedPrices(): Promise { await this.page.locator(this.priceCells).first().waitFor({ state: 'visible', timeout: 10000 }); const texts = await this.page.locator(this.priceCells).allTextContents(); return texts.map(t => parseFloat(t.replace('$', '').trim())); } - public async validatePricesSorted(order: 'asc' | 'desc') { - const prices = await this.getAllPrices(); - if (prices.length !== 6) { - throw new Error(`Expected 6 items, found ${prices.length}. Prices: [${prices.join(', ')}]`); + public async assertPricesAreSorted(direction: 'asc' | 'desc') { + const displayedPrices = await this.getDisplayedPrices(); + if (displayedPrices .length !== 6) { + throw new Error(`Expected 6 products, got ${displayedPrices.length}. Prices: [${displayedPrices.join(', ')}]`); } - const expected = [...prices].sort((a, b) => a - b); - if (order === 'desc') expected.reverse(); + const sorted = [...displayedPrices].sort((a, b) => a - b); + if (direction === 'desc') sorted.reverse(); - expect(prices, `Prices not sorted ${order}. Actual: [${prices.join(', ')}]`) - .toEqual(expected); + expect(displayedPrices, `Prices not sorted ${direction}. Actual: [${displayedPrices.join(', ')}]`) + .toEqual(sorted); } - //purchase flow - public async openCart() { - await this.page.locator(this.cartIcon).click(); - } - public async checkout() { - await this.page.locator(this.checkoutBtn).click(); - } - - public async fillCheckoutInfo(first: string, last: string, zip: string) { - await this.page.locator(this.firstName).fill(first); - await this.page.locator(this.lastName).fill(last); - await this.page.locator(this.postalCode).fill(zip); - } - - public async continueCheckout() { - await this.page.locator(this.continueBtn).click(); - } - - public async finishCheckout() { - await this.page.locator(this.finishBtn).click(); - } - - public async validateConfirmationMessage(expected: string) { - const actual = - (await this.page.locator(this.completeHeader).textContent())?.trim() || ""; - expect(actual).toBe(expected.trim()); - } } \ No newline at end of file diff --git a/steps/common.steps.ts b/steps/common.steps.ts index c4bee79..90f34b2 100644 --- a/steps/common.steps.ts +++ b/steps/common.steps.ts @@ -1,6 +1,12 @@ -import { Given } from "@cucumber/cucumber"; +import { Given, Then } from "@cucumber/cucumber"; import { getPage } from "../playwrightUtilities"; +import { expect } from '@playwright/test'; Given('I open the {string} page', async (url) => { await getPage().goto(url); - }); \ No newline at end of file + }); + + +Then('I should be on the inventory page', async () => { + await expect(getPage()).toHaveURL(/inventory\.html/); +}); \ No newline at end of file diff --git a/steps/login.steps.ts b/steps/login.steps.ts index 2a9209a..a391b0d 100644 --- a/steps/login.steps.ts +++ b/steps/login.steps.ts @@ -1,23 +1,20 @@ import { Then } from '@cucumber/cucumber'; import { getPage } from '../playwrightUtilities'; import { Login } from '../pages/login.page'; -import { expect } from '@playwright/test'; + Then('I should see the title {string}', async (expectedTitle) => { await new Login(getPage()).validateTitle(expectedTitle); }); Then('I will login as {string}', async (userName) => { - await new Login(getPage()).loginAsUser(userName); + await new Login(getPage()).login(userName); }); //added by Senia -Then('I should see the login error {string}', async (expectedMessage) => { - const errorText = (await getPage().locator('[data-test="error"]').textContent()) || ''; - - expect(errorText.trim().toLowerCase()) - .toBe(expectedMessage.trim().toLowerCase()); +Then('I should see the login error {string}', async (expectedMessage: string) => { + await new Login(getPage()).assertErrorMessage(expectedMessage); }); diff --git a/steps/product.steps.ts b/steps/product.steps.ts index e021160..88ff478 100644 --- a/steps/product.steps.ts +++ b/steps/product.steps.ts @@ -2,44 +2,52 @@ import { Then } from '@cucumber/cucumber'; import { getPage } from '../playwrightUtilities'; import { Product } from '../pages/product.page'; +import { CartPage } from '../pages/cart.page'; +import { CheckoutPage } from '../pages/checkout.page'; + //product sorting -Then('I sort the product list by price option {string}', async (sortText: string) => { - await new Product(getPage()).selectSort(sortText); +Then('I apply sort option {string}', async (sortText: string) => { + await new Product(getPage()).applySort(sortText); }); -Then('the product prices should be sorted in {string} order', async (order: 'asc' | 'desc') => { - await new Product(getPage()).validatePricesSorted(order); +Then('the product prices should be in {string} order', async (direction: 'asc' | 'desc') => { + await new Product(getPage()).assertPricesAreSorted(direction); }); -//cart & checkout +//add to cart & checkout Then('I will add the backpack to the cart', async () => { - await new Product(getPage()).addBackPackToCart(); + await new Product(getPage()).addBackpack(); }); //added by Senia Then('I open the shopping cart', async () => { - await new Product(getPage()).openCart(); + await new CartPage(getPage()).open(); + await new CartPage(getPage()).open(); }); Then('I proceed to checkout', async () => { - await new Product(getPage()).checkout(); + await new CartPage(getPage()).startCheckout(); }); Then('I enter checkout information: first name {string}, last name {string}, postal code {string}', async (firstName: string, lastName: string, postalCode: string) => { - await new Product(getPage()).fillCheckoutInfo(firstName, lastName, postalCode); + await new CheckoutPage(getPage()).fillCustomerInfo(firstName, lastName, postalCode); }); Then('I continue to the checkout overview', async () => { - await new Product(getPage()).continueCheckout(); + await new CheckoutPage(getPage()).continue(); }); Then('I complete the purchase', async () => { - await new Product(getPage()).finishCheckout(); + await new CheckoutPage(getPage()).placeOrder(); }); Then('I should see the order confirmation message {string}', async (message: string) => { - await new Product(getPage()).validateConfirmationMessage(message); + await new CheckoutPage(getPage()).assertOrderCompleted(message); +}); + +Then('I should see the checkout error message {string}', async (message: string) => { + await new CheckoutPage(getPage()).assertCheckoutError(message); }); \ No newline at end of file