Skip to content

Webdriver addon

webdriver_addon

module with webdriver class extensions

Webdriver extensions include: - speed-up for finding elements using parsel - access to shadow dom (with translation of xpath to css) - helper closing pop-ups - wait functions

EventFiringWebDriverExtendedMixedin(webdriver: _WebDriver, eventlistener: AbstractEventListener)

EventFiringWebDriverExtendedMixedin - mixin class for eventfiring webdriver

Source code in src/utils_seleniumxp/eventfiring_addon.py
def __init__(self, webdriver: _WebDriver, eventlistener: AbstractEventListener):
    super().__init__(webdriver, eventlistener)
    self._switch_to = EventFiringSwitchTo(super().wrapped_driver._switch_to, self)
    self._dispatcherobject = self._driver
    self._wrapperobject = self

closepopup(locator_click: utils_seleniumxp.SeleniumLocator, wait_click: float, check_click: bool = False, locator_iframe: Optional[utils_seleniumxp.SeleniumLocator] = None, wait_iframe: float = 1, locator_shadowdomhost: Optional[utils_seleniumxp.SeleniumLocator] = None) -> bool

Source code in src/utils_seleniumxp/webdriver_addon.py
def closepopup(
    self,
    locator_click: utils_seleniumxp.SeleniumLocator,
    wait_click: float,
    check_click: bool = False,
    locator_iframe: Optional[utils_seleniumxp.SeleniumLocator] = None,
    wait_iframe: float = 1,
    locator_shadowdomhost: Optional[utils_seleniumxp.SeleniumLocator] = None
) -> bool:
    return closepopup_eventfiring(self, locator_click, wait_click, check_click, locator_iframe, wait_iframe, locator_shadowdomhost)

closepopup_queueadd(locator_click: utils_seleniumxp.SeleniumLocator, wait_click: float, check_click: bool = False, locator_iframe: Optional[utils_seleniumxp.SeleniumLocator] = None, wait_iframe: float = 1, locator_shadowdomhost: Optional[utils_seleniumxp.SeleniumLocator] = None) -> bool

Source code in src/utils_seleniumxp/webdriver_addon.py
def closepopup_queueadd(
    self, locator_click:
    utils_seleniumxp.SeleniumLocator,
    wait_click: float,
    check_click: bool = False,
    locator_iframe: Optional[utils_seleniumxp.SeleniumLocator] = None,
    wait_iframe: float = 1,
    locator_shadowdomhost: Optional[utils_seleniumxp.SeleniumLocator] = None
) -> bool:
    return closepopup_queueadd_eventfiring(self, locator_click, wait_click, check_click, locator_iframe, wait_iframe, locator_shadowdomhost)

closepopup_queueprocessing(firstonly: bool = True) -> bool

Source code in src/utils_seleniumxp/webdriver_addon.py
def closepopup_queueprocessing(self, firstonly: bool = True) -> bool:
    return closepopup_queueprocessing_eventfiring(self, firstonly)

WebDriverMixedin(cls_webdriver: type[utils_seleniumxp._RemoteWebDriver]) -> type[utils_seleniumxp._RemoteWebDriver]

WebDriverMixedin - factory function for custom class with fixed 3rd party mixins and own mixin

The factory function mechanism is required to use alternative base classes like undetected_chromedriver.

Parameters:

Name Type Description Default
cls_webdriver type[_RemoteWebDriver]

base class

required

Returns:

Type Description
type[_RemoteWebDriver]

type[utils_seleniumxp._RemoteWebDriver]: custom webdriver class with mixin

Source code in src/utils_seleniumxp/webdriver_addon.py
def WebDriverMixedin(
    cls_webdriver: type[utils_seleniumxp._RemoteWebDriver]
) -> type[utils_seleniumxp._RemoteWebDriver]:
    """
    WebDriverMixedin - factory function for custom class with fixed 3rd party mixins and own mixin

    The factory function mechanism is required to use alternative base classes like
    undetected_chromedriver.

    Args:
        cls_webdriver (type[utils_seleniumxp._RemoteWebDriver]): base class

    Returns:
        type[utils_seleniumxp._RemoteWebDriver]: custom webdriver class with mixin
    """

    # class WebDriverClassCustom(_WebDriverMixin, _RequestsSessionMixin, cls_webdriver):
    #     pass
    #
    # return WebDriverClassCustom
    return type("WebDriverMixedin", (cls_webdriver, _RequestsSessionMixin, _WebDriverMixin), {})

WebDriverMixedinOnly3rdParty(cls_webdriver: type[utils_seleniumxp._RemoteWebDriver]) -> type[utils_seleniumxp._RemoteWebDriver]

WebDriverMixedinOnly3rdParty - factory function for custom class with fixed 3rd party mixins

The factory function mechanism is required to use alternative base classes like undetected_chromedriver.

Parameters:

Name Type Description Default
cls_webdriver type[_RemoteWebDriver]

base class

required

Returns:

Type Description
type[_RemoteWebDriver]

type[utils_seleniumxp._RemoteWebDriver]: custom webdriver class with mixin

Source code in src/utils_seleniumxp/webdriver_addon.py
def WebDriverMixedinOnly3rdParty(
    cls_webdriver: type[utils_seleniumxp._RemoteWebDriver]
) -> type[utils_seleniumxp._RemoteWebDriver]:
    """
    WebDriverMixedinOnly3rdParty - factory function for custom class with fixed 3rd party mixins

    The factory function mechanism is required to use alternative base classes like
    undetected_chromedriver.

    Args:
        cls_webdriver (type[utils_seleniumxp._RemoteWebDriver]): base class

    Returns:
        type[utils_seleniumxp._RemoteWebDriver]: custom webdriver class with mixin
    """

    # class WebDriverClassCustom(_RequestsSessionMixin, cls_webdriver):
    #     pass
    #
    # return WebDriverClassCustom
    return type("WebDriverMixedinOnly3rdParty", (cls_webdriver, _RequestsSessionMixin), {})

closepopup(webdriver: utils_seleniumxp._RemoteWebDriver, locator_click: utils_seleniumxp.SeleniumLocator, wait_click: float = 0.1, check_click: bool = False, locator_iframe: Optional[utils_seleniumxp.SeleniumLocator] = None, wait_iframe: float = 1, locator_shadowdomhost: Optional[utils_seleniumxp.SeleniumLocator] = None) -> bool

closepopup - close popups with use of parsel

Parameters:

Name Type Description Default
webdriver _RemoteWebDriver

webdriver

