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
Binary file added .DS_Store
Binary file not shown.
2 changes: 1 addition & 1 deletion cucumber.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module.exports = {
default: `--require-module ts-node/register --require './steps/**/*.ts' --require './hooks/**/*.ts --format @cucumber/pretty-formatter`
default: `--require-module ts-node/register --require './steps/**/*.ts' --require './hooks/**/*.ts' --format @cucumber/pretty-formatter`
};
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 login error "Epic sadface: Sorry, this user has been locked out."

Scenario: Validate standard_user can login successfully
Then I will login as 'standard_user'
Then I should see the products page header "Products"
15 changes: 8 additions & 7 deletions features/product.feature
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ 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 sort products by price "<sort>"
Then I should see the products sorted by price "<sort>"

Examples:
# TODO: extend the datatable to paramterize this test
| sort |
| sort |
| Price (low to high)|
| Price (high to low)|
17 changes: 8 additions & 9 deletions features/purchase.feature
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,11 @@ Feature: Purchase Feature
Background:
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!'
Scenario: Validate successful purchase text
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 with first name "John" last name "Doe" postal code "12345"
Then I continue the checkout
Then I finish the checkout
Then I should see the order confirmation "Thank you for your order!"
2 changes: 1 addition & 1 deletion package-lock.json

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

27 changes: 27 additions & 0 deletions pages/login.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,32 @@ export class Login {
await this.page.locator(this.userNameField).fill(userName)
await this.page.locator(this.passwordField).fill(this.password)
await this.page.locator(this.loginButton).click()

// Wait for navigation to inventory page (or error message).
await Promise.race([
this.page.waitForURL('**/inventory.html', { timeout: 10000 }),
this.page.locator('[data-test="error"]').waitFor({ state: 'visible', timeout: 10000 })
]).catch(() => null);

// If login succeeded, ensure inventory content is visible.
if (this.page.url().includes('/inventory.html')) {
await this.page.waitForSelector('.inventory_list', { timeout: 10000 });
}
}

public async validateProductsPage(expectedHeader: string) {
const actualHeader = (await this.page.locator('.title').textContent())?.trim() || '';
if (actualHeader !== expectedHeader) {
throw new Error(`Expected products page header to be "${expectedHeader}" but found "${actualHeader}"`);
}
}

public async validateErrorMessage(expectedError: string) {
const errorLocator = this.page.locator('[data-test="error"]');
const actualError = (await errorLocator.textContent())?.trim() || '';

if (actualError !== expectedError) {
throw new Error(`Expected login error to be "${expectedError}" but found "${actualError}"`);
}
}
}
39 changes: 39 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 cartButton: string = '.shopping_cart_link'
private readonly sortSelect: string = 'select.product_sort_container'
private readonly itemPrices: string = '.inventory_item_price'

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

public async sortByPrice(option: string) {
const value = option === 'Price (low to high)' ? 'lohi' : 'hilo'
const sortLocator = this.page.locator(this.sortSelect)
await sortLocator.waitFor({ state: 'visible', timeout: 10000 })

const selected = await sortLocator.selectOption(value)
if (!selected || selected.length === 0) {
throw new Error(`Cannot set sort option '${option}' (value: '${value}')`)
}

await this.page.waitForTimeout(500)
}

public async getPrices(): Promise<number[]> {
const priceElements = this.page.locator(this.itemPrices)
const count = await priceElements.count()
const prices: number[] = []

for (let i = 0; i < count; i++) {
const text = (await priceElements.nth(i).textContent())?.trim() || ''
const price = Number(text.replace('$', ''))
prices.push(price)
}

return prices
}

public async addBackPackAndGoToCart() {
await this.addBackPackToCart()
await this.page.locator(this.cartButton).click()
}

public async goToCart() {
await this.page.locator(this.cartButton).click()
}
}
36 changes: 36 additions & 0 deletions pages/purchase.page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Page } from "@playwright/test";

export class Purchase {
private readonly page: Page;

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

public async goToCart() {
await this.page.locator('.shopping_cart_link').click();
}

public async clickCheckout() {
await this.page.locator('[data-test="checkout"]').click();
}

public async fillCheckoutInfo(firstName: string, lastName: string, postalCode: string) {
await this.page.locator('[data-test="firstName"]').fill(firstName);
await this.page.locator('[data-test="lastName"]').fill(lastName);
await this.page.locator('[data-test="postalCode"]').fill(postalCode);
}

public async continueCheckout() {
await this.page.locator('[data-test="continue"]').click();
}

public async finishCheckout() {
await this.page.locator('[data-test="finish"]').click();
}

public async getConfirmationText(): Promise<string> {
const text = await this.page.locator('.complete-header').textContent();
return text ? text.trim() : '';
}
}
7 changes: 5 additions & 2 deletions playwrightUtilities.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { Browser, chromium, Page } from 'playwright';
import { Browser, chromium, Page } from '@playwright/test';

let browser: Browser | null = null;
let page: Page | null = null;
const DEFAULT_TIMEOUT = 30000;

export const initializeBrowser = async () => {
if (!browser) {
browser = await chromium.launch({ headless: false });
browser = await chromium.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage'],
});
}
};

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 login error {string}', async (expectedError) => {
await new Login(getPage()).validateErrorMessage(expectedError);
});

Then('I should see the products page header {string}', async (expectedHeader) => {
await new Login(getPage()).validateProductsPage(expectedHeader);
});
32 changes: 31 additions & 1 deletion steps/product.steps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,34 @@ import { Product } from '../pages/product.page';

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

Then('I go to the cart', async () => {
await new Product(getPage()).goToCart();
});

Then('I sort products by price {string}', async (sortOption) => {
await new Product(getPage()).sortByPrice(sortOption);
});

Then('I should see the products sorted by price {string}', async (sortOption) => {
const product = new Product(getPage());
const prices = await product.getPrices();

if (prices.length !== 6) {
throw new Error(`Expected 6 products, but found ${prices.length}`);
}

const sorted = [...prices];
sorted.sort((a, b) => a - b);

if (sortOption === 'Price (high to low)') {
sorted.reverse();
}

const match = sorted.every((value, index) => value === prices[index]);

if (!match) {
throw new Error(`Expected products to be sorted by ${sortOption}, but were ${prices}`);
}
});
25 changes: 25 additions & 0 deletions steps/purchase.steps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Then } from '@cucumber/cucumber';
import { getPage } from '../playwrightUtilities';
import { Purchase } from '../pages/purchase.page';

Then('I checkout with first name {string} last name {string} postal code {string}', async (firstName, lastName, postalCode) => {
const purchase = new Purchase(getPage());
await purchase.clickCheckout();
await purchase.fillCheckoutInfo(firstName, lastName, postalCode);
});

Then('I continue the checkout', async () => {
await new Purchase(getPage()).continueCheckout();
});

Then('I finish the checkout', async () => {
await new Purchase(getPage()).finishCheckout();
});

Then('I should see the order confirmation {string}', async (expectedText) => {
const text = await new Purchase(getPage()).getConfirmationText();

if (text !== expectedText) {
throw new Error(`Expected order confirmation to be "${expectedText}" but found "${text}"`);
}
});
Loading