Skip to content

Commit

Permalink
Merge pull request #2417 from plotly/testing-improvments
Browse files Browse the repository at this point in the history
Testing improvements
  • Loading branch information
T4rk1n committed Feb 14, 2023
2 parents 8ffb331 + 356005b commit 6b27856
Show file tree
Hide file tree
Showing 7 changed files with 135 additions and 36 deletions.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@
All notable changes to `dash` will be documented in this file.
This project adheres to [Semantic Versioning](https://semver.org/).

## [UNRELEASED]

## Added

- [#2417](https://github.com/plotly/dash/pull/2417) Add wait_timeout property to customize the behavior of the default wait timeout used for by wait_for_page, fix [#1595](https://github.com/plotly/dash/issues/1595)
- [#2417](https://github.com/plotly/dash/pull/2417) Add the element target text for wait_for_text* error message, fix [#945](https://github.com/plotly/dash/issues/945)

## Fixed

- [#2417](https://github.com/plotly/dash/pull/2417) Disable the pytest plugin if `dash[testing]` not installed, fix [#946](https://github.com/plotly/dash/issues/946).
- [#2417](https://github.com/plotly/dash/pull/2417) Do not swallow the original error to get the webdriver, easier to know what is wrong after updating the browser but the driver.

## [2.8.1] - 2023-01-30

Expand Down
3 changes: 2 additions & 1 deletion dash/dash.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,8 @@
html.Div(id=_ID_DUMMY, disable_n_clicks=True),
]
)
except AttributeError:
# pylint: disable-next=bare-except
except: # noqa: E722
page_container = None


Expand Down
78 changes: 46 additions & 32 deletions dash/testing/browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand All @@ -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}",
)
Expand All @@ -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}",
)
Expand All @@ -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",
)
Expand All @@ -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",
)
Expand All @@ -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):
Expand All @@ -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",
)
Expand All @@ -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):
Expand Down Expand Up @@ -449,11 +458,7 @@ def open_new_tab(self, url=None):
)

def get_webdriver(self):
try:
return getattr(self, f"_get_{self._browser}")()
except WebDriverException:
logger.exception("<<<Webdriver not initialized correctly>>>")
return None
return getattr(self, f"_get_{self._browser}")()

def _get_wd_options(self):
options = (
Expand Down Expand Up @@ -640,3 +645,12 @@ def server_url(self, value):
@property
def download_path(self):
return self._download_path

@property
def wait_timeout(self):
return self._wait_timeout

@wait_timeout.setter
def wait_timeout(self, value):
self._wait_timeout = value
self._wd_wait = WebDriverWait(self.driver, value)
12 changes: 12 additions & 0 deletions dash/testing/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ def __init__(self, **kwargs):
)
from dash.testing.browser import Browser
from dash.testing.composite import DashComposite, DashRComposite, DashJuliaComposite
# pylint: disable=unused-import
import dash_testing_stub # noqa: F401

_installed = True
except ImportError:
# Running pytest without dash[testing] installed.
ThreadedRunner = MissingDashTesting
Expand All @@ -33,9 +37,13 @@ def __init__(self, **kwargs):
DashComposite = MissingDashTesting
DashRComposite = MissingDashTesting
DashJuliaComposite = MissingDashTesting
_installed = False


def pytest_addoption(parser):
if not _installed:
return

dash = parser.getgroup("Dash", "Dash Integration Tests")

dash.addoption(
Expand Down Expand Up @@ -82,6 +90,8 @@ def pytest_addoption(parser):

@pytest.mark.tryfirst
def pytest_addhooks(pluginmanager):
if not _installed:
return
# https://github.com/pytest-dev/pytest-xdist/blob/974bd566c599dc6a9ea291838c6f226197208b46/xdist/plugin.py#L67
# avoid warnings with pytest-2.8
from dash.testing import newhooks # pylint: disable=import-outside-toplevel
Expand All @@ -94,6 +104,8 @@ def pytest_addhooks(pluginmanager):

@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call): # pylint: disable=unused-argument
if not _installed:
return
# execute all other hooks to obtain the report object
outcome = yield
rep = outcome.get_result()
Expand Down
30 changes: 27 additions & 3 deletions dash/testing/wait.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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):
Expand All @@ -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
Expand All @@ -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):
Expand Down
1 change: 1 addition & 0 deletions requires-testing.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ selenium>=3.141.0,<=4.2.0
waitress>=1.4.4
multiprocess>=0.70.12
psutil>=5.8.0
dash_testing_stub>=0.0.2
36 changes: 36 additions & 0 deletions tests/integration/test_duo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import pytest
from selenium.common.exceptions 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"
)

0 comments on commit 6b27856

Please sign in to comment.