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
9 changes: 6 additions & 3 deletions features/login.feature
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ 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 valid account login
Then I will login as 'standard_user'
Then I should see the Inventory page "https://www.saucedemo.com/inventory.html"
25 changes: 18 additions & 7 deletions features/product.feature
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,22 @@ 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
Scenario Outline: Validate product sort by price <sort>
Then I will login as 'standard_user'
Then I will sort the products by '<sort>'
Then I should see the products sorted by '<sort>'

Examples:
| sort |
| Price (high to low) |
| Price (low to high) |

Scenario Outline: Validate product sort by name <sort>
Then I will login as 'standard_user'
Then I will sort the products by '<sort>'
Then I should see the products sorted by '<sort>'

Examples:
# TODO: extend the datatable to paramterize this test
| sort |
| sort |
| Name (A to Z) |
| Name (Z to A) |
12 changes: 6 additions & 6 deletions features/purchase.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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!'
Then I will click the cart icon
Then I will click Checkout
Then I will fill in first name as 'Noble', last name as 'Obodum', and zip code as '28206'
Then I will click Continue
Then I will click Finish
Then I should see the confirmation message 'Thank you for your order!'
9 changes: 8 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions pages/login.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -23,4 +24,14 @@ 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 errorLocator = this.page.locator(this.errorMessage)
await errorLocator.waitFor({ state: 'visible' })
const actualMessage = (await errorLocator.textContent())?.trim()

if (actualMessage !== expectedMessage) {
throw new Error(`Expected error message to be ${expectedMessage} but found ${actualMessage}`)
}
}
}
69 changes: 69 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 sortDropdown: string = 'select[data-test="product-sort-container"]'
private readonly productPrices: string = '.inventory_item_price'
private readonly productNames: string = '.inventory_item_name'

constructor(page: Page) {
this.page = page;
Expand All @@ -11,4 +14,70 @@ export class Product {
public async addBackPackToCart() {
await this.page.locator(this.addToCart).click()
}

public async sortProductsByOption(sortOption: string) {
const dropdown = this.page.locator(this.sortDropdown)
await dropdown.waitFor({ state: 'visible' })
await dropdown.selectOption({ value: this.getSortOptionValue(sortOption) })
}

public async validateProductsSorted(sortOption: string) {
const normalized = sortOption.toLowerCase()

if (normalized.includes('price')) {
await this.validatePriceSort(sortOption)
return
}

if (normalized.includes('name')) {
await this.validateNameSort(sortOption)
return
}

throw new Error(`Unsupported sort option: ${sortOption}`)
}

private async validatePriceSort(sortOption: string) {
const prices = await this.page.locator(this.productPrices).allTextContents()
const values = prices.map((value) => Number(value.replace('$', '')))
const sortedValues = [...values].sort((a, b) => a - b)
const expectedValues = this.isHighToLow(sortOption) ? [...sortedValues].reverse() : sortedValues

if (JSON.stringify(values) !== JSON.stringify(expectedValues)) {
throw new Error(`Expected prices to be sorted ${sortOption}, but found ${values.join(', ')}`)
}
}

private async validateNameSort(sortOption: string) {
const names = await this.page.locator(this.productNames).allTextContents()
const normalizedNames = names.map((name) => name.trim().toLowerCase())
const sortedNames = [...normalizedNames].sort((a, b) => a.localeCompare(b))
const expectedNames = this.isNameDescending(sortOption) ? [...sortedNames].reverse() : sortedNames

if (JSON.stringify(normalizedNames) !== JSON.stringify(expectedNames)) {
throw new Error(`Expected names to be sorted ${sortOption}, but found ${names.join(', ')}`)
}
}

private getSortOptionValue(sortOption: string): string {
const normalized = sortOption.toLowerCase()

if (normalized.includes('price')) {
return normalized.includes('high') ? 'hilo' : 'lohi'
}

if (normalized.includes('name')) {
return normalized.includes('z to a') || normalized.includes('z') ? 'za' : 'az'
}

throw new Error(`Unsupported sort option: ${sortOption}`)
}

private isHighToLow(sortOption: string): boolean {
return sortOption.toLowerCase().includes('high')
}

private isNameDescending(sortOption: string): boolean {
return sortOption.toLowerCase().includes('z')
}
}
57 changes: 57 additions & 0 deletions pages/purchase.page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Page } from "@playwright/test"

