def __init__(
     self: "SeleniumTestability",
     ctx: SeleniumLibrary,
     automatic_wait: bool = True,
     timeout: str = "30 seconds",
     error_on_timeout: bool = True,
     automatic_injection: bool = True,
 ) -> None:
     LibraryComponent.__init__(self, ctx)
     self.logger = get_logger("SeleniumTestability")
     self.logger.debug("__init__({},{},{},{},{})".format(ctx, automatic_wait, timeout, error_on_timeout, automatic_injection))
     self.el = ElementKeywords(ctx)
     self.CWD = abspath(dirname(__file__))
     self.js_bundle = join(self.CWD, "js", "testability.js")
     self.ctx.event_firing_webdriver = TestabilityListener
     self.ctx.testability_settings = {"testability": self}
     self.automatic_wait = is_truthy(automatic_wait)
     self.automatic_injection = is_truthy(automatic_injection)
     self.error_on_timeout = is_truthy(error_on_timeout)
     self.timeout = timeout  # type: ignore
     self.hidden_elements = {}  # type: Dict[str, str]
     self.browser_warn_shown = False
     self.empty_log_warn_shown = False
     self.ff_log_pos = {}  # type: Dict[str, int]
     self.testability_config = None  # type: OptionalDictType
 def __init__(self, ctx):
     LibraryComponent.__init__(self, ctx)
     self.element = ElementKeywords(ctx)
class WaitingKeywords(LibraryComponent):

    def __init__(self, ctx):
        LibraryComponent.__init__(self, ctx)
        self.element = ElementKeywords(ctx)

    @keyword
    def wait_for_condition(self, condition, timeout=None, error=None):
        """Waits until the given ``condition`` is true or ``timeout`` expires.

        The ``condition`` can be arbitrary JavaScript expression but it
        must return a value to be evaluated. See `Execute JavaScript` for
        information about accessing content on pages.

        ``error`` can be used to override the default error message.

        See `timeouts` for more information about using timeouts and their
        default value.

        See also `Wait Until Page Contains`, `Wait Until Page Contains
        Element`, `Wait Until Element Is Visible` and BuiltIn keyword
        `Wait Until Keyword Succeeds`.
        """
        if 'return' not in condition:
            raise ValueError("Condition '%s' did not have mandatory 'return'."
                             % condition)
        if is_falsy(error):
            error = "Condition '%s' did not become true in <TIMEOUT>" % condition
        self._wait_until(
            timeout, error,
            lambda: self.browser.execute_script(condition) is True)

    @keyword
    def wait_until_page_contains(self, text, timeout=None, error=None):
        """Waits until `text` appears on current page.

        Fails if `timeout` expires before the text appears. See
        `introduction` for more information about `timeout` and its
        default value.

        `error` can be used to override the default error message.

        See also `Wait Until Page Contains Element`, `Wait For Condition`,
        `Wait Until Element Is Visible` and BuiltIn keyword `Wait Until
        Keyword Succeeds`.
        """
        if is_falsy(error):
            error = "Text '%s' did not appear in <TIMEOUT>" % text
        self._wait_until(timeout, error, self.element.is_text_present, text)

    @keyword
    def wait_until_page_does_not_contain(self, text, timeout=None, error=None):
        """Waits until `text` disappears from current page.

        Fails if `timeout` expires before the `text` disappears. See
        `introduction` for more information about `timeout` and its
        default value.

        `error` can be used to override the default error message.

        See also `Wait Until Page Contains`, `Wait For Condition`,
        `Wait Until Element Is Visible` and BuiltIn keyword `Wait Until
        Keyword Succeeds`.
        """
        def check_present():
            present = self.element.is_text_present(text)
            if not present:
                return
            else:
                return error or "Text '%s' did not disappear in %s" % (text, self._format_timeout(timeout))
        self._wait_until_no_error(timeout, check_present)

    @keyword
    def wait_until_page_contains_element(self, locator, timeout=None, error=None):
        """Waits until element specified with `locator` appears on current page.

        Fails if `timeout` expires before the element appears. See
        `introduction` for more information about `timeout` and its
        default value.

        `error` can be used to override the default error message.

        See also `Wait Until Page Contains`, `Wait For Condition`,
        `Wait Until Element Is Visible` and BuiltIn keyword `Wait Until
        Keyword Succeeds`.
        """
        def is_element_present(locator):
            return self.find_element(locator, required=False) is not None
        if is_falsy(error):
            error = "Element '%s' did not appear in <TIMEOUT>" % locator
        self._wait_until(timeout, error, is_element_present, locator)

    @keyword
    def wait_until_page_does_not_contain_element(self, locator, timeout=None, error=None):
        """Waits until element specified with `locator` disappears from current page.

        Fails if `timeout` expires before the element disappears. See
        `introduction` for more information about `timeout` and its
        default value.

        `error` can be used to override the default error message.

        See also `Wait Until Page Contains`, `Wait For Condition`,
        `Wait Until Element Is Visible` and BuiltIn keyword `Wait Until
        Keyword Succeeds`.
        """
        def check_present():
            present = self.find_element(locator, required=False)
            if not present:
                return
            else:
                return error or "Element '%s' did not disappear in %s" % (locator, self._format_timeout(timeout))
        self._wait_until_no_error(timeout, check_present)

    @keyword
    def wait_until_element_is_visible(self, locator, timeout=None, error=None):
        """Waits until element specified with `locator` is visible.

        Fails if `timeout` expires before the element is visible. See
        `introduction` for more information about `timeout` and its
        default value.

        `error` can be used to override the default error message.

        See also `Wait Until Page Contains`, `Wait Until Page Contains
        Element`, `Wait For Condition` and BuiltIn keyword `Wait Until Keyword
        Succeeds`.
        """
        def check_visibility():
            visible = self.element.is_visible(locator)
            if visible:
                return
            elif visible is None:
                return error or "Element locator '%s' did not match any elements after %s" % (locator, self._format_timeout(timeout))
            else:
                return error or "Element '%s' was not visible in %s" % (locator, self._format_timeout(timeout))
        self._wait_until_no_error(timeout, check_visibility)

    @keyword
    def wait_until_element_is_not_visible(self, locator, timeout=None, error=None):
        """Waits until element specified with `locator` is not visible.

        Fails if `timeout` expires before the element is not visible. See
        `introduction` for more information about `timeout` and its
        default value.

        `error` can be used to override the default error message.

        See also `Wait Until Page Contains`, `Wait Until Page Contains
        Element`, `Wait For Condition` and BuiltIn keyword `Wait Until Keyword
        Succeeds`.
        """
        def check_hidden():
            visible = self.element.is_visible(locator)
            if not visible:
                return
            elif visible is None:
                return error or "Element locator '%s' did not match any elements after %s" % (locator, self._format_timeout(timeout))
            else:
                return error or "Element '%s' was still visible in %s" % (locator, self._format_timeout(timeout))
        self._wait_until_no_error(timeout, check_hidden)

    @keyword
    def wait_until_element_is_enabled(self, locator, timeout=None, error=None):
        """Waits until element specified with `locator` is enabled.

        Fails if `timeout` expires before the element is enabled. See
        `introduction` for more information about `timeout` and its
        default value.

        `error` can be used to override the default error message.

        See also `Wait Until Page Contains`, `Wait Until Page Contains
        Element`, `Wait For Condition` and BuiltIn keyword `Wait Until Keyword
        Succeeds`.
        """
        def check_enabled():
            element = self.find_element(locator, required=False)
            if not element:
                return error or "Element locator '%s' did not match any elements after %s" % (locator, self._format_timeout(timeout))

            enabled = not element.get_attribute("disabled")
            if enabled:
                return
            else:
                return error or "Element '%s' was not enabled in %s" % (locator, self._format_timeout(timeout))

        self._wait_until_no_error(timeout, check_enabled)

    @keyword
    def wait_until_element_contains(self, locator, text, timeout=None, error=None):
        """Waits until given element contains `text`.

        Fails if `timeout` expires before the text appears on given element. See
        `introduction` for more information about `timeout` and its
        default value.

        `error` can be used to override the default error message.

        See also `Wait Until Page Contains`, `Wait Until Page Contains Element`, `Wait For Condition`,
        `Wait Until Element Is Visible` and BuiltIn keyword `Wait Until
        Keyword Succeeds`.
        """
        element = self.find_element(locator)
        def check_text():
            actual = element.text
            if text in actual:
                return
            else:
                return error or "Text '%s' did not appear in %s to element '%s'. " \
                            "Its text was '%s'." % (text, self._format_timeout(timeout), locator, actual)
        self._wait_until_no_error(timeout, check_text)

    @keyword
    def wait_until_element_does_not_contain(self, locator, text, timeout=None, error=None):
        """Waits until given element does not contain `text`.

        Fails if `timeout` expires before the text disappears from given element. See
        `introduction` for more information about `timeout` and its
        default value.

        `error` can be used to override the default error message.

        See also `Wait Until Page Contains`, `Wait Until Page Contains Element`, `Wait For Condition`,
        `Wait Until Element Is Visible` and BuiltIn keyword `Wait Until
        Keyword Succeeds`.
        """
        element = self.find_element(locator)
        def check_text():
            actual = element.text
            if text not in actual:
                return
            else:
                return error or "Text '%s' did not disappear in %s from element '%s'." % (text, self._format_timeout(timeout), locator)
        self._wait_until_no_error(timeout, check_text)

    def _wait_until(self, timeout, error, function, *args):
        error = error.replace('<TIMEOUT>', self._format_timeout(timeout))
        def wait_func():
            return None if function(*args) else error
        self._wait_until_no_error(timeout, wait_func)

    def _wait_until_no_error(self, timeout, wait_func, *args):
        maxtime = time.time() + self.get_timeout(timeout)
        while True:
            timeout_error = wait_func(*args)
            if not timeout_error:
                return
            if time.time() > maxtime:
                raise AssertionError(timeout_error)
            time.sleep(0.2)

    def _format_timeout(self, timeout):
        return secs_to_timestr(self.get_timeout(timeout))
