๐Ÿ”ฅ 0
โญ 0
Lesson 6 of 10 30 min +250 XP

Page Object Model for E-Commerce

Why Page Object Model?

Without POM, locators and actions are scattered across tests:

# Without POM - Maintenance nightmare
def test_add_to_cart():
    driver.find_element(By.CSS_SELECTOR, ".product-card button.add-to-cart").click()
    driver.find_element(By.CSS_SELECTOR, ".cart-icon").click()
    items = driver.find_elements(By.CSS_SELECTOR, ".cart-item")
    assert len(items) == 1

def test_remove_from_cart():
    driver.find_element(By.CSS_SELECTOR, ".product-card button.add-to-cart").click()
    driver.find_element(By.CSS_SELECTOR, ".cart-icon").click()
    driver.find_element(By.CSS_SELECTOR, ".cart-item .remove-btn").click()
    # If .cart-item changes to .basket-item, fix it in EVERY test

With POM, changes are isolated:

# With POM - Change once, works everywhere
def test_add_to_cart(product_page, cart_page):
    product_page.add_first_product_to_cart()
    cart_page.open()
    assert cart_page.get_item_count() == 1

def test_remove_from_cart(product_page, cart_page):
    product_page.add_first_product_to_cart()
    cart_page.open()
    cart_page.remove_first_item()
    # Locator change? Fix it once in cart_page.py

POM Architecture

Page Object Model Structure Page Objects (pages/ directory):
  • base_page.py โ€” Common methods for all pages
  • login_page.py โ€” Login page interactions
  • products_page.py โ€” Product listing page
  • product_detail_page.py โ€” Single product page
  • cart_page.py โ€” Shopping cart
  • checkout_page.py โ€” Checkout flow
Reusable Components (components/ subfolder):
  • header.py โ€” Header component (shared across pages)
  • footer.py โ€” Footer component
  • cart_widget.py โ€” Mini cart widget

Base Page Class

# pages/base_page.py
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from utils.config import Config

class BasePage:
    """Base class for all page objects"""

    def __init__(self, driver):
        self.driver = driver
        self.wait = WebDriverWait(driver, Config.EXPLICIT_WAIT)

    def open(self, path=""):
        """Navigate to page"""
        url = f"{Config.BASE_URL}{path}"
        self.driver.get(url)
        return self

    def find(self, locator):
        """Find element with explicit wait"""
        return self.wait.until(EC.presence_of_element_located(locator))

    def find_all(self, locator):
        """Find all elements"""
        return self.driver.find_elements(*locator)

    def click(self, locator):
        """Wait for element and click"""
        element = self.wait.until(EC.element_to_be_clickable(locator))
        element.click()
        return self

    def type(self, locator, text):
        """Clear field and type text"""
        element = self.wait.until(EC.element_to_be_clickable(locator))
        element.clear()
        element.send_keys(text)
        return self

    def get_text(self, locator):
        """Get element text"""
        return self.find(locator).text

    def is_visible(self, locator, timeout=5):
        """Check if element is visible"""
        try:
            WebDriverWait(self.driver, timeout).until(
                EC.visibility_of_element_located(locator)
            )
            return True
        except:
            return False

    def wait_for_url_contains(self, text):
        """Wait for URL to contain text"""
        self.wait.until(EC.url_contains(text))
        return self

    def take_screenshot(self, name):
        """Take screenshot for debugging"""
        self.driver.save_screenshot(f"screenshots/{name}.png")

Products Page

# pages/products_page.py
from selenium.webdriver.common.by import By
from pages.base_page import BasePage

class ProductsPage(BasePage):
    """Product listing page"""

    # Locators
    PRODUCT_CARDS = (By.CSS_SELECTOR, "[data-testid='product-card']")
    PRODUCT_TITLE = (By.CSS_SELECTOR, "[data-testid='product-title']")
    PRODUCT_PRICE = (By.CSS_SELECTOR, "[data-testid='product-price']")
    ADD_TO_CART_BTN = (By.CSS_SELECTOR, "[data-testid='add-to-cart']")
    SEARCH_INPUT = (By.CSS_SELECTOR, "[data-testid='search-input']")
    SEARCH_BTN = (By.CSS_SELECTOR, "[data-testid='search-button']")
    SORT_DROPDOWN = (By.CSS_SELECTOR, "[data-testid='sort-select']")
    LOADING_SPINNER = (By.CSS_SELECTOR, ".loading-spinner")

    def open(self):
        """Open products page"""
        super().open("/products")
        self.wait_for_products_to_load()
        return self

    def wait_for_products_to_load(self):
        """Wait for product grid to load"""
        self.wait.until(EC.invisibility_of_element_located(self.LOADING_SPINNER))
        self.wait.until(EC.presence_of_element_located(self.PRODUCT_CARDS))
        return self

    def get_all_products(self):
        """Get all product cards"""
        return self.find_all(self.PRODUCT_CARDS)

    def get_product_count(self):
        """Get number of products displayed"""
        return len(self.get_all_products())

    def get_product_by_index(self, index):
        """Get specific product by index"""
        products = self.get_all_products()
        if index < len(products):
            return products[index]
        raise IndexError(f"Product index {index} out of range")

    def add_product_to_cart(self, index=0):
        """Add product to cart by index"""
        product = self.get_product_by_index(index)
        add_btn = product.find_element(*self.ADD_TO_CART_BTN)
        add_btn.click()
        # Wait for cart update animation
        self.wait.until(EC.invisibility_of_element_located(
            (By.CSS_SELECTOR, ".cart-updating")
        ))
        return self

    def search(self, query):
        """Search for products"""
        self.type(self.SEARCH_INPUT, query)
        self.click(self.SEARCH_BTN)
        self.wait_for_products_to_load()
        return self

    def sort_by(self, option):
        """Sort products by option"""
        from selenium.webdriver.support.ui import Select
        select = Select(self.find(self.SORT_DROPDOWN))
        select.select_by_visible_text(option)
        self.wait_for_products_to_load()
        return self

    def get_product_titles(self):
        """Get all product titles"""
        return [p.find_element(*self.PRODUCT_TITLE).text
                for p in self.get_all_products()]

    def get_product_prices(self):
        """Get all product prices as floats"""
        prices = []
        for product in self.get_all_products():
            price_text = product.find_element(*self.PRODUCT_PRICE).text
            # Convert "$29.99" to 29.99
            price = float(price_text.replace("$", "").replace(",", ""))
            prices.append(price)
        return prices

Cart Page

# pages/cart_page.py
from selenium.webdriver.common.by import By
from pages.base_page import BasePage

class CartPage(BasePage):
    """Shopping cart page"""

    # Locators
    CART_ITEMS = (By.CSS_SELECTOR, "[data-testid='cart-item']")
    ITEM_TITLE = (By.CSS_SELECTOR, "[data-testid='item-title']")
    ITEM_PRICE = (By.CSS_SELECTOR, "[data-testid='item-price']")
    ITEM_QUANTITY = (By.CSS_SELECTOR, "[data-testid='item-quantity']")
    REMOVE_BTN = (By.CSS_SELECTOR, "[data-testid='remove-item']")
    CART_TOTAL = (By.CSS_SELECTOR, "[data-testid='cart-total']")
    CHECKOUT_BTN = (By.CSS_SELECTOR, "[data-testid='checkout-button']")
    EMPTY_CART_MSG = (By.CSS_SELECTOR, "[data-testid='empty-cart']")
    CONTINUE_SHOPPING = (By.CSS_SELECTOR, "[data-testid='continue-shopping']")

    def open(self):
        """Open cart page"""
        super().open("/cart")
        return self

    def get_items(self):
        """Get all cart items"""
        return self.find_all(self.CART_ITEMS)

    def get_item_count(self):
        """Get number of items in cart"""
        return len(self.get_items())

    def is_empty(self):
        """Check if cart is empty"""
        return self.is_visible(self.EMPTY_CART_MSG)

    def get_item_by_index(self, index):
        """Get cart item by index"""
        items = self.get_items()
        if index < len(items):
            return CartItem(items[index])
        raise IndexError(f"Cart item index {index} out of range")

    def remove_item(self, index=0):
        """Remove item from cart"""
        item = self.get_item_by_index(index)
        item.remove()
        # Wait for item to be removed from DOM
        self.wait.until(lambda d: len(self.get_items()) < index + 1)
        return self

    def update_quantity(self, index, quantity):
        """Update item quantity"""
        item = self.get_item_by_index(index)
        item.set_quantity(quantity)
        return self

    def get_total(self):
        """Get cart total as float"""
        total_text = self.get_text(self.CART_TOTAL)
        return float(total_text.replace("$", "").replace(",", ""))

    def proceed_to_checkout(self):
        """Click checkout button"""
        self.click(self.CHECKOUT_BTN)
        self.wait_for_url_contains("/checkout")
        from pages.checkout_page import CheckoutPage
        return CheckoutPage(self.driver)


class CartItem:
    """Represents a single cart item"""

    def __init__(self, element):
        self.element = element

    @property
    def title(self):
        return self.element.find_element(
            By.CSS_SELECTOR, "[data-testid='item-title']"
        ).text

    @property
    def price(self):
        text = self.element.find_element(
            By.CSS_SELECTOR, "[data-testid='item-price']"
        ).text
        return float(text.replace("$", ""))

    @property
    def quantity(self):
        input_el = self.element.find_element(
            By.CSS_SELECTOR, "[data-testid='item-quantity']"
        )
        return int(input_el.get_attribute("value"))

    def set_quantity(self, qty):
        input_el = self.element.find_element(
            By.CSS_SELECTOR, "[data-testid='item-quantity']"
        )
        input_el.clear()
        input_el.send_keys(str(qty))
        # Trigger blur/change event
        input_el.send_keys(Keys.TAB)

    def remove(self):
        self.element.find_element(
            By.CSS_SELECTOR, "[data-testid='remove-item']"
        ).click()

Shared Components

# pages/components/header.py
from selenium.webdriver.common.by import By
from pages.base_page import BasePage

class Header(BasePage):
    """Header component (appears on all pages)"""

    LOGO = (By.CSS_SELECTOR, "[data-testid='logo']")
    SEARCH_INPUT = (By.CSS_SELECTOR, "[data-testid='header-search']")
    CART_ICON = (By.CSS_SELECTOR, "[data-testid='cart-icon']")
    CART_COUNT = (By.CSS_SELECTOR, "[data-testid='cart-count']")
    USER_MENU = (By.CSS_SELECTOR, "[data-testid='user-menu']")
    LOGIN_LINK = (By.CSS_SELECTOR, "[data-testid='login-link']")
    LOGOUT_BTN = (By.CSS_SELECTOR, "[data-testid='logout-btn']")

    def get_cart_count(self):
        """Get current cart count"""
        try:
            text = self.get_text(self.CART_COUNT)
            return int(text) if text else 0
        except:
            return 0

    def go_to_cart(self):
        """Click cart icon"""
        self.click(self.CART_ICON)
        from pages.cart_page import CartPage
        return CartPage(self.driver)

    def search(self, query):
        """Search from header"""
        self.type(self.SEARCH_INPUT, query)
        self.find(self.SEARCH_INPUT).submit()
        from pages.products_page import ProductsPage
        return ProductsPage(self.driver)

    def is_logged_in(self):
        """Check if user is logged in"""
        return self.is_visible(self.USER_MENU)

    def logout(self):
        """Logout user"""
        self.click(self.USER_MENU)
        self.click(self.LOGOUT_BTN)
        return self

Using Page Objects in Tests

# tests/test_cart.py
import pytest
from pages.products_page import ProductsPage
from pages.cart_page import CartPage
from pages.components.header import Header

class TestCart:

    def test_add_single_product_to_cart(self, driver):
        """Verify adding a product to cart"""
        products_page = ProductsPage(driver).open()
        header = Header(driver)

        # Initial cart should be empty
        assert header.get_cart_count() == 0

        # Add first product
        products_page.add_product_to_cart(0)

        # Cart count should update
        assert header.get_cart_count() == 1

    def test_cart_shows_added_product(self, driver):
        """Verify cart displays correct product"""
        products_page = ProductsPage(driver).open()

        # Get product info before adding
        product_titles = products_page.get_product_titles()
        first_product_title = product_titles[0]

        # Add to cart and go to cart
        products_page.add_product_to_cart(0)
        cart_page = Header(driver).go_to_cart()

        # Verify product is in cart
        assert cart_page.get_item_count() == 1
        cart_item = cart_page.get_item_by_index(0)
        assert cart_item.title == first_product_title

    def test_remove_product_from_cart(self, driver):
        """Verify removing product from cart"""
        # Setup: Add product
        products_page = ProductsPage(driver).open()
        products_page.add_product_to_cart(0)

        # Go to cart and remove
        cart_page = Header(driver).go_to_cart()
        assert cart_page.get_item_count() == 1

        cart_page.remove_item(0)

        # Cart should be empty
        assert cart_page.is_empty()

    def test_update_quantity(self, driver):
        """Verify updating item quantity"""
        products_page = ProductsPage(driver).open()
        products_page.add_product_to_cart(0)

        cart_page = Header(driver).go_to_cart()
        original_total = cart_page.get_total()

        # Update quantity to 3
        cart_page.update_quantity(0, 3)

        # Total should update (roughly 3x, accounting for formatting)
        new_total = cart_page.get_total()
        assert new_total > original_total

Key Takeaways

  • One class per page - Clear separation of concerns
  • Locators at the top - Easy to find and update
  • Methods return self or new page - Enable method chaining
  • Base class for common logic - DRY principle
  • Components for shared UI - Header, footer, widgets

Next Lesson

With POM in place, let's dive deep into Testing the Shopping Cartโ€”the heart of e-commerce testing.

Waiting & Synchronization