required
locator_click SeleniumLocator

locator for webelement to lcick for closing

required
wait_click float

wait after click. DEfaults to 0.1.

0.1
check_click bool

check after click if popup closed. Defaults to False.

False
locator_iframe SeleniumLocator

locator for iframe containing click object. Defaults to None.

None
wait_iframe float

wait to switch to iframe. Defaults to 1.

1
locator_shadowdomhost SeleniumLocator

locator to shadowDOM host. Defaults to None.

None

Returns:

Name Type Description
bool bool

flag if a matching popup was found and closed

Source code in src/utils_seleniumxp/webdriver_addon.py
def closepopup(
        webdriver: utils_seleniumxp._RemoteWebDriver,
        locator_click: utils_seleniumxp.SeleniumLocator,
        wait_click: float = 0.1, check_click: bool = False,
        locator_iframe: Optional[utils_seleniumxp.SeleniumLocator] = None,
        wait_iframe: float = 1,
        locator_shadowdomhost: Optional[utils_seleniumxp.SeleniumLocator] = None
) -> bool:
    """
    closepopup - close popups with use of parsel

    Args:
        webdriver (utils_seleniumxp._RemoteWebDriver): webdriver
        locator_click (utils_seleniumxp.SeleniumLocator): locator for webelement to lcick for closing
        wait_click (float, optional): wait after click. DEfaults to 0.1.
        check_click (bool, optional): check after click if popup closed. Defaults to False.
        locator_iframe (utils_seleniumxp.SeleniumLocator, optional): locator for iframe containing click object. Defaults to None.
        wait_iframe (float, optional): wait to switch to iframe. Defaults to 1.
        locator_shadowdomhost (utils_seleniumxp.SeleniumLocator, optional): locator to shadowDOM host. Defaults to None.

    Returns:
        bool: flag if a matching popup was found and closed
    """

    closepopup_queueadd(webdriver, locator_click, wait_click, check_click, locator_iframe, wait_iframe, locator_shadowdomhost)
    return closepopup_queueprocessing(webdriver)

closepopup_eventfiring(webdriver: utils_seleniumxp.eventfiring_addon.EventFiringWebDriverExtended, locator_click: utils_seleniumxp.SeleniumLocator, wait_click: float, check_click: bool = False, locator_iframe: Optional[utils_seleniumxp.SeleniumLocator] = None, wait_iframe: float = 1, locator_shadowdomhost: Optional[utils_seleniumxp.SeleniumLocator] = None) -> bool

closepopup_eventfiring - closepopup for eventfiring webdriver, required to avoid endless recurion loop)

Source code in src/utils_seleniumxp/webdriver_addon.py
def closepopup_eventfiring(
    webdriver: utils_seleniumxp.eventfiring_addon.EventFiringWebDriverExtended,
    locator_click: utils_seleniumxp.SeleniumLocator,
    wait_click: float,
    check_click: bool = False,
    locator_iframe: Optional[utils_seleniumxp.SeleniumLocator] = None,
    wait_iframe: float = 1,
    locator_shadowdomhost: Optional[utils_seleniumxp.SeleniumLocator] = None
) -> bool:
    """
    closepopup_eventfiring - closepopup for eventfiring webdriver, required to avoid endless recurion loop)
    """
    return webdriver._dispatch("closepopuphandler", ("simpleclose", webdriver._driver), "_closepopup", (locator_click, wait_click, check_click, locator_iframe, wait_iframe, locator_shadowdomhost), b_recursive=True)

closepopup_queueadd(webdriver: utils_seleniumxp._RemoteWebDriver, locator_click: utils_seleniumxp.SeleniumLocator, wait_click: float = 0.1, check_click: bool = False, locator_iframe: Optional[utils_seleniumxp.SeleniumLocator] = None, wait_iframe: float = 1, locator_shadowdomhost: Optional[utils_seleniumxp.SeleniumLocator] = None) -> None

closepopup_queueadd - add entry to closepopop processing queue

Parameters:

Name Type Description Default
webdriver _RemoteWebDriver

webdriver

required
locator_click SeleniumLocator

locator for webelement to lcick for closing

required
wait_click float

wait after click. DEfaults to 0.1.

0.1
check_click bool

check after click if popup closed. Defaults to False.

False
locator_iframe SeleniumLocator

locator for iframe containing click object. Defaults to None.

None
wait_iframe float

wait to switch to iframe. Defaults to 1.

1
locator_shadowdomhost SeleniumLocator

locator to shadowDOM host. Defaults to None.

None
Source code in src/utils_seleniumxp/webdriver_addon.py
def closepopup_queueadd(
        webdriver: utils_seleniumxp._RemoteWebDriver,
        locator_click: utils_seleniumxp.SeleniumLocator,
        wait_click: float = 0.1, check_click: bool = False,
        locator_iframe: Optional[utils_seleniumxp.SeleniumLocator] = None,
        wait_iframe: float = 1,
        locator_shadowdomhost: Optional[utils_seleniumxp.SeleniumLocator] = None
) -> None:
    """
    closepopup_queueadd - add entry to closepopop processing queue

    Args:
        webdriver (utils_seleniumxp._RemoteWebDriver): webdriver
        locator_click (utils_seleniumxp.SeleniumLocator): locator for webelement to lcick for closing
        wait_click (float, optional): wait after click. DEfaults to 0.1.
        check_click (bool, optional): check after click if popup closed. Defaults to False.
        locator_iframe (utils_seleniumxp.SeleniumLocator, optional): locator for iframe containing click object. Defaults to None.
        wait_iframe (float, optional): wait to switch to iframe. Defaults to 1.
        locator_shadowdomhost (utils_seleniumxp.SeleniumLocator, optional): locator to shadowDOM host. Defaults to None.
    """

    # check if webdriver has queue attribute already
    if hasattr(webdriver, 'closepopup_queue'):
        pass
    else:
        closepopup_queue = collections.deque()  # type: ignore[var-annotated]
        setattr(webdriver, 'closepopup_queue', closepopup_queue)

    # add to queue
    queueentry = {
        "locator_click": locator_click,
        "wait_click": wait_click,
        "check_click": check_click,
        "locator_iframe": locator_iframe,
        "wait_iframe": wait_iframe,
        "locator_shadowdomhost": locator_shadowdomhost
    }
    webdriver.closepopup_queue.append(queueentry)