Пример #4
0
 def __init__(self, ctx):
     LibraryComponent.__init__(self, ctx)
     self.element = ElementKeywords(ctx)
Пример #5
0
class WaitingKeywords(LibraryComponent):
    def __init__(self, ctx):
        LibraryComponent.__init__(self, ctx)
        self.element = ElementKeywords(ctx)

    @keyword
    def wait_for_condition(self, condition, timeout=None, error=None):
        """Waits until the given ``condition`` is true or ``timeout`` expires.

        The ``condition`` can be arbitrary JavaScript expression but it
        must return a value to be evaluated. See `Execute JavaScript` for
        information about accessing content on pages.

        ``error`` can be used to override the default error message.

        See `timeouts` for more information about using timeouts and their
        default value.

        See also `Wait Until Page Contains`, `Wait Until Page Contains
        Element`, `Wait Until Element Is Visible` and BuiltIn keyword
        `Wait Until Keyword Succeeds`.
        """
        if 'return' not in condition:
            raise ValueError(
                "Condition '%s' did not have mandatory 'return'." % condition)
        if is_falsy(error):
            error = "Condition '%s' did not become true in <TIMEOUT>" % condition
        self._wait_until(
            timeout, error,
            lambda: self.browser.execute_script(condition) is True)

    @keyword
    def wait_until_page_contains(self, text, timeout=None, error=None):
        """Waits until `text` appears on current page.

        Fails if `timeout` expires before the text appears. See
        `introduction` for more information about `timeout` and its
        default value.

        `error` can be used to override the default error message.

        See also `Wait Until Page Contains Element`, `Wait For Condition`,
        `Wait Until Element Is Visible` and BuiltIn keyword `Wait Until
        Keyword Succeeds`.
        """
        if is_falsy(error):
            error = "Text '%s' did not appear in <TIMEOUT>" % text
        self._wait_until(timeout, error, self.element.is_text_present, text)

    @keyword
    def wait_until_page_does_not_contain(self, text, timeout=None, error=None):
        """Waits until `text` disappears from current page.

        Fails if `timeout` expires before the `text` disappears. See
        `introduction` for more information about `timeout` and its
        default value.

        `error` can be used to override the default error message.

        See also `Wait Until Page Contains`, `Wait For Condition`,
        `Wait Until Element Is Visible` and BuiltIn keyword `Wait Until
        Keyword Succeeds`.
        """
        def check_present():
            present = self.element.is_text_present(text)
            if not present:
                return
            else:
                return error or "Text '%s' did not disappear in %s" % (
                    text, self._format_timeout(timeout))

        self._wait_until_no_error(timeout, check_present)

    @keyword
    def wait_until_page_contains_element(self,
                                         locator,
                                         timeout=None,
                                         error=None):
        """Waits until element specified with `locator` appears on current page.

        Fails if `timeout` expires before the element appears. See
        `introduction` for more information about `timeout` and its
        default value.

        `error` can be used to override the default error message.

        See also `Wait Until Page Contains`, `Wait For Condition`,
        `Wait Until Element Is Visible` and BuiltIn keyword `Wait Until
        Keyword Succeeds`.
        """
        def is_element_present(locator):
            return self.find_element(locator, required=False) is not None

        if is_falsy(error):
            error = "Element '%s' did not appear in <TIMEOUT>" % locator
        self._wait_until(timeout, error, is_element_present, locator)

    @keyword
    def wait_until_page_does_not_contain_element(self,
                                                 locator,
                                                 timeout=None,
                                                 error=None):
        """Waits until element specified with `locator` disappears from current page.

        Fails if `timeout` expires before the element disappears. See
        `introduction` for more information about `timeout` and its
        default value.

        `error` can be used to override the default error message.

        See also `Wait Until Page Contains`, `Wait For Condition`,
        `Wait Until Element Is Visible` and BuiltIn keyword `Wait Until
        Keyword Succeeds`.
        """
        def check_present():
            present = self.find_element(locator, required=False)
            if not present:
                return
            else:
                return error or "Element '%s' did not disappear in %s" % (
                    locator, self._format_timeout(timeout))

        self._wait_until_no_error(timeout, check_present)

    @keyword
    def wait_until_element_is_visible(self, locator, timeout=None, error=None):
        """Waits until element specified with `locator` is visible.

        Fails if `timeout` expires before the element is visible. See
        `introduction` for more information about `timeout` and its
        default value.

        `error` can be used to override the default error message.

        See also `Wait Until Page Contains`, `Wait Until Page Contains
        Element`, `Wait For Condition` and BuiltIn keyword `Wait Until Keyword
        Succeeds`.
        """
        def check_visibility():
            visible = self.element.is_visible(locator)
            if visible:
                return
            elif visible is None:
                return error or "Element locator '%s' did not match any elements after %s" % (
                    locator, self._format_timeout(timeout))
            else:
                return error or "Element '%s' was not visible in %s" % (
                    locator, self._format_timeout(timeout))

        self._wait_until_no_error(timeout, check_visibility)

    @keyword
    def wait_until_element_is_not_visible(self,
                                          locator,
                                          timeout=None,
                                          error=None):
        """Waits until element specified with `locator` is not visible.

        Fails if `timeout` expires before the element is not visible. See
        `introduction` for more information about `timeout` and its
        default value.

        `error` can be used to override the default error message.

        See also `Wait Until Page Contains`, `Wait Until Page Contains
        Element`, `Wait For Condition` and BuiltIn keyword `Wait Until Keyword
        Succeeds`.
        """
        def check_hidden():
            visible = self.element.is_visible(locator)
            if not visible:
                return
            elif visible is None:
                return error or "Element locator '%s' did not match any elements after %s" % (
                    locator, self._format_timeout(timeout))
            else:
                return error or "Element '%s' was still visible in %s" % (
                    locator, self._format_timeout(timeout))

        self._wait_until_no_error(timeout, check_hidden)

    @keyword
    def wait_until_element_is_enabled(self, locator, timeout=None, error=None):
        """Waits until element specified with `locator` is enabled.

        Fails if `timeout` expires before the element is enabled. See
        `introduction` for more information about `timeout` and its
        default value.

        `error` can be used to override the default error message.

        See also `Wait Until Page Contains`, `Wait Until Page Contains
        Element`, `Wait For Condition` and BuiltIn keyword `Wait Until Keyword
        Succeeds`.
        """
        def check_enabled():
            element = self.find_element(locator, required=False)
            if not element:
                return error or "Element locator '%s' did not match any elements after %s" % (
                    locator, self._format_timeout(timeout))

            enabled = not element.get_attribute("disabled")
            if enabled:
                return
            else:
                return error or "Element '%s' was not enabled in %s" % (
                    locator, self._format_timeout(timeout))

        self._wait_until_no_error(timeout, check_enabled)

    @keyword
    def wait_until_element_contains(self,
                                    locator,
                                    text,
                                    timeout=None,
                                    error=None):
        """Waits until given element contains `text`.

        Fails if `timeout` expires before the text appears on given element. See
        `introduction` for more information about `timeout` and its
        default value.

        `error` can be used to override the default error message.

        See also `Wait Until Page Contains`, `Wait Until Page Contains Element`, `Wait For Condition`,
        `Wait Until Element Is Visible` and BuiltIn keyword `Wait Until
        Keyword Succeeds`.
        """
        element = self.find_element(locator)

        def check_text():
            actual = element.text
            if text in actual:
                return
            else:
                return error or "Text '%s' did not appear in %s to element '%s'. " \
                            "Its text was '%s'." % (text, self._format_timeout(timeout), locator, actual)

        self._wait_until_no_error(timeout, check_text)

    @keyword
    def wait_until_element_does_not_contain(self,
                                            locator,
                                            text,
                                            timeout=None,
                                            error=None):
        """Waits until given element does not contain `text`.

        Fails if `timeout` expires before the text disappears from given element. See
        `introduction` for more information about `timeout` and its
        default value.

        `error` can be used to override the default error message.

        See also `Wait Until Page Contains`, `Wait Until Page Contains Element`, `Wait For Condition`,
        `Wait Until Element Is Visible` and BuiltIn keyword `Wait Until
        Keyword Succeeds`.
        """
        element = self.find_element(locator)

        def check_text():
            actual = element.text
            if text not in actual:
                return
            else:
                return error or "Text '%s' did not disappear in %s from element '%s'." % (
                    text, self._format_timeout(timeout), locator)

        self._wait_until_no_error(timeout, check_text)

    def _wait_until(self, timeout, error, function, *args):
        error = error.replace('<TIMEOUT>', self._format_timeout(timeout))

        def wait_func():
            return None if function(*args) else error

        self._wait_until_no_error(timeout, wait_func)

    def _wait_until_no_error(self, timeout, wait_func, *args):
        maxtime = time.time() + self.get_timeout(timeout)
        while True:
            timeout_error = wait_func(*args)
            if not timeout_error:
                return
            if time.time() > maxtime:
                raise AssertionError(timeout_error)
            time.sleep(0.2)

    def _format_timeout(self, timeout):
        return secs_to_timestr(self.get_timeout(timeout))
