Skip to content

Commit 6b27856

Browse files
authored
Merge pull request #2417 from plotly/testing-improvments
Testing improvements
2 parents 8ffb331 + 356005b commit 6b27856

File tree

7 files changed

+135
-36
lines changed

7 files changed

+135
-36
lines changed

CHANGELOG.md

+11
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,17 @@
22
All notable changes to `dash` will be documented in this file.
33
This project adheres to [Semantic Versioning](https://semver.org/).
44

5+
## [UNRELEASED]
6+
7+
## Added
8+
9+
- [#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)
10+
- [#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)
11+
12+
## Fixed
13+
14+
- [#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).
15+
- [#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.
516

617
## [2.8.1] - 2023-01-30
718

dash/dash.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,8 @@
125125
html.Div(id=_ID_DUMMY, disable_n_clicks=True),
126126
]
127127
)
128-
except AttributeError:
128+
# pylint: disable-next=bare-except
129+
except: # noqa: E722
129130
page_container = None
130131

131132

dash/testing/browser.py

+46-32
Original file line numberDiff line numberDiff line change
@@ -262,19 +262,28 @@ def _get_element(self, elem_or_selector):
262262
return self.find_element(elem_or_selector)
263263
return elem_or_selector
264264

265-
def _wait_for(self, method, args, timeout, msg):
265+
def _wait_for(self, method, timeout, msg):
266266
"""Abstract generic pattern for explicit WebDriverWait."""
267-
_wait = (
268-
self._wd_wait if timeout is None else WebDriverWait(self.driver, timeout)
269-
)
270-
logger.debug(
271-
"method, timeout, poll => %s %s %s",
272-
method,
273-
_wait._timeout, # pylint: disable=protected-access
274-
_wait._poll, # pylint: disable=protected-access
275-
)
267+
try:
268+
_wait = (
269+
self._wd_wait
270+
if timeout is None
271+
else WebDriverWait(self.driver, timeout)
272+
)
273+
logger.debug(
274+
"method, timeout, poll => %s %s %s",
275+
method,
276+
_wait._timeout, # pylint: disable=protected-access
277+
_wait._poll, # pylint: disable=protected-access
278+
)
276279

277-
return _wait.until(method(*args), msg)
280+
return _wait.until(method)
281+
except Exception as err:
282+
if callable(msg):
283+
message = msg(self.driver)
284+
else:
285+
message = msg
286+
raise TimeoutException(message) from err
278287

279288
def wait_for_element(self, selector, timeout=None):
280289
"""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):
286295
equals to the fixture's `wait_timeout` shortcut to `WebDriverWait` with
287296
`EC.presence_of_element_located`."""
288297
return self._wait_for(
289-
EC.presence_of_element_located,
290-
((By.CSS_SELECTOR, selector),),
298+
EC.presence_of_element_located(
299+
(By.CSS_SELECTOR, selector),
300+
),
291301
timeout,
292302
f"timeout {timeout or self._wait_timeout}s => waiting for selector {selector}",
293303
)
@@ -310,8 +320,9 @@ def wait_for_element_by_id(self, element_id, timeout=None):
310320
equals to the fixture's `wait_timeout` shortcut to `WebDriverWait` with
311321
`EC.presence_of_element_located`."""
312322
return self._wait_for(
313-
EC.presence_of_element_located,
314-
((By.ID, element_id),),
323+
EC.presence_of_element_located(
324+
(By.ID, element_id),
325+
),
315326
timeout,
316327
f"timeout {timeout or self._wait_timeout}s => waiting for element id {element_id}",
317328
)
@@ -321,8 +332,7 @@ def wait_for_class_to_equal(self, selector, classname, timeout=None):
321332
if not set, equals to the fixture's `wait_timeout` shortcut to
322333
`WebDriverWait` with customized `class_to_equal` condition."""
323334
return self._wait_for(
324-
method=class_to_equal,
325-
args=(selector, classname),
335+
method=class_to_equal(selector, classname),
326336
timeout=timeout,
327337
msg=f"classname => {classname} not found within {timeout or self._wait_timeout}s",
328338
)
@@ -332,8 +342,7 @@ def wait_for_style_to_equal(self, selector, style, val, timeout=None):
332342
if not set, equals to the fixture's `wait_timeout` shortcut to
333343
`WebDriverWait` with customized `style_to_equal` condition."""
334344
return self._wait_for(
335-
method=style_to_equal,
336-
args=(selector, style, val),
345+
method=style_to_equal(selector, style, val),
337346
timeout=timeout,
338347
msg=f"style val => {style} {val} not found within {timeout or self._wait_timeout}s",
339348
)
@@ -345,11 +354,12 @@ def wait_for_text_to_equal(self, selector, text, timeout=None):
345354
shortcut to `WebDriverWait` with customized `text_to_equal`
346355
condition.
347356
"""
357+
method = text_to_equal(selector, text, timeout or self.wait_timeout)
358+
348359
return self._wait_for(
349-
method=text_to_equal,
350-
args=(selector, text),
360+
method=method,
351361
timeout=timeout,
352-
msg=f"text -> {text} not found within {timeout or self._wait_timeout}s",
362+
msg=method.message,
353363
)
354364

355365
def wait_for_contains_class(self, selector, classname, timeout=None):
@@ -360,8 +370,7 @@ def wait_for_contains_class(self, selector, classname, timeout=None):
360370
condition.
361371
"""
362372
return self._wait_for(
363-
method=contains_class,
364-
args=(selector, classname),
373+
method=contains_class(selector, classname),
365374
timeout=timeout,
366375
msg=f"classname -> {classname} not found inside element within {timeout or self._wait_timeout}s",
367376
)
@@ -373,11 +382,11 @@ def wait_for_contains_text(self, selector, text, timeout=None):
373382
shortcut to `WebDriverWait` with customized `contains_text`
374383
condition.
375384
"""
385+
method = contains_text(selector, text, timeout or self.wait_timeout)
376386
return self._wait_for(
377-
method=contains_text,
378-
args=(selector, text),
387+
method=method,
379388
timeout=timeout,
380-
msg=f"text -> {text} not found inside element within {timeout or self._wait_timeout}s",
389+
msg=method.message,
381390
)
382391

383392
def wait_for_page(self, url=None, timeout=10):
@@ -449,11 +458,7 @@ def open_new_tab(self, url=None):
449458
)
450459

451460
def get_webdriver(self):
452-
try:
453-
return getattr(self, f"_get_{self._browser}")()
454-
except WebDriverException:
455-
logger.exception("<<<Webdriver not initialized correctly>>>")
456-
return None
461+
return getattr(self, f"_get_{self._browser}")()
457462

458463
def _get_wd_options(self):
459464
options = (
@@ -640,3 +645,12 @@ def server_url(self, value):
640645
@property
641646
def download_path(self):
642647
return self._download_path
648+
649+
@property
650+
def wait_timeout(self):
651+
return self._wait_timeout
652+
653+
@wait_timeout.setter
654+
def wait_timeout(self, value):
655+
self._wait_timeout = value
656+
self._wd_wait = WebDriverWait(self.driver, value)

dash/testing/plugin.py

+12
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ def __init__(self, **kwargs):
2222
)
2323
from dash.testing.browser import Browser
2424
from dash.testing.composite import DashComposite, DashRComposite, DashJuliaComposite
25+
# pylint: disable=unused-import
26+
import dash_testing_stub # noqa: F401
27+
28+
_installed = True
2529
except ImportError:
2630
# Running pytest without dash[testing] installed.
2731
ThreadedRunner = MissingDashTesting
@@ -33,9 +37,13 @@ def __init__(self, **kwargs):
3337
DashComposite = MissingDashTesting
3438
DashRComposite = MissingDashTesting
3539
DashJuliaComposite = MissingDashTesting
40+
_installed = False
3641

3742

3843
def pytest_addoption(parser):
44+
if not _installed:
45+
return
46+
3947
dash = parser.getgroup("Dash", "Dash Integration Tests")
4048

4149
dash.addoption(
@@ -82,6 +90,8 @@ def pytest_addoption(parser):
8290

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

95105
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
96106
def pytest_runtest_makereport(item, call): # pylint: disable=unused-argument
107+
if not _installed:
108+
return
97109
# execute all other hooks to obtain the report object
98110
outcome = yield
99111
rep = outcome.get_result()

dash/testing/wait.py

+27-3
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,10 @@ def until_not(
5353

5454

5555
class contains_text:
56-
def __init__(self, selector, text):
56+
def __init__(self, selector, text, timeout):
5757
self.selector = selector
5858
self.text = text
59+
self.timeout = timeout
5960

6061
def __call__(self, driver):
6162
try:
@@ -67,6 +68,17 @@ def __call__(self, driver):
6768
except WebDriverException:
6869
return False
6970

71+
def message(self, driver):
72+
try:
73+
element = self._get_element(driver)
74+
text = "found: " + str(element.text) or str(element.get_attribute("value"))
75+
except WebDriverException:
76+
text = f"{self.selector} not found"
77+
return f"text -> {self.text} not found inside element within {self.timeout}s, {text}"
78+
79+
def _get_element(self, driver):
80+
return driver.find_element(By.CSS_SELECTOR, self.selector)
81+
7082

7183
class contains_class:
7284
def __init__(self, selector, classname):
@@ -86,13 +98,14 @@ def __call__(self, driver):
8698

8799

88100
class text_to_equal:
89-
def __init__(self, selector, text):
101+
def __init__(self, selector, text, timeout):
90102
self.selector = selector
91103
self.text = text
104+
self.timeout = timeout
92105

93106
def __call__(self, driver):
94107
try:
95-
elem = driver.find_element(By.CSS_SELECTOR, self.selector)
108+
elem = self._get_element(driver)
96109
logger.debug("text to equal {%s} => expected %s", elem.text, self.text)
97110
return (
98111
str(elem.text) == self.text
@@ -101,6 +114,17 @@ def __call__(self, driver):
101114
except WebDriverException:
102115
return False
103116

117+
def message(self, driver):
118+
try:
119+
element = self._get_element(driver)
120+
text = "found: " + str(element.text) or str(element.get_attribute("value"))
121+
except WebDriverException:
122+
text = f"{self.selector} not found"
123+
return f"text -> {self.text} not found within {self.timeout}s, {text}"
124+
125+
def _get_element(self, driver):
126+
return driver.find_element(By.CSS_SELECTOR, self.selector)
127+
104128

105129
class style_to_equal:
106130
def __init__(self, selector, style, val):

requires-testing.txt

+1
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ selenium>=3.141.0,<=4.2.0
99
waitress>=1.4.4
1010
multiprocess>=0.70.12
1111
psutil>=5.8.0
12+
dash_testing_stub>=0.0.2

tests/integration/test_duo.py

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import pytest
2+
from selenium.common.exceptions import TimeoutException
3+
4+
from dash import Dash, html
5+
6+
7+
def test_duo001_wait_for_text_error(dash_duo):
8+
app = Dash(__name__)
9+
app.layout = html.Div([html.Div("Content", id="content")])
10+
dash_duo.start_server(app)
11+
12+
with pytest.raises(TimeoutException) as err:
13+
dash_duo.wait_for_text_to_equal("#content", "Invalid", timeout=1.0)
14+
15+
assert err.value.args[0] == "text -> Invalid not found within 1.0s, found: Content"
16+
17+
with pytest.raises(TimeoutException) as err:
18+
dash_duo.wait_for_text_to_equal("#none", "None", timeout=1.0)
19+
20+
assert err.value.args[0] == "text -> None not found within 1.0s, #none not found"
21+
22+
with pytest.raises(TimeoutException) as err:
23+
dash_duo.wait_for_contains_text("#content", "invalid", timeout=1.0)
24+
25+
assert (
26+
err.value.args[0]
27+
== "text -> invalid not found inside element within 1.0s, found: Content"
28+
)
29+
30+
with pytest.raises(TimeoutException) as err:
31+
dash_duo.wait_for_contains_text("#none", "none", timeout=1.0)
32+
33+
assert (
34+
err.value.args[0]
35+
== "text -> none not found inside element within 1.0s, #none not found"
36+
)

0 commit comments

Comments
 (0)