From 7a6ac6a53dcdc127f0e21d87505fb66ddb1cf5b3 Mon Sep 17 00:00:00 2001 From: philippe Date: Wed, 8 Feb 2023 13:46:58 -0500 Subject: [PATCH] Display target text on wait_for texts errors. --- dash/testing/browser.py | 63 ++++++++++++++++++++--------------- dash/testing/plugin.py | 1 + dash/testing/wait.py | 30 +++++++++++++++-- tests/integration/test_duo.py | 36 ++++++++++++++++++++ 4 files changed, 100 insertions(+), 30 deletions(-) create mode 100644 tests/integration/test_duo.py diff --git a/dash/testing/browser.py b/dash/testing/browser.py index 6338a903da..1ce5eea68c 100644 --- a/dash/testing/browser.py +++ b/dash/testing/browser.py @@ -262,19 +262,28 @@ def _get_element(self, elem_or_selector): return self.find_element(elem_or_selector) return elem_or_selector - def _wait_for(self, method, args, timeout, msg): + def _wait_for(self, method, timeout, msg): """Abstract generic pattern for explicit WebDriverWait.""" - _wait = ( - self._wd_wait if timeout is None else WebDriverWait(self.driver, timeout) - ) - logger.debug( - "method, timeout, poll => %s %s %s", - method, - _wait._timeout, # pylint: disable=protected-access - _wait._poll, # pylint: disable=protected-access - ) + try: + _wait = ( + self._wd_wait + if timeout is None + else WebDriverWait(self.driver, timeout) + ) + logger.debug( + "method, timeout, poll => %s %s %s", + method, + _wait._timeout, # pylint: disable=protected-access + _wait._poll, # pylint: disable=protected-access + ) - return _wait.until(method(*args), msg) + return _wait.until(method) + except Exception as err: + if callable(msg): + message = msg(self.driver) + else: + message = msg + raise TimeoutException(message) from err def wait_for_element(self, selector, timeout=None): """wait_for_element is shortcut to `wait_for_element_by_css_selector` @@ -286,8 +295,9 @@ def wait_for_element_by_css_selector(self, selector, timeout=None): equals to the fixture's `wait_timeout` shortcut to `WebDriverWait` with `EC.presence_of_element_located`.""" return self._wait_for( - EC.presence_of_element_located, - ((By.CSS_SELECTOR, selector),), + EC.presence_of_element_located( + (By.CSS_SELECTOR, selector), + ), timeout, f"timeout {timeout or self._wait_timeout}s => waiting for selector {selector}", ) @@ -310,8 +320,9 @@ def wait_for_element_by_id(self, element_id, timeout=None): equals to the fixture's `wait_timeout` shortcut to `WebDriverWait` with `EC.presence_of_element_located`.""" return self._wait_for( - EC.presence_of_element_located, - ((By.ID, element_id),), + EC.presence_of_element_located( + (By.ID, element_id), + ), timeout, f"timeout {timeout or self._wait_timeout}s => waiting for element id {element_id}", ) @@ -321,8 +332,7 @@ def wait_for_class_to_equal(self, selector, classname, timeout=None): if not set, equals to the fixture's `wait_timeout` shortcut to `WebDriverWait` with customized `class_to_equal` condition.""" return self._wait_for( - method=class_to_equal, - args=(selector, classname), + method=class_to_equal(selector, classname), timeout=timeout, msg=f"classname => {classname} not found within {timeout or self._wait_timeout}s", ) @@ -332,8 +342,7 @@ def wait_for_style_to_equal(self, selector, style, val, timeout=None): if not set, equals to the fixture's `wait_timeout` shortcut to `WebDriverWait` with customized `style_to_equal` condition.""" return self._wait_for( - method=style_to_equal, - args=(selector, style, val), + method=style_to_equal(selector, style, val), timeout=timeout, msg=f"style val => {style} {val} not found within {timeout or self._wait_timeout}s", ) @@ -345,11 +354,12 @@ def wait_for_text_to_equal(self, selector, text, timeout=None): shortcut to `WebDriverWait` with customized `text_to_equal` condition. """ + method = text_to_equal(selector, text, timeout or self.wait_timeout) + return self._wait_for( - method=text_to_equal, - args=(selector, text), + method=method, timeout=timeout, - msg=f"text -> {text} not found within {timeout or self._wait_timeout}s", + msg=method.message, ) def wait_for_contains_class(self, selector, classname, timeout=None): @@ -360,8 +370,7 @@ def wait_for_contains_class(self, selector, classname, timeout=None): condition. """ return self._wait_for( - method=contains_class, - args=(selector, classname), + method=contains_class(selector, classname), timeout=timeout, msg=f"classname -> {classname} not found inside element within {timeout or self._wait_timeout}s", ) @@ -373,11 +382,11 @@ def wait_for_contains_text(self, selector, text, timeout=None): shortcut to `WebDriverWait` with customized `contains_text` condition. """ + method = contains_text(selector, text, timeout or self.wait_timeout) return self._wait_for( - method=contains_text, - args=(selector, text), + method=method, timeout=timeout, - msg=f"text -> {text} not found inside element within {timeout or self._wait_timeout}s", + msg=method.message, ) def wait_for_page(self, url=None, timeout=10): diff --git a/dash/testing/plugin.py b/dash/testing/plugin.py index 1fe5f39bcb..d7369bfa05 100644 --- a/dash/testing/plugin.py +++ b/dash/testing/plugin.py @@ -22,6 +22,7 @@ def __init__(self, **kwargs): ) from dash.testing.browser import Browser from dash.testing.composite import DashComposite, DashRComposite, DashJuliaComposite + _installed = True except ImportError: # Running pytest without dash[testing] installed. diff --git a/dash/testing/wait.py b/dash/testing/wait.py index c0d41baa70..202322e96b 100644 --- a/dash/testing/wait.py +++ b/dash/testing/wait.py @@ -53,9 +53,10 @@ def until_not( class contains_text: - def __init__(self, selector, text): + def __init__(self, selector, text, timeout): self.selector = selector self.text = text + self.timeout = timeout def __call__(self, driver): try: @@ -67,6 +68,17 @@ def __call__(self, driver): except WebDriverException: return False + def message(self, driver): + try: + element = self._get_element(driver) + text = "found: " + str(element.text) or str(element.get_attribute("value")) + except WebDriverException: + text = f"{self.selector} not found" + return f"text -> {self.text} not found inside element within {self.timeout}s, {text}" + + def _get_element(self, driver): + return driver.find_element(By.CSS_SELECTOR, self.selector) + class contains_class: def __init__(self, selector, classname): @@ -86,13 +98,14 @@ def __call__(self, driver): class text_to_equal: - def __init__(self, selector, text): + def __init__(self, selector, text, timeout): self.selector = selector self.text = text + self.timeout = timeout def __call__(self, driver): try: - elem = driver.find_element(By.CSS_SELECTOR, self.selector) + elem = self._get_element(driver) logger.debug("text to equal {%s} => expected %s", elem.text, self.text) return ( str(elem.text) == self.text @@ -101,6 +114,17 @@ def __call__(self, driver): except WebDriverException: return False + def message(self, driver): + try: + element = self._get_element(driver) + text = "found: " + str(element.text) or str(element.get_attribute("value")) + except WebDriverException: + text = f"{self.selector} not found" + return f"text -> {self.text} not found within {self.timeout}s, {text}" + + def _get_element(self, driver): + return driver.find_element(By.CSS_SELECTOR, self.selector) + class style_to_equal: def __init__(self, selector, style, val): diff --git a/tests/integration/test_duo.py b/tests/integration/test_duo.py new file mode 100644 index 0000000000..fac5863353 --- /dev/null +++ b/tests/integration/test_duo.py @@ -0,0 +1,36 @@ +import pytest +from selenium.common import TimeoutException + +from dash import Dash, html + + +def test_duo001_wait_for_text_error(dash_duo): + app = Dash(__name__) + app.layout = html.Div([html.Div("Content", id="content")]) + dash_duo.start_server(app) + + with pytest.raises(TimeoutException) as err: + dash_duo.wait_for_text_to_equal("#content", "Invalid", timeout=1.0) + + assert err.value.args[0] == "text -> Invalid not found within 1.0s, found: Content" + + with pytest.raises(TimeoutException) as err: + dash_duo.wait_for_text_to_equal("#none", "None", timeout=1.0) + + assert err.value.args[0] == "text -> None not found within 1.0s, #none not found" + + with pytest.raises(TimeoutException) as err: + dash_duo.wait_for_contains_text("#content", "invalid", timeout=1.0) + + assert ( + err.value.args[0] + == "text -> invalid not found inside element within 1.0s, found: Content" + ) + + with pytest.raises(TimeoutException) as err: + dash_duo.wait_for_contains_text("#none", "none", timeout=1.0) + + assert ( + err.value.args[0] + == "text -> none not found inside element within 1.0s, #none not found" + )