This module brings together all the concepts and techniques you've learned throughout the WebDriverIO course into a comprehensive real-world project. You'll build a complete test automation framework for an e-commerce application, implementing page objects, custom commands, data-driven testing, visual regression, and reporting.
By the end of this module, you will be able to:
Our project will create a test automation framework for an e-commerce website with the following requirements:
We'll implement a layered architecture:
e-commerce-tests/
├── config/ # Configuration files
│ ├── wdio.conf.js # Base WebDriverIO configuration
│ ├── wdio.local.conf.js # Local execution configuration
│ └── wdio.ci.conf.js # CI execution configuration
├── test/ # Test files
│ ├── specs/ # Test specifications
│ │ ├── account/ # Account management tests
│ │ ├── product/ # Product browsing tests
│ │ └── checkout/ # Checkout process tests
│ └── visual/ # Visual regression tests
├── pages/ # Page objects
│ ├── base.page.js # Base page object
│ ├── home.page.js # Home page object
│ ├── product.page.js # Product page object
│ └── checkout.page.js # Checkout page object
├── components/ # Reusable UI components
│ ├── header.component.js # Header component
│ ├── footer.component.js # Footer component
│ └── cart.component.js # Shopping cart component
├── utils/ # Utility functions
│ ├── selectors.js # Selector helpers
│ ├── waits.js # Custom wait functions
│ └── dataGenerator.js # Test data generation
├── data/ # Test data
│ ├── users.json # User credentials
│ └── products.json # Product information
├── reports/ # Test reports
├── screenshots/ # Screenshots for failed tests
├── .eslintrc.js # ESLint configuration
├── .gitignore # Git ignore file
├── package.json # Project dependencies
└── README.md # Project documentation
Let's start by setting up the project:
# Create project directory
mkdir e-commerce-tests
cd e-commerce-tests
# Initialize npm project
npm init -y
# Install WebDriverIO and dependencies
npm install @wdio/cli --save-dev
npx wdio config
# Install additional dependencies
npm install faker chai axios moment lodash --save-dev
npm install eslint eslint-plugin-wdio --save-dev
Create the base WebDriverIO configuration:
// config/wdio.conf.js
const path = require('path');
const moment = require('moment');
exports.config = {
//
// ====================
// Runner Configuration
// ====================
runner: 'local',
//
// ==================
// Specify Test Files
// ==================
specs: [
'./test/specs/**/*.js'
],
exclude: [],
//
// ============
// Capabilities
// ============
maxInstances: 5,
capabilities: [{
maxInstances: 5,
browserName: 'chrome',
acceptInsecureCerts: true,
'goog:chromeOptions': {
args: ['--headless', '--disable-gpu', '--window-size=1920,1080']
}
}],
//
// ===================
// Test Configurations
// ===================
logLevel: 'info',
bail: 0,
baseUrl: 'https://www.saucedemo.com',
waitforTimeout: 10000,
connectionRetryTimeout: 120000,
connectionRetryCount: 3,
//
// Test framework
framework: 'mocha',
mochaOpts: {
ui: 'bdd',
timeout: 60000
},
//
// =====
// Hooks
// =====
before: function (capabilities, specs) {
// Add custom commands
require('../utils/commands');
// Set up chai
const chai = require('chai');
global.expect = chai.expect;
},
beforeTest: function (test, context) {
// Setup for test
},
afterTest: async function(test, context, { error, result, duration, passed, retries }) {
// Take screenshot if test fails
if (error) {
const timestamp = moment().format('YYYYMMDD-HHmmss');
const testName = test.title.replace(/\s+/g, '-');
const screenshotPath = path.join(
process.cwd(),
'screenshots',
`${testName}-${timestamp}.png`
);
await browser.saveScreenshot(screenshotPath);
console.log(`Screenshot saved to: ${screenshotPath}`);
}
},
//
// =====
// Reporters
// =====
reporters: [
'spec',
['allure', {
outputDir: './reports/allure-results',
disableWebdriverStepsReporting: true,
disableWebdriverScreenshotsReporting: false,
}],
['junit', {
outputDir: './reports/junit',
outputFileFormat: function(options) {
return `results-${options.cid}.xml`;
}
}]
],
};
Create configurations for different environments:
// config/wdio.local.conf.js
const { config } = require('./wdio.conf');
exports.config = {
...config,
maxInstances: 1,
capabilities: [{
maxInstances: 1,
browserName: 'chrome',
acceptInsecureCerts: true,
'goog:chromeOptions': {
args: ['--window-size=1920,1080']
}
}],
baseUrl: 'https://www.saucedemo.com',
logLevel: 'info'
};
// config/wdio.ci.conf.js
const { config } = require('./wdio.conf');
exports.config = {
...config,
maxInstances: 5,
capabilities: [{
maxInstances: 5,
browserName: 'chrome',
acceptInsecureCerts: true,
'goog:chromeOptions': {
args: ['--headless', '--disable-gpu', '--window-size=1920,1080', '--no-sandbox']
}
}],
baseUrl: 'https://www.saucedemo.com',
logLevel: 'error'
};
Create a base page object that all other page objects will extend:
// pages/base.page.js
class BasePage {
constructor() {
this.url = '/';
}
async open() {
await browser.url(this.url);
}
async waitForPageLoad() {
await browser.waitUntil(
async () => await browser.execute(() => document.readyState === 'complete'),
{
timeout: 30000,
timeoutMsg: 'Page did not finish loading'
}
);
}
async getTitle() {
return browser.getTitle();
}
async getUrl() {
return browser.getUrl();
}
async scrollToElement(element) {
await element.scrollIntoView();
}
async waitForElementDisplayed(element, timeout = 10000) {
await element.waitForDisplayed({ timeout });
}
async waitForElementClickable(element, timeout = 10000) {
await element.waitForClickable({ timeout });
}
async waitForElementExist(element, timeout = 10000) {
await element.waitForExist({ timeout });
}
}
module.exports = BasePage;
Create a reusable header component:
// components/header.component.js
class HeaderComponent {
constructor() {
this.logo = $('.app_logo');
this.shoppingCart = $('.shopping_cart_link');
this.burgerMenu = $('#react-burger-menu-btn');
this.logoutLink = $('#logout_sidebar_link');
this.resetAppLink = $('#reset_sidebar_link');
this.allItemsLink = $('#inventory_sidebar_link');
}
async getCartItemCount() {
const badge = await $('.shopping_cart_badge');
if (await badge.isExisting()) {
return parseInt(await badge.getText());
}
return 0;
}
async openCart() {
await this.shoppingCart.click();
}
async openMenu() {
await this.burgerMenu.click();
// Wait for menu to open
await $('#logout_sidebar_link').waitForDisplayed({ timeout: 5000 });
}
async logout() {
await this.openMenu();
await this.logoutLink.click();
}
async resetApp() {
await this.openMenu();
await this.resetAppLink.click();
}
async goToAllItems() {
await this.openMenu();
await this.allItemsLink.click();
}
}
module.exports = HeaderComponent;
Create a page object for the login page:
// pages/login.page.js
const BasePage = require('./base.page');
class LoginPage extends BasePage {
constructor() {
super();
this.url = '/';
this.usernameInput = $('#user-name');
this.passwordInput = $('#password');
this.loginButton = $('#login-button');
this.errorMessage = $('.error-message-container');
}
async login(username, password) {
await this.usernameInput.setValue(username);
await this.passwordInput.setValue(password);
await this.loginButton.click();
}
async getErrorMessage() {
if (await this.errorMessage.isDisplayed()) {
return this.errorMessage.getText();
}
return null;
}
async isErrorDisplayed() {
return this.errorMessage.isDisplayed();
}
}
module.exports = LoginPage;
Create a page object for the products page:
// pages/products.page.js
const BasePage = require('./base.page');
const HeaderComponent = require('../components/header.component');
class ProductsPage extends BasePage {
constructor() {
super();
this.url = '/inventory.html';
this.header = new HeaderComponent();
this.productItems = $('.inventory_item');
this.productTitles = $('.inventory_item_name');
this.productPrices = $('.inventory_item_price');
this.addToCartButtons = $('.btn_inventory');
this.sortDropdown = $('.product_sort_container');
}
async getProductCount() {
return (await $$('.inventory_item')).length;
}
async getProductTitle(index) {
const titles = await $$('.inventory_item_name');
if (index < titles.length) {
return titles[index].getText();
}
throw new Error(`Product index ${index} out of bounds`);
}
async getProductPrice(index) {
const prices = await $$('.inventory_item_price');
if (index < prices.length) {
const priceText = await prices[index].getText();
return parseFloat(priceText.replace('$', ''));
}
throw new Error(`Product index ${index} out of bounds`);
}
async addProductToCart(index) {
const buttons = await $$('.btn_inventory');
if (index < buttons.length) {
await buttons[index].click();
} else {
throw new Error(`Product index ${index} out of bounds`);
}
}
async openProductDetails(index) {
const titles = await $$('.inventory_item_name');
if (index < titles.length) {
await titles[index].click();
} else {
throw new Error(`Product index ${index} out of bounds`);
}
}
async sortProducts(option) {
await this.sortDropdown.click();
await $(`option[value="${option}"]`).click();
}
async getAllProductTitles() {
const titles = await $$('.inventory_item_name');
return Promise.all(titles.map(title => title.getText()));
}
async getAllProductPrices() {
const prices = await $$('.inventory_item_price');
return Promise.all(prices.map(async (price) => {
const priceText = await price.getText();
return parseFloat(priceText.replace('$', ''));
}));
}
}
module.exports = ProductsPage;
Create a page object for the product details page:
// pages/product-details.page.js
const BasePage = require('./base.page');
const HeaderComponent = require('../components/header.component');
class ProductDetailsPage extends BasePage {
constructor() {
super();
this.header = new HeaderComponent();
this.productTitle = $('.inventory_details_name');
this.productDescription = $('.inventory_details_desc');
this.productPrice = $('.inventory_details_price');
this.addToCartButton = $('.btn_inventory');
this.backButton = $('#back-to-products');
}
async getProductTitle() {
return this.productTitle.getText();
}
async getProductDescription() {
return this.productDescription.getText();
}
async getProductPrice() {
const priceText = await this.productPrice.getText();
return parseFloat(priceText.replace('$', ''));
}
async addToCart() {
await this.addToCartButton.click();
}
async goBack() {
await this.backButton.click();
}
async isAddToCartButtonDisplayed() {
return this.addToCartButton.isDisplayed();
}
}
module.exports = ProductDetailsPage;
Create a page object for the shopping cart page:
// pages/cart.page.js
const BasePage = require('./base.page');
const HeaderComponent = require('../components/header.component');
class CartPage extends BasePage {
constructor() {
super();
this.url = '/cart.html';
this.header = new HeaderComponent();
this.cartItems = $('.cart_item');
this.checkoutButton = $('#checkout');
this.continueShoppingButton = $('#continue-shopping');
this.removeButtons = $('.cart_button');
}
async getCartItemCount() {
return (await $$('.cart_item')).length;
}
async getCartItemTitle(index) {
const titles = await $$('.inventory_item_name');
if (index < titles.length) {
return titles[index].getText();
}
throw new Error(`Cart item index ${index} out of bounds`);
}
async getCartItemPrice(index) {
const prices = await $$('.inventory_item_price');
if (index < prices.length) {
const priceText = await prices[index].getText();
return parseFloat(priceText.replace('$', ''));
}
throw new Error(`Cart item index ${index} out of bounds`);
}
async removeCartItem(index) {
const buttons = await $$('.cart_button');
if (index < buttons.length) {
await buttons[index].click();
} else {
throw new Error(`Cart item index ${index} out of bounds`);
}
}
async proceedToCheckout() {
await this.checkoutButton.click();
}
async continueShopping() {
await this.continueShoppingButton.click();
}
async getAllCartItemTitles() {
const titles = await $$('.inventory_item_name');
return Promise.all(titles.map(title => title.getText()));
}
async getTotalPrice() {
const prices = await $$('.inventory_item_price');
const priceValues = await Promise.all(prices.map(async (price) => {
const priceText = await price.getText();
return parseFloat(priceText.replace('$', ''));
}));
return priceValues.reduce((total, price) => total + price, 0);
}
}
module.exports = CartPage;
Create page objects for the checkout process:
// pages/checkout-step-one.page.js
const BasePage = require('./base.page');
const HeaderComponent = require('../components/header.component');
class CheckoutStepOnePage extends BasePage {
constructor() {
super();
this.url = '/checkout-step-one.html';
this.header = new HeaderComponent();
this.firstNameInput = $('#first-name');
this.lastNameInput = $('#last-name');
this.postalCodeInput = $('#postal-code');
this.continueButton = $('#continue');
this.cancelButton = $('#cancel');
this.errorMessage = $('.error-message-container');
}
async fillShippingInfo(firstName, lastName, postalCode) {
await this.firstNameInput.setValue(firstName);
await this.lastNameInput.setValue(lastName);
await this.postalCodeInput.setValue(postalCode);
}
async continue() {
await this.continueButton.click();
}
async cancel() {
await this.cancelButton.click();
}
async getErrorMessage() {
if (await this.errorMessage.isDisplayed()) {
return this.errorMessage.getText();
}
return null;
}
}
module.exports = CheckoutStepOnePage;
// pages/checkout-step-two.page.js
const BasePage = require('./base.page');
const HeaderComponent = require('../components/header.component');
class CheckoutStepTwoPage extends BasePage {
constructor() {
super();
this.url = '/checkout-step-two.html';
this.header = new HeaderComponent();
this.cartItems = $('.cart_item');
this.finishButton = $('#finish');
this.cancelButton = $('#cancel');
this.subtotalLabel = $('.summary_subtotal_label');
this.taxLabel = $('.summary_tax_label');
this.totalLabel = $('.summary_total_label');
}
async getCartItemCount() {
return (await $$('.cart_item')).length;
}
async getSubtotal() {
const subtotalText = await this.subtotalLabel.getText();
return parseFloat(subtotalText.replace('Item total: $', ''));
}
async getTax() {
const taxText = await this.taxLabel.getText();
return parseFloat(taxText.replace('Tax: $', ''));
}
async getTotal() {
const totalText = await this.totalLabel.getText();
return parseFloat(totalText.replace('Total: $', ''));
}
async finish() {
await this.finishButton.click();
}
async cancel() {
await this.cancelButton.click();
}
}
module.exports = CheckoutStepTwoPage;
// pages/checkout-complete.page.js
const BasePage = require('./base.page');
const HeaderComponent = require('../components/header.component');
class CheckoutCompletePage extends BasePage {
constructor() {
super();
this.url = '/checkout-complete.html';
this.header = new HeaderComponent();
this.thankYouHeader = $('.complete-header');
this.completeText = $('.complete-text');
this.backHomeButton = $('#back-to-products');
}
async getThankYouMessage() {
return this.thankYouHeader.getText();
}
async getCompleteText() {
return this.completeText.getText();
}
async backToHome() {
await this.backHomeButton.click();
}
async isOrderCompleted() {
return this.thankYouHeader.isDisplayed();
}
}
module.exports = CheckoutCompletePage;
Create custom commands to extend WebDriverIO functionality:
// utils/commands.js
browser.addCommand('waitForPageLoad', async function() {
await browser.waitUntil(
async () => await browser.execute(() => document.readyState === 'complete'),
{
timeout: 30000,
timeoutMsg: 'Page did not finish loading'
}
);
});
browser.addCommand('waitForUrlContains', async function(fragment, timeout = 10000) {
await browser.waitUntil(
async () => {
const url = await browser.getUrl();
return url.includes(fragment);
},
{
timeout,
timeoutMsg: `URL did not contain "${fragment}" within ${timeout}ms`
}
);
});
browser.addCommand('waitForTextContains', async function(selector, text, timeout = 10000) {
const element = await $(selector);
await browser.waitUntil(
async () => {
const elementText = await element.getText();
return elementText.includes(text);
},
{
timeout,
timeoutMsg: `Element "${selector}" text did not contain "${text}" within ${timeout}ms`
}
);
});
browser.addCommand('login', async function(username, password) {
await browser.url('/');
await $('#user-name').setValue(username);
await $('#password').setValue(password);
await $('#login-button').click();
await browser.waitForUrlContains('/inventory.html');
});
browser.addCommand('logout', async function() {
await $('#react-burger-menu-btn').click();
await $('#logout_sidebar_link').waitForDisplayed({ timeout: 5000 });
await $('#logout_sidebar_link').click();
await browser.waitForUrlContains('/');
});
browser.addCommand('resetApp', async function() {
await $('#react-burger-menu-btn').click();
await $('#reset_sidebar_link').waitForDisplayed({ timeout: 5000 });
await $('#reset_sidebar_link').click();
});
browser.addCommand('addProductToCart', async function(index) {
const buttons = await $$('.btn_inventory');
if (index < buttons.length) {
await buttons[index].click();
} else {
throw new Error(`Product index ${index} out of bounds`);
}
});
browser.addCommand('getCartCount', async function() {
const badge = await $('.shopping_cart_badge');
if (await badge.isExisting()) {
return parseInt(await badge.getText());
}
return 0;
});
Create a utility for generating test data:
// utils/dataGenerator.js
const faker = require('faker');
class DataGenerator {
static generateUser() {
return {
firstName: faker.name.firstName(),
lastName: faker.name.lastName(),
postalCode: faker.address.zipCode()
};
}
static generateCreditCard() {
return {
number: faker.finance.creditCardNumber(),
expiry: `${faker.datatype.number({ min: 1, max: 12 })}/25`,
cvv: faker.finance.creditCardCVV()
};
}
static generateAddress() {
return {
street: faker.address.streetAddress(),
city: faker.address.city(),
state: faker.address.state(),
zipCode: faker.address.zipCode()
};
}
}
module.exports = DataGenerator;
Create custom wait functions:
// utils/waits.js
class WaitUtils {
static async forElementDisplayed(selector, timeout = 10000) {
const element = await $(selector);
await element.waitForDisplayed({ timeout });
return element;
}
static async forElementClickable(selector, timeout = 10000) {
const element = await $(selector);
await element.waitForClickable({ timeout });
return element;
}
static async forElementExist(selector, timeout = 10000) {
const element = await $(selector);
await element.waitForExist({ timeout });
return element;
}
static async forTextContains(selector, text, timeout = 10000) {
const element = await $(selector);
await browser.waitUntil(
async () => {
const elementText = await element.getText();
return elementText.includes(text);
},
{
timeout,
timeoutMsg: `Element "${selector}" text did not contain "${text}" within ${timeout}ms`
}
);
return element;
}
static async forUrlContains(fragment, timeout = 10000) {
await browser.waitUntil(
async () => {
const url = await browser.getUrl();
return url.includes(fragment);
},
{
timeout,
timeoutMsg: `URL did not contain "${fragment}" within ${timeout}ms`
}
);
}
static async forElementCount(selector, count, timeout = 10000) {
await browser.waitUntil(
async () => {
const elements = await $$(selector);
return elements.length === count;
},
{
timeout,
timeoutMsg: `Element count for "${selector}" did not equal ${count} within ${timeout}ms`
}
);
}
}
module.exports = WaitUtils;
Create tests for the login functionality:
// test/specs/account/login.spec.js
const LoginPage = require('../../../pages/login.page');
const ProductsPage = require('../../../pages/products.page');
describe('Login Functionality', () => {
let loginPage;
let productsPage;
beforeEach(async () => {
loginPage = new LoginPage();
productsPage = new ProductsPage();
await loginPage.open();
});
it('should login with valid credentials', async () => {
await loginPage.login('standard_user', 'secret_sauce');
// Verify redirect to products page
const url = await browser.getUrl();
expect(url).to.include('/inventory.html');
// Verify products are displayed
const productCount = await productsPage.getProductCount();
expect(productCount).to.be.greaterThan(0);
});
it('should show error with invalid credentials', async () => {
await loginPage.login('invalid_user', 'invalid_password');
// Verify error message
const errorMessage = await loginPage.getErrorMessage();
expect(errorMessage).to.include('Username and password do not match');
// Verify still on login page
const url = await browser.getUrl();
expect(url).not.to.include('/inventory.html');
});
it('should show error with locked out user', async () => {
await loginPage.login('locked_out_user', 'secret_sauce');
// Verify error message
const errorMessage = await loginPage.getErrorMessage();
expect(errorMessage).to.include('locked out');
// Verify still on login page
const url = await browser.getUrl();
expect(url).not.to.include('/inventory.html');
});
it('should login and logout successfully', async () => {
await loginPage.login('standard_user', 'secret_sauce');
// Verify login successful
const url = await browser.getUrl();
expect(url).to.include('/inventory.html');
// Logout
await browser.logout();
// Verify back on login page
const loginUrl = await browser.getUrl();
expect(loginUrl).not.to.include('/inventory.html');
expect(await loginPage.usernameInput.isDisplayed()).to.be.true;
});
});
Create tests for product browsing functionality:
// test/specs/product/product-browsing.spec.js
const LoginPage = require('../../../pages/login.page');
const ProductsPage = require('../../../pages/products.page');
const ProductDetailsPage = require('../../../pages/product-details.page');
describe('Product Browsing', () => {
let productsPage;
let productDetailsPage;
before(async () => {
// Login before all tests
await browser.login('standard_user', 'secret_sauce');
});
beforeEach(async () => {
productsPage = new ProductsPage();
productDetailsPage = new ProductDetailsPage();
await productsPage.open();
});
it('should display correct number of products', async () => {
const productCount = await productsPage.getProductCount();
expect(productCount).to.equal(6);
});
it('should sort products by price low to high', async () => {
await productsPage.sortProducts('lohi');
const prices = await productsPage.getAllProductPrices();
// Verify prices are in ascending order
for (let i = 0; i < prices.length - 1; i++) {
expect(prices[i]).to.be.at.most(prices[i + 1]);
}
});
it('should sort products by price high to low', async () => {
await productsPage.sortProducts('hilo');
const prices = await productsPage.getAllProductPrices();
// Verify prices are in descending order
for (let i = 0; i < prices.length - 1; i++) {
expect(prices[i]).to.be.at.least(prices[i + 1]);
}
});
it('should sort products alphabetically', async () => {
await productsPage.sortProducts('az');
const titles = await productsPage.getAllProductTitles();
// Verify titles are in alphabetical order
const sortedTitles = [...titles].sort();
expect(titles).to.deep.equal(sortedTitles);
});
it('should sort products reverse alphabetically', async () => {
await productsPage.sortProducts('za');
const titles = await productsPage.getAllProductTitles();
// Verify titles are in reverse alphabetical order
const sortedTitles = [...titles].sort().reverse();
expect(titles).to.deep.equal(sortedTitles);
});
it('should open product details page', async () => {
const productTitle = await productsPage.getProductTitle(0);
await productsPage.openProductDetails(0);
// Verify on product details page
const detailsTitle = await productDetailsPage.getProductTitle();
expect(detailsTitle).to.equal(productTitle);
});
it('should add product to cart from products page', async () => {
// Get initial cart count
const initialCartCount = await browser.getCartCount();
// Add product to cart
await productsPage.addProductToCart(0);
// Verify cart count increased
const newCartCount = await browser.getCartCount();
expect(newCartCount).to.equal(initialCartCount + 1);
});
it('should add product to cart from details page', async () => {
// Open product details
await productsPage.openProductDetails(1);
// Get initial cart count
const initialCartCount = await browser.getCartCount();
// Add product to cart
await productDetailsPage.addToCart();
// Verify cart count increased
const newCartCount = await browser.getCartCount();
expect(newCartCount).to.equal(initialCartCount + 1);
});
after(async () => {
// Reset app state after all tests
await browser.resetApp();
});
});
Create tests for the shopping cart functionality:
// test/specs/product/cart.spec.js
const ProductsPage = require('../../../pages/products.page');
const CartPage = require('../../../pages/cart.page');
describe('Shopping Cart', () => {
let productsPage;
let cartPage;
before(async () => {
// Login before all tests
await browser.login('standard_user', 'secret_sauce');
});
beforeEach(async () => {
productsPage = new ProductsPage();
cartPage = new CartPage();
// Reset app state before each test
await browser.resetApp();
await productsPage.open();
});
it('should add products to cart', async () => {
// Add two products to cart
await productsPage.addProductToCart(0);
await productsPage.addProductToCart(1);
// Verify cart count
const cartCount = await browser.getCartCount();
expect(cartCount).to.equal(2);
// Open cart
await productsPage.header.openCart();
// Verify cart items
const cartItemCount = await cartPage.getCartItemCount();
expect(cartItemCount).to.equal(2);
});
it('should remove products from cart', async () => {
// Add two products to cart
await productsPage.addProductToCart(0);
await productsPage.addProductToCart(1);
// Open cart
await productsPage.header.openCart();
// Verify initial cart items
const initialCartItemCount = await cartPage.getCartItemCount();
expect(initialCartItemCount).to.equal(2);
// Remove one item
await cartPage.removeCartItem(0);
// Verify cart count updated
const updatedCartItemCount = await cartPage.getCartItemCount();
expect(updatedCartItemCount).to.equal(1);
});
it('should continue shopping from cart', async () => {
// Add product to cart
await productsPage.addProductToCart(0);
// Open cart
await productsPage.header.openCart();
// Continue shopping
await cartPage.continueShopping();
// Verify back on products page
const url = await browser.getUrl();
expect(url).to.include('/inventory.html');
});
it('should calculate correct total price', async () => {
// Add two products to cart
await productsPage.addProductToCart(0);
await productsPage.addProductToCart(1);
// Get product prices
const price1 = await productsPage.getProductPrice(0);
const price2 = await productsPage.getProductPrice(1);
const expectedTotal = price1 + price2;
// Open cart
await productsPage.header.openCart();
// Get total price
const totalPrice = await cartPage.getTotalPrice();
// Verify total price
expect(totalPrice).to.equal(expectedTotal);
});
after(async () => {
// Reset app state after all tests
await browser.resetApp();
});
});
Create tests for the checkout process:
// test/specs/checkout/checkout-process.spec.js
const ProductsPage = require('../../../pages/products.page');
const CartPage = require('../../../pages/cart.page');
const CheckoutStepOnePage = require('../../../pages/checkout-step-one.page');
const CheckoutStepTwoPage = require('../../../pages/checkout-step-two.page');
const CheckoutCompletePage = require('../../../pages/checkout-complete.page');
const DataGenerator = require('../../../utils/dataGenerator');
describe('Checkout Process', () => {
let productsPage;
let cartPage;
let checkoutStepOnePage;
let checkoutStepTwoPage;
let checkoutCompletePage;
before(async () => {
// Login before all tests
await browser.login('standard_user', 'secret_sauce');
});
beforeEach(async () => {
productsPage = new ProductsPage();
cartPage = new CartPage();
checkoutStepOnePage = new CheckoutStepOnePage();
checkoutStepTwoPage = new CheckoutStepTwoPage();
checkoutCompletePage = new CheckoutCompletePage();
// Reset app state before each test
await browser.resetApp();
await productsPage.open();
// Add products to cart
await productsPage.addProductToCart(0);
await productsPage.addProductToCart(1);
// Go to cart
await productsPage.header.openCart();
});
it('should complete checkout process with valid information', async () => {
// Proceed to checkout
await cartPage.proceedToCheckout();
// Generate user data
const userData = DataGenerator.generateUser();
// Fill shipping information
await checkoutStepOnePage.fillShippingInfo(
userData.firstName,
userData.lastName,
userData.postalCode
);
await checkoutStepOnePage.continue();
// Verify on checkout step two
const stepTwoUrl = await browser.getUrl();
expect(stepTwoUrl).to.include('/checkout-step-two.html');
// Verify cart items
const cartItemCount = await checkoutStepTwoPage.getCartItemCount();
expect(cartItemCount).to.equal(2);
// Verify totals
const subtotal = await checkoutStepTwoPage.getSubtotal();
const tax = await checkoutStepTwoPage.getTax();
const total = await checkoutStepTwoPage.getTotal();
expect(total).to.equal(subtotal + tax);
// Complete checkout
await checkoutStepTwoPage.finish();
// Verify on checkout complete page
const completeUrl = await browser.getUrl();
expect(completeUrl).to.include('/checkout-complete.html');
// Verify thank you message
const thankYouMessage = await checkoutCompletePage.getThankYouMessage();
expect(thankYouMessage).to.include('THANK YOU');
// Verify order completed
const isOrderCompleted = await checkoutCompletePage.isOrderCompleted();
expect(isOrderCompleted).to.be.true;
});
it('should show error with missing information', async () => {
// Proceed to checkout
await cartPage.proceedToCheckout();
// Submit without filling information
await checkoutStepOnePage.continue();
// Verify error message
const errorMessage = await checkoutStepOnePage.getErrorMessage();
expect(errorMessage).to.include('First Name is required');
// Fill only first name
await checkoutStepOnePage.fillShippingInfo('John', '', '');
await checkoutStepOnePage.continue();
// Verify error message
const lastNameError = await checkoutStepOnePage.getErrorMessage();
expect(lastNameError).to.include('Last Name is required');
// Fill first and last name
await checkoutStepOnePage.fillShippingInfo('John', 'Doe', '');
await checkoutStepOnePage.continue();
// Verify error message
const postalCodeError = await checkoutStepOnePage.getErrorMessage();
expect(postalCodeError).to.include('Postal Code is required');
});
it('should cancel checkout and return to cart', async () => {
// Proceed to checkout
await cartPage.proceedToCheckout();
// Cancel checkout
await checkoutStepOnePage.cancel();
// Verify back on cart page
const cartUrl = await browser.getUrl();
expect(cartUrl).to.include('/cart.html');
});
it('should cancel checkout at review step', async () => {
// Proceed to checkout
await cartPage.proceedToCheckout();
// Generate user data
const userData = DataGenerator.generateUser();
// Fill shipping information
await checkoutStepOnePage.fillShippingInfo(
userData.firstName,
userData.lastName,
userData.postalCode
);
await checkoutStepOnePage.continue();
// Cancel at review step
await checkoutStepTwoPage.cancel();
// Verify back on products page
const productsUrl = await browser.getUrl();
expect(productsUrl).to.include('/inventory.html');
});
it('should navigate back to home after order completion', async () => {
// Proceed to checkout
await cartPage.proceedToCheckout();
// Generate user data
const userData = DataGenerator.generateUser();
// Fill shipping information
await checkoutStepOnePage.fillShippingInfo(
userData.firstName,
userData.lastName,
userData.postalCode
);
await checkoutStepOnePage.continue();
// Complete checkout
await checkoutStepTwoPage.finish();
// Back to home
await checkoutCompletePage.backToHome();
// Verify back on products page
const productsUrl = await browser.getUrl();
expect(productsUrl).to.include('/inventory.html');
// Verify cart is empty
const cartCount = await browser.getCartCount();
expect(cartCount).to.equal(0);
});
after(async () => {
// Reset app state after all tests
await browser.resetApp();
});
});
Create end-to-end tests that cover complete user journeys:
// test/specs/e2e/complete-purchase.spec.js
const LoginPage = require('../../../pages/login.page');
const ProductsPage = require('../../../pages/products.page');
const ProductDetailsPage = require('../../../pages/product-details.page');
const CartPage = require('../../../pages/cart.page');
const CheckoutStepOnePage = require('../../../pages/checkout-step-one.page');
const CheckoutStepTwoPage = require('../../../pages/checkout-step-two.page');
const CheckoutCompletePage = require('../../../pages/checkout-complete.page');
const DataGenerator = require('../../../utils/dataGenerator');
describe('End-to-End Purchase Flow', () => {
let loginPage;
let productsPage;
let productDetailsPage;
let cartPage;
let checkoutStepOnePage;
let checkoutStepTwoPage;
let checkoutCompletePage;
beforeEach(async () => {
loginPage = new LoginPage();
productsPage = new ProductsPage();
productDetailsPage = new ProductDetailsPage();
cartPage = new CartPage();
checkoutStepOnePage = new CheckoutStepOnePage();
checkoutStepTwoPage = new CheckoutStepTwoPage();
checkoutCompletePage = new CheckoutCompletePage();
// Start from login page
await loginPage.open();
});
it('should complete purchase flow from product listing', async () => {
// Step 1: Login
await loginPage.login('standard_user', 'secret_sauce');
// Step 2: Add products to cart
await productsPage.addProductToCart(0);
await productsPage.addProductToCart(1);
// Step 3: Go to cart
await productsPage.header.openCart();
// Step 4: Proceed to checkout
await cartPage.proceedToCheckout();
// Step 5: Fill shipping information
const userData = DataGenerator.generateUser();
await checkoutStepOnePage.fillShippingInfo(
userData.firstName,
userData.lastName,
userData.postalCode
);
await checkoutStepOnePage.continue();
// Step 6: Review and complete order
await checkoutStepTwoPage.finish();
// Step 7: Verify order completion
const thankYouMessage = await checkoutCompletePage.getThankYouMessage();
expect(thankYouMessage).to.include('THANK YOU');
// Step 8: Return to home
await checkoutCompletePage.backToHome();
// Verify back on products page with empty cart
const productsUrl = await browser.getUrl();
expect(productsUrl).to.include('/inventory.html');
const cartCount = await browser.getCartCount();
expect(cartCount).to.equal(0);
});
it('should complete purchase flow from product details', async () => {
// Step 1: Login
await loginPage.login('standard_user', 'secret_sauce');
// Step 2: Open product details
await productsPage.openProductDetails(0);
// Step 3: Add product to cart from details page
await productDetailsPage.addToCart();
// Step 4: Go back to products
await productDetailsPage.goBack();
// Step 5: Open another product details
await productsPage.openProductDetails(1);
// Step 6: Add second product to cart
await productDetailsPage.addToCart();
// Step 7: Go to cart
await productDetailsPage.header.openCart();
// Step 8: Proceed to checkout
await cartPage.proceedToCheckout();
// Step 9: Fill shipping information
const userData = DataGenerator.generateUser();
await checkoutStepOnePage.fillShippingInfo(
userData.firstName,
userData.lastName,
userData.postalCode
);
await checkoutStepOnePage.continue();
// Step 10: Review and complete order
await checkoutStepTwoPage.finish();
// Step 11: Verify order completion
const thankYouMessage = await checkoutCompletePage.getThankYouMessage();
expect(thankYouMessage).to.include('THANK YOU');
});
after(async () => {
// Reset app state after all tests
await browser.resetApp();
});
});
Implement visual regression tests using WebDriverIO's image comparison service:
// test/visual/visual.spec.js
const LoginPage = require('../../pages/login.page');
const ProductsPage = require('../../pages/products.page');
const ProductDetailsPage = require('../../pages/product-details.page');
const CartPage = require('../../pages/cart.page');
describe('Visual Regression Tests', () => {
let loginPage;
let productsPage;
let productDetailsPage;
let cartPage;
before(async () => {
loginPage = new LoginPage();
productsPage = new ProductsPage();
productDetailsPage = new ProductDetailsPage();
cartPage = new CartPage();
});
it('should match login page baseline', async () => {
await loginPage.open();
// Take screenshot of entire page
const result = await browser.checkFullPageScreen('login-page');
expect(result).toEqual(0);
});
it('should match products page baseline', async () => {
await browser.login('standard_user', 'secret_sauce');
// Take screenshot of entire page
const result = await browser.checkFullPageScreen('products-page');
expect(result).toEqual(0);
});
it('should match product details baseline', async () => {
await browser.login('standard_user', 'secret_sauce');
await productsPage.openProductDetails(0);
// Take screenshot of entire page
const result = await browser.checkFullPageScreen('product-details-page');
expect(result).toEqual(0);
});
it('should match cart page baseline', async () => {
await browser.login('standard_user', 'secret_sauce');
await productsPage.addProductToCart(0);
await productsPage.header.openCart();
// Take screenshot of entire page
const result = await browser.checkFullPageScreen('cart-page');
expect(result).toEqual(0);
});
it('should match specific components', async () => {
await browser.login('standard_user', 'secret_sauce');
// Check header component
const headerResult = await browser.checkElement('.primary_header', 'header-component');
expect(headerResult).toEqual(0);
// Check product card
const productCardResult = await browser.checkElement('.inventory_item:nth-child(1)', 'product-card');
expect(productCardResult).toEqual(0);
// Check footer
const footerResult = await browser.checkElement('.footer', 'footer-component');
expect(footerResult).toEqual(0);
});
after(async () => {
// Reset app state after all tests
await browser.resetApp();
});
});
Implement API tests to complement UI tests:
// utils/apiClient.js
const axios = require('axios');
class ApiClient {
constructor(baseUrl = 'https://www.saucedemo.com/api') {
this.baseUrl = baseUrl;
this.token = null;
}
async login(username, password) {
try {
const response = await axios.post(`${this.baseUrl}/login`, {
username,
password
});
this.token = response.data.token;
return response.data;
} catch (error) {
console.error('Login API error:', error.message);
throw error;
}
}
async getProducts() {
try {
const response = await axios.get(`${this.baseUrl}/products`, {
headers: this.getAuthHeader()
});
return response.data;
} catch (error) {
console.error('Get products API error:', error.message);
throw error;
}
}
async getProductDetails(productId) {
try {
const response = await axios.get(`${this.baseUrl}/products/${productId}`, {
headers: this.getAuthHeader()
});
return response.data;
} catch (error) {
console.error('Get product details API error:', error.message);
throw error;
}
}
async addToCart(productId) {
try {
const response = await axios.post(`${this.baseUrl}/cart`, {
productId
}, {
headers: this.getAuthHeader()
});
return response.data;
} catch (error) {
console.error('Add to cart API error:', error.message);
throw error;
}
}
async getCart() {
try {
const response = await axios.get(`${this.baseUrl}/cart`, {
headers: this.getAuthHeader()
});
return response.data;
} catch (error) {
console.error('Get cart API error:', error.message);
throw error;
}
}
async checkout(checkoutInfo) {
try {
const response = await axios.post(`${this.baseUrl}/checkout`, checkoutInfo, {
headers: this.getAuthHeader()
});
return response.data;
} catch (error) {
console.error('Checkout API error:', error.message);
throw error;
}
}
getAuthHeader() {
return this.token ? { Authorization: `Bearer ${this.token}` } : {};
}
}
module.exports = ApiClient;
// test/specs/api/api.spec.js
const ApiClient = require('../../../utils/apiClient');
const DataGenerator = require('../../../utils/dataGenerator');
describe('API Tests', () => {
let apiClient;
before(async () => {
apiClient = new ApiClient();
// Note: In a real implementation, you would use actual API endpoints
// This is a mock implementation for demonstration purposes
});
it('should login via API', async () => {
try {
const loginResponse = await apiClient.login('standard_user', 'secret_sauce');
expect(loginResponse.token).to.exist;
} catch (error) {
// Skip test if API is not available
console.log('API not available, skipping test');
this.skip();
}
});
it('should get products via API', async () => {
try {
await apiClient.login('standard_user', 'secret_sauce');
const products = await apiClient.getProducts();
expect(products).to.be.an('array');
expect(products.length).to.be.greaterThan(0);
// Verify product structure
const firstProduct = products[0];
expect(firstProduct).to.have.property('id');
expect(firstProduct).to.have.property('name');
expect(firstProduct).to.have.property('price');
} catch (error) {
// Skip test if API is not available
console.log('API not available, skipping test');
this.skip();
}
});
it('should add product to cart via API', async () => {
try {
await apiClient.login('standard_user', 'secret_sauce');
const products = await apiClient.getProducts();
// Add first product to cart
const addToCartResponse = await apiClient.addToCart(products[0].id);
expect(addToCartResponse.success).to.be.true;
// Get cart and verify product was added
const cart = await apiClient.getCart();
expect(cart.items).to.be.an('array');
expect(cart.items.length).to.equal(1);
expect(cart.items[0].id).to.equal(products[0].id);
} catch (error) {
// Skip test if API is not available
console.log('API not available, skipping test');
this.skip();
}
});
it('should complete checkout via API', async () => {
try {
await apiClient.login('standard_user', 'secret_sauce');
const products = await apiClient.getProducts();
// Add product to cart
await apiClient.addToCart(products[0].id);
// Generate checkout data
const userData = DataGenerator.generateUser();
// Complete checkout
const checkoutResponse = await apiClient.checkout({
firstName: userData.firstName,
lastName: userData.lastName,
postalCode: userData.postalCode
});
expect(checkoutResponse.success).to.be.true;
expect(checkoutResponse.orderNumber).to.exist;
} catch (error) {
// Skip test if API is not available
console.log('API not available, skipping test');
this.skip();
}
});
});
Set up CI/CD integration for the test framework:
# .github/workflows/wdio-tests.yml
name: WebDriverIO Tests
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
schedule:
- cron: '0 0 * * *' # Run daily at midnight
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14.x]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
- name: Install dependencies
run: npm ci
- name: Install Chrome
run: |
wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add -
echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" | sudo tee /etc/apt/sources.list.d/google-chrome.list
sudo apt-get update
sudo apt-get install -y google-chrome-stable
- name: Run WebDriverIO tests
run: npm test
- name: Upload test results
uses: actions/upload-artifact@v2
if: always()
with:
name: test-results
path: |
reports/
screenshots/
- name: Generate Allure Report
if: always()
run: npx allure generate reports/allure-results --clean -o allure-report
- name: Upload Allure Report
uses: actions/upload-artifact@v2
if: always()
with:
name: allure-report
path: allure-report
// Jenkinsfile
pipeline {
agent {
docker {
image 'node:14-buster'
args '-p 5900:5900 -v /dev/shm:/dev/shm'
}
}
stages {
stage('Setup') {
steps {
sh 'npm ci'
}
}
stage('Lint') {
steps {
sh 'npm run lint'
}
}
stage('Test') {
steps {
sh 'npm test'
}
post {
always {
archiveArtifacts artifacts: 'screenshots/**/*', allowEmptyArchive: true
junit 'reports/junit/*.xml'
}
}
}
stage('Generate Report') {
steps {
sh 'npx allure generate reports/allure-results --clean -o allure-report'
}
post {
always {
archiveArtifacts artifacts: 'allure-report/**/*', allowEmptyArchive: true
}
}
}
}
post {
always {
cleanWs()
}
}
}
Create comprehensive documentation for the test framework:
# E-Commerce Test Automation Framework
This repository contains an automated testing framework for the Sauce Demo e-commerce website using WebDriverIO.
## Features
- Page Object Model design pattern
- Custom commands and utilities
- Data-driven testing
- Visual regression testing
- API testing integration
- Comprehensive reporting
- CI/CD integration
## Prerequisites
- Node.js (v14 or higher)
- npm (v6 or higher)
- Chrome browser
## Installation
1. Clone the repository:
git clone https://github.com/yourusername/e-commerce-tests.git cd e-commerce-tests
2. Install dependencies:
npm install ```
npm test
npm run test:login
npm run test:products
npm run test:cart
npm run test:checkout
npm run test:local
npm run test:ci
npm run test:visual
config/
: WebDriverIO configuration filestest/specs/
: Test specifications
account/
: Account management testsproduct/
: Product browsing testscheckout/
: Checkout process teststest/visual/
: Visual regression testspages/
: Page objectscomponents/
: Reusable UI componentsutils/
: Utility functionsdata/
: Test datareports/
: Test reportsscreenshots/
: Screenshots for failed testsThe framework uses the Page Object Model pattern to create an abstraction layer for the web pages. Each page object represents a page or a component of the application and encapsulates the page structure and behavior.
Example:
class LoginPage extends BasePage {
constructor() {
super();
this.url = '/';
this.usernameInput = $('#user-name');
this.passwordInput = $('#password');
this.loginButton = $('#login-button');
}
async login(username, password) {
await this.usernameInput.setValue(username);
await this.passwordInput.setValue(password);
await this.loginButton.click();
}
}
The framework extends WebDriverIO with custom commands to simplify test scripts:
Example:
browser.addCommand('login', async function(username, password) {
await browser.url('/');
await $('#user-name').setValue(username);
await $('#password').setValue(password);
await $('#login-button').click();
});
Test reports are generated in multiple formats:
To generate and open the Allure report:
npm run report
The framework includes configuration files for:
git checkout -b feature/amazing-feature
)git commit -m 'Add some amazing feature'
)git push origin feature/amazing-feature
)This project is licensed under the MIT License - see the LICENSE file for details. ```
Extend the test suite with additional test cases:
Improve the reporting capabilities:
Set up cross-browser testing:
This module provided a comprehensive real-world project that brings together all the concepts and techniques learned throughout the WebDriverIO course. By implementing a complete test automation framework for an e-commerce application, you've applied best practices for test organization, maintainability, and reliability.
Key takeaways:
You now have the skills and knowledge to design, implement, and maintain professional-grade test automation frameworks using WebDriverIO.