closepopup_queueadd_eventfiring(webdriver: utils_seleniumxp.eventfiring_addon.EventFiringWebDriverExtended, locator_click: utils_seleniumxp.SeleniumLocator, wait_click: float, check_click: bool = False, locator_iframe: Optional[utils_seleniumxp.SeleniumLocator] = None, wait_iframe: float = 1, locator_shadowdomhost: Optional[utils_seleniumxp.SeleniumLocator] = None) -> bool

closepopup_queueadd_eventfiring - closepopup_queueadd for eventfiring webdriver, required to avoid endless recurion loop)

Source code in src/utils_seleniumxp/webdriver_addon.py
def closepopup_queueadd_eventfiring(
    webdriver: utils_seleniumxp.eventfiring_addon.EventFiringWebDriverExtended,
    locator_click: utils_seleniumxp.SeleniumLocator,
    wait_click: float, check_click: bool = False,
    locator_iframe: Optional[utils_seleniumxp.SeleniumLocator] = None,
    wait_iframe: float = 1,
    locator_shadowdomhost: Optional[utils_seleniumxp.SeleniumLocator] = None
) -> bool:
    """
    closepopup_queueadd_eventfiring - closepopup_queueadd for eventfiring webdriver, required to avoid endless recurion loop)
    """
    return webdriver._dispatch(None, (), "_closepopup_queueadd", (locator_click, wait_click, check_click, locator_iframe, wait_iframe, locator_shadowdomhost), b_recursive=True)

closepopup_queueprocessing(webdriver: utils_seleniumxp._RemoteWebDriver, firstonly: bool = True) -> bool

closepopup_queueprocessing - process closepopup queue

Parameters:

Name Type Description Default
webdriver _RemoteWebDriver

webdriver

required
firstonly bool

process first found target webelement only. Defaults to True.

True

Returns:

Name Type Description
bool bool

flag if a matching popup was found and closed

Source code in src/utils_seleniumxp/webdriver_addon.py
def closepopup_queueprocessing(webdriver: utils_seleniumxp._RemoteWebDriver, firstonly: bool = True) -> bool:
    """
    closepopup_queueprocessing - process closepopup queue

    Args:
        webdriver (utils_seleniumxp._RemoteWebDriver): webdriver
        firstonly (bool, optional): process  first found target webelement only. Defaults to True.

    Returns:
        bool: flag if a matching popup was found and closed
    """

    # set return default
    closedpopup: bool = False

    if hasattr(webdriver, 'closepopup_queue'):

        if len(webdriver.closepopup_queue) > 0:
            webdriver.wait4AJAX()
            parsedhtml = utils_seleniumxp.locatorutils.parselSelectorExtension(text=webdriver.page_source)

        while len(webdriver.closepopup_queue) > 0:

            webdriver.wait4AJAX()
            queueentry = webdriver.closepopup_queue.popleft()

            locator_click = queueentry["locator_click"]
            wait_click = queueentry["wait_click"]
            check_click = queueentry["check_click"]
            locator_iframe = queueentry["locator_iframe"]
            wait_iframe = queueentry["wait_iframe"]
            locator_shadowdomhost = queueentry["locator_shadowdomhost"]

            if not firstonly or not closedpopup:

                # convert locator within shadowDOM to CSS
                if locator_shadowdomhost is not None:
                    if locator_click.by == utils_seleniumxp.By.XPATH:
                        locator_click = utils_seleniumxp.SeleniumLocator(utils_seleniumxp.By.CSS_SELECTOR, cssify.cssify(locator_click.value))
                    elif locator_click.by != utils_seleniumxp.By.CSS_SELECTOR:
                        utils_seleniumxp.ErrorUtilsSelenium("Only CSS selector allowed together with element in shadowDOM.")

                # initialize parsedhtml at beginning of loop
                parsedhtml = utils_seleniumxp.locatorutils.parselSelectorExtension(text=webdriver.page_source)

                # go to frame of popup (if any)
                if locator_iframe is not None:
                    if utils_seleniumxp.locatorutils.check_locator(locator_iframe, [utils_seleniumxp.By.XPATH, utils_seleniumxp.By.CSS_SELECTOR]):
                        frames = parsedhtml.css_or_xpath(locator_iframe).getall()
                        # re-parse once, sometimes timing problem -> caused by switch-to error
                        if len(frames) == 0:
                            parsedhtml = utils_seleniumxp.locatorutils.parselSelectorExtension(text=webdriver.page_source)
                            frames = parsedhtml.css_or_xpath(locator_iframe).getall()
                    else:
                        frames = webdriver.find_elements(*locator_iframe)
                    if len(frames) == 0:
                        if hasattr(webdriver, "closepopup_logger"):
                            webdriver.closepopup_logger.info(f"FRAME not found\t{webdriver.current_url}\t({locator_click})\t({locator_iframe})\t({locator_shadowdomhost})")
                        continue
                    elif len(frames) == 1:
                        utils_seleniumxp.WebDriverWait(webdriver, wait_iframe).until(utils_seleniumxp.ExpectedConditions.frame_to_be_available_and_switch_to_it(locator_iframe))
                        if (utils_seleniumxp.locatorutils.check_locator(locator_click, [utils_seleniumxp.By.XPATH, utils_seleniumxp.By.CSS_SELECTOR]) or
                            utils_seleniumxp.locatorutils.check_locator(locator_shadowdomhost, [utils_seleniumxp.By.XPATH, utils_seleniumxp.By.CSS_SELECTOR])
                        ):
                            parsedhtml = utils_seleniumxp.locatorutils.parselSelectorExtension(text=webdriver.page_source)
                    elif len(frames) > 1:
                        if hasattr(webdriver, "closepopup_logger"):
                            webdriver.closepopup_logger.info(f"FRAME not unique\t{webdriver.current_url}\t({locator_click})\t({locator_iframe})\t({locator_shadowdomhost})")
                        continue

                # find close popup (considering also shadow DOM webelements)
                if locator_shadowdomhost is not None:
                    if utils_seleniumxp.locatorutils.check_locator(locator_shadowdomhost, [utils_seleniumxp.By.XPATH, utils_seleniumxp.By.CSS_SELECTOR]):
                        shadowhosts = parsedhtml.css_or_xpath(locator_shadowdomhost).getall()
                        # re-parse once, sometimes timing problem
                        if len(shadowhosts) == 0:
                            parsedhtml = utils_seleniumxp.locatorutils.parselSelectorExtension(text=webdriver.page_source)
                            shadowhosts = parsedhtml.css_or_xpath(locator_shadowdomhost).getall()
                    else:
                        shadowhosts = webdriver.find_elements(*locator_shadowdomhost)
                    if len(shadowhosts) == 0:
                        webelts = []
                        if hasattr(webdriver, "closepopup_logger"):
                            webdriver.closepopup_logger.info(f"ShadowDOM host not found\t{webdriver.current_url}\t({locator_click})\t({locator_iframe})\t({locator_shadowdomhost})")
                        continue
                    elif len(shadowhosts) == 1:
                        shadowhost = webdriver.find_element(*locator_shadowdomhost)
                        shadowroot = webdriver.execute_script("return arguments[0].shadowRoot", shadowhost)
                        webelts = shadowroot.find_elements(*locator_click)
                    else:
                        webelts = []
                        if hasattr(webdriver, "closepopup_logger"):
                            webdriver.closepopup_logger.info(f"ShadowDOM host not unique\t{webdriver.current_url}\t({locator_click})\t({locator_iframe})\t({locator_shadowdomhost})")
                        continue
                else:
                    if utils_seleniumxp.locatorutils.check_locator(locator_click, {utils_seleniumxp.By.XPATH, utils_seleniumxp.By.CSS_SELECTOR}):
                        webelts = parsedhtml.css_or_xpath(locator_click).getall()
                        webelts = webdriver.find_elements(*locator_click) if len(webelts) > 0 else []
                    else:
                        webelts = webdriver.find_elements(*locator_click)

                # close popup
                if len(webelts) > 0:

                    closedpopup_thisrun = False
                    for webelt in webelts:
                        try:
                            if webelt.is_displayed() and not webelt.is_overlapped():  # type: ignore[union-attr]
                                countwindows = len(webdriver.window_handles)
                                keepwindows = webdriver.window_handles
                                webelt.click()
                                if wait_click > 0:
                                    time.sleep(wait_click)
                                if len(webdriver.window_handles) > countwindows:
                                    closewindows(webdriver, keepwindows)
                                closedpopup = True
                                closedpopup_thisrun = True
                                if hasattr(webdriver, "closepopup_logger"):
                                    webdriver.closepopup_logger.info(f"found target\t{webdriver.current_url}\t({locator_click})\t({locator_iframe})\t({locator_shadowdomhost})")
                                break
                        except utils_seleniumxp.WebDriverExceptions.StaleElementReferenceException:
                            pass   # simple rerun
                        except utils_seleniumxp.WebDriverExceptions.ElementClickInterceptedException:
                            pass  # simple rerun
                        except:
                            raise

                    if check_click and closedpopup_thisrun:
                        if locator_shadowdomhost is not None:
                            webelts = shadowroot.find_elements(*locator_click)
                        else:
                            if utils_seleniumxp.locatorutils.check_locator(locator_click, {utils_seleniumxp.By.XPATH, utils_seleniumxp.By.CSS_SELECTOR}):
                                webelts = parsedhtml.css_or_xpath(locator_click).getall()
                                webelts = webdriver.find_elements(*locator_click) if len(webelts) > 0 else []
                            else:
                                webelts = webdriver.find_elements(*locator_click)
                        for webelt in webelts:
                            if webelt.is_displayed():
                                err_msg = "Popup could not be closed."
                                raise utils_seleniumxp.ErrorUtilsSelenium(err_msg)

                else:
                    if hasattr(webdriver, "closepopup_logger"):
                        webdriver.closepopup_logger.info(f"not found\t{webdriver.current_url}\t({locator_click})\t({locator_iframe})\t({locator_shadowdomhost})")

                # switch back from frame if necessary
                if locator_iframe is not None:
                    if len(frames) != 0:
                        webdriver.switch_to.default_content()

            else:
                if hasattr(webdriver, "closepopup_logger"):
                    webdriver.closepopup_logger.info(f"not checked\t{webdriver.current_url}\t({locator_click})\t({locator_iframe})\t({locator_shadowdomhost})")

    return closedpopup