export class Purchase {
private readonly page: Page
private readonly goToCart: string = 'a.shopping_cart_link'
private readonly checkoutButton: string = 'button[data-test="checkout"]'
private readonly firstNameField: string = 'input[id="first-name"]'
private readonly lastNameField: string = 'input[id="last-name"]'
private readonly zipField: string = 'input[id="postal-code"]'
private readonly continueButton: string = 'input[data-test="continue"]'
private readonly finishButton: string = 'button[data-test="finish"]'
private readonly confirmationMessage: string = 'h2.complete-header'

constructor(page: Page) {
this.page = page;
}

public async goToCartPage() {
const cartLink = this.page.locator(this.goToCart)
await cartLink.waitFor({ state: 'visible' })
await cartLink.click()
}

public async clickCheckout() {
const checkout = this.page.locator(this.checkoutButton)
await checkout.waitFor({ state: 'visible' })
await checkout.click()
}

public async fillCheckoutForm(firstName: string, lastName: string, zip: string) {
await this.page.locator(this.firstNameField).fill(firstName)
await this.page.locator(this.lastNameField).fill(lastName)
await this.page.locator(this.zipField).fill(zip)
}

public async clickContinue() {
const continueButton = this.page.locator(this.continueButton)
await continueButton.waitFor({ state: 'visible' })
await continueButton.click()
}

public async clickFinish() {
const finishButton = this.page.locator(this.finishButton)
await finishButton.waitFor({ state: 'visible' })
await finishButton.click()
}

public async validateConfirmationMessage(expectedMessage: string) {
const confirmation = this.page.locator(this.confirmationMessage)
await confirmation.waitFor({ state: 'visible' })
const actualMessage = (await confirmation.textContent())?.trim()

if (actualMessage !== expectedMessage) {
throw new Error(`Expected text to be ${expectedMessage} but found ${actualMessage}`)
}
}
}
15 changes: 14 additions & 1 deletion playwrightUtilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,22 @@ let browser: Browser | null = null;
let page: Page | null = null;
const DEFAULT_TIMEOUT = 30000;

const launchOptions = {
headless: true,
args: ['--no-sandbox', '--disable-dev-shm-usage'],
};

export const initializeBrowser = async () => {
if (!browser) {
browser = await chromium.launch({ headless: false });
try {
browser = await chromium.launch({
...launchOptions,
channel: 'chrome',
});
} catch (error) {
console.warn('System Chrome was not available, falling back to bundled Chromium.', error);
browser = await chromium.launch(launchOptions);
}
}
};

Expand Down
8 changes: 8 additions & 0 deletions steps/login.steps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 (expectedMessage) => {
await new Login(getPage()).validateErrorMessage(expectedMessage);
});

Then('I should see the Inventory page {string}', async (url) => {
await getPage().goto(url);
});
8 changes: 8 additions & 0 deletions steps/product.steps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,12 @@ import { Product } from '../pages/product.page';

Then('I will add the backpack to the cart', async () => {
await new Product(getPage()).addBackPackToCart();
});

Then('I will sort the products by {string}', async (sortOption: string) => {
await new Product(getPage()).sortProductsByOption(sortOption);
});

Then('I should see the products sorted by {string}', async (sortOption: string) => {
await new Product(getPage()).validateProductsSorted(sortOption);
});
30 changes: 30 additions & 0 deletions steps/purchase.steps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Then } from '@cucumber/cucumber';
import { getPage } from '../playwrightUtilities';
import { Purchase } from '../pages/purchase.page';

Then('I will click the cart icon', async () => {
await new Purchase(getPage()).goToCartPage();
});

Then('I will click Checkout', async () => {
await new Purchase(getPage()).clickCheckout();
});

Then(
'I will fill in first name as {string}, last name as {string}, and zip code as {string}',
async (firstName: string, lastName: string, zip: string) => {
await new Purchase(getPage()).fillCheckoutForm(firstName, lastName, zip)
}
)

Then('I will click Continue', async () => {
await new Purchase(getPage()).clickContinue();
});

Then('I will click Finish', async () => {
await new Purchase(getPage()).clickFinish();
});

Then('I should see the confirmation message {string}', async (expectedMessage: string) => {
await new Purchase(getPage()).validateConfirmationMessage(expectedMessage);
});
Loading