Base Page Class with Driver and Wait Helpers

6 min read

BasePage is the foundation every page object builds on. It centralises driver access, wait configuration, and common helper methods so page objects stay concise and consistent.

Complete BasePage implementation

# pages/base_page.py
from __future__ import annotations
from typing import List
import time
 
from selenium.common.exceptions import (
    TimeoutException,
    NoSuchElementException,
    StaleElementReferenceException,
)
from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
 
 
class BasePage:
    DEFAULT_TIMEOUT = 15
    SHORT_TIMEOUT = 5
    LONG_TIMEOUT = 30
 
    def __init__(self, driver):
        self.driver = driver
 
    # --- Element finders ---
 
    def find(self, locator: tuple) -> WebElement:
        return self.driver.find_element(*locator)
 
    def find_all(self, locator: tuple) -> List[WebElement]:
        return self.driver.find_elements(*locator)
 
    # --- Waits ---
 
    def wait_for_visible(self, locator: tuple, timeout: int = None) -> WebElement:
        t = timeout or self.DEFAULT_TIMEOUT
        return WebDriverWait(self.driver, t).until(
            EC.visibility_of_element_located(locator)
        )
 
    def wait_for_clickable(self, locator: tuple, timeout: int = None) -> WebElement:
        t = timeout or self.DEFAULT_TIMEOUT
        return WebDriverWait(self.driver, t).until(
            EC.element_to_be_clickable(locator)
        )
 
    def wait_for_invisible(self, locator: tuple, timeout: int = None):
        t = timeout or self.LONG_TIMEOUT
        WebDriverWait(self.driver, t).until(
            EC.invisibility_of_element_located(locator)
        )
 
    def wait_for_text(self, locator: tuple, text: str, timeout: int = None) -> bool:
        t = timeout or self.DEFAULT_TIMEOUT
        return WebDriverWait(self.driver, t).until(
            EC.text_to_be_present_in_element(locator, text)
        )
 
    # --- Presence checks (no exception) ---
 
    def is_present(self, locator: tuple, timeout: int = SHORT_TIMEOUT) -> bool:
        try:
            WebDriverWait(self.driver, timeout).until(
                EC.presence_of_element_located(locator)
            )
            return True
        except TimeoutException:
            return False
 
    def is_visible(self, locator: tuple, timeout: int = SHORT_TIMEOUT) -> bool:
        try:
            WebDriverWait(self.driver, timeout).until(
                EC.visibility_of_element_located(locator)
            )
            return True
        except TimeoutException:
            return False
 
    # --- Interaction helpers ---
 
    def clear_and_type(self, locator: tuple, text: str):
        element = self.wait_for_clickable(locator)
        element.clear()
        element.send_keys(text)
 
    def tap(self, locator: tuple):
        self.wait_for_clickable(locator).click()
 
    def get_text(self, locator: tuple) -> str:
        return self.wait_for_visible(locator).text
 
    def get_attribute(self, locator: tuple, attribute: str) -> str:
        return self.wait_for_visible(locator).get_attribute(attribute)
 
    # --- Scroll helpers ---
 
    def scroll_to_text_android(self, text: str) -> WebElement:
        from appium.webdriver.common.appiumby import AppiumBy
        return self.driver.find_element(
            AppiumBy.ANDROID_UIAUTOMATOR,
            f'new UiScrollable(new UiSelector().scrollable(true))'
            f'.scrollIntoView(new UiSelector().text("{text}"))'
        )
 
    # --- Page source for debugging ---
 
    def get_page_source(self) -> str:
        return self.driver.page_source
 
    # --- Screenshot ---
 
    def take_screenshot(self, path: str):
        self.driver.get_screenshot_as_file(path)
        return path

Using BasePage in page objects

# pages/home_page.py
from appium.webdriver.common.appiumby import AppiumBy
from pages.base_page import BasePage
 
 
class HomePage(BasePage):
    PRODUCT_LIST  = (AppiumBy.ACCESSIBILITY_ID, "productList")
    PRODUCT_ITEMS = (AppiumBy.ACCESSIBILITY_ID, "productItem")
    CART_BADGE    = (AppiumBy.ACCESSIBILITY_ID, "cartBadge")
    SORT_BUTTON   = (AppiumBy.ACCESSIBILITY_ID, "sortButton")
 
    def get_product_count(self) -> int:
        return len(self.find_all(self.PRODUCT_ITEMS))
 
    def get_product_names(self) -> list[str]:
        return [item.text for item in self.find_all(self.PRODUCT_ITEMS)]
 
    def get_cart_count(self) -> int:
        if not self.is_present(self.CART_BADGE):
            return 0
        return int(self.get_text(self.CART_BADGE))
 
    def tap_sort(self) -> "SortOptionsSheet":
        self.tap(self.SORT_BUTTON)
        from pages.sort_options_sheet import SortOptionsSheet
        return SortOptionsSheet(self.driver)
 
    def tap_product(self, name: str) -> "ProductDetailPage":
        self.scroll_to_text_android(name).click()
        from pages.product_detail_page import ProductDetailPage
        return ProductDetailPage(self.driver)

Verifying page load in init

A common pattern: verify a landmark element is visible when the page object is constructed. If the navigation didn't land on the expected screen, this raises early with a clear message:

class CheckoutPage(BasePage):
    CHECKOUT_TITLE = (AppiumBy.ACCESSIBILITY_ID, "checkoutTitle")
 
    def __init__(self, driver):
        super().__init__(driver)
        try:
            self.wait_for_visible(self.CHECKOUT_TITLE, timeout=10)
        except TimeoutException:
            activity = getattr(driver, "current_activity", "unknown")
            raise AssertionError(
                f"CheckoutPage not loaded — current activity: {activity}"
            )

Handling keyboard dismissal

After send_keys() on a text field, the soft keyboard may cover other elements. Dismiss it before tapping the next element:

def clear_and_type(self, locator: tuple, text: str, dismiss_keyboard: bool = True):
    element = self.wait_for_clickable(locator)
    element.clear()
    element.send_keys(text)
    if dismiss_keyboard:
        try:
            self.driver.hide_keyboard()
        except Exception:
            pass  # hide_keyboard raises on some devices if keyboard isn't shown

Short-timeout presence check for optional elements

def dismiss_if_present(self, locator: tuple, timeout: int = 3):
    """Tap the element if it appears within timeout, silently continue if not."""
    try:
        self.wait_for_clickable(locator, timeout=timeout).click()
    except TimeoutException:
        pass

Usage in page objects:

def __init__(self, driver):
    super().__init__(driver)
    self.dismiss_if_present(
        (AppiumBy.ACCESSIBILITY_ID, "ratingPromptDismiss")
    )

// tip to track lessons you complete and pick up where you left off across devices.