Source code for holmium.core.pageobject

"""
implementation of page objects, element(s) and sections
"""

import contextlib
import inspect
import threading
import types
import weakref
from collections import OrderedDict
from collections.abc import Sequence
from functools import wraps

import selenium.webdriver.common.by
from selenium.common.exceptions import (
    NoSuchElementException,
    NoSuchFrameException,
    StaleElementReferenceException,
    TimeoutException,
)
from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver.support.ui import WebDriverWait

from holmium.core.conditions import BaseCondition

from .enhancers import get_enhancers
from .facets import CopyOnCreateFacetCollectionMeta, ElementFacet, Faceted
from .logger import log

# pylint: disable=unnecessary-lambda,too-few-public-methods,too-many-arguments


[docs]class Locators(selenium.webdriver.common.by.By): """ proxy class to access locator types """ pass
[docs]class ElementList(list): """ proxy to a standard list which would be stored in a :class:`holmium.core.Page`. """ def __init__(self, instance, *args, **kwargs): self.instance = weakref.ref(instance) list.__init__(self, *args, **kwargs) def __getitem__(self, index): return list.__getitem__(self, index).__get__( self.instance(), self.instance().__class__ ) def __iter__(self): for idx in range(len(self)): yield self[idx]
[docs]class ElementDict(dict): """ proxy to a standard dict which would be stored in a :class:`holmium.core.Page`. """ def __init__(self, instance, *args, **kwargs): self.instance = weakref.ref(instance) dict.__init__(self, *args, **kwargs) def __getitem__(self, key): return dict.__getitem__(self, key).__get__( self.instance(), self.instance().__class__ )
[docs] def values(self): return [self[key] for key in self]
[docs] def items(self): return [(key, self[key]) for key in self.keys()]
class Registry(CopyOnCreateFacetCollectionMeta): """ simple meta class to keep track of all page objects registered """ pages = {} def __new__(mcs, *args, **kwargs): page = super().__new__(mcs, *args, **kwargs) Registry.pages[args[0]] = page return page
[docs]class Page(Faceted, metaclass=Registry): """ Base class for all page objects to extend from. void Instance methods implemented by subclasses are provisioned with fluent wrappers to facilitate with writing code such as:: class Google(Page): def query(self): .... def submit(self): .... def get_results(self): .... assert len(Google().query("page objects").submit().get_results()) > 0 """ local = threading.local() def __init__(self, driver, url=None, iframe=None): # pylint: disable=too-many-branches super().__init__() self.driver = driver self.touched = False self.initialized = False if url: self.home = url elif driver.current_url: self.home = driver.current_url else: self.home = None self.iframe = iframe def update_element(element, name): """ check if the element is a facet and register it. """ if issubclass(element.__class__, ElementGetter): element.iframe = self.iframe if element.is_facet: facet = ElementFacet(element, name, debug=element.is_debug_facet) facet.register(self) return True return False for element in inspect.getmembers(self.__class__): if issubclass(element[1].__class__, list): hit = True for item in element[1]: hit &= update_element(item, element[0]) if hit: self.__setattr__(element[0], ElementList(self, element[1])) elif issubclass(element[1].__class__, dict): hit = True for item in element[1].values(): hit &= update_element(item, element[0]) if hit: self.__setattr__(element[0], ElementDict(self, element[1])) else: update_element(element[1], element[0]) if url: self.driver.get(url) self.initialized = True @contextlib.contextmanager def scope(self): """ context manager to manage the current webdriver in use. """ Page.local.driver = object.__getattribute__(self, "driver") yield @classmethod def get_driver(cls): """ returns the thread local driver """ return cls.local.driver def go_home(self): """ returns the page object to the url it was initialized with """ self.driver.get(self.home) def __getattribute__(self, key): """ to enable fluent access to page objects, instance methods that don't return a value, instead return the page object instance. """ def attr_getter(key): return object.__getattribute__(self, key) def attr_setter(key, value): return object.__setattr__(self, key, value) with attr_getter("scope")(): if not attr_getter("touched") and attr_getter("initialized"): attr_getter("evaluate")() attr_setter("touched", True) attr = attr_getter(key) # check if home url is set, else update. if not attr_getter("home"): log.debug("home url not set, attempting to update.") attr_setter("home", attr_getter("driver").current_url) if isinstance(attr, types.MethodType): @wraps(attr) def wrap(*args, **kwargs): """ fluent wrapper """ resp = attr(*args, **kwargs) if resp is None: resp = self return resp return wrap return attr
[docs]class ElementGetter: """ internal class to encapsulate the logic used by :class:`holmium.core.Element` & :class:`holmium.core.Elements` """ # pylint: disable=too-many-instance-attributes def __init__( self, locator_type, query_string, base_element=None, timeout=0, value=lambda el: el, only_if=lambda el: el is not None, facet=False, filter_by=lambda el: el is not None, ): """ :param holmium.core.Locators locator_type: selenium locator to use when locating the element :param str query_string: the value to pass to the locator :param holmium.core.Element base_element: a reference to another i element under which to locate this element. :param int timeout: time to implicitely wait for the element :param lambda value: transform function for the value of the element. The located :class:`selenium.webdriver.remote.webelement.WebElement` instance is passed as the only argument to the function. :param function only_if: extra validation function that is called repeatedly until :attr:`timeout` has elapsed. If not provided the default function used checks that the element is present. The located :class:`selenium.webdriver.remote.webelement.WebElement` instance is passed as the only argument to the function. :param bool facet: flag to treat this element as a facet. :param function filter_by: condition function that determines if the located :class:`selenium.webdriver.remote.webelement.WebElement`, the only argument passed to the function, should be returned. If not provided, the default function used checks that the element is present. """ self.query_string = query_string self.locator_type = locator_type self.timeout = timeout self.driver = None self.iframe = None self.base_element = base_element self.value_mapper = value self.root_fn = lambda: Page.get_driver() self.only_if = only_if log.debug( "locator:%s, query_string:%s, timeout:%d" % (locator_type, query_string, timeout) ) self.is_facet = facet self.is_debug_facet = False self.filter_by = filter_by @property def root(self): "returns the root webelement" return self.root_fn() @root.setter def root(self, root_fn): "sets the root element extraction function" self.root_fn = root_fn def _get_element(self, method=None): """ extracts the webelement(s) :param function method: the method used to query the webdriver """ if self.base_element: if isinstance(self.base_element, types.LambdaType): _ = self.base_element() _meth = getattr(_, method.__name__) elif isinstance(self.base_element, Element): _meth = getattr( self.base_element.__get__(self, self.__class__), method.__name__ ) elif isinstance(self.base_element, WebElement): _meth = getattr(self.base_element, "find_element") else: raise TypeError( "invalid base_element type (%s) used" % (type(self.base_element)) ) else: _meth = method log.debug( "looking up locator:%s, query_string:%s, timeout:%d" % (self.locator_type, self.query_string, self.timeout) ) if self.iframe: Page.local.driver.switch_to.default_content() Page.local.driver.switch_to.frame(self.iframe) if self.timeout: try: def callback(_): """ timeout & only_if explicit wait. """ return _meth(self.locator_type, self.query_string) and ( BaseCondition.get_current() or self.only_if )(_meth(self.locator_type, self.query_string)) WebDriverWait( self.root, self.timeout, ignored_exceptions=[ StaleElementReferenceException, ], ).until(callback) except TimeoutException: log.debug( "unable to find element %s after waiting for %d seconds" % (self.query_string, self.timeout) ) raise retval = _meth(self.locator_type, self.query_string) if isinstance(retval, list): return [el for el in retval if self.filter_by(el)] elif self.filter_by(retval): return retval else: return None
[docs] @classmethod def enhance(cls, element): """ incase a higher level abstraction for a WebElement is available we will use that in Pages. (e.g. a select element is converted into :class:`selenium.webdriver.support.ui.Select`) """ if element is None: return None for enhancer in get_enhancers(): if enhancer.matches(element): return enhancer(element) return element
[docs]class Element(ElementGetter): """ Utility to get a :class:`selenium.webdriver.remote.webelement.WebElement` by querying via one of :class:`holmium.core.Locators` :param holmium.core.Locators locator_type: selenium locator to use when locating the element :param str query_string: the value to pass to the locator :param holmium.core.Element base_element: a reference to another element under which to locate this element. :param int timeout: time to implicitely wait for the element :param lambda value: transform function for the value of the element. The located :class:`selenium.webdriver.remote.webelement.WebElement` instance is passed as the only argument to the function. :param function only_if: extra validation function that is called repeatedly until :attr:`timeout` elapses. If not provided the default function used checks that the element is present. The located :class:`selenium.webdriver.remote.webelement.WebElement` instance is passed as the only argument to the function. :param bool facet: flag to treat this element as a facet. :param function filter_by: condition function that determines if the located :class:`selenium.webdriver.remote.webelement.WebElement`, the only argument passed to the function, should be returned. If not provided, the default function used checks that the element is present. """ def __get__(self, instance, owner): if not instance: return self try: return ( self.value_mapper( self.enhance(self._get_element(self.root.find_element)) ) if self.root else None ) except ( NoSuchElementException, TimeoutException, StaleElementReferenceException, ): return None
[docs]class Elements(ElementGetter): """ Utility to get a collection of :class:`selenium.webdriver.remote.webelement.WebElement` objects by querying via one of :class:`holmium.core.Locators` :param holmium.core.Locators locator_type: selenium locator to use when locating the element :param str query_string: the value to pass to the locator :param holmium.core.Element base_element: a reference to another element under which to locate this element. :param int timeout: time to implicitely wait for the element :param lambda value: transform function for each element in the collection. The located :class:`selenium.webdriver.remote.webelement.WebElement` instance is passed as the only argument to the function. :param function only_if: extra validation function that is called repeatedly until :attr:`timeout` elapses. If not provided the default function used checks that the element collection is not empty. The list of located :class:`selenium.webdriver.remote.webelement.WebElement` instances is passed as the only argument to the function. :param bool facet: flag to treat this element as a facet. :param function filter_by: condition function determines which elements are included in the collection. If not provided the default function used includes all elements identified by :attr:`query_string`. A :class:`selenium.webdriver.remote.webelement.WebElement` instance is passed as the only argument to the function. """ # pylint: disable=incomplete-protocol,line-too-long def __init__( self, locator_type, query_string=None, base_element=None, timeout=0, value=lambda el: el, only_if=lambda els: len(els) > 0, facet=False, filter_by=lambda el: el is not None, ): super().__init__( locator_type, query_string, base_element=base_element, timeout=timeout, facet=facet, value=value, only_if=only_if, filter_by=filter_by, ) def __getitem__(self, idx): return lambda: self.__get__(self, self.__class__)[idx] def __get__(self, instance, owner): if not instance: return self try: return ( [ self.value_mapper(self.enhance(el)) for el in self._get_element(self.root.find_elements) ] if self.root else [] ) except ( NoSuchElementException, TimeoutException, StaleElementReferenceException, ): return []
[docs]class ElementMap(Elements): """ Used to create dynamic dictionaries based on an element locator specified by one of :class:`holmium.core.Locators`. The wrapped dictionary is an :class:`collections.OrderedDict` instance. :param holmium.core.Locators locator_type: selenium locator to use when locating the element :param str query_string: the value to pass to the locator :param holmium.core.Element base_element: a reference to another element under which to locate this element. :param int timeout: time to implicitely wait for the element :param bool facet: flag to treat this element as a facet. :param lambda key: transform function for mapping a key to a WebElement in the collection. The located :class:`selenium.webdriver.remote.webelement.WebElement` instance is passed as the only argument to the function. :param lambda value: transform function for the value when accessed via the key. The located :class:`selenium.webdriver.remote.webelement.WebElement` instance is passed as the only argument to the function. :param function only_if: extra validation function that is called repeatedly until :attr:`timeout` elapses. If not provided the default function used checks that the element collection is not empty. The list of located :class:`selenium.webdriver.remote.webelement.WebElement` instances is passed as the only argument to the function. :param function filter_by: condition function determines which elements are included in the collection. If not provided the default function used includes all elements identified by :attr:`query_string`. A :class:`selenium.webdriver.remote.webelement.WebElement` instance is passed as the only argument to the function. """ # pylint: disable=incomplete-protocol,line-too-long def __init__( self, locator_type, query_string=None, base_element=None, timeout=0, key=lambda el: el.text, value=lambda el: el, only_if=lambda els: len(els) > 0, facet=False, filter_by=lambda el: el is not None, ): super().__init__( locator_type, query_string, base_element, timeout, facet=facet, only_if=only_if, filter_by=filter_by, ) self.key_mapper = key self.value_mapper = value def __get__(self, instance, owner): if not instance: return self try: return ( OrderedDict( (self.key_mapper(el), self.value_mapper(self.enhance(el))) for el in self._get_element(self.root.find_elements) ) if self.root else {} ) except ( NoSuchElementException, TimeoutException, StaleElementReferenceException, ): return {} def __getitem__(self, key): return lambda: self.__get__(self, self.__class__)[key]
[docs]class Section(Faceted): """ Base class to encapsulate reusable page sections:: class MySection(Section): things = Elements( .... ) class MyPage(Page): section_1 = MySection(Locators.CLASS_NAME, "section") section_2 = MySection(Locators.ID, "unique_section") """ def __init__(self, locator_type, query_string, iframe=None, timeout=0): super().__init__() self.touched = False self.locator_type = locator_type self.query_string = query_string self.iframe = iframe self.timeout = timeout self.__root_val = None self.element_members = {} for element in inspect.getmembers(self.__class__): if issubclass(element[1].__class__, ElementGetter): self.element_members[element[0]] = element[1] if element[1].is_facet: facet = ElementFacet( element[1], element[0], debug=element[1].is_debug_facet ) facet.register(self) def __get__(self, instance, owner): for element in self.element_members.values(): element.root = lambda: self.root return self def __getattribute__(self, item): def attr_getter(key): return super(Section, self).__getattribute__(key) def attr_setter(key, value): return super(Section, self).__setattr__(key, value) members = attr_getter("element_members") touched = attr_getter("touched") if not touched and item in members: attr_getter("evaluate")() attr_setter("touched", True) return attr_getter(item) @property def root(self): """ returns the element the section is rooted at """ if self.iframe: try: Page.get_driver().switch_to.default_content() Page.get_driver().switch_to.frame(self.iframe) except NoSuchFrameException: log.error("unable to switch to iframe %s" % self.iframe) try: if not self.__root_val: WebDriverWait(Page.get_driver(), self.timeout).until( lambda _: Page.get_driver().find_element( self.locator_type, self.query_string ) ) return self.__root_val or Page.get_driver().find_element( self.locator_type, self.query_string ) except ( NoSuchElementException, TimeoutException, StaleElementReferenceException, ): return None @root.setter def root(self, val): """ sets the root of the section """ self.__root_val = val
[docs]class Sections(Section, Sequence): """ Base class for an Iterable view of a collection of :class:`holmium.core.Section` objects. """ def __init__(self, locator_type, query_string, iframe=None, timeout=0): super().__init__(locator_type, query_string, iframe, timeout) def __getelements__(self): if self.timeout: try: WebDriverWait(Page.get_driver(), self.timeout).until( lambda _: Page.get_driver().find_elements( self.locator_type, self.query_string ) ) except TimeoutException: log.debug( "unable to find element %s after waiting for %d seconds" % (self.query_string, self.timeout) ) return Page.get_driver().find_elements(self.locator_type, self.query_string) def __iter__(self): for element in self.__getelements__(): self.root = element yield self def __len__(self): return len(self.__getelements__()) def __getitem__(self, idx): if idx < 0: idx = len(self) + idx if idx < 0 or idx >= len(self): raise IndexError("Sections index (%d) out of range" % idx) for i, _ in enumerate(self): if i == idx: break return self