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
4 changes: 2 additions & 2 deletions 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 message "Epic sadface: Sorry, this user has been locked out."

Scenario: Validate successful login
Then I will login as 'standard_user'
Then I should see the products page
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 will sort the items by "<sort>"
Then I should see all products sorted by price "<sort>"

Examples:
# TODO: extend the datatable to paramterize this test
| sort |
| sort |
| Price (low to high) |
| Price (high to low) |
18 changes: 9 additions & 9 deletions features/purchase.feature
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ 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 will open the cart
Then I will proceed to checkout
Then I will fill checkout information with "John" "Doe" "12345"
Then I will continue checkout
Then I will finish checkout
Then I should see the successful purchase text "Thank you for your order!"
19 changes: 13 additions & 6 deletions pages/login.page.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,33 @@
import { Page } from "@playwright/test"
import { expect, Page } from "@playwright/test"

export class Login {
private readonly page: Page
private readonly password: string = 'secret_sauce'
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 = 'h3[data-test="error"]'
private readonly productsTitle: string = '[data-test="title"]'

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

public async validateTitle(expectedTitle: string) {
const pageTitle = await this.page.title();
if (pageTitle !== expectedTitle) {
throw new Error(`Expected title to be ${expectedTitle} but found ${pageTitle}`);
}
await expect(this.page).toHaveTitle(expectedTitle)
}

public async loginAsUser(userName: string) {
await this.page.locator(this.userNameField).fill(userName)
await this.page.locator(this.passwordField).fill(this.password)
await this.page.locator(this.loginButton).click()
}
}

public async validateLoginErrorMessage(expectedErrorMessage: string) {
await expect(this.page.locator(this.errorMessage)).toHaveText(expectedErrorMessage)
}

public async validateProductsPage() {
await expect(this.page.locator(this.productsTitle)).toHaveText('Products')
}
}
38 changes: 36 additions & 2 deletions pages/product.page.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { Page } from "@playwright/test"
import { expect, 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 priceSortOptions: Record<string, string> = {
'Price (low to high)': 'lohi',
'Price (high to low)': 'hilo',
}

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

public async sortItemsBy(sortOption: string) {
const optionValue = this.priceSortOptions[sortOption]

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

await this.page.locator(this.sortDropdown).selectOption(optionValue)
}

public async validateProductsSortedByPrice(sortOption: string) {
const prices = await this.getProductPrices()
const sortedPrices = [...prices].sort((firstPrice, secondPrice) => {
return sortOption === 'Price (high to low)'
? secondPrice - firstPrice
: firstPrice - secondPrice
})

expect(prices).toHaveLength(6)
expect(prices).toEqual(sortedPrices)
}

private async getProductPrices() {
const priceTexts = await this.page.locator(this.productPrices).allTextContents()

return priceTexts.map((priceText) => Number(priceText.replace('$', '')))
}
}
43 changes: 43 additions & 0 deletions pages/purchase.page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { expect, Page } from "@playwright/test"

export class Purchase {
private readonly page: Page
private readonly cartLink: string = '[data-test="shopping-cart-link"]'
private readonly checkoutButton: string = 'button[id="checkout"]'
private readonly firstNameField: string = 'input[id="first-name"]'
private readonly lastNameField: string = 'input[id="last-name"]'
private readonly postalCodeField: string = 'input[id="postal-code"]'
private readonly continueButton: string = 'input[id="continue"]'
private readonly finishButton: string = 'button[id="finish"]'
private readonly completeHeader: string = '[data-test="complete-header"]'

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

public async openCart() {
await this.page.locator(this.cartLink).click()
}

public async proceedToCheckout() {
await this.page.locator(this.checkoutButton).click()
}

public async fillCheckoutInformation(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 finishCheckout() {
await this.page.locator(this.finishButton).click()
}

public async validateSuccessfulPurchaseText(expectedText: string) {
await expect(this.page.locator(this.completeHeader)).toHaveText(expectedText)
}
}
5 changes: 3 additions & 2 deletions playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import { PlaywrightTestConfig } from '@playwright/test';

const config: PlaywrightTestConfig = {
use: {
headless: false,
headless: process.env.HEADLESS !== 'false',
channel: process.env.BROWSER_CHANNEL,
},
};

export default config;
export default config;
27 changes: 25 additions & 2 deletions playwrightUtilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import { Browser, chromium, Page } from 'playwright';
let browser: Browser | null = null;
let page: Page | null = null;
const DEFAULT_TIMEOUT = 30000;
const HEADLESS = process.env.HEADLESS !== 'false';
const BROWSER_CHANNEL = process.env.BROWSER_CHANNEL;

export const initializeBrowser = async () => {
if (!browser) {
browser = await chromium.launch({ headless: false });
browser = await launchBrowser();
}
};

Expand All @@ -30,4 +32,25 @@ export const closeBrowser = async () => {
browser = null;
page = null;
}
};
};

const launchBrowser = async () => {
const launchOptions = getLaunchOptions(BROWSER_CHANNEL);

try {
return await chromium.launch(launchOptions);
} catch (launchError) {
if (BROWSER_CHANNEL) {
throw launchError;
}

return await chromium.launch(getLaunchOptions('chrome'));
}
};

const getLaunchOptions = (channel?: string): Parameters<typeof chromium.launch>[0] => {
return {
headless: HEADLESS,
...(channel ? { channel } : {}),
};
};
10 changes: 9 additions & 1 deletion 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 message {string}', async (expectedErrorMessage) => {
await new Login(getPage()).validateLoginErrorMessage(expectedErrorMessage);
});

Then('I should see the products page', async () => {
await new Login(getPage()).validateProductsPage();
});
10 changes: 9 additions & 1 deletion 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 items by {string}', async (sortOption) => {
await new Product(getPage()).sortItemsBy(sortOption);
});

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

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

Then('I will proceed to checkout', async () => {
await new Purchase(getPage()).proceedToCheckout();
});

Then('I will fill checkout information with {string} {string} {string}', async (firstName, lastName, postalCode) => {
await new Purchase(getPage()).fillCheckoutInformation(firstName, lastName, postalCode);
});

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

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

Then('I should see the successful purchase text {string}', async (expectedText) => {
await new Purchase(getPage()).validateSuccessfulPurchaseText(expectedText);
});
Loading