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
25 changes: 18 additions & 7 deletions features/login.feature
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
Feature: Login Feature
Feature: Login

Background:
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"
# Verifies the correct site loaded.
Scenario: Login page displays the correct title
Then the page title should be "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
# Tests both ways a login can fail using a single outline.
Scenario Outline: Login failure displays an appropriate error
When I login as '<username>'
Then I should see an error containing '<error>'

Examples:
| username | error |
| locked_out_user | Sorry, this user has been locked out |
| invalid_user | Username and password do not match |

# Confirms the user was actually redirected after a successful login.
Scenario: Valid credentials navigate to the inventory page
When I login as 'standard_user'
Then I should be on the "inventory" page
30 changes: 21 additions & 9 deletions features/product.feature
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
Feature: Product Feature
Feature: Product

# All product tests require a logged-in user.
Background:
Given I open the "https://www.saucedemo.com/" page
When I login as 'standard_user'

# 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
Examples:
# TODO: extend the datatable to paramterize this test
| sort |
# Makes sure all products loaded on the page.
Scenario: Inventory page displays all products
Then the inventory should show 6 products

# Tests both sort options since either one can break on its own.
Scenario Outline: Products can be sorted by price
When I sort products by price "<sort>"
Then all products should be sorted by price "<sort>"

Examples:
| sort |
| low to high |
| high to low |

# Checks the cart icon updates when an item is added.
Scenario: Adding a product updates the cart badge count
When I add the backpack to the cart
Then the cart badge should show "1"
22 changes: 12 additions & 10 deletions features/purchase.feature
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
Feature: Purchase Feature
Feature: Purchase

# All purchase tests require a logged-in user.
Background:
Given I open the "https://www.saucedemo.com/" page
When I login as 'standard_user'

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!'
# Walks through the full purchase flow from cart to confirmation.
Scenario: A user can complete a purchase successfully
When I add the backpack to the cart
And I go to the cart
And I proceed to checkout
And I enter checkout details "John" "Doe" "12345"
And I continue to the order summary
And I place the order
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.

28 changes: 18 additions & 10 deletions pages/login.page.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,34 @@
import { Page } from "@playwright/test"
import { Page, expect } 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 passwordField: string = 'input[id="password"]'
private readonly loginButton: string = 'input[id="login-button"]'
private readonly errorBanner: string = '[data-test="error"]'

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);
}

// toContainText checks for a partial match so minor wording changes don't break the test.
public async validateErrorContains(fragment: string) {
await expect(this.page.locator(this.errorBanner)).toContainText(fragment);
}

// Checks the URL to confirm the page actually navigated, not just that something appeared.
public async validateCurrentUrl(urlFragment: string) {
await expect(this.page).toHaveURL(new RegExp(urlFragment));
}

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()
await this.page.locator(this.userNameField).fill(userName);
await this.page.locator(this.passwordField).fill(this.password);
await this.page.locator(this.loginButton).click();
}
}
}
43 changes: 40 additions & 3 deletions pages/product.page.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,51 @@
import { Page } from "@playwright/test"
import { Page, expect } 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 = '.product_sort_container'
private readonly inventoryItems: string = '.inventory_item'
private readonly itemPrices: string = '.inventory_item_price'
private readonly cartBadge: string = '.shopping_cart_badge'

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

public async addBackPackToCart() {
await this.page.locator(this.addToCart).click()
await this.page.locator(this.addToCart).click();
}
}

public async validateProductCount(expectedCount: number) {
await expect(this.page.locator(this.inventoryItems)).toHaveCount(expectedCount);
}

public async sortByPrice(sort: string) {
const value = sort === 'low to high' ? 'lohi' : 'hilo';
await this.page.locator(this.sortDropdown).selectOption(value);
}

public async validatePriceSortOrder(sort: string) {
const priceElements = await this.page.locator(this.itemPrices).all();
const prices: number[] = [];

for (const el of priceElements) {
const text = await el.textContent();
prices.push(parseFloat(text!.replace('$', '')));
}

// Compare each price to the next one to verify the order is correct.
for (let i = 0; i < prices.length - 1; i++) {
if (sort === 'low to high') {
expect(prices[i]).toBeLessThanOrEqual(prices[i + 1]);
} else {
expect(prices[i]).toBeGreaterThanOrEqual(prices[i + 1]);
}
}
}

// Checks the cart icon shows the right item count.
public async validateCartCount(expectedCount: string) {
await expect(this.page.locator(this.cartBadge)).toHaveText(expectedCount);
}
}
44 changes: 44 additions & 0 deletions pages/purchase.page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Page, expect } from "@playwright/test"

export class Purchase {
private readonly page: Page
private readonly cartIcon: string = '.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 zipField: string = '[data-test="postalCode"]'
private readonly continueButton: string = '[data-test="continue"]'
private readonly finishButton: string = '[data-test="finish"]'
private readonly successHeader: string = '.complete-header'

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

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

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

// Fills each field separately so it's clear which one failed.
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 continueCheckout() {
await this.page.locator(this.continueButton).click();
}

public async finishCheckout() {
await this.page.locator(this.finishButton).click();
}

public async validateSuccessMessage(expectedMessage: string) {
await expect(this.page.locator(this.successHeader)).toHaveText(expectedMessage);
}
}
20 changes: 14 additions & 6 deletions steps/login.steps.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import { Then } from '@cucumber/cucumber';
import { When, Then } from '@cucumber/cucumber';
import { getPage } from '../playwrightUtilities';
import { Login } from '../pages/login.page';

Then('I should see the title {string}', async (expectedTitle) => {
await new Login(getPage()).validateTitle(expectedTitle);
Then('the page title should be {string}', async (expectedTitle: string) => {
await new Login(getPage()).validateTitle(expectedTitle);
});

Then('I will login as {string}', async (userName) => {
await new Login(getPage()).loginAsUser(userName);
});
When('I login as {string}', async (userName: string) => {
await new Login(getPage()).loginAsUser(userName);
});

Then('I should see an error containing {string}', async (errorFragment: string) => {
await new Login(getPage()).validateErrorContains(errorFragment);
});

Then('I should be on the {string} page', async (urlFragment: string) => {
await new Login(getPage()).validateCurrentUrl(urlFragment);
});
25 changes: 21 additions & 4 deletions steps/product.steps.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,24 @@
import { Then } from '@cucumber/cucumber';
import { When, Then } from '@cucumber/cucumber';
import { getPage } from '../playwrightUtilities';
import { Product } from '../pages/product.page';

Then('I will add the backpack to the cart', async () => {
await new Product(getPage()).addBackPackToCart();
});
// {int} automatically converts the value to a number.
Then('the inventory should show {int} products', async (expectedCount: number) => {
await new Product(getPage()).validateProductCount(expectedCount);
});

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

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

Then('all products should be sorted by price {string}', async (sort: string) => {
await new Product(getPage()).validatePriceSortOrder(sort);
});

Then('the cart badge should show {string}', async (count: string) => {
await new Product(getPage()).validateCartCount(count);
});
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 { When, Then } from '@cucumber/cucumber';
import { getPage } from '../playwrightUtilities';
import { Purchase } from '../pages/purchase.page';

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

When('I proceed to checkout', async () => {
await new Purchase(getPage()).goToCheckout();
});

When('I enter checkout details {string} {string} {string}', async (firstName: string, lastName: string, zip: string) => {
await new Purchase(getPage()).fillCheckoutForm(firstName, lastName, zip);
});

When('I continue to the order summary', async () => {
await new Purchase(getPage()).continueCheckout();
});

When('I place the order', async () => {
await new Purchase(getPage()).finishCheckout();
});

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