Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 24 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
17 changes: 14 additions & 3 deletions features/login.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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
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 <description>
When I attempt to login with username "<username>" and password "<password>"
Then I should see the error message "<error>"
Examples:
| description | username | password | error |
| empty username | | | Epic sadface: Username is required |
| empty password | standard_user | | Epic sadface: Password is required |
10 changes: 5 additions & 5 deletions features/product.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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 <sort>
Then I will login as 'standard_user'
# TODO: Sort the items by <sort>
# TODO: Validate all 6 items are sorted correctly by price
Then I sort the products by "<sort>"
Then the products should be sorted by price "<order>"
Examples:
# TODO: extend the datatable to paramterize this test
| sort |
| sort | order |
| Price (low to high) | asc |
| Price (high to low) | desc |
20 changes: 14 additions & 6 deletions features/purchase.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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!'
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"
15 changes: 15 additions & 0 deletions pages/login.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
}
}
}
27 changes: 27 additions & 0 deletions pages/product.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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}`);
}
}

}
54 changes: 54 additions & 0 deletions pages/purchase.page.ts
Original file line number Diff line number Diff line change
@@ -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}`)
}
}
}
12 changes: 10 additions & 2 deletions steps/login.steps.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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);
});
});

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);
});
14 changes: 13 additions & 1 deletion steps/product.steps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 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);
});
31 changes: 31 additions & 0 deletions steps/purchase.steps.ts
Original file line number Diff line number Diff line change
@@ -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);
});
Loading