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
13 changes: 13 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"cucumberautocomplete.steps": [
"steps/**/*.ts"
],
"cucumberautocomplete.syncfeatures": "features/**/*.feature",
"cucumberautocomplete.strictGherkinCompletion": true,
"cucumberautocomplete.smartSnippets": true,
"editor.quickSuggestions": {
"comments": false,
"strings": true,
"other": true
}
}
5 changes: 2 additions & 3 deletions features/login.feature
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@ 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
And I should see the error message "Epic sadface: Sorry, this user has been locked out."
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'
And I will sort products by "<sort>"
Then all 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 |
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'
And I will add the backpack to the cart
And I go to the cart
And I click checkout
And I fill in my details with first name "John" last name "Doe" and zip "12345"
And I click continue
And I click finish
Then I should see the confirmation message "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.

12 changes: 12 additions & 0 deletions pages/login.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ 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 lockOutMessage: string = '[data-test="error"]'
private readonly lockedOutMessage: string = 'This user has been locked out.'


constructor(page: Page) {
this.page = page;
Expand All @@ -23,4 +26,13 @@ export class Login {
await this.page.locator(this.passwordField).fill(this.password)
await this.page.locator(this.loginButton).click()
}

// Waits for the error message to appear, then checks it matches the expected text
public async validateErrorMessage(expectedMessage: string) {
await this.page.locator(this.lockOutMessage).waitFor({ state: 'visible' })
const actualMessage = await this.page.locator(this.lockOutMessage).innerText()
if (actualMessage !== expectedMessage) {
throw new Error(`Expected error message "${expectedMessage}" but found "${actualMessage}"`)
}
}
}
32 changes: 32 additions & 0 deletions pages/product.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,44 @@ 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 itemPrice: string = '.inventory_item_price'

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

// Clicks the "Add to cart" button for the Sauce Labs Backpack item
public async addBackPackToCart() {
await this.page.locator(this.addToCart).click()
}

// Selects an option from the sort dropdown (e.g. "Price (low to high)")
public async sortBy(sortLabel: string) {
await this.page.locator(this.sortDropdown).selectOption({ label: sortLabel })
}

// Reads every product price on the page and returns them as numbers (e.g. [9.99, 15.99])
public async getPrices(): Promise<number[]> {
const priceTexts = await this.page.locator(this.itemPrice).allTextContents()
return priceTexts.map(text => parseFloat(text.replace('$', '')))
}

// Checks that the product prices are in the expected order ('asc' or 'desc'), throws if not
public async validateSortedByPrice(order: string) {
const prices = await this.getPrices()

for (let i = 1; i < prices.length; i++) {
const previousPrice = prices[i - 1]
const currentPrice = prices[i]

if (order === 'asc' && currentPrice < previousPrice) {
throw new Error(`Expected prices to be sorted low to high: [${prices.join(', ')}]`)
}

if (order === 'desc' && currentPrice > previousPrice) {
throw new Error(`Expected prices to be sorted high to low: [${prices.join(', ')}]`)
}
}
}
}
52 changes: 52 additions & 0 deletions pages/purchase.page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Page } 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 confirmationMessage: string = '[data-test="complete-header"]'

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

// Clicks the cart icon in the top-right to open the cart
public async goToCart() {
await this.page.locator(this.cartIcon).click()
}

// Clicks the Checkout button inside the cart
public async clickCheckout() {
await this.page.locator(this.checkoutButton).click()
}

// Fills in the customer information form fields
public async fillInDetails(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)
}

// Clicks Continue to proceed to the order summary
public async clickContinue() {
await this.page.locator(this.continueButton).click()
}

// Clicks Finish to complete the purchase
public async clickFinish() {
await this.page.locator(this.finishButton).click()
}

// Reads the confirmation message and checks it matches the expected text
public async validateConfirmationMessage(expectedMessage: string) {
const actualMessage = await this.page.locator(this.confirmationMessage).innerText()
if (actualMessage !== expectedMessage) {
throw new Error(`Expected "${expectedMessage}" but found "${actualMessage}"`)
}
}
}
2 changes: 1 addition & 1 deletion steps/common.steps.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Given } from "@cucumber/cucumber";
import { getPage } from "../playwrightUtilities";

Given('I open the {string} page', async (url) => {
Given('I open the {string} page', async (url: string) => {
await getPage().goto(url);
});
10 changes: 7 additions & 3 deletions steps/login.steps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ import { Then } from '@cucumber/cucumber';
import { getPage } from '../playwrightUtilities';
import { Login } from '../pages/login.page';

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

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

Then('I should see the error message {string}', async (expectedMessage: string) => {
await new Login(getPage()).validateErrorMessage(expectedMessage);
});
9 changes: 9 additions & 0 deletions steps/product.steps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,13 @@ 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 products by {string}', async (sortLabel: string) => {
await new Product(getPage()).sortBy(sortLabel);
});


Then('all products should be sorted by price {string}', async (order: string) => {
await new Product(getPage()).validateSortedByPrice(order);
});
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 go to the cart', async () => {
await new Purchase(getPage()).goToCart();
});

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

Then('I fill in my details with first name {string} last name {string} and zip {string}', async (firstName: string, lastName: string, zip: string) => {
await new Purchase(getPage()).fillInDetails(firstName, lastName, zip);
});

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

Then('I 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