closepopup_queueprocessing_eventfiring(webdriver: utils_seleniumxp.eventfiring_addon.EventFiringWebDriverExtended, firstonly: bool = True) -> bool

closepopup_queueprocessing_eventfiring - closepopup_queueprocessing for eventfiring webdriver, required to avoid endless recurion loop)

Source code in src/utils_seleniumxp/webdriver_addon.py
def closepopup_queueprocessing_eventfiring(
    webdriver: utils_seleniumxp.eventfiring_addon.EventFiringWebDriverExtended,
    firstonly: bool = True
) -> bool:
    """
    closepopup_queueprocessing_eventfiring - closepopup_queueprocessing for eventfiring webdriver, required to avoid endless recurion loop)
    """
    return webdriver._dispatch("closepopuphandler", ("queueprocessing", webdriver._driver), "_closepopup_queueprocessing", (firstonly,), b_recursive=True)

closewindows(webdriver: utils_seleniumxp._RemoteWebDriver, keepwindows: list) -> None

closewindows - close windows except those in 'keeplist'

Parameters:

Name Type Description Default
webdriver _RemoteWebDriver

webdriver

required
keepwindows list

list with windows to keep

required
Source code in src/utils_seleniumxp/webdriver_addon.py
def closewindows(webdriver: utils_seleniumxp._RemoteWebDriver, keepwindows: list) -> None:
    """
    closewindows - close windows except those in 'keeplist'

    Args:
        webdriver (utils_seleniumxp._RemoteWebDriver): webdriver
        keepwindows (list): list with windows to keep
    """

    keepcount: int = len(keepwindows)
    if keepcount == 0:
        return

    while len(webdriver.window_handles) > keepcount:
        for handle in reversed(webdriver.window_handles):
            if handle not in keepwindows:
                webdriver.switch_to.window(handle)
                webdriver.close()
                break

find_element_shadowdom(webdriver: utils_seleniumxp._RemoteWebDriver, locator_list: list[utils_seleniumxp.SeleniumLocator], shadowroot: Optional[Union[utils_seleniumxp._ShadowRoot, utils_seleniumxp._WebElement]] = None) -> Optional[utils_seleniumxp._WebElement]

find_element_shadowdom - find element in shadowDOM with support for nested shadowDOM

Note: every item in the locator list must be a locator to a host of another shadowDOM root.

Parameters:

Name Type Description Default
webdriver _RemoteWebDriver

webdriver

required
locator_list list[SeleniumLocator]

locator list

required
shadowroot Optional[Union[_ShadowRoot, _WebElement]]

shadowDOM root. Defaults to None.

None

Returns:

Type Description
Optional[_WebElement]