class SeleniumTestability(LibraryComponent):
    """
    SeleniumTestability is plugin for SeleniumLibrary that provides either manual or automatic waiting asyncronous events within SUT. This works by injecting small javascript snippets that can monitor the web application's state and when any supported events are happening within the sut, execution of SeleniumLibrary's keywords are blocked until timeout or those events are processed.

    On top of this, there are some more or less useful utilities for web application testing.

    == Usage ==

    Example:
    | ***** Settings *****
    | Library   SeleniumLibrary    plugins=SeleniumTestability;True;30 Seconds;True
    |
    | Suite Setup       Open Browser    https://127.0.0.1:5000/   browser=Firefox
    | Suite Teardown    Close Browser
    |
    | ***** Test Cases *****
    | Instrument Browser Example
    |   ${x}=   `Testability Loaded`
    |   Should Be True    ${x}

    == Parameters ==

    Plugin can take parameters when it is initialized. All of these values can be modified at runtim too with corresponding keywords. Here's a list of the parameters:
    ===  automatic_wait ===
    a truthy value, if SeleniumTestabily should automatically wait for sut to be in state that it can accept more actions.
    Can be enabled/disable at runtime.
    Defaults to True
    === timeout  ===
    Robot timestring, amount of time to wait for SUT to be in state that it can be safely interacted with.
    Can be set at runtime.
    Defaults to 30 seconds.
    === error_on_timeout ===
    A truthy value, if timeout does occur either in manual or automatic mode, this determines if error should be thrown that marks marks the exection as failure.
    Can be enabled/disabled at runtime.
    Defaults to True
    === automatic_injection ===
    A truthy value. User can choose if he wants to instrument the SUT manually with appropriate keywords (or even, if the SUT is instrumented at build time?) or should SeleniumTestability determinine if SUT has testability features and if not then inject & instrument it automatically.
    Can be enabled/disabled at runtime.
    Defaults to True

    ==  Waiting ==

    There are two modes of waiting, automatic and non-automatic. When automatic waiting is enabled, when SeleniumLibrary keywords are used, plugin waits until all currently running and supported asyncronous events are done before commencing to use the locator.

    === Automatic ===
    | ***** Settings *****
    | Library   SeleniumLibrary    plugins=SeleniumTestability;True;30 Seconds;True
    |
    | Suite Setup       Open Browser    https://127.0.0.1:5000/   browser=Firefox
    | Suite Teardown    Close Browser
    |
    | ***** Test Cases *****
    | Basic Example
    |   Click Element     id:fetch-button
    |   Click Element     id:xhr-button
    |   `Wait For Testability Ready`
    In this scenario, test is clicking first element, waits for the fetch call to finish before clicking on the next. Also, do note that in this example, we are calling `Wait For Testability Ready` to also wait for xhr request to finish as there is no other SeleniumLibrary calls after the second click.

    === Non Automatic ===
    | ***** Settings *****
    | Library   SeleniumLibrary    plugins=SeleniumTestability;False;30 Seconds;True
    |
    | Suite Setup       Open Browser    https://127.0.0.1:5000/   browser=Firefox
    | Suite Teardown    Close Browser
    |
    | ***** Test Cases *****
    | Basic Example
    |   Click Element     id:fetch-button
    |   `Wait For Testability Ready`
    |   Click Element     id:xhr-button
    |   `Wait For Testability Ready`

    = Currently supported Asyncronouse features =

    - setTimeout & setImmediate calls and wait for them.
    - fetch() call and wait for it to finish
    - XHR requests and wait for them to finish
    - CSS Animations and wait form them to finish
    - CSS Transitions and wait form them to finish
    - Viewport scrolling.

    *Do note* that catching css animations and transitions is browser dependant. In the past
    certain browsers did not implement these features as "the standard" would require.

    = Other functionality. =

    SeleniumTestability also provides other conveniance keywords that do not make sense to incorporate into
    SeleniumLibrary itself, mainly due to functionality not being in scope of SeleniumLibrary and Selenium
    python bindings. Do check the keyword documentation for up to date list of keywords.

    """

    BROWSERS = {
        "googlechrome": DesiredCapabilities.CHROME,
        "gc": DesiredCapabilities.CHROME,
        "chrome": DesiredCapabilities.CHROME,
        "headlesschrome": DesiredCapabilities.CHROME,
        "ff": DesiredCapabilities.FIREFOX,
        "firefox": DesiredCapabilities.FIREFOX,
        "headlessfirefox": DesiredCapabilities.FIREFOX,
        "ie": DesiredCapabilities.INTERNETEXPLORER,
        "internetexplorer": DesiredCapabilities.INTERNETEXPLORER,
        "edge": DesiredCapabilities.EDGE,
        "opera": DesiredCapabilities.OPERA,
        "safari": DesiredCapabilities.SAFARI,
        "htmlunit": DesiredCapabilities.HTMLUNIT,
        "htmlunitwithjs": DesiredCapabilities.HTMLUNITWITHJS,
        "iphone": DesiredCapabilities.IPHONE,
    }

    @property
    def automatic_wait(self: "SeleniumTestability") -> bool:
        return self.ctx.testability_settings["automatic_wait"]

    @automatic_wait.setter
    def automatic_wait(self: "SeleniumTestability", value: bool) -> None:
        self.ctx.testability_settings["automatic_wait"] = value

    @property
    def error_on_timeout(self: "SeleniumTestability") -> bool:
        return self.ctx.testability_settings["error_on_timeout"]

    @error_on_timeout.setter
    def error_on_timeout(self: "SeleniumTestability", value: bool) -> None:
        self.ctx.testability_settings["error_on_timeout"] = value

    @property
    def timeout(self: "SeleniumTestability") -> float:
        return self.ctx.testability_settings["timeout"]

    @timeout.setter
    def timeout(self: "SeleniumTestability", value: str) -> None:
        self.ctx.testability_settings["timeout"] = timestr_to_secs(value)

    @property
    def automatic_injection(self: "SeleniumTestability") -> bool:
        return self.ctx.testability_settings["automatic_injection"]

    @automatic_injection.setter
    def automatic_injection(self: "SeleniumTestability", value: bool) -> None:
        self.ctx.testability_settings["automatic_injection"] = value

    def __init__(
        self: "SeleniumTestability",
        ctx: SeleniumLibrary,
        automatic_wait: bool = True,
        timeout: str = "30 seconds",
        error_on_timeout: bool = True,
        automatic_injection: bool = True,
    ) -> None:
        LibraryComponent.__init__(self, ctx)
        self.logger = get_logger("SeleniumTestability")
        self.logger.debug("__init__({},{},{},{},{})".format(ctx, automatic_wait, timeout, error_on_timeout, automatic_injection))
        self.el = ElementKeywords(ctx)
        self.CWD = abspath(dirname(__file__))
        self.js_bundle = join(self.CWD, "js", "testability.js")
        self.ctx.event_firing_webdriver = TestabilityListener
        self.ctx.testability_settings = {"testability": self}
        self.automatic_wait = is_truthy(automatic_wait)
        self.automatic_injection = is_truthy(automatic_injection)
        self.error_on_timeout = is_truthy(error_on_timeout)
        self.timeout = timeout  # type: ignore
        self.hidden_elements = {}  # type: Dict[str, str]
        self.browser_warn_shown = False
        self.empty_log_warn_shown = False
        self.ff_log_pos = {}  # type: Dict[str, int]
        self.testability_config = None  # type: OptionalDictType

    @log_wrapper
    def _inject_testability(self: "SeleniumTestability") -> None:
        """
        Injects SeleniumTestability javascript bindings into a current browser's current window. This should happen automatically vie SeleniumTestability's internal `Event Firing Webdriver` support but keyword is provided also.
        """

        if self.testability_config:
            self.ctx.driver.execute_script(JS_LOOKUP["testability_config"], self.testability_config)

        with open(self.js_bundle, "r") as f:
            buf = f.read()
            self.ctx.driver.execute_script("{};".format(buf))

    @log_wrapper
    @keyword
    def set_testability_config(self: "SeleniumTestability", config: Dict) -> None:
        """
        Sets configuration that is used by SUT.  `config` dictionary should can have following keys: `maxTimeout` and `blacklist`.

        Configuration has to be set before opening any browsers or before SUT is instrumented either automatically or manually.

        - `maxTimeout` affects affects javascript `setTimeout()` calls and what is the maximum timeout that should be observed and waited. Value is milliseconds and defaults to 5000
        - `blacklist` is array of dictionaries where each array element have 2 fields. `url` and `method`.  `url` should be a regular expression that matches the actual url or url of the Request object's url field.
        Do note that the regular expression should match what is actually passed to the async method - not the fully qualied url.

        Parameters:
        - ``config`` dictionary of testability.js config options.

        Example:

        | &{longfetch}=           | `Create Dictionary`   | url=.*longfetch.*            |  method=GET              |
        | @{blacklist}=           | `Create List`         | ${longfetch}                 |                          |
        | ${tc}=                  | `Create Dictionary`   | maxTimeout=5000              |  blacklist=${blacklist}  |
        | Set Testability Config  |  ${tc}                |                              |                          |
.
        """
        self.testability_config = config

    @log_wrapper
    @keyword
    def instrument_browser(self: "SeleniumTestability") -> None:
        """
        Instruments the current webpage for testability. This should happen automatically vie SeleniumTestability's internal `Event Firing Webdriver` support but keyword is provided also. Calls `Inject Testability` keyword automatically.
        """
        if not self.is_testability_installed():
            self._inject_testability()

    @log_wrapper
    @keyword
    def is_testability_installed(self: "SeleniumTestability") -> bool:
        """
        Returns True if testability api's are loaded and current browser/window is instrumented, False if not.
        """
        return self.ctx.driver.execute_script(JS_LOOKUP["is_installed"])

    @log_wrapper
    @keyword
    def wait_for_document_ready(self: "SeleniumTestability") -> None:
        """
        Explicit waits until document.readyState is complete.
        """
        self.ctx.driver.execute_async_script(JS_LOOKUP["wait_for_document_ready"])

    @log_wrapper
    @keyword
    def set_testability_automatic_wait(self: "SeleniumTestability", enabled: bool) -> None:
        """
        Sets the state to TestabilityListener if it should automically call `Wait For Testability Ready` when interactions are done.
        Parameters:
         - ``enabled`` state of automatic waits.
        """
        self.automatic_wait = enabled

    @log_wrapper
    @keyword
    def enable_testability_automatic_wait(self: "SeleniumTestability") -> None:
        """
        Enables TestabilityListener to call `Wait For Testability Ready` onn all interactions that are done.
        """
        self.set_testability_automatic_wait(True)

    @log_wrapper
    @keyword
    def disable_testability_automatic_wait(self: "SeleniumTestability") -> None:
        """
        Disables TestabilityListener to call `Wait For Testability Ready` onn all interactions that are done.
        """
        self.set_testability_automatic_wait(False)

    @log_wrapper
    @keyword
    def wait_for_testability_ready(
        self: "SeleniumTestability", timeout: OptionalStrType = None, error_on_timeout: OptionalBoolType = None
    ) -> None:
        """
        Explicitly waits until testability is ready or timeout happens.
        Parameters:
        - ``timeout`` Amount of time to wait until giving up for testability to be ready. Robot framework timestring
        - ``error_on_timeout`` if timeout occurs, should we throw an error

        Both parameters are optional, if not provided, default values from plugin initialization time are used.
        """
        local_timeout = self.timeout
        if timeout is not None:
            local_timeout = timestr_to_secs(timeout)
        local_error_on_timeout = self.error_on_timeout
        if error_on_timeout is not None:
            local_error_on_timeout = is_truthy(error_on_timeout)

        try:
            WebDriverWait(self.ctx.driver, local_timeout, 0.15, ignored_exceptions=[TimeoutException]).until(
                lambda x: self.ctx.driver.execute_async_script(JS_LOOKUP["wait_for_testability"])
            )
        except TimeoutException:
            if local_error_on_timeout:
                raise TimeoutException("Timed out waiting for testability ready callback to trigger.")
        except Exception as e:
            self.warn(e)

    @log_wrapper
    @keyword
    def set_testability_timeout(self: "SeleniumTestability", timeout: str) -> str:
        """
        Sets the global timeout value for waiting testability ready. Overrides the defaults set from plugin parameters.
        Parameters:
        - ``timeout`` Amount of time to wait until giving up for testability to be ready. Robot framework timestring
        """
        current = self.timeout
        self.timeout = timeout  # type: ignore
        return secs_to_timestr(current)

    @log_wrapper
    @keyword
    def get_testability_timeout(self: "SeleniumTestability") -> str:
        """
        Returns the global timeout value in robot framework timestr format for waiting testability ready.
        """
        return secs_to_timestr(self.timeout)

    @log_wrapper
    @keyword
    def set_testability_error_on_timeout(self: "SeleniumTestability", error_on_timeout: bool) -> None:
        """Sets the global error_on_timeout value. eg, should SeleniumTestability throw exception when timeout occurs and there are still events in the testability queue.
        Parameters:
        - ``error_on_timeout``  - any value that robot framework considers truthy can be provided here.
        """
        self.error_on_timeout = error_on_timeout

    @keyword
    @log_wrapper
    def get_testability_error_on_timeout(self: "SeleniumTestability") -> bool:
        """Returns error_on_timeout value set via plugin parameters or via `Set Testability Error On Timeout`keyword.
        """
        return self.error_on_timeout

    @staticmethod
    @keyword("Add Basic Authentication To Url")
    def add_authentication(url: str, user: str, password: str) -> str:
        """
        For websites that require basic auth authentication, add user and password into the given url.
        Parameters:
        - ``url``  - url where user and password should be added to.
        - ``user``  - username
        - ``password``  - password
        """
        data = furl(url)
        data.username = user
        data.password = password
        return data.tostr()

    @staticmethod
    @keyword
    def split_url_to_host_and_path(url: str) -> dict:
        """
        Returs given url as dict with property "base" set to a protocol and hostname and "path" as the trailing path.
        This is useful when constructing requests sessions from urls used within SeleniumLibrary.
        """
        data = furl(url)
        return {"base": str(data.copy().remove(path=True)), "path": str(data.path)}

    @log_wrapper
    @keyword
    def set_testability_automatic_injection(self: "SeleniumTestability", enabled: bool) -> None:
        """
        Sets the state to TestabilityListener if it should automically inject testability.
        Parameters:
         - ``enabled`` state of automatic injection
        """
        self.automatic_injection = enabled

    @log_wrapper
    @keyword
    def enable_testability_automatic_injection(self: "SeleniumTestability") -> None:
        """
        Enables TestabilityListener to automatically inject testability.
        """
        self.set_testability_automatic_injection(True)

    @log_wrapper
    @keyword
    def disable_testability_automatic_injection(self: "SeleniumTestability") -> None:
        """
        Disables TestabilityListener to automatically inject testability
        """
        self.set_testability_automatic_injection(False)

    @staticmethod
    @keyword
    def cookies_to_dict(cookies: str) -> dict:  # FIX: cookies can be dict also
        """
        Converts a cookie string into python dict.
        """
        ret = {}
        cookie = SimpleCookie()  # type: SimpleCookie
        cookie.load(cookies)
        for key, morsel in cookie.items():
            ret[key] = morsel.value
        return ret

    @log_wrapper
    @keyword
    def get_navigator_useragent(self: "SeleniumTestability") -> str:
        """
        Returns useragent string of current browser.
        """
        return self.ctx.driver.execute_script(JS_LOOKUP["navigator"], "userAgent")

    @log_wrapper
    @keyword
    def get_navigator_appCodeName(self: "SeleniumTestability") -> str:
        """
        Returns appCoedName string of current browser.
        """
        return self.ctx.driver.execute_script(JS_LOOKUP["navigator"], "appCodeName")

    @log_wrapper
    @keyword
    def get_navigator_appname(self: "SeleniumTestability") -> str:
        """
        Returns appName string of current browser.
        """
        return self.ctx.driver.execute_script(JS_LOOKUP["navigator"], "appName")

    @log_wrapper
    @keyword
    def get_navigator_appversion(self: "SeleniumTestability") -> str:
        """
        Returns appVersion string of current browser.
        """
        return self.ctx.driver.execute_script(JS_LOOKUP["navigator"], "appVersion")

    @log_wrapper
    @keyword
    def get_navigator_cookieenabled(self: "SeleniumTestability") -> bool:
        """
        Returns cookieEnabled boolean of current browser.
        """
        return self.ctx.driver.execute_script(JS_LOOKUP["navigator"], "cookieEnabled")

    @log_wrapper
    @keyword
    def get_navigator_language(self: "SeleniumTestability") -> str:
        """
        Returns language string of current browser.
        """
        return self.ctx.driver.execute_script(JS_LOOKUP["navigator"], "language")

    @log_wrapper
    @keyword
    def get_navigator_online(self: "SeleniumTestability") -> bool:
        """
        Returns online boolean of current browser.
        """
        return self.ctx.driver.execute_script(JS_LOOKUP["navigator"], "onLine")

    @log_wrapper
    @keyword
    def get_navigator_platform(self: "SeleniumTestability") -> str:
        """
        Returns platform string of current browser.
        """
        return self.ctx.driver.execute_script(JS_LOOKUP["navigator"], "platform")

    @log_wrapper
    @keyword
    def get_navigator_product(self: "SeleniumTestability") -> str:
        """
        Returns product string of current browser.
        """
        return self.ctx.driver.execute_script(JS_LOOKUP["navigator"], "product")

    @log_wrapper
    @keyword
    def drag_and_drop(self: "SeleniumTestability", locator: LocatorType, target: LocatorType, html5: bool = False) -> None:
        """Drags element identified by ``locator`` into ``target`` element.

        The ``locator`` argument is the locator of the dragged element
        and the ``target`` is the locator of the target. See the
        `Locating elements` section for details about the locator syntax.

        If you wish to drag and drop a file from a local filesystem, you can specify the locator as `file:/full/path/to/filename`
        and SeleniumTestability will generate a drag'n'drop events to upload a file into a given `target` element.

        ``html5`` parameter is optional and if provided, `drag_and_drop`will utilize
        javascript to trigger the suitable events ensuring that html5 applications
        receive the right events. If `locator` starts with file: prefix, html5 defaults to True.

        Example:
        | `Drag And Drop` | css:div#element | css:div.target |  html5=True |
        | `Drag And Drop` | file:/home/rasjani/testfile.txt  |  id:demo-upload |
        """
        file_prefix = "file:"
        html5 = is_truthy(html5)
        if file_prefix in locator:
            html5 = True

        if not html5:
            self.el.drag_and_drop(locator, target)
        else:
            to_element = self.el.find_element(target)
            filename = None
            if type(locator) == str and file_prefix in locator:
                filename = locator[locator.startswith(file_prefix) and len(file_prefix):]

            if filename is not None:
                if Path(filename).exists():
                    file_input = self.driver.execute_script(JS_LOOKUP["drag_and_drop_file"], to_element, 0, 0)
                    file_input.send_keys(filename)
                else:
                    raise RuntimeError(f"Unable to upload {filename} - its missing")
            else:
                from_element = self.el.find_element(locator)
                self.ctx.driver.execute_script(JS_LOOKUP["dragdrop"], from_element, to_element)

    @log_wrapper
    @keyword
    def scroll_to_bottom(self: "SeleniumTestability", smooth: bool = False) -> None:
        """
        Scrolls current window to the bottom of the page
        Parameters:
         - ``smooth`` if sets to true, enables smooth scrolling, otherwise instant.
        """
        smooth = bool(smooth)
        behavior = "smooth" if smooth else "instant"
        self.ctx.driver.execute_script(JS_LOOKUP["scroll_to_bottom"], behavior)

    @log_wrapper
    @keyword
    def scroll_to_top(self: "SeleniumTestability", smooth: bool = False) -> None:
        """
        Scrolls current window to the top of the page
        Parameters:
         - ``smooth`` if sets to true, enables smooth scrolling, otherwise instant.
        """
        smooth = bool(smooth)
        behavior = "smooth" if smooth else "instant"
        self.ctx.driver.execute_script(JS_LOOKUP["scroll_to_top"], behavior)

    @log_wrapper
    @keyword
    def toggle_element_visibility(self: "SeleniumTestability", locator: LocatorType) -> None:
        """
        Toggles visiblity state of element via ``locator``
        """
        if locator in self.hidden_elements:
            self.hide_element(locator)
        else:
            self.show_element(locator)

    @log_wrapper
    @keyword
    def hide_element(self: "SeleniumTestability", locator: LocatorType) -> None:
        """
        Hides element via ``locator``. Typically one would use this to avoid getting
        Toggles visiblity state of element via ``locator``
        past overlays that are on top of element that is to be interacted with.
        """
        from_element = self.el.find_element(locator)
        current_display = self.ctx.driver.execute_script(JS_LOOKUP["get_style_display"], from_element)
        self.hidden_elements[locator] = current_display
        self.ctx.driver.execute_script(JS_LOOKUP["set_style_display"], from_element, "none")

    @log_wrapper
    @keyword
    def show_element(self: "SeleniumTestability", locator: LocatorType) -> None:
        """
        Shows element via ``locator`` that has been previously been hidden with `Hide Element` keyword.
        """
        from_element = self.el.find_element(locator)
        state = self.hidden_elements.get(locator, "")
        self.ctx.driver.execute_script(JS_LOOKUP["set_style_display"], from_element, state)
        del self.hidden_elements[locator]

    def _element_blocked(self: "SeleniumTestability", locator: LocatorType) -> bool:
        from_element = self.el.find_element(locator)
        rect = self.ctx.driver.execute_script(JS_LOOKUP["get_rect"], from_element)
        y = rect["y"] + (rect["height"] / 2)
        x = rect["x"] + (rect["width"] / 2)
        elem = self.ctx.driver.execute_script(JS_LOOKUP["get_element_at"], x, y)
        try:
            return from_element != elem.wrapped_element
        except AttributeError:
            return from_element != elem

    @log_wrapper
    @keyword
    def get_webelement_at(self: "SeleniumTestability", x: int, y: int) -> WebElementType:
        """Returns a topmost WebElement at given coordinates"""
        element = self.ctx.driver.execute_script(JS_LOOKUP["get_element_at"], x, y)
        # NOTE: Maybe we should always return just straight element  and not
        # really care if its event firing or not?
        try:
            return element.wrapped_element
        except AttributeError:
            return element

    @log_wrapper
    @keyword
    def is_element_blocked(self: "SeleniumTestability", locator: LocatorType) -> bool:
        """
        Returns `True` is ``locator`` is blocked, `False` if it is not.
        Example:
        | ${blocked}=       | Is Element Blocked    | id:some_id    |                  |
        | Run Keyword If    | ${blocked} == True    | Hide  Element | id:some_other_id |
        This will hide the element with id:some_other_id if  element id:some_id is being blocked
        """
        return self._element_blocked(locator)

    @log_wrapper
    @keyword
    def element_should_be_blocked(self: "SeleniumTestability", locator: LocatorType) -> None:
        """
        Throws exception if element found with ``locator`` is not blocked by any overlays.
        Example:
        | Element Should Be Blocked  |  id:some_id |
        If nothing is on top of of provided element, throws an exception
        """
        is_blocked = self._element_blocked(locator)
        if not is_blocked:
            raise AssertionError("Element with locator {} is not blocked".format(locator))

    @log_wrapper
    @keyword
    def element_should_not_be_blocked(self: "SeleniumTestability", locator: LocatorType) -> None:
        """
        Throws exception if element found with ``locator`` is being blocked by overlays.
        Example:
        | Element Should Not Be Blocked  |  id:some_id |
        If there's element on top of provided selector, throws an exception
        """
        is_blocked = self._element_blocked(locator)
        if is_blocked:
            raise AssertionError("Element with locator {} is blocked".format(locator))

    def _get_ff_log(self: "SeleniumTestability", name: str) -> BrowserLogsType:
        matcher = (
            r"^(?P<source>JavaScript|console)(\s|\.)(?P<level>warn.*?|debug|trace|log|error|info):\s(?P<message>(?!resource:).*)$"
        )
        LEVEL_LOOKUP = {
            "log": "INFO",
            "warn": "WARN",
            "warning": "WARN",
            "error": "SEVERE",
            "info": "INFO",
            "trace": "SEVERE",
            "debug": "DEBUG",
        }
        SOURCE_LOOKUP = {"JavaScript": "javascript", "console": "console-api"}
        log = []
        skip_lines = self.ff_log_pos.get(name, 0)
        buff: BrowserLogsType = []
        with open(name, "r") as f:
            buff = f.read().split("\n")
        self.ff_log_pos[name] = skip_lines + len(buff)
        buff = buff[skip_lines:]

        for line in buff:
            matches = re.search(matcher, line)
            if matches:
                row = {
                    "level": LEVEL_LOOKUP[matches.group("level")],
                    "message": matches.group("message"),
                    "source": SOURCE_LOOKUP[matches.group("source")],
                    "timestamp": int(time() * 1000),
                }
                log.append(json.dumps(row))
        return log

    @log_wrapper
    @keyword
    def get_log(self: "SeleniumTestability", log_type: str = "browser") -> BrowserLogsType:
        """
        Returns logs determined by ``log_type`` from the current browser. What is returned
        depends on desired_capabilities passed to `Open Browser`.

        Note: On firefox, the firefox profile has to have `devtools.console.stdout.content` property to be set.
        This can be done automatically with `Generate Firefox Profile` and then pass that to `Open Browser`.

        This keyword will mostly likely not work with remote seleniun driver!
        """
        ret = []  # type: BrowserLogsType
        try:
            if is_firefox(self.ctx.driver) and log_type == "browser":
                ret = self._get_ff_log(self.ctx.driver.service.log_file.name)
            else:
                ret = self.ctx.driver.get_log(log_type)
        except WebDriverException:
            if not self.browser_warn_shown:
                self.browser_warn_shown = True
                self.warn("Current browser does not support fetching logs from the browser with log_type: {}".format(log_type))
                return []
        if not ret and not self.empty_log_warn_shown:
            self.empty_log_warn_shown = True
            self.warn("No logs available - you might need to enable loggingPrefs in desired_capabilities")
        return ret

    @log_wrapper
    @keyword
    def get_default_capabilities(self: "SeleniumTestability", browser_name: str) -> OptionalDictType:
        """
        Returns a set of default capabilities for given ``browser``.
        """
        try:
            browser = browser_name.lower().replace(" ", "")
            return self.BROWSERS[browser].copy()
        except Exception as e:
            self.logger.debug(e)
            return None

    @log_wrapper
    @keyword
    def set_element_attribute(self: "SeleniumTestability", locator: LocatorType, attribute: str, value: str) -> None:
        """
        Sets ``locator`` attribute ``attribute`` to ``value``
        """
        from_element = self.el.find_element(locator)
        self.ctx.driver.execute_script(JS_LOOKUP["set_element_attribute"], from_element, attribute, value)

    @log_wrapper
    @keyword
    def get_location_hash(self: "SeleniumTestability") -> str:
        """
        returns the fragment identifier of the URL prefexed by a '#'
        """
        return self.ctx.driver.execute_script(JS_LOOKUP["get_window_location"], "hash")

    @log_wrapper
    @keyword
    def get_location_host(self: "SeleniumTestability") -> str:
        """
        returns the hostname and the port of the URL appended by a ':'
        """
        return self.ctx.driver.execute_script(JS_LOOKUP["get_window_location"], "host")

    @log_wrapper
    @keyword
    def get_location_hostname(self: "SeleniumTestability") -> str:
        """
        return the hostname of the URL
        """
        return self.ctx.driver.execute_script(JS_LOOKUP["get_window_location"], "hostname")

    @log_wrapper
    @keyword
    def get_location_href(self: "SeleniumTestability") -> str:
        """
        returns the entire URL
        """
        return self.ctx.driver.execute_script(JS_LOOKUP["get_window_location"], "href")

    @log_wrapper
    @keyword
    def get_location_origin(self: "SeleniumTestability") -> str:
        """
        returns the canonical form of the origin of the specific location
        """
        return self.ctx.driver.execute_script(JS_LOOKUP["get_window_location"], "origin")

    @log_wrapper
    @keyword
    def get_location_port(self: "SeleniumTestability") -> str:
        """
        returns the port number of the URL
        """
        return self.ctx.driver.execute_script(JS_LOOKUP["get_window_location"], "port")

    @log_wrapper
    @keyword
    def get_location_protocol(self: "SeleniumTestability") -> str:
        """
        returns the protocol scheme of the URL
        """
        return self.ctx.driver.execute_script(JS_LOOKUP["get_window_location"], "protocol")

    @log_wrapper
    @keyword
    def get_location_search(self: "SeleniumTestability") -> str:
        """
        returns the sting containing a '?' followed by the parameters or ``querystring`` of the URL
        """
        return self.ctx.driver.execute_script(JS_LOOKUP["get_window_location"], "search")

    @log_wrapper
    @keyword
    def generate_firefox_profile(
        self: "SeleniumTestability",
        preferences: OptionalDictType = None,
        accept_untrusted_certs: bool = False,
        proxy: OptionalStrType = None,
    ) -> FirefoxProfile:
        """
        Generates a firefox profile that sets up few required preferences for SeleniumTestability to support all necessary features.
        Parameters:
        - ``preferences`` - firefox profile preferences in dictionary format.
        - ``accept_untrusted_certs`` should we accept untrusted/self-signed certificates.
        - ``proxy`` proxy options

        Note: If you opt out using this keyword, you are not able to get logs with ``Get Logs`` and Firefox.
        """
        profile = FirefoxProfile()
        if preferences:
            for key, value in preferences.items():  # type: ignore
                profile.set_preference(key, value)

        profile.set_preference("devtools.console.stdout.content", True)

        profile.accept_untrusted_certs = accept_untrusted_certs

        if proxy:
            profile.set_proxy(proxy)

        profile.update_preferences()
        return profile

    @log_wrapper
    @keyword
    def get_storage_length(self: "SeleniumTestability", storage_type: str = "localStorage") -> int:
        """
        Returns a length (# of items) in specified storage.
        Parameters:
        - ``storage_type`` name of the storage. Valid options: localStorage, sessionStorage
        """
        return self.ctx.driver.execute_script(JS_LOOKUP["storage_length"], storage_type)

    @log_wrapper
    @keyword
    def get_storage_keys(self: "SeleniumTestability", storage_type: str = "localStorage") -> StringArray:
        """
        Returns a list of keys in specified storage.
        Parameters:
        - ``storage_type`` name of the storage. Valid options: localStorage, sessionStorage
        """
        return self.ctx.driver.execute_script(JS_LOOKUP["storage_keys"], storage_type)

    @log_wrapper
    @keyword
    def get_storage_item(self: "SeleniumTestability", key: str, storage_type: str = "localStorage") -> StorageType:
        """
        Returns value of ``key`` from specified storage.
        Parameters:
        - ``key`` name of the storage key
        - ``storage_type`` name of the storage. Valid options: localStorage, sessionStorage
        """
        matcher = r"^{.*}$"
        storage_item = self.ctx.driver.execute_script(JS_LOOKUP["storage_getitem"], storage_type, key)
        if isinstance(storage_item, str) and re.match(matcher, storage_item):
            storage_item = json.loads(storage_item)
        return storage_item

    @log_wrapper
    @keyword
    def set_storage_item(self: "SeleniumTestability", key: str, value: StorageType, storage_type: str = "localStorage") -> None:
        """
        Sets a value to the key in specified storage_type
        Parameters:
        - ``key`` name of the key
        - ``value`` value that should be set to key
        - ``storage_type`` name of the storage. Valid options: localStorage, sessionStorage
        """
        if isinstance(value, dict):
            value = json.dumps(value)
        self.ctx.driver.execute_script(JS_LOOKUP["storage_setitem"], storage_type, key, value)

    @log_wrapper
    @keyword
    def clear_storage(self: "SeleniumTestability", storage_type: str = "localStorage") -> None:
        """
        Clears all values in specified storage.
        Parameters:
        - ``storage_type`` name of the storage. Valid options: localStorage, sessionStorage
        """
        self.ctx.driver.execute_script(JS_LOOKUP["storage_clear"], storage_type)

    @log_wrapper
    @keyword
    def remove_storage_item(self: "SeleniumTestability", key: str, storage_type: str = "localStorage") -> None:
        """
        Removes a key and its value from specified storage
        Parameters:
        - ``key`` name of the key
        - ``storage_type`` name of the storage. Valid options: localStorage, sessionStorage
        """
        return self.ctx.driver.execute_script(JS_LOOKUP["storage_removeitem"], storage_type, key)
