Tutorials / Selenium WebDriver series

Part 2: Wrapping Selenium UI elements

Raw WebDriver code repeats the same three steps for every element: wait for it, find it, act on it. A small UiObject class does that once, so page objects and tests only say what to do.

The UiObject class

A UiObject is a locator plus the operations you need on it. It doesn't look the element up until you use it, so page objects can define all their elements up front, before the page is even open:

src/pages/UiObject.pypython
from selenium.common.exceptions import TimeoutException
from selenium.webdriver import ActionChains
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.wait import WebDriverWait

from src.pages.Browser import Browser


class UiObject:

    def __init__(self, by, locator, web_element=None):
        self.__by = by
        self.__locator = locator
        self.__web_element = web_element

    @staticmethod
    def from_web_element(web_element):
        # wrap an element you already found, e.g. one of get_elements()
        return UiObject(by=None, locator=None, web_element=web_element)

    @property
    def by(self):
        return self.__by

    @property
    def locator(self):
        return self.__locator

    def __wait(self, condition, wait):
        try:
            WebDriverWait(Browser.get_driver(), wait).until(condition((self.by, self.locator)))
            return True
        except TimeoutException:
            return False

    def get_element(self, wait=10):
        if self.__web_element is not None:
            return self.__web_element
        self.wait_to_appear(wait)
        return Browser.get_driver().find_element(self.by, self.locator)

    def get_elements(self, wait=10):
        if self.__web_element is not None:
            return [self.__web_element]
        self.wait_to_appear(wait)
        return Browser.get_driver().find_elements(self.by, self.locator)

    def exists(self, wait=1):
        return self.__wait(EC.presence_of_element_located, wait)

    def visible(self, wait=1):
        return self.__wait(EC.visibility_of_element_located, wait)

    def clickable(self, wait=1):
        return self.__wait(EC.element_to_be_clickable, wait)

    def wait_to_appear(self, wait=10):
        if self.exists(wait):
            return self
        raise AssertionError("Locator did not appear: {} in {} seconds".format(self.locator, wait))

    def wait_to_disappear(self, wait=10):
        if self.__wait(EC.invisibility_of_element_located, wait):
            return self
        raise AssertionError("Locator did not disappear: {} in {} seconds".format(self.locator, wait))

    def wait_to_be_clickable(self, wait=10):
        if self.clickable(wait):
            return self
        if self.exists():
            raise AssertionError("Locator did not become clickable: {} in {} seconds".format(self.locator, wait))
        raise AssertionError("Locator does not exist: {}".format(self.locator))

    def get_text(self, wait=10):
        return self.get_element(wait).text

    def set_text(self, value, wait=10):
        element = self.get_element(wait)
        element.clear()
        element.send_keys(value)
        return self

    def press_key(self, key, use_ac=False, wait=10):
        if use_ac:
            ActionChains(Browser.get_driver()).send_keys(key).perform()
        else:
            self.get_element(wait).send_keys(key)
        return self

    def get_attribute(self, name, wait=10):
        return self.get_element(wait).get_attribute(name)

    def click(self, use_ac=False, wait=10):
        if use_ac:
            ActionChains(Browser.get_driver()).move_to_element(self.get_element(wait)).click().perform()
        elif self.__web_element is not None:
            self.__web_element.click()
        else:
            self.wait_to_be_clickable(wait)
            self.get_element(wait).click()
        return self
  • Every lookup waits first: get_element() waits up to 10 seconds for the element to appear, click() for it to be clickable. Tests don't need time.sleep().
  • exists(), visible() and clickable() answer a question and never raise; the wait_to_... methods raise a readable AssertionError instead, which Test Junkie reports as a failure.
  • from_web_element() wraps an element you already found, e.g. one of several matches from get_elements().
Keep it about elements
UiObject only deals with elements on the page. Navigation, cookies and tabs belong in Browser; what a page can do belongs in its page object (part 3).

Try it

demo_part2.pypython
from selenium.webdriver.common.by import By

from src.pages.Browser import Browser
from src.pages.UiObject import UiObject
from src.utils.Settings import Settings

docs_link = UiObject(By.XPATH, "//nav//a[normalize-space() = 'DOCUMENTATION']")
first_heading = UiObject(By.TAG_NAME, "h1")

Browser.get_driver().get(Settings.DOMAIN)
print("home page h1:", first_heading.get_text())
docs_link.click()
print("after the click:", Browser.get_driver().current_url)
print("left menu visible:", UiObject(By.ID, "leftCol").visible())
Browser.shutdown()
python demo_part2.pytext
home page h1: The Python test runner built for precision.
after the click: https://www.test-junkie.com/documentation/
left menu visible: True