diff --git a/README.md b/README.md index b9111057..f7d348ec 100644 --- a/README.md +++ b/README.md @@ -47,12 +47,29 @@ It is not expected that you complete every task, however, please give your best You will be scored based on your ability to complete the following tasks: -- [ ] Install and setup this repository on your personal computer -- [ ] Complete the automation tasks listed below +- [x] Install and setup this repository on your personal computer +- [x] 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 + +## Implemented Tests + +All four required tasks are complete, plus additional bonus coverage. The full suite runs **9 scenarios (38 steps)**, all passing. + +### Required tasks +- **Login page title** ([login.feature](features/login.feature)) — corrected the expected title to "Swag Labs" so the failing scenario passes. +- **Login error message** ([login.feature](features/login.feature)) — added a step asserting the locked-out user's error banner. +- **Successful purchase flow** ([purchase.feature](features/purchase.feature)) — implemented the full checkout (cart → checkout → customer info → continue → finish → order confirmation) via a new [purchase.steps.ts](steps/purchase.steps.ts) and [purchase.page.ts](pages/purchase.page.ts). +- **Product price sort** ([product.feature](features/product.feature)) — parameterized the price (low→high / high→low) sort using a Scenario Outline and Examples table. + +### Bonus coverage +Added to cover important paths the original suite missed: + +- **Parameterized login errors** ([login.feature](features/login.feature)) — a Scenario Outline validating the "Username is required" and "Password is required" errors for empty credentials. Reuses the existing error-message validation to demonstrate data-driven testing. +- **Successful login happy-path** ([login.feature](features/login.feature)) — confirms a valid login lands on the inventory page and renders all 6 products (the suite previously had no positive login assertion). +- **Checkout required-field validation** ([purchase.feature](features/purchase.feature)) — confirms the checkout form blocks continuing without customer info, asserting the "First Name is required" error. diff --git a/features/login.feature b/features/login.feature index fb9f1fa5..79ae5f86 100644 --- a/features/login.feature +++ b/features/login.feature @@ -4,9 +4,20 @@ 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 error message "Epic sadface: Sorry, this user has been locked out." + + Scenario: Validate successful login displays the inventory page + Then I will login as 'standard_user' + Then I should see 6 products on the inventory page + + Scenario Outline: Validate login error for + When I attempt to login with username "" and password "" + Then I should see the error message "" + Examples: + | description | username | password | error | + | empty username | | | Epic sadface: Username is required | + | empty password | standard_user | | Epic sadface: Password is required | diff --git a/features/product.feature b/features/product.feature index 8a7ceab9..9288118f 100644 --- a/features/product.feature +++ b/features/product.feature @@ -3,11 +3,11 @@ 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 sort the products by "" + Then the products should be sorted by price "" Examples: - # TODO: extend the datatable to paramterize this test - | sort | \ No newline at end of file + | sort | order | + | Price (low to high) | asc | + | Price (high to low) | desc | diff --git a/features/purchase.feature b/features/purchase.feature index 28634789..f8a41948 100644 --- a/features/purchase.feature +++ b/features/purchase.feature @@ -6,9 +6,17 @@ 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 I go to the cart + Then I checkout + Then I fill in the checkout information with first name "Brian", last name "Johnson", and postal code "12345" + Then I continue to the overview + Then I finish the checkout + Then I should see the text "Thank you for your order!" + + Scenario: Validate checkout requires customer information + Then I will login as 'standard_user' + Then I will add the backpack to the cart + Then I go to the cart + Then I checkout + Then I continue to the overview + Then I should see the checkout error "Error: First Name is required" diff --git a/pages/login.page.ts b/pages/login.page.ts index 5a01614b..79262c19 100644 --- a/pages/login.page.ts +++ b/pages/login.page.ts @@ -23,4 +23,19 @@ export class Login { await this.page.locator(this.passwordField).fill(this.password) await this.page.locator(this.loginButton).click() } + + public async attemptLogin(userName: string, password: string) { + await this.page.locator(this.userNameField).fill(userName) + await this.page.locator(this.passwordField).fill(password) + await this.page.locator(this.loginButton).click() + } + + private readonly errorMessage: string = '[data-test="error"]'; + + public async validateErrorMessage(expectedError: string) { + const actualError = await this.page.locator(this.errorMessage).textContent(); + if (actualError !== expectedError) { + throw new Error(`Expected error to be ${expectedError} but found ${actualError}`); + } + } } \ No newline at end of file diff --git a/pages/product.page.ts b/pages/product.page.ts index 14bedb1b..0c7f1ce4 100644 --- a/pages/product.page.ts +++ b/pages/product.page.ts @@ -3,6 +3,9 @@ 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 sortContainer: string = '[data-test="product-sort-container"]'; + private readonly itemPrice: string = '[data-test="inventory-item-price"]'; + private readonly inventoryItem: string = '[data-test="inventory-item"]'; constructor(page: Page) { this.page = page; @@ -11,4 +14,28 @@ export class Product { public async addBackPackToCart() { await this.page.locator(this.addToCart).click() } + + public async sortBy(sortOption: string) { + await this.page.locator(this.sortContainer).selectOption({ label: sortOption }); + } + + + public async validatePriceSort(order: string) { + const priceTexts = await this.page.locator(this.itemPrice).allTextContents(); + const prices = priceTexts.map(t => parseFloat(t.replace('$', ''))); + const expected = [...prices].sort((a, b) => order === 'asc' ? a - b : b - a); + if (JSON.stringify(prices) !== JSON.stringify(expected)) { + throw new Error(`Prices not sorted ${order}: got ${prices}, expected ${expected}`); + } + } + + public async validateInventoryPage(expectedCount: number) { + await this.page.waitForURL('**/inventory.html'); + await this.page.locator(this.inventoryItem).first().waitFor(); + const count = await this.page.locator(this.inventoryItem).count(); + if (count !== expectedCount) { + throw new Error(`Expected ${expectedCount} products on the inventory page but found ${count}`); + } + } + } \ No newline at end of file diff --git a/pages/purchase.page.ts b/pages/purchase.page.ts new file mode 100644 index 00000000..70cc0b8a --- /dev/null +++ b/pages/purchase.page.ts @@ -0,0 +1,54 @@ +import { Page } from "@playwright/test" + +export class Purchase { + private readonly page: Page + private readonly cartLink: string = '[data-test="shopping-cart-link"]' + private readonly checkoutButton: string = '[data-test="checkout"]' + private readonly firstNameField: string = '[data-test="firstName"]' + private readonly lastNameField: string = '[data-test="lastName"]' + private readonly postalCodeField: string = '[data-test="postalCode"]' + private readonly continueButton: string = '[data-test="continue"]' + private readonly finishButton: string = '[data-test="finish"]' + private readonly completeHeader: string = '[data-test="complete-header"]' + private readonly errorMessage: string = '[data-test="error"]' + + constructor(page: Page) { + this.page = page; + } + + public async goToCart() { + await this.page.locator(this.cartLink).click() + } + + public async checkout() { + await this.page.locator(this.checkoutButton).click() + } + + public async fillCheckoutInfo(firstName: string, lastName: string, postalCode: string) { + await this.page.locator(this.firstNameField).fill(firstName) + await this.page.locator(this.lastNameField).fill(lastName) + await this.page.locator(this.postalCodeField).fill(postalCode) + } + + public async continueCheckout() { + await this.page.locator(this.continueButton).click() + } + + public async finish() { + await this.page.locator(this.finishButton).click() + } + + public async validateCompleteHeader(expectedText: string) { + const actual = await this.page.locator(this.completeHeader).textContent() + if (actual !== expectedText) { + throw new Error(`Expected text to be ${expectedText} but found ${actual}`) + } + } + + public async validateCheckoutError(expectedError: string) { + const actual = await this.page.locator(this.errorMessage).textContent() + if (actual !== expectedError) { + throw new Error(`Expected checkout error to be ${expectedError} but found ${actual}`) + } + } +} \ No newline at end of file diff --git a/steps/login.steps.ts b/steps/login.steps.ts index c2aa0d80..12b20e42 100644 --- a/steps/login.steps.ts +++ b/steps/login.steps.ts @@ -1,4 +1,4 @@ -import { Then } from '@cucumber/cucumber'; +import { Then, When } from '@cucumber/cucumber'; import { getPage } from '../playwrightUtilities'; import { Login } from '../pages/login.page'; @@ -8,4 +8,12 @@ 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 (expectedError) => { + await new Login(getPage()).validateErrorMessage(expectedError); +}); + +When('I attempt to login with username {string} and password {string}', async (userName, password) => { + await new Login(getPage()).attemptLogin(userName, password); +}); diff --git a/steps/product.steps.ts b/steps/product.steps.ts index bb52fb98..317950a3 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(); -}); \ No newline at end of file +}); + +Then('I sort the products by {string}', async (sortOption) => { + await new Product(getPage()).sortBy(sortOption); +}); + +Then('the products should be sorted by price {string}', async (order) => { + await new Product(getPage()).validatePriceSort(order); +}); + +Then('I should see {int} products on the inventory page', async (count) => { + await new Product(getPage()).validateInventoryPage(count); +}); diff --git a/steps/purchase.steps.ts b/steps/purchase.steps.ts new file mode 100644 index 00000000..11d24dca --- /dev/null +++ b/steps/purchase.steps.ts @@ -0,0 +1,31 @@ +import { Then } from '@cucumber/cucumber'; +import { getPage } from '../playwrightUtilities'; +import { Purchase } from '../pages/purchase.page'; + +Then('I go to the cart', async () => { + await new Purchase(getPage()).goToCart(); +}); + +Then('I checkout', async () => { + await new Purchase(getPage()).checkout(); +}); + +Then('I fill in the checkout information with first name {string}, last name {string}, and postal code {string}', async (firstName, lastName, postalCode) => { + await new Purchase(getPage()).fillCheckoutInfo(firstName, lastName, postalCode); +}); + +Then('I continue to the overview', async () => { + await new Purchase(getPage()).continueCheckout(); +}); + +Then('I finish the checkout', async () => { + await new Purchase(getPage()).finish(); +}); + +Then('I should see the text {string}', async (expectedText) => { + await new Purchase(getPage()).validateCompleteHeader(expectedText); +}); + +Then('I should see the checkout error {string}', async (expectedError) => { + await new Purchase(getPage()).validateCheckoutError(expectedError); +});