Optional[utils_seleniumxp._WebElement]: webelement in shadowDOM if found

Source code in src/utils_seleniumxp/webdriver_addon.py
def find_element_shadowdom(
    webdriver: utils_seleniumxp._RemoteWebDriver,
    locator_list: list[utils_seleniumxp.SeleniumLocator],
    shadowroot: Optional[Union[utils_seleniumxp._ShadowRoot, utils_seleniumxp._WebElement]] = None
) -> Optional[utils_seleniumxp._WebElement]:
    """
    find_element_shadowdom - find element in shadowDOM with support for nested shadowDOM

    Note: every item in the locator list must be a locator to a host of another shadowDOM root.

    Args:
        webdriver (utils_seleniumxp._RemoteWebDriver): webdriver
        locator_list (list[utils_seleniumxp.SeleniumLocator]): locator list
        shadowroot (Optional[Union[utils_seleniumxp._ShadowRoot, utils_seleniumxp._WebElement]], optional): shadowDOM root. Defaults to None.

    Returns:
        Optional[utils_seleniumxp._WebElement]: webelement in shadowDOM if found
    """

    webelts = find_elements_shadowdom(webdriver, locator_list, shadowroot)
    if webelts is not None and webelts != []:
        if len(webelts) == 1:
            return webelts[0]
        else:
            err_msg = "Multiple elements for descriptor in shadow DOM"
            raise utils_seleniumxp.ErrorUtilsSelenium(err_msg)
    else:
        return None

find_element_shadowdom_simple(webdriver: utils_seleniumxp._RemoteWebDriver, by: utils_seleniumxp.By, value: str, shadowroot: Union[utils_seleniumxp._ShadowRoot, utils_seleniumxp._WebElement]) -> Optional[utils_seleniumxp._WebElement]

find_element_shadowdom_simple - find element in shadowDOM

Parameters:

Name Type Description Default
webdriver _RemoteWebDriver

webdriver

required
by By

locator type

required
value str

locator value

required
shadowroot Union[_ShadowRoot, _WebElement]

shadowDOM root

required

Returns:

Type Description
Optional[_WebElement]

Optional[utils_seleniumxp._WebElement]: webelement in shadowDOM if found

Source code in src/utils_seleniumxp/webdriver_addon.py
def find_element_shadowdom_simple(
    webdriver: utils_seleniumxp._RemoteWebDriver,
    by: utils_seleniumxp.By,
    value: str,
    shadowroot: Union[utils_seleniumxp._ShadowRoot, utils_seleniumxp._WebElement]
) -> Optional[utils_seleniumxp._WebElement]:
    """
    find_element_shadowdom_simple - find element in shadowDOM

    Args:
        webdriver (utils_seleniumxp._RemoteWebDriver): webdriver
        by (utils_seleniumxp.By): locator type
        value (str): locator value
        shadowroot (Union[utils_seleniumxp._ShadowRoot, utils_seleniumxp._WebElement]): shadowDOM root

    Returns:
        Optional[utils_seleniumxp._WebElement]: webelement in shadowDOM if found
    """
    return find_element_shadowdom(webdriver, [utils_seleniumxp.SeleniumLocator(by, value)], shadowroot)

find_elements_optimized(webdriver: utils_seleniumxp._RemoteWebDriver, by: utils_seleniumxp.By, value: str, use_parsel: bool = True) -> list[utils_seleniumxp._WebElement]

find_elements_optimized - optimized find_elements using parsel

Parameters:

Name Type Description Default
webdriver _RemoteWebDriver

webdriver

required
by By

locator type

required
value str

locator value

required
use_parsel bool

flag to use parsel. Defaults to True.

True

Returns:

Type Description
list[_WebElement]

list[utils_seleniumxp._WebElement]: list of webelements matching locator

Source code in src/utils_seleniumxp/webdriver_addon.py
def find_elements_optimized(
    webdriver: utils_seleniumxp._RemoteWebDriver,
    by: utils_seleniumxp.By,
    value: str,
    use_parsel: bool = True
) -> list[utils_seleniumxp._WebElement]:
    """
    find_elements_optimized - optimized find_elements using parsel

    Args:
        webdriver (utils_seleniumxp._RemoteWebDriver): webdriver
        by (utils_seleniumxp.By): locator type
        value (str): locator value
        use_parsel (bool, optional): flag to use parsel. Defaults to True.

    Returns:
        list[utils_seleniumxp._WebElement]: list of webelements matching locator
    """

    if use_parsel and by in {utils_seleniumxp.By.XPATH, utils_seleniumxp.By.CSS_SELECTOR}:
        if webdriver.is_present(by, value, use_parsel):
            return webdriver.find_elements(by, value)
        else:
            return []
    else:
        return webdriver.find_elements(by, value)

find_elements_shadowdom(webdriver: utils_seleniumxp._RemoteWebDriver, locator_list: list[utils_seleniumxp.SeleniumLocator], shadowroot: Optional[Union[utils_seleniumxp._ShadowRoot, utils_seleniumxp._WebElement]] = None) -> Optional[list[utils_seleniumxp._WebElement]]

find_elements_shadowdom - find elements in shadowDOM with support for nested shadowDOM

Note: every item in the locator list must be a locator to a host of another shadowDOM root. The routine also accepts the shadowhost element and derives the shadowroot automatically.

Parameters:

Name Type Description Default
webdriver _RemoteWebDriver

webdriver

required
locator_list list[SeleniumLocator]

locator list

required
shadowroot Optional[Union[_ShadowRoot, _WebElement]]

shadowDOM root. Defaults to None.

None

Returns:

Type Description
Optional[list[_WebElement]]

Optional[list[utils_seleniumxp._WebElement]]: list of webelements in shadowDOM if found

