diff --git a/cucumber.js b/cucumber.js index e8192d63..3ef5820c 100644 --- a/cucumber.js +++ b/cucumber.js @@ -1,3 +1,3 @@ module.exports = { - default: `--require-module ts-node/register --require './steps/**/*.ts' --require './hooks/**/*.ts --format @cucumber/pretty-formatter` -}; \ No newline at end of file + default: `--require-module ts-node/register --require './steps/**/*.ts' --require './hooks/**/*.ts' --format @cucumber/pretty-formatter` +}; diff --git a/features/login.feature b/features/login.feature index fb9f1fa5..15db918a 100644 --- a/features/login.feature +++ b/features/login.feature @@ -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 \ No newline at end of file + 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 diff --git a/features/product.feature b/features/product.feature index 8a7ceab9..5ae3cf5d 100644 --- a/features/product.feature +++ b/features/product.feature @@ -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 - Then I will login as 'standard_user' - # TODO: Sort the items by - # TODO: Validate all 6 items are sorted correctly by price + Scenario Outline: Validate product sort by price + Then I will login as 'standard_user' + Then I will sort the items by "" + Then I should see all products sorted by price "" + Examples: - # TODO: extend the datatable to paramterize this test - | sort | \ No newline at end of file + | sort | + | Price (low to high) | + | Price (high to low) | diff --git a/features/purchase.feature b/features/purchase.feature index 28634789..83208287 100644 --- a/features/purchase.feature +++ b/features/purchase.feature @@ -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!' \ No newline at end of file + 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!" diff --git a/pages/login.page.ts b/pages/login.page.ts index 5a01614b..8a9d1412 100644 --- a/pages/login.page.ts +++ b/pages/login.page.ts @@ -1,4 +1,4 @@ -import { Page } from "@playwright/test" +import { expect, Page } from "@playwright/test" export class Login { private readonly page: Page @@ -6,16 +6,15 @@ 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 = '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) { @@ -23,4 +22,12 @@ export class Login { await this.page.locator(this.passwordField).fill(this.password) await this.page.locator(this.loginButton).click() } -} \ No newline at end of file + + 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') + } +} diff --git a/pages/product.page.ts b/pages/product.page.ts index 14bedb1b..a3218682 100644 --- a/pages/product.page.ts +++ b/pages/product.page.ts @@ -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 = { + 'Price (low to high)': 'lohi', + 'Price (high to low)': 'hilo', + } constructor(page: Page) { this.page = page; @@ -11,4 +17,32 @@ export class Product { public async addBackPackToCart() { await this.page.locator(this.addToCart).click() } -} \ No newline at end of file + + 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('$', ''))) + } +} diff --git a/pages/purchase.page.ts b/pages/purchase.page.ts new file mode 100644 index 00000000..7de195f4 --- /dev/null +++ b/pages/purchase.page.ts @@ -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) + } +} diff --git a/playwright.config.ts b/playwright.config.ts index 7adb387b..7b60c500 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -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; \ No newline at end of file +export default config; diff --git a/playwrightUtilities.ts b/playwrightUtilities.ts index 93244a29..6b6008d6 100644 --- a/playwrightUtilities.ts +++ b/playwrightUtilities.ts @@ -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(); } }; @@ -30,4 +32,25 @@ export const closeBrowser = async () => { browser = null; page = null; } -}; \ No newline at end of file +}; + +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[0] => { + return { + headless: HEADLESS, + ...(channel ? { channel } : {}), + }; +}; diff --git a/steps/login.steps.ts b/steps/login.steps.ts index c2aa0d80..1f6563e5 100644 --- a/steps/login.steps.ts +++ b/steps/login.steps.ts @@ -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); -}); \ No newline at end of file +}); + +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(); +}); diff --git a/steps/product.steps.ts b/steps/product.steps.ts index bb52fb98..f8623b93 100644 --- a/steps/product.steps.ts +++ b/steps/product.steps.ts @@ -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(); -}); \ No newline at end of file +}); + +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); +}); diff --git a/steps/purchase.steps.ts b/steps/purchase.steps.ts new file mode 100644 index 00000000..cf36500e --- /dev/null +++ b/steps/purchase.steps.ts @@ -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); +});