diff --git a/README.md b/README.md index b911105..445bbf4 100644 --- a/README.md +++ b/README.md @@ -6,19 +6,16 @@ node >= v18.5.x npm >= v7 - ## Setup // Install Visual Studio Code (or any editor) https://code.visualstudio.com/download - // Install Node.js https://nodejs.org/en/download - ```bash git clone https://github.com/automationExamples/Playwright-Cucumber-Exercise.git npm install @@ -31,19 +28,21 @@ Cucumber v1.7.0 Cucumber (Gherkin) Support enhanced for Behat - ## Instructions + To run the test + ```bash npm run test ``` After running, to generate the cucumber report (cucumber_report.html) + ```bash npm run report ``` -It is not expected that you complete every task, however, please give your best effort +It is not expected that you complete every task, however, please give your best effort You will be scored based on your ability to complete the following tasks: @@ -51,8 +50,9 @@ You will be scored based on your ability to complete the following tasks: - [ ] Complete the automation tasks listed below ### Tasks -- [ ] Modify the scenario 'Validate the login page title' from [login.feature](features/login.feature#8) which runs but fails. Determine the cause of the failure and update the scenario to pass in the test -- [ ] Extend the scenario 'Validate login error message' from [login.feature](features/login.feature#10) which runs and passes but is missing a step. Extend the scenario to validate the error message received. -- [ ] Modify and extend the 'Validate successful purchase text' from [purchase.feature](features/purchase.feature#6) with steps for each comment listed. Consider writing a new steps.ts file along with an appropriate page.ts -- [ ] Modify and extend the 'Validate product sort by price sort' from [product.feature](features/product.feature#6) with steps for each comment listed. Utilize the Scenario Outline and Examples table to parameterize the test -- [ ] Extend the testing coverage with anything you believe would be beneficial + +- [X] Modify the scenario 'Validate the login page title' from [login.feature](features/login.feature#8) which runs but fails. Determine the cause of the failure and update the scenario to pass in the test +- [X] Extend the scenario 'Validate login error message' from [login.feature](features/login.feature#10) which runs and passes but is missing a step. Extend the scenario to validate the error message received. +- [X] Modify and extend the 'Validate successful purchase text' from [purchase.feature](features/purchase.feature#6) with steps for each comment listed. Consider writing a new steps.ts file along with an appropriate page.ts +- [X] Modify and extend the 'Validate product sort by price sort' from [product.feature](features/product.feature#6) with steps for each comment listed. Utilize the Scenario Outline and Examples table to parameterize the test +- [X] Extend the testing coverage with anything you believe would be beneficial diff --git a/data/userDetails.json b/data/userDetails.json new file mode 100644 index 0000000..55710a8 --- /dev/null +++ b/data/userDetails.json @@ -0,0 +1,5 @@ +{ + "firstName": "Dmitry", + "lastName": "Ivanov", + "ZIP": "28277" +} diff --git a/features/login.feature b/features/login.feature index fb9f1fa..7693d9e 100644 --- a/features/login.feature +++ b/features/login.feature @@ -5,8 +5,12 @@ 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 + Then I should see the error message "Epic sadface: Sorry, this user has been locked out." + + Scenario: Validate login with invalid credentials + Then I will login as 'invalid_user' + Then I should see the error message "Epic sadface: Username and password do not match any user in this service" \ No newline at end of file diff --git a/features/product.feature b/features/product.feature index 8a7ceab..848605c 100644 --- a/features/product.feature +++ b/features/product.feature @@ -3,11 +3,20 @@ 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 - Examples: - # TODO: extend the datatable to paramterize this test - | sort | \ No newline at end of file + Scenario Outline: Validate product sort by price + Then I will login as 'standard_user' + Then I sort items by "" + Then items should be sorted by price "" + Examples: + | sort | order | + | Price (low to high) | asc | + | Price (high to low) | desc | + + Scenario Outline: Validate product sort by name + Then I will login as 'standard_user' + Then I sort items by "" + Then items should be sorted by name "" + Examples: + | sort | order | + | Name (A to Z) | asc | + | Name (Z to A) | desc | \ No newline at end of file diff --git a/features/purchase.feature b/features/purchase.feature index 2863478..942110e 100644 --- a/features/purchase.feature +++ b/features/purchase.feature @@ -4,11 +4,11 @@ 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 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 login as 'standard_user' + Then I will add the backpack to the cart + Then Select the cart (top-right) + Then Click on Checkout button + Then I fill out checkout form + Then I click on continue + Then I click on Finish button + Then I have to validate success message \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index b90d7c6..c1bb909 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,7 +7,7 @@ "devDependencies": { "@cucumber/cucumber": "^10.0.1", "@cucumber/pretty-formatter": "^1.0.0", - "@playwright/test": "^1.40.1", + "@playwright/test": "^1.58.2", "@types/node": "^20.10.3", "cucumber-html-reporter": "^7.1.1", "ts-node": "^10.9.1", @@ -491,18 +491,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.58.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", + "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "playwright": "1.40.1" + "playwright": "1.58.2" }, "bin": { "playwright": "cli.js" }, "engines": { - "node": ">=16" + "node": ">=18" } }, "node_modules/@teppeis/multimaps": { @@ -1308,6 +1309,7 @@ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "dev": true, "hasInstallScript": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -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.58.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", + "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.40.1" + "playwright-core": "1.58.2" }, "bin": { "playwright": "cli.js" }, "engines": { - "node": ">=16" + "node": ">=18" }, "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.58.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", + "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", "dev": true, + "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" }, "engines": { - "node": ">=16" + "node": ">=18" } }, "node_modules/progress": { diff --git a/package.json b/package.json index ed38f6f..9b6d3fb 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "devDependencies": { "@cucumber/cucumber": "^10.0.1", "@cucumber/pretty-formatter": "^1.0.0", - "@playwright/test": "^1.40.1", + "@playwright/test": "^1.58.2", "@types/node": "^20.10.3", "cucumber-html-reporter": "^7.1.1", "ts-node": "^10.9.1", diff --git a/pages/category.page.ts b/pages/category.page.ts new file mode 100644 index 0000000..17e555b --- /dev/null +++ b/pages/category.page.ts @@ -0,0 +1,39 @@ +import { Page } from "@playwright/test" + +export class Category { + private readonly page: Page + private readonly sortDropdown: string = '[data-test="product-sort-container"]' + private readonly itemPrices: string = '[data-test="inventory-item-price"]' + + constructor(page: Page) { + this.page = page; + } + + public async sortBy(option: string) { + await this.page.locator(this.sortDropdown).selectOption({ label: option }); + } + + public async validatePriceSort(order: string) { + const priceTexts = await this.page.locator(this.itemPrices).allTextContents(); + const prices = priceTexts.map(p => parseFloat(p.replace('$', ''))); + + const sorted = order === 'asc' + ? [...prices].sort((a, b) => a - b) + : [...prices].sort((a, b) => b - a); + + if (JSON.stringify(prices) !== JSON.stringify(sorted)) { + throw new Error(`Expected prices [${sorted}] but found [${prices}]`); + } + } + + public async validateNameSort(order: string) { + const names = await this.page.locator('[data-test="inventory-item-name"]').allTextContents(); + const sorted = order === 'asc' + ? [...names].sort() + : [...names].sort().reverse(); + + if (JSON.stringify(names) !== JSON.stringify(sorted)) { + throw new Error(`Expected names [${sorted}] but found [${names}]`); + } +} +} \ No newline at end of file diff --git a/pages/checkout.page.ts b/pages/checkout.page.ts new file mode 100644 index 0000000..438b49e --- /dev/null +++ b/pages/checkout.page.ts @@ -0,0 +1,52 @@ +import { Page } from "@playwright/test" +import userData from '../data/userDetails.json' + +export class Checkout { + private readonly page: Page + private readonly headerCartIcon: string = '[data-test="shopping-cart-link"]' + private readonly checkoutButton: string = '[data-test="checkout"]' + private readonly firstName: string = '[data-test="firstName"]' + private readonly lastName: string = '[data-test="lastName"]' + private readonly zip: string = '[data-test="postalCode"]' + private readonly checkoutContinue: string = '[data-test="continue"]' + private readonly finishButton: string = '[data-test="finish"]' + private readonly orderSuccessMessage: string = '[data-test="complete-header"]' + + + constructor(page: Page) { + this.page = page; + } + + public async goToCart() { + await this.page.locator(this.headerCartIcon).click() + } + + public async goToCheckout() { + await this.page.locator(this.checkoutButton).click() + } + + public async fillOutCheckoutForm() { + await this.page.locator(this.firstName).fill(userData.firstName) + await this.page.locator(this.lastName).fill(userData.lastName) + await this.page.locator(this.zip).fill(userData.ZIP) + } + + public async checkoutClickContinue() { + await this.page.locator(this.checkoutContinue).click() + } + + public async checkoutClickFinish() { + await this.page.locator(this.finishButton).click() + } + + public async orderSuccess() { + await this.page.locator(this.orderSuccessMessage) + const expectedMessage = 'Thank you for your order!'; + const actualMessage = await this.page.locator(this.orderSuccessMessage).textContent() + if (actualMessage?.trim() !== expectedMessage) { + throw new Error(`Expected "${expectedMessage}" but found "${actualMessage}"`) + } + } + + +} \ No newline at end of file diff --git a/pages/login.page.ts b/pages/login.page.ts index 5a01614..cff7e9f 100644 --- a/pages/login.page.ts +++ b/pages/login.page.ts @@ -6,6 +6,7 @@ export class Login { 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= '[data-test="error"]' constructor(page: Page) { this.page = page; @@ -13,7 +14,11 @@ export class Login { public async validateTitle(expectedTitle: string) { const pageTitle = await this.page.title(); - if (pageTitle !== expectedTitle) { + /** + * // This change is not necessary, + * but, if register isn't important it's better to check use toLowerCase() to avoid possible issues + */ + if (pageTitle.toLowerCase() !== expectedTitle.toLowerCase()) { throw new Error(`Expected title to be ${expectedTitle} but found ${pageTitle}`); } } @@ -23,4 +28,11 @@ export class Login { await this.page.locator(this.passwordField).fill(this.password) await this.page.locator(this.loginButton).click() } + + public async validateErrorMessage(expectedMessage: string) { + const actualMessage = await this.page.locator(this.errorMessage).textContent(); + if (actualMessage !== expectedMessage) { + throw new Error(`Expected error "${expectedMessage}" but found "${actualMessage}"`); + } +} } \ No newline at end of file diff --git a/steps/category.steps.ts b/steps/category.steps.ts new file mode 100644 index 0000000..16a4676 --- /dev/null +++ b/steps/category.steps.ts @@ -0,0 +1,15 @@ +import { Then } from '@cucumber/cucumber'; +import { getPage } from '../playwrightUtilities'; +import { Category } from '../pages/category.page'; + +Then('I sort items by {string}', async (sort: string) => { + await new Category(getPage()).sortBy(sort); +}); + +Then('items should be sorted by price {string}', async (order: string) => { + await new Category(getPage()).validatePriceSort(order); +}); + +Then('items should be sorted by name {string}', async (order: string) => { + await new Category(getPage()).validateNameSort(order); +}); \ No newline at end of file diff --git a/steps/checkout.steps.ts b/steps/checkout.steps.ts new file mode 100644 index 0000000..3337381 --- /dev/null +++ b/steps/checkout.steps.ts @@ -0,0 +1,28 @@ +import { Then } from "@cucumber/cucumber"; +import { getPage } from "../playwrightUtilities"; +import { Checkout } from '../pages/checkout.page'; + +Then('Select the cart \\(top-right)', async () => { + await new Checkout(getPage()).goToCart(); +}); + +Then('Click on Checkout button', async () => { + await new Checkout(getPage()).goToCheckout() +}); + +Then('I fill out checkout form', async () => { + await new Checkout(getPage()).fillOutCheckoutForm() +}); + +Then('I click on continue', async () => { + await new Checkout(getPage()).checkoutClickContinue() +}); + +Then('I click on Finish button', async () => { + await new Checkout(getPage()).checkoutClickFinish() +}); + + +Then('I have to validate success message', async () => { + await new Checkout(getPage()).orderSuccess() +}); \ No newline at end of file diff --git a/steps/login.steps.ts b/steps/login.steps.ts index c2aa0d8..ec922f3 100644 --- a/steps/login.steps.ts +++ b/steps/login.steps.ts @@ -8,4 +8,9 @@ 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 +}); + +Then('I should see the error message {string}', async (expectedMessage) => { + await new Login(getPage()).validateErrorMessage(expectedMessage); +}); + diff --git a/tsconfig.json b/tsconfig.json index 9776ba9..a2f17cf 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,6 +4,7 @@ "module": "CommonJS", "strict": true, "esModuleInterop": true, + "resolveJsonModule": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true }, @@ -13,4 +14,4 @@ "exclude": [ "node_modules" ] - } \ No newline at end of file +} \ No newline at end of file