Source code in src/utils_seleniumxp/webdriver_addon.py
def find_elements_shadowdom(
    webdriver: utils_seleniumxp._RemoteWebDriver,
    locator_list: list[utils_seleniumxp.SeleniumLocator],
    shadowroot: Optional[Union[utils_seleniumxp._ShadowRoot, utils_seleniumxp._WebElement]] = None
) -> Optional[list[utils_seleniumxp._WebElement]]:
    """
    find_elements_shadowdom - find elements in shadowDOM with support for nested shadowDOM

    Note: every item in the locator list must be a locator to a host of another shadowDOM root.
    The routine also accepts the shadowhost element and derives the shadowroot automatically.

    Args:
        webdriver (utils_seleniumxp._RemoteWebDriver): webdriver
        locator_list (list[utils_seleniumxp.SeleniumLocator]): locator list
        shadowroot (Optional[Union[utils_seleniumxp._ShadowRoot, utils_seleniumxp._WebElement]], optional): shadowDOM root. Defaults to None.

    Returns:
        Optional[list[utils_seleniumxp._WebElement]]: list of webelements in shadowDOM if found
    """

    def prepare_selector(locator: utils_seleniumxp.SeleniumLocator) -> str:

        if locator.by == utils_seleniumxp.By.XPATH:
            return cssify.cssify(locator.value)
        elif locator.by == utils_seleniumxp.By.CSS_SELECTOR:
            return locator.value
        else:
            err_msg = "Only CSS selector allowed together with element in shadowDOM."
            raise utils_seleniumxp.ErrorUtilsSelenium(err_msg)

    if len(locator_list) > 1 or shadowroot is not None:
        queryselector = ""
        for i in range(0, len(locator_list) - 1):
            queryselector = queryselector + ".querySelector('" + prepare_selector(locator_list[i]) + "').shadowRoot"
        queryselector = queryselector + ".querySelectorAll('" + prepare_selector(locator_list[-1]) + "')"
        if shadowroot is not None:
            if shadowroot.__class__.__name__ != "ShadowRoot":
                # overcome issue that ShadwoDOM host but not root is provided
                # try to treat shadowroot as host to derive correct root
                try:
                    shadowroot = webdriver.execute_script("return arguments[0].shadowRoot", shadowroot)
                except Exception:
                    return None
            # overcome issue with id attribute, discovered missing in JSON return when accessing shadow root in May 2025
            if not hasattr(shadowroot, "id"):
                shadowroot.id = shadowroot._id  # type: ignore[misc]
            try:
                return webdriver.execute_script(f"return arguments[0]{queryselector}", shadowroot)
            except Exception:
                return None
        else:
            try:
                return webdriver.execute_script(f"return document{queryselector}")
            except Exception:
                return None
    else:
        err_msg = "No sufficient selector information provided to access element in shadowDOM."
        raise utils_seleniumxp.ErrorUtilsSelenium(err_msg)

find_elements_shadowdom_simple(webdriver: utils_seleniumxp._RemoteWebDriver, by: utils_seleniumxp.By, value: str, shadowroot: Union[utils_seleniumxp._ShadowRoot, utils_seleniumxp._WebElement]) -> Optional[list[utils_seleniumxp._WebElement]]

find_elements_shadowdom_simple - find elements in shadowDOM

Parameters:

Name Type Description Default
webdriver _RemoteWebDriver

webdriver

required
by By

locator type

required
value str

locator value

required
shadowroot Union[_ShadowRoot, _WebElement]

shadowDOM root

required

Returns:

Type Description
Optional[list[_WebElement]]

Optional[utils_seleniumxp._WebElement]: list of webelements in shadowDOM if found

Source code in src/utils_seleniumxp/webdriver_addon.py
def find_elements_shadowdom_simple(
    webdriver: utils_seleniumxp._RemoteWebDriver,
    by: utils_seleniumxp.By,
    value: str,
    shadowroot: Union[utils_seleniumxp._ShadowRoot, utils_seleniumxp._WebElement]
) -> Optional[list[utils_seleniumxp._WebElement]]:
    """
    find_elements_shadowdom_simple - find elements in shadowDOM

    Args:
        webdriver (utils_seleniumxp._RemoteWebDriver): webdriver
        by (utils_seleniumxp.By): locator type
        value (str): locator value
        shadowroot (Union[utils_seleniumxp._ShadowRoot, utils_seleniumxp._WebElement]): shadowDOM root

    Returns:
        Optional[utils_seleniumxp._WebElement]: list of webelements in shadowDOM if found
    """
    return find_elements_shadowdom(webdriver, [utils_seleniumxp.SeleniumLocator(by, value)], shadowroot)

find_root_shadowdom(webdriver: utils_seleniumxp._RemoteWebDriver, by: utils_seleniumxp.By, value: str) -> Optional[utils_seleniumxp._ShadowRoot]

find_root_shadowdom - find shadowDOM root

Parameters:

Name Type Description Default
webdriver _RemoteWebDriver

browser

required
by By

locator type of element hosting shadowDOM root

required
value str

locator value of element hosting shadowDOM root

required

Returns:

Type Description
Optional[_ShadowRoot]

Optional[utils_seleniumxp._WebElement]: shadowDOM root if found

Source code in src/utils_seleniumxp/webdriver_addon.py
def find_root_shadowdom(
    webdriver: utils_seleniumxp._RemoteWebDriver,
    by: utils_seleniumxp.By,
    value: str
) -> Optional[utils_seleniumxp._ShadowRoot]:
    """
    find_root_shadowdom - find shadowDOM root

    Args:
        webdriver (utils_seleniumxp._RemoteWebDriver): browser
        by (utils_seleniumxp.By): locator type of element hosting shadowDOM root
        value (str): locator value of element hosting shadowDOM root

    Returns:
        Optional[utils_seleniumxp._WebElement]: shadowDOM root if found
    """

    shadowhost = webdriver.find_element(by, value)
    shadowroot = webdriver.execute_script("return arguments[0].shadowRoot", shadowhost)
    return shadowroot

get_user_agent(webdriver: utils_seleniumxp._RemoteWebDriver) -> str

get_user_agent - get user agent via javascript

Parameters:

Name Type Description Default
webdriver _RemoteWebDriver

webdriver

required

Returns:

Name Type Description
str str

user agent string

Source code in src/utils_seleniumxp/webdriver_addon.py
def get_user_agent(webdriver: utils_seleniumxp._RemoteWebDriver) -> str:
    """
    get_user_agent - get user agent via javascript

    Args:
        webdriver (utils_seleniumxp._RemoteWebDriver): webdriver

    Returns:
        str: user agent string
    """
    return webdriver.execute_script("return navigator.userAgent")

handle_alert(webdriver: utils_seleniumxp._RemoteWebDriver, alerttext: str, accept: bool = False) -> bool

handle_alert - handle alert

Parameters:

Name Type Description Default
webdriver _RemoteWebDriver

webdriver

required
alerttext str

alert text

required
accept bool

accept yes or no. Defaults to False.

False

Returns:

Name Type Description
bool bool

flag if alert was processed successfully or not