class SeleniumTestability(LibraryComponent):
    """
    SeleniumTestability is plugin for SeleniumLibrary that provides either manual or automatic waiting asyncronous events within SUT. This works by injecting small javascript snippets that can monitor the web application's state and when any supported events are happening within the sut, execution of SeleniumLibrary's keywords are blocked until timeout or those events are processed.

    On top of this, there are some more or less useful utilities for web application testing.

    TODO: Document constructor variables and Listener

    == Usage ==

    Example:
    | ***** Settings *****
    | Library   SeleniumLibrary    plugins=SeleniumTestability;True;30 Seconds;True
    |
    | Suite Setup       Open Browser    https://127.0.0.1:5000/   browser=Firefox
    | Suite Teardown    Close Browser
    |
    | ***** Test Cases *****
    | Instrument Browser Example
    |   ${x}=   `Testability Loaded`
    |   Should Be True    ${x}


    ==  Waiting ==

    There are two modes of waiting, automatic and non-automatic. When automatic waiting is enabled, when SeleniumLibrary keywords are used, plugin waits until all currently running and supported asyncronous events are done before commencing to use the locator.

    === Automatic ===
    | ***** Settings *****
    | Library   SeleniumLibrary    plugins=SeleniumTestability;True;30 Seconds;True
    |
    | Suite Setup       Open Browser    https://127.0.0.1:5000/   browser=Firefox
    | Suite Teardown    Close Browser
    |
    | ***** Test Cases *****
    | Basic Example
    |   Click Element     id:fetch-button
    |   Click Element     id:xhr-button
    |   `Wait For Testability Ready`
    In this scenario, test is clicking first element, waits for the fetch call to finish before clicking on the next. Also, do note that in this example, we are calling `Wait For Testability Ready` to also wait for xhr request to finish as there is no other SeleniumLibrary calls after the second click.

    === Non Automatic ===
    | ***** Settings *****
    | Library   SeleniumLibrary    plugins=SeleniumTestability;False;30 Seconds;True
    |
    | Suite Setup       Open Browser    https://127.0.0.1:5000/   browser=Firefox
    | Suite Teardown    Close Browser
    |
    | ***** Test Cases *****
    | Basic Example
    |   Click Element     id:fetch-button
    |   `Wait For Testability Ready`
    |   Click Element     id:xhr-button
    |   `Wait For Testability Ready`

    = Current Features =
    - Can detect setTimeout & setImmediate calls and wait for them.
    - Can detect fetch() call and wait for it to finish
    - Can detect XHR requests and wait for them to finish
    - Can detect CSS Animations and wait form them to finish
    - Can detect CSS Transitions and wait form them to finish

    *Do note* that CSS animations and transitions might not work properly in *Chrome*.

    """

    BROWSERS = {
        "googlechrome": DesiredCapabilities.CHROME,
        "gc": DesiredCapabilities.CHROME,
        "chrome": DesiredCapabilities.CHROME,
        "headlesschrome": DesiredCapabilities.CHROME,
        "ff": DesiredCapabilities.FIREFOX,
        "firefox": DesiredCapabilities.FIREFOX,
        "headlessfirefox": DesiredCapabilities.FIREFOX,
        "ie": DesiredCapabilities.INTERNETEXPLORER,
        "internetexplorer": DesiredCapabilities.INTERNETEXPLORER,
        "edge": DesiredCapabilities.EDGE,
        "opera": DesiredCapabilities.OPERA,
        "safari": DesiredCapabilities.SAFARI,
        "phantomjs": DesiredCapabilities.PHANTOMJS,
        "htmlunit": DesiredCapabilities.HTMLUNIT,
        "htmlunitwithjs": DesiredCapabilities.HTMLUNITWITHJS,
        "android": DesiredCapabilities.ANDROID,
        "iphone": DesiredCapabilities.IPHONE,
    }

    @property
    def automatic_wait(self: "SeleniumTestability") -> bool:
        return self.ctx.testability_settings["automatic_wait"]

    @automatic_wait.setter
    def automatic_wait(self: "SeleniumTestability", value: bool) -> None:
        self.ctx.testability_settings["automatic_wait"] = value

    @property
    def error_on_timeout(self: "SeleniumTestability") -> bool:
        return self.ctx.testability_settings["error_on_timeout"]

    @error_on_timeout.setter
    def error_on_timeout(self: "SeleniumTestability", value: bool) -> None:
        self.ctx.testability_settings["error_on_timeout"] = value

    @property
    def timeout(self: "SeleniumTestability") -> float:
        return self.ctx.testability_settings["timeout"]

    @timeout.setter
    def timeout(self: "SeleniumTestability", value: str) -> None:
        self.ctx.testability_settings["timeout"] = timestr_to_secs(value)

    @property
    def automatic_injection(self: "SeleniumTestability") -> bool:
        return self.ctx.testability_settings["automatic_injection"]

    @automatic_injection.setter
    def automatic_injection(self: "SeleniumTestability", value: bool) -> None:
        self.ctx.testability_settings["automatic_injection"] = value

    def __init__(
        self: "SeleniumTestability",
        ctx: SeleniumLibrary,
        automatic_wait: bool = True,
        timeout: str = "30 seconds",
        error_on_timeout: bool = True,
        automatic_injection: bool = True,
    ) -> None:
        LibraryComponent.__init__(self, ctx)
        self.logger = get_logger("SeleniumTestability")
        self.logger.debug("__init__({},{},{},{},{})".format(
            ctx, automatic_wait, timeout, error_on_timeout,
            automatic_injection))
        self.el = ElementKeywords(ctx)
        self.CWD = abspath(dirname(__file__))
        self.js_bundle = join(self.CWD, "js", "testability.js")
        self.ctx.event_firing_webdriver = TestabilityListener
        self.ctx.testability_settings = {"testability": self}
        self.automatic_wait = automatic_wait
        self.automatic_injection = automatic_injection
        self.error_on_timeout = error_on_timeout
        self.timeout = timeout  # type: ignore
        self.hidden_elements = {}  # type: Dict[str, str]
        self.browser_warn_shown = False
        self.empty_log_warn_shown = False
        self.ff_log_pos = {}  # type: Dict[str, int]

    @log_wrapper
    def _inject_testability(self: "SeleniumTestability") -> None:
        """
        Injects SeleniumTestability javascript bindings into a current browser's current window. This should happen automatically vie SeleniumTestability's internal `Event Firing Webdriver` support but keyword is provided also.
        """
        with open(self.js_bundle, "r") as f:
            buf = f.read()
            self.ctx.driver.execute_script("{};".format(buf))

    @log_wrapper
    @keyword
    def instrument_browser(self: "SeleniumTestability") -> None:
        """
        Instruments the current webpage for testability. This should happen automatically vie SeleniumTestability's internal `Event Firing Webdriver` support but keyword is provided also. Calls `Inject Testability` keyword automatically.
        """
        if not self.is_testability_installed():
            self._inject_testability()

    @log_wrapper
    @keyword
    def is_testability_installed(self: "SeleniumTestability") -> bool:
        """
        Returns True if testability api's are loaded and current browser/window is instrumented, False if not.
        """
        return self.ctx.driver.execute_script(JS_LOOKUP["is_installed"])

    @log_wrapper
    @keyword
    def wait_for_document_ready(self: "SeleniumTestability") -> None:
        """
        Explicit waits until document.readyState is complete.
        """
        self.ctx.driver.execute_async_script(
            JS_LOOKUP["wait_for_document_ready"])

    @log_wrapper
    @keyword
    def set_testability_automatic_wait(self: "SeleniumTestability",
                                       enabled: bool) -> None:
        """
        Sets the state to TestabilityListener if it should automically call `Wait For Testability Ready` when interactions are done.
        Parameters:
         - ``enabled`` state of automatic waits.
        """
        self.automatic_wait = enabled

    @log_wrapper
    @keyword
    def enable_testability_automatic_wait(self: "SeleniumTestability") -> None:
        """
        Enables TestabilityListener to call `Wait For Testability Ready` onn all interactions that are done.
        """
        self.set_testability_automatic_wait(True)

    @log_wrapper
    @keyword
    def disable_testability_automatic_wait(
            self: "SeleniumTestability") -> None:
        """
        Disables TestabilityListener to call `Wait For Testability Ready` onn all interactions that are done.
        """
        self.set_testability_automatic_wait(False)

    @log_wrapper
    @keyword
    def wait_for_testability_ready(
            self: "SeleniumTestability",
            timeout: OptionalStrType = None,
            error_on_timeout: OptionalBoolType = None) -> None:
        """
        Explicitly waits until testability is ready or timeout happens.
        Parameters:
        - ``timeout`` Amount of time to wait until giving up for testability to be ready. Robot framework timestring
        - ``error_on_timeout`` if timeout occurs, should we throw an error

        Both parameters are optional, if not provided, default values from plugin initialization time are used.
        """
        local_timeout = self.timeout
        if timeout is not None:
            local_timeout = timestr_to_secs(timeout)
        local_error_on_timeout = self.error_on_timeout
        if error_on_timeout is not None:
            local_error_on_timeout = is_truthy(error_on_timeout)

        try:
            WebDriverWait(
                self.ctx.driver, local_timeout,
                0.15).until(lambda x: self.ctx.driver.execute_async_script(
                    JS_LOOKUP["wait_for_testability"]))
        except TimeoutException:
            if local_error_on_timeout:
                raise TimeoutException(
                    "Timed out waiting for testability ready callback to trigger."
                )
        except Exception as e:
            self.warn(e)

    @log_wrapper
    @keyword
    def set_testability_timeout(self: "SeleniumTestability",
                                timeout: str) -> str:
        """
        Sets the global timeout value for waiting testability ready. Overrides the defaults set from plugin parameters.
        Parameters:
        - ``timeout`` Amount of time to wait until giving up for testability to be ready. Robot framework timestring
        """
        current = self.timeout
        self.timeout = timeout  # type: ignore
        return secs_to_timestr(current)

    @log_wrapper
    @keyword
    def get_testability_timeout(self: "SeleniumTestability") -> str:
        """
        Returns the global timeout value in robot framework timestr format for waiting testability ready.
        """
        return secs_to_timestr(self.timeout)

    @log_wrapper
    @keyword
    def set_testability_error_on_timeout(self: "SeleniumTestability",
                                         error_on_timeout: bool) -> None:
        """Sets the global error_on_timeout value. eg, should SeleniumTestability throw exception when timeout occurs and there are still events in the testability queue.
        Parameters:
        - ``error_on_timeout``  - any value that robot framework considers truthy can be provided here.
        """
        self.error_on_timeout = error_on_timeout

    @keyword
    @log_wrapper
    def get_testability_error_on_timeout(self: "SeleniumTestability") -> bool:
        """Returns error_on_timeout value set via plugin parameters or via `Set Testability Error On Timeout`keyword.
        """
        return self.error_on_timeout

    @staticmethod
    @keyword("Add Basic Authentication To Url")
    def add_authentication(url: str, user: str, password: str) -> str:
        """
        For websites that require basic auth authentication, add user and password into the given url.
        Parameters:
        - ``url``  - url where user and password should be added to.
        - ``user``  - username
        - ``password``  - password
        """
        data = furl(url)
        data.username = user
        data.password = password
        return data.tostr()

    @staticmethod
    @keyword
    def split_url_to_host_and_path(url: str) -> dict:
        """
        Returs given url as dict with property "base" set to a protocol and hostname and "path" as the trailing path.
        This is useful when constructing requests sessions from urls used within SeleniumLibrary.
        """
        data = furl(url)
        return {
            "base": str(data.copy().remove(path=True)),
            "path": str(data.path)
        }

    @log_wrapper
    @keyword
    def set_testability_automatic_injection(self: "SeleniumTestability",
                                            enabled: bool) -> None:
        """
        Sets the state to TestabilityListener if it should automically inject testability.
        Parameters:
         - ``enabled`` state of automatic injection
        """
        self.automatic_injection = enabled

    @log_wrapper
    @keyword
    def enable_testability_automatic_injection(
            self: "SeleniumTestability") -> None:
        """
        Enables TestabilityListener to automatically inject testability.
        """
        self.set_testability_automatic_injection(True)

    @log_wrapper
    @keyword
    def disable_testability_automatic_injection(
            self: "SeleniumTestability") -> None:
        """
        Disables TestabilityListener to automatically inject testability
        """
        self.set_testability_automatic_injection(False)

    @staticmethod
    @keyword
    def cookies_to_dict(cookies: str) -> dict:  # FIX: cookies can be dict also
        """
        Converts a cookie string into python dict.
        """
        ret = {}
        cookie = SimpleCookie()
        cookie.load(cookies)
        for key, morsel in cookie.items():
            ret[key] = morsel.value
        return ret

    @log_wrapper
    @keyword
    def get_current_useragent(self: "SeleniumTestability") -> str:
        """
        Returns useragent string of current browser.
        """
        return self.ctx.driver.execute_script(JS_LOOKUP["useragent"])

    @log_wrapper
    @keyword
    def drag_and_drop(self: "SeleniumTestability",
                      locator: LocatorType,
                      target: LocatorType,
                      html5: bool = False) -> None:
        """Drags element identified by ``locator`` into ``target`` element.

        The ``locator`` argument is the locator of the dragged element
        and the ``target`` is the locator of the target. See the
        `Locating elements` section for details about the locator syntax.

        ``html5`` parameter is optional and if provided, `drag_and_drop`will utilize
        javascript to trigger the suitable events ensuring that html5 applications
        receive the right events

        Example:
        | `Drag And Drop` | css:div#element | css:div.target |  True |
        """
        html5 = is_truthy(html5)
        if not html5:
            self.el.drag_and_drop(locator, target)
        else:
            from_element = self.el.find_element(locator)
            to_element = self.el.find_element(target)
            self.ctx.driver.execute_script(JS_LOOKUP["dragdrop"], from_element,
                                           to_element)

    @log_wrapper
    @keyword
    def scroll_to_bottom(self: "SeleniumTestability") -> None:
        """
        Scrolls current window to the bottom of the page
        """
        self.ctx.driver.execute_script(JS_LOOKUP["scroll_to_bottom"])

    @log_wrapper
    @keyword
    def scroll_to_top(self: "SeleniumTestability") -> None:
        """
        Scrolls current window to the bottom of the page
        """
        self.ctx.driver.execute_script(JS_LOOKUP["scroll_to_top"])

    @log_wrapper
    @keyword
    def toggle_element_visibility(self: "SeleniumTestability",
                                  locator: LocatorType) -> None:
        """
        Toggles visiblity state of element via ``locator``
        """
        if locator in self.hidden_elements:
            self.hide_element(locator)
        else:
            self.show_element(locator)

    @log_wrapper
    @keyword
    def hide_element(self: "SeleniumTestability",
                     locator: LocatorType) -> None:
        """
        Hides element via ``locator``. Typically one would use this to avoid getting
        Toggles visiblity state of element via ``locator``
        past overlays that are on top of element that is to be interacted with.
        """
        from_element = self.el.find_element(locator)
        current_display = self.ctx.driver.execute_script(
            JS_LOOKUP["get_style_display"], from_element)
        self.hidden_elements[locator] = current_display
        self.ctx.driver.execute_script(JS_LOOKUP["set_style_display"],
                                       from_element, "none")

    @log_wrapper
    @keyword
    def show_element(self: "SeleniumTestability",
                     locator: LocatorType) -> None:
        """
        Shows element via ``locator`` that has been previously been hidden with `Hide Element` keyword.
        """
        from_element = self.el.find_element(locator)
        state = self.hidden_elements.get(locator, "")
        self.ctx.driver.execute_script(JS_LOOKUP["set_style_display"],
                                       from_element, state)
        del self.hidden_elements[locator]

    def _element_blocked(self: "SeleniumTestability",
                         locator: LocatorType) -> bool:
        from_element = self.el.find_element(locator)
        rect = self.ctx.driver.execute_script(JS_LOOKUP["get_rect"],
                                              from_element)
        y = rect["y"] + (rect["height"] / 2)
        x = rect["x"] + (rect["width"] / 2)
        elem = self.ctx.driver.execute_script(JS_LOOKUP["get_element_at"], x,
                                              y)
        try:
            return from_element != elem.wrapped_element
        except AttributeError:
            return from_element != elem

    @log_wrapper
    @keyword
    def get_webelement_at(self: "SeleniumTestability", x: int,
                          y: int) -> WebElementType:
        """Returns a topmost WebElement at given coordinates"""
        element = self.ctx.driver.execute_script(JS_LOOKUP["get_element_at"],
                                                 x, y)
        # NOTE: Maybe we should always return just straight element  and not
        # really care if its event firing or not?
        try:
            return element.wrapped_element
        except AttributeError:
            return element

    @log_wrapper
    @keyword
    def is_element_blocked(self: "SeleniumTestability",
                           locator: LocatorType) -> bool:
        """
        Returns `True` is ``locator`` is blocked, `False` if it is not.
        Example:
        | ${blocked}=       | Is Element Blocked    | id:some_id    |                  |
        | Run Keyword If    | ${blocked} == True    | Hide  Element | id:some_other_id |
        This will hide the element with id:some_other_id if  element id:some_id is being blocked
        """
        return self._element_blocked(locator)

    @log_wrapper
    @keyword
    def element_should_be_blocked(self: "SeleniumTestability",
                                  locator: LocatorType) -> None:
        """
        Throws exception if element found with ``locator`` is not blocked by any overlays.
        #TODO: Add examples
        """
        is_blocked = self._element_blocked(locator)
        if not is_blocked:
            raise AssertionError(
                "Element with locator {} is not blocked".format(locator))

    @log_wrapper
    @keyword
    def element_should_not_be_blocked(self: "SeleniumTestability",
                                      locator: LocatorType) -> None:
        """
        Throws exception if element found with ``locator`` is being blocked by overlays.
        #TODO: Add examples
        """
        is_blocked = self._element_blocked(locator)
        if is_blocked:
            raise AssertionError(
                "Element with locator {} is blocked".format(locator))

    def _get_ff_log(self: "SeleniumTestability", name: str) -> BrowserLogsType:
        matcher = r"^(?P<source>JavaScript|console)(\s|\.)(?P<level>warn.*?|debug|trace|log|error|info):\s(?P<message>.*)$"
        LEVEL_LOOKUP = {
            "log": "INFO",
            "warn": "WARN",
            "warning": "WARN",
            "error": "SEVERE",
            "info": "INFO",
            "trace": "SEVERE",
            "debug": "DEBUG",
        }
        SOURCE_LOOKUP = {"JavaScript": "javascript", "console": "console-api"}
        log = []
        skip_lines = self.ff_log_pos.get(name, 0)
        buff = []
        with open(name, "r") as f:
            buff = f.read().split("\n")
        self.ff_log_pos[name] = skip_lines + len(buff)
        buff = buff[skip_lines:]

        for line in buff:
            matches = re.search(matcher, line)
            if matches:
                row = {
                    "level": LEVEL_LOOKUP[matches.group("level")],
                    "message": matches.group("message"),
                    "source": SOURCE_LOOKUP[matches.group("source")],
                    "timestamp": int(time() * 1000),
                }
                log.append(json.dumps(row))
        return log

    @log_wrapper
    @keyword
    def get_log(self: "SeleniumTestability",
                log_type: str = "browser") -> BrowserLogsType:
        """
        Returns logs determined by ``log_type`` from the current browser. What is returned
        depends on desired_capabilities passed to `Open Browser`.

        Note: On firefox, the firefox profile has to have `devtools.console.stdout.content` property to be set.
        This can be done automatically with `Generate Firefox Profile` and then pass that to `Open Browser`.
        """
        ret = []  # type: BrowserLogsType
        try:
            if is_firefox(self.ctx.driver) and log_type == "browser":
                ret = self._get_ff_log(self.ctx.driver.service.log_file.name)
            else:
                ret = self.ctx.driver.get_log(log_type)
        except WebDriverException:
            if not self.browser_warn_shown:
                self.browser_warn_shown = True
                self.warn(
                    "Current browser does not support fetching logs from the browser with log_type: {}"
                    .format(log_type))
                return []
        if not ret and not self.empty_log_warn_shown:
            self.empty_log_warn_shown = True
            self.warn(
                "No logs available - you might need to enable loggingPrefs in desired_capabilities"
            )
        return ret

    @log_wrapper
    @keyword
    def get_default_capabilities(self: "SeleniumTestability",
                                 browser_name: str) -> OptionalDictType:
        """
        Returns a set of default capabilities for given ``browser``.
        """
        try:
            browser = browser_name.lower().replace(" ", "")
            return self.BROWSERS[browser].copy()
        except Exception as e:
            self.logger.debug(e)
            return None

    @log_wrapper
    @keyword
    def set_element_attribute(self: "SeleniumTestability",
                              locator: LocatorType, attribute: str,
                              value: str) -> None:
        """
        Sets ``locator`` attribute ``attribute`` to ``value``
        """
        from_element = self.el.find_element(locator)
        self.ctx.driver.execute_script(JS_LOOKUP["set_element_attribute"],
                                       from_element, attribute, value)

    @log_wrapper
    @keyword
    def generate_firefox_profile(
        self: "SeleniumTestability",
        options: OptionalDictType = None,
        accept_untrusted_certs: bool = False,
        proxy: OptionalStrType = None,
    ) -> FirefoxProfile:
        profile = FirefoxProfile()
        if options:
            for key, value in options.items():  # type: ignore
                profile.set_preference(key, value)

        profile.set_preference("devtools.console.stdout.content", True)

        if accept_untrusted_certs:
            profile.accept_untrusted_certs

        if proxy:
            profile.set_proxy(proxy)

        profile.update_preferences()
        return profile