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 pathUsing 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 shownShort-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:
passUsage in page objects:
def __init__(self, driver):
super().__init__(driver)
self.dismiss_if_present(
(AppiumBy.ACCESSIBILITY_ID, "ratingPromptDismiss")
)