Source code in src/utils_seleniumxp/webdriver_addon.py
def handle_alert(webdriver: utils_seleniumxp._RemoteWebDriver, alerttext: str, accept: bool = False) -> bool:
    """
    handle_alert - handle alert

    Args:
        webdriver (utils_seleniumxp._RemoteWebDriver): webdriver
        alerttext (str): alert text
        accept (bool, optional): accept yes or no. Defaults to False.

    Returns:
        bool: flag if alert was processed successfully or not
    """

    if utils_seleniumxp.ExpectedConditions.alert_is_present():  # type: ignore[truthy-function]
        try:
            alert = webdriver._switch_to.alert
            alertmatch = (not alerttext or fnmatch.fnmatch(alert.text, "*" + alerttext + "*"))
            if alertmatch:
                if accept:
                    alert.accept()
                else:
                    alert.dismiss()
                return True
            else:
                return False
        except utils_seleniumxp.WebDriverExceptions.NoAlertPresentException:
            return False
    else:
        return False

hover_over_element(webdriver: utils_seleniumxp._RemoteWebDriver, by: utils_seleniumxp.By, value: str, by_host: Optional[utils_seleniumxp.By], value_host: Optional[str]) -> None

hover_over_element - action chain to hover over element

Parameters:

Name Type Description Default
webdriver _RemoteWebDriver

webdriver

required
by By

locator type

required
value str

locator value

required
by_host By

locator type of shadowhost. Defaults to None.

required
value_host str

locator value of shadowhost. Defaults to None.

required
Source code in src/utils_seleniumxp/webdriver_addon.py
def hover_over_element(
    webdriver: utils_seleniumxp._RemoteWebDriver,
    by: utils_seleniumxp.By,
    value: str,
    by_host: Optional[utils_seleniumxp.By],
    value_host: Optional[str]
) -> None:
    """
    hover_over_element - action chain to hover over element

    Args:
        webdriver (utils_seleniumxp._RemoteWebDriver): webdriver
        by (utils_seleniumxp.By): locator type
        value (str): locator value
        by_host (utils_seleniumxp.By, optional): locator type of shadowhost. Defaults to None.
        value_host (str, optional): locator value of shadowhost. Defaults to None.
"""

    if value_host is None:
        if webdriver.is_present(by, value):
            webelement = webdriver.find_element(by, value)
    else:
        shadowroot = webdriver.find_root_shadowdom(by_host, value_host)
        webelement = webdriver.find_element_shadowdom_simple(by, value, shadowroot)
    utils_seleniumxp.WebDriver.ActionChains(webdriver).move_to_element(webelement).perform()

is_element_present(webdriver: utils_seleniumxp._RemoteWebDriver, by: utils_seleniumxp.By, value: str, use_parsel: bool = True) -> bool

is_element_present - check if webelement is present (alternative caller vor _is_present)

Source code in src/utils_seleniumxp/webdriver_addon.py
def is_element_present(
    webdriver: utils_seleniumxp._RemoteWebDriver,
    by: utils_seleniumxp.By,
    value: str,
    use_parsel: bool = True
) -> bool:
    """
    is_element_present - check if webelement is present (alternative caller vor _is_present)
    """
    return is_present(webdriver, by, value, use_parsel)

is_element_present_shadowdom(webdriver: utils_seleniumxp._RemoteWebDriver, locator_list: list[utils_seleniumxp.SeleniumLocator], shadowroot: Optional[Union[utils_seleniumxp._ShadowRoot, utils_seleniumxp._WebElement]] = None) -> bool

is_element_present_shadowdom - check if element is present in shadowDOM (alternative caller for is_present_shadowdom)

Source code in src/utils_seleniumxp/webdriver_addon.py
def is_element_present_shadowdom(
    webdriver: utils_seleniumxp._RemoteWebDriver,
    locator_list: list[utils_seleniumxp.SeleniumLocator],
    shadowroot: Optional[Union[utils_seleniumxp._ShadowRoot, utils_seleniumxp._WebElement]] = None
) -> bool:
    """
    is_element_present_shadowdom - check if element is present in shadowDOM (alternative caller for is_present_shadowdom)
    """
    return is_present_shadowdom(webdriver, locator_list, shadowroot)

is_present(webdriver: utils_seleniumxp._RemoteWebDriver, by: utils_seleniumxp.By, value: str, use_parsel: bool = True) -> bool

is_present - check if webelement is present

If element is not present, the standard routine is pretty time-consuming compared to parsel using C implemented lxml library. The overhead caused by the library is comparably slow. However, parsel can only be used for XPATH and CSS.

Parameters:

Name Type Description Default
webdriver _RemoteWebDriver

webdriver

required
by By

locator type

required
value str

locator value

required
use_parsel bool

flag to use parsel. Defaults to True.

True

Returns:

Name Type Description
bool bool

flag if element is present or not

Source code in src/utils_seleniumxp/webdriver_addon.py
def is_present(
    webdriver: utils_seleniumxp._RemoteWebDriver,
    by: utils_seleniumxp.By,
    value: str,
    use_parsel: bool = True
) -> bool:
    """
    is_present - check if webelement is present

    If element is not present, the standard routine is pretty time-consuming compared to parsel
    using C implemented lxml library. The overhead caused by the library is comparably slow.
    However, parsel can only be used for XPATH and CSS.

    Args:
        webdriver (utils_seleniumxp._RemoteWebDriver): webdriver
        by (utils_seleniumxp.By): locator type
        value (str): locator value
        use_parsel (bool, optional): flag to use parsel. Defaults to True.

    Returns:
        bool: flag if element is present or not
    """

    if use_parsel and by in {utils_seleniumxp.By.XPATH, utils_seleniumxp.By.CSS_SELECTOR}:
        parsedhtml = utils_seleniumxp.locatorutils.parselSelectorExtension(text=webdriver.page_source)
        return len(parsedhtml.css_or_xpath(utils_seleniumxp.SeleniumLocator(by, value)).getall()) > 0
    else:
        # alternative variant -> as slow as try-variant if not present (even slightly slower in case element is present)
        # but potentially more errors if descriptor is not unique
        # return len(webdriver.find_elements(by, value)) > 0
        try:
            webdriver.find_element(by, value)
        except Exception:
            return False
        else:
            return True

is_present_shadowdom(webdriver: utils_seleniumxp._RemoteWebDriver, locator_list: list[utils_seleniumxp.SeleniumLocator], shadowroot: Optional[Union[utils_seleniumxp._ShadowRoot, utils_seleniumxp._WebElement]] = None) -> bool

is_present_shadowdom - check if element is present in shadowDOM

Note: every item in the locator list must be a locator to a host of another shadowDOM root.

