The BasePage Pattern — Shared Methods, Waits and DRY Automation

As your page object collection grows, you will notice every page object contains the same boilerplate: a constructor that stores the driver, a WebDriverWait instance, a method to click elements safely, a method to type text with clearing, and a method to wait for elements. The BasePage pattern extracts this shared logic into a parent class that all page objects inherit. This eliminates duplication, standardises wait behaviour, and makes adding new page objects trivially fast.

The BasePage — Foundation of Every Page Object

BasePage provides three categories of shared functionality: driver and wait management, wrapped element interactions (click, type, read), and utility methods (screenshot, scroll, URL check).

from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from selenium.common.exceptions import TimeoutException
import os


class BasePage:
    def __init__(self, driver, timeout=10):
        self.driver = driver
        self.wait = WebDriverWait(driver, timeout)

    # ── Element interaction wrappers ──

    def find(self, locator):
        return self.wait.until(EC.visibility_of_element_located(locator))

    def find_all(self, locator):
        self.wait.until(EC.presence_of_element_located(locator))
        return self.driver.find_elements(*locator)

    def click(self, locator):
        element = self.wait.until(EC.element_to_be_clickable(locator))
        element.click()

    def type_text(self, locator, text):
        element = self.find(locator)
        element.clear()
        element.send_keys(text)

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

    def is_visible(self, locator, timeout=3):
        try:
            WebDriverWait(self.driver, timeout).until(
                EC.visibility_of_element_located(locator)
            )
            return True
        except TimeoutException:
            return False

    # ── Navigation utilities ──

    def get_current_url(self):
        return self.driver.current_url

    def get_title(self):
        return self.driver.title

    def wait_for_url_contains(self, text):
        self.wait.until(EC.url_contains(text))

    # ── Utility methods ──

    def take_screenshot(self, name="screenshot"):
        os.makedirs("reports/screenshots", exist_ok=True)
        path = f"reports/screenshots/{name}.png"
        self.driver.save_screenshot(path)
        return path

    def scroll_to_element(self, locator):
        element = self.driver.find_element(*locator)
        self.driver.execute_script(
            "arguments[0].scrollIntoView({behavior:'smooth',block:'center'});",
            element
        )


# ── LoginPage inherits from BasePage ──
class LoginPage(BasePage):
    URL            = "https://www.saucedemo.com"
    USERNAME_INPUT = (By.ID, "user-name")
    PASSWORD_INPUT = (By.ID, "password")
    LOGIN_BUTTON   = (By.ID, "login-button")
    ERROR_MESSAGE  = (By.CSS_SELECTOR, "[data-test='error']")

    def open(self):
        self.driver.get(self.URL)
        self.wait.until(EC.visibility_of_element_located(self.LOGIN_BUTTON))
        return self

    def login(self, username, password):
        self.type_text(self.USERNAME_INPUT, username)    # inherited from BasePage
        self.type_text(self.PASSWORD_INPUT, password)    # inherited from BasePage
        self.click(self.LOGIN_BUTTON)                    # inherited from BasePage
        return InventoryPage(self.driver)

    def login_expecting_error(self, username, password):
        self.type_text(self.USERNAME_INPUT, username)
        self.type_text(self.PASSWORD_INPUT, password)
        self.click(self.LOGIN_BUTTON)
        return self

    def get_error_message(self):
        return self.get_text(self.ERROR_MESSAGE)         # inherited from BasePage


# ── InventoryPage inherits from BasePage ──
class InventoryPage(BasePage):
    INVENTORY_LIST = (By.CLASS_NAME, "inventory_list")
    INVENTORY_ITEM = (By.CLASS_NAME, "inventory_item")
    ITEM_NAME      = (By.CLASS_NAME, "inventory_item_name")

    def __init__(self, driver):
        super().__init__(driver)
        self.wait.until(EC.visibility_of_element_located(self.INVENTORY_LIST))

    def get_product_count(self):
        return len(self.find_all(self.INVENTORY_ITEM))   # inherited from BasePage

    def get_product_names(self):
        elements = self.find_all(self.ITEM_NAME)         # inherited from BasePage
        return [el.text for el in elements]


# ── Compare: without BasePage vs with BasePage ──
print("Without BasePage — each page object repeats:")
print("  self.driver = driver")
print("  self.wait = WebDriverWait(driver, 10)")
print("  WebDriverWait(driver,10).until(EC.visibility_of...)")
print("  element.clear(); element.send_keys(text)")
print()
print("With BasePage — page objects call:")
print("  self.type_text(locator, text)")
print("  self.click(locator)")
print("  self.get_text(locator)")
print("  self.is_visible(locator)")
print()
print("Result: LoginPage is 20 lines instead of 45. Zero duplicated wait logic.")
Note: The find() method in BasePage uses visibility_of_element_located rather than presence_of_element_located. Presence means the element exists in the DOM (even if hidden). Visibility means it exists AND is displayed to the user. For interaction methods (click, type), you almost always want visibility — clicking a hidden element throws an exception, and typing into a hidden field has no visible effect. Use presence only when you need to check that an element was loaded into the DOM regardless of its display state.
Tip: Add a take_screenshot method to BasePage and call it automatically when a test fails. In pytest, you can use a fixture that checks the test result and captures a screenshot on failure: if request.node.rep_call.failed: page.take_screenshot(request.node.name). These failure screenshots are invaluable for debugging CI/CD failures where you cannot see the browser.
Warning: Do not add business logic or test assertions to BasePage. It should contain only generic, reusable browser interaction methods. If you find yourself adding methods like verify_login_success() to BasePage, that logic belongs in LoginPage or in the test itself. BasePage is infrastructure; page objects are application-specific; tests are business-rule-specific. Keep these three layers distinct.

Common Mistakes

Mistake 1 — Not using a BasePage, duplicating wait logic in every page object

❌ Wrong: Every page object has its own WebDriverWait setup, its own try/except TimeoutException blocks, and its own element.clear(); element.send_keys() pattern — duplicated 15 times across 15 page objects.

✅ Correct: BasePage defines these once. Page objects call self.type_text(locator, text) and inherit all the wait and interaction logic.

Mistake 2 — Making BasePage too specific to one application

❌ Wrong: BasePage contains methods like login(), add_to_cart(), and checkout() — application-specific actions that do not belong in a generic base class.

✅ Correct: BasePage contains only generic browser actions: click(), type_text(), get_text(), is_visible(), take_screenshot(). Application-specific actions belong in their respective page objects.

🧠 Test Yourself

What should the BasePage class contain?