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.
24 changes: 12 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,16 @@ node >= v18.5.x

npm >= v7


## Setup

// Install Visual Studio Code (or any editor)

https://code.visualstudio.com/download


// Install Node.js

https://nodejs.org/en/download


```bash
git clone https://github.com/automationExamples/Playwright-Cucumber-Exercise.git
npm install
Expand All @@ -31,28 +28,31 @@ Cucumber v1.7.0

Cucumber (Gherkin) Support enhanced for Behat


## Instructions

To run the test

```bash
npm run test
```

After running, to generate the cucumber report (cucumber_report.html)

```bash
npm run report
```

It is not expected that you complete every task, however, please give your best effort
It is not expected that you complete every task, however, please give your best effort

You will be scored based on your ability to complete the following tasks:

- [ ] Install and setup this repository on your personal computer
- [ ] Complete the automation tasks listed below
- [Y] Install and setup this repository on your personal computer
- [Y] Complete the automation tasks listed below

### Tasks
- [ ] Modify the scenario 'Validate the login page title' from [login.feature](features/login.feature#8) which runs but fails. Determine the cause of the failure and update the scenario to pass in the test
- [ ] Extend the scenario 'Validate login error message' from [login.feature](features/login.feature#10) which runs and passes but is missing a step. Extend the scenario to validate the error message received.
- [ ] Modify and extend the 'Validate successful purchase text' from [purchase.feature](features/purchase.feature#6) with steps for each comment listed. Consider writing a new steps.ts file along with an appropriate page.ts
- [ ] Modify and extend the 'Validate product sort by price sort' from [product.feature](features/product.feature#6) with steps for each comment listed. Utilize the Scenario Outline and Examples table to parameterize the test
- [ ] Extend the testing coverage with anything you believe would be beneficial

- [Y] Modify the scenario 'Validate the login page title' from [login.feature](features/login.feature#8) which runs but fails. Determine the cause of the failure and update the scenario to pass in the test
- [Y] Extend the scenario 'Validate login error message' from [login.feature](features/login.feature#10) which runs and passes but is missing a step. Extend the scenario to validate the error message received.
- [Y] Modify and extend the 'Validate successful purchase text' from [purchase.feature](features/purchase.feature#6) with steps for each comment listed. Consider writing a new steps.ts file along with an appropriate page.ts
- [Y] Modify and extend the 'Validate product sort by price sort' from [product.feature](features/product.feature#6) with steps for each comment listed. Utilize the Scenario Outline and Examples table to parameterize the test
- [Y] Extend the testing coverage with anything you believe would be beneficial
11 changes: 7 additions & 4 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
Scenario: Validate the 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 navigation
Then I will login as 'standard_user'
Then I should be redirected to the inventory 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 sort the products by "<sort>"
Then I should see 6 products sorted by price in "<order>" order

Examples:
# TODO: extend the datatable to paramterize this test
| sort |
| sort | order |
| Price (low to high) | ascending |
| Price (high to low) | descending |
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 open the cart
Then I proceed to checkout
Then I enter checkout details with first name "John", last name "Doe", and postal code "12345"
Then I continue checkout
Then I finish the purchase
Then I should see the purchase confirmation text "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.

55 changes: 55 additions & 0 deletions pages/checkout.page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Page } from "@playwright/test";

export class Checkout {
private readonly page: Page;
private readonly cartLink: string = ".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 purchaseConfirmation: string = ".complete-header";

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

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

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

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

public async validatePurchaseConfirmationText(expectedText: string) {
const confirmationText =
(
await this.page.locator(this.purchaseConfirmation).textContent()
)?.trim() ?? "";
if (confirmationText !== expectedText) {
throw new Error(
`Expected confirmation text to be "${expectedText}" but found "${confirmationText}"`,
);
}
}
}
39 changes: 27 additions & 12 deletions pages/login.page.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { Page } from "@playwright/test"
import { 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 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 loginError: string = 'h3[data-test="error"]';

constructor(page: Page) {
this.page = page;
Expand All @@ -14,13 +15,27 @@ export class Login {
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}`);
throw new Error(
`Expected title to be ${expectedTitle} but found ${pageTitle}`,
);
}
}

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();
}
public async validateLoginErrorMessage(expectedMessage: string) {
const actualMessage =
(await this.page.locator(this.loginError).textContent())?.trim() ??
"";
if (actualMessage !== expectedMessage) {
throw new Error(
`Expected error message to be "${expectedMessage}" but found "${actualMessage}"`,
);
}
}
public async validateInventoryPageNavigation() {
await this.page.waitForURL("**/inventory.html");
}
}
}
50 changes: 45 additions & 5 deletions pages/product.page.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,54 @@
import { Page } from "@playwright/test"
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 page: Page;
private readonly addToCart: string =
'button[id="add-to-cart-sauce-labs-backpack"]';
private readonly sortDropdown: string = ".product_sort_container";
private readonly itemPrices: string = ".inventory_item_price";

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 sortProductsBy(sort: string) {
await this.page
.locator(this.sortDropdown)
.selectOption({ label: sort });
}
public async validateProductPricesSorted(
order: string,
expectedCount: number,
) {
const rawPrices = await this.page
.locator(this.itemPrices)
.allTextContents();
const parsedPrices = rawPrices.map((price) =>
Number(price.replace("$", "").trim()),
);

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

const sortedPrices = [...parsedPrices].sort((a, b) => a - b);
const expectedPrices =
order.toLowerCase() === "descending"
? sortedPrices.reverse()
: sortedPrices;
const isSorted = parsedPrices.every(
(price, index) => price === expectedPrices[index],
);

if (!isSorted) {
throw new Error(
`Expected prices to be sorted in ${order} order, but found [${parsedPrices.join(", ")}]`,
);
}
}
}
48 changes: 30 additions & 18 deletions playwrightUtilities.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,45 @@
import { Browser, chromium, Page } from 'playwright';
import { Browser, chromium, Page, firefox, webkit } from "playwright";

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 });
}
if (!browser) {
const browserName =
process.env.BROWSER ??
(process.platform === "darwin" ? "webkit" : "chromium");
const browserType =
browserName === "firefox"
? firefox
: browserName === "webkit"
? webkit
: chromium;
const isHeadless = process.env.HEADLESS !== "false";
browser = await browserType.launch({ headless: isHeadless });
}
};

export const initializePage = async () => {
if (browser && !page) {
page = await browser.newPage();
page.setDefaultTimeout(DEFAULT_TIMEOUT);
}
if (browser && !page) {
page = await browser.newPage();
page.setDefaultTimeout(DEFAULT_TIMEOUT);
}
};

export const getPage = (): Page => {
if (!page) {
throw new Error('Page has not been initialized. Please call initializePage first.');
}
return page;
if (!page) {
throw new Error(
"Page has not been initialized. Please call initializePage first.",
);
}
return page;
};

export const closeBrowser = async () => {
if (browser) {
await browser.close();
browser = null;
page = null;
}
};
if (browser) {
await browser.close();
browser = null;
page = null;
}
};
26 changes: 18 additions & 8 deletions steps/login.steps.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
import { Then } from '@cucumber/cucumber';
import { getPage } from '../playwrightUtilities';
import { Login } from '../pages/login.page';
import { 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("I should see the title {string}", async (expectedTitle) => {
await new Login(getPage()).validateTitle(expectedTitle);
});

Then('I will login as {string}', async (userName) => {
await new Login(getPage()).loginAsUser(userName);
});
Then("I will login as {string}", async (userName) => {
await new Login(getPage()).loginAsUser(userName);
});
Then(
"I should see the login error message {string}",
async (expectedMessage) => {
await new Login(getPage()).validateLoginErrorMessage(expectedMessage);
},
);

Then("I should be redirected to the inventory page", async () => {
await new Login(getPage()).validateInventoryPageNavigation();
});
Loading