Parameters:

Name Type Description Default
webdriver _RemoteWebDriver

webdriver

required
locator_list list[SeleniumLocator]

locator list

required
shadowroot Optional[Union[_ShadowRoot, _WebElement]]

shadowDOM root. Defaults to None.

None

Returns:

Name Type Description
bool bool

flag if element is present or not

Source code in src/utils_seleniumxp/webdriver_addon.py
def is_present_shadowdom(
    webdriver: utils_seleniumxp._RemoteWebDriver,
    locator_list: list[utils_seleniumxp.SeleniumLocator],
    shadowroot: Optional[Union[utils_seleniumxp._ShadowRoot, utils_seleniumxp._WebElement]] = None
) -> bool:
    """
    is_present_shadowdom - check if element is present in shadowDOM

    Note: every item in the locator list must be a locator to a host of another shadowDOM root.

    Args:
        webdriver (utils_seleniumxp._RemoteWebDriver): webdriver
        locator_list (list[utils_seleniumxp.SeleniumLocator]): locator list
        shadowroot (Optional[Union[utils_seleniumxp._ShadowRoot, utils_seleniumxp._WebElement]], optional): shadowDOM root. Defaults to None.

    Returns:
        bool: flag if element is present or not
    """

    webelts = find_elements_shadowdom(webdriver, locator_list, shadowroot)
    return webelts is not None and webelts != []

wait4AJAX(webdriver: utils_seleniumxp._RemoteWebDriver, timeout: int = 10, min_wait: float = 0.5) -> bool

wait4AJAX - wait routine checking jQuery.active via JavaScript and document.readyState

Parameters:

Name Type Description Default
webdriver _RemoteWebDriver

webdriver

required
timeout int

timeout

10
min_wait float

minimum wait time. Defaults to 0.5.

0.5

Returns:

Name Type Description
bool bool

flag if timeout reached

Source code in src/utils_seleniumxp/webdriver_addon.py
def wait4AJAX(webdriver: utils_seleniumxp._RemoteWebDriver, timeout: int = 10, min_wait: float = 0.5) -> bool:
    """
    wait4AJAX - wait routine checking jQuery.active via JavaScript and document.readyState

    Args:
        webdriver (utils_seleniumxp._RemoteWebDriver): webdriver
        timeout (int): timeout
        min_wait (float, optional): minimum wait time. Defaults to 0.5.

    Returns:
        bool: flag if timeout reached
    """

    starttime = time.time()
    timeoutscript = False
    try:
        utils_seleniumxp.WebDriverWait(webdriver, 1).until(lambda webdriver: webdriver.execute_script('return jQuery.active') == 0)
    except (utils_seleniumxp.WebDriverExceptions.JavascriptException, utils_seleniumxp.WebDriverExceptions.TimeoutException):
        pass
    else:
        try:
            utils_seleniumxp.WebDriverWait(webdriver, timeout).until(lambda webdriver: webdriver.execute_script('return jQuery.active') == 0)
        except utils_seleniumxp.WebDriverExceptions.TimeoutException:
            timeoutscript = True

    timeoutdocrs = False
    try:
        utils_seleniumxp.WebDriverWait(webdriver, timeout).until(lambda webdriver: webdriver.execute_script('return document.readyState') == 'complete')
    except utils_seleniumxp.WebDriverExceptions.TimeoutException:
        timeoutdocrs = True

    while (time.time() < starttime + min_wait):
        time.sleep(0.1)

    return timeoutscript or timeoutdocrs

wait4HTMLstable(webdriver: utils_seleniumxp._RemoteWebDriver, wait: float = 0.1, max_wait: float = 0.5) -> bool

wait4HTMLstable - wait routine checking stability page source

Parameters:

Name Type Description Default
webdriver _RemoteWebDriver

webdriver

required
wait float

wait between re-checks. Defaults to 0.1.

0.1
max_wait float

timeout. Defaults to 0.5.

0.5

Returns:

Name Type Description
bool bool

flag if page source is stable

Source code in src/utils_seleniumxp/webdriver_addon.py
def wait4HTMLstable(webdriver: utils_seleniumxp._RemoteWebDriver, wait: float = 0.1, max_wait: float = 0.5) -> bool:
    """
    wait4HTMLstable - wait routine checking stability page source

    Args:
        webdriver (utils_seleniumxp._RemoteWebDriver): webdriver
        wait (float, optional): wait between re-checks. Defaults to 0.1.
        max_wait (float, optional): timeout. Defaults to 0.5.

    Returns:
        bool: flag if page source is stable
    """

    wait4AJAX(webdriver)
    HTMLprev = ""
    starttime = time.time()
    while (HTMLprev != webdriver.page_source and (time.time() < starttime + max_wait)):
        HTMLprev = webdriver.page_source
        time.sleep(wait)

    return HTMLprev == webdriver.page_source

wait_for_page_load(webdriver: utils_seleniumxp._RemoteWebDriver, timeout=10) -> Generator

wait_for_page_load - wait routine with contextmanager from ObeyTheTestingGoat

method checks for the staleness of the old page (i.e., that the new page has loaded) prior to moving forward with further actions. Therefore, it only works in situations where the URL changes between page loads.

Usage: with self.wait_for_page_load(): click a button or do whatever do the next thing that was failing before using this

Parameters:

Name Type Description Default
webdriver _RemoteWebDriver

webdriver

required
timeout int

timeout. Defaults to 10.

10

Return

(Generator): contextmanager generator

Source code in src/utils_seleniumxp/webdriver_addon.py
@contextlib.contextmanager
def wait_for_page_load(webdriver: utils_seleniumxp._RemoteWebDriver, timeout=10) -> Generator:
    """
    wait_for_page_load - wait routine with contextmanager from ObeyTheTestingGoat

    method checks for the staleness of the old page (i.e., that the new page has loaded)
    prior to moving forward with further actions. Therefore, it only works in situations
    where the URL changes between page loads.

    Usage:
    with self.wait_for_page_load():
        click a button or do whatever
    do the next thing that was failing before using this

    Args:
        webdriver (utils_seleniumxp._RemoteWebDriver): webdriver
        timeout (int, optional): timeout. Defaults to 10.

    Return:
        (Generator): contextmanager generator
    """
    old_page = webdriver.find_element(utils_seleniumxp.By.TAG_NAME, 'html')
    yield
    utils_seleniumxp.WebDriverWait(webdriver, timeout).until(utils_seleniumxp.ExpectedConditions.staleness_of(old_page))