Skip to content

Commit 45ca204

Browse files
committed
feat: cleaning up type definitions for testing files
1 parent 33153a1 commit 45ca204

File tree

2 files changed

+47
-122
lines changed

2 files changed

+47
-122
lines changed

dash/testing/browser.py

+11-16
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,7 @@
44
import time
55
import logging
66
import warnings
7-
from typing import List
8-
97
import percy
10-
import percy.errors # to satisfy type checking
118
import requests
129

1310
from selenium import webdriver
@@ -105,7 +102,7 @@ def __exit__(self, exc_type, exc_val, traceback):
105102
logger.info("percy finalize relies on CI job")
106103
except WebDriverException:
107104
logger.exception("webdriver quit was not successful")
108-
except percy.errors.Error:
105+
except percy.errors.Error: # type: ignore[reportAttributeAccessIssue]
109106
logger.exception("percy runner failed to finalize properly")
110107

111108
def visit_and_snapshot(
@@ -118,7 +115,7 @@ def visit_and_snapshot(
118115
stay_on_page=False,
119116
widths=None,
120117
):
121-
path = resource_path.lstrip("/") # moved before 'try' to satisfy type checking
118+
path = resource_path.lstrip("/")
122119
try:
123120
if path != resource_path:
124121
logger.warning("we stripped the left '/' in resource_path")
@@ -240,16 +237,16 @@ def take_snapshot(self, name):
240237

241238
self.driver.save_screenshot(f"{target}/{name}_{self.session_id}.png")
242239

243-
def find_element(self, locator, attribute="CSS_SELECTOR"):
244-
"""find_element returns the first found element by the attribute `locator`
240+
def find_element(self, selector, attribute="CSS_SELECTOR"):
241+
"""find_element returns the first found element by the attribute `selector`
245242
shortcut to `driver.find_element(By.CSS_SELECTOR, ...)`.
246243
args:
247244
- attribute: the attribute type to search for, aligns with the Selenium
248245
API's `By` class. default "CSS_SELECTOR"
249246
valid values: "CSS_SELECTOR", "ID", "NAME", "TAG_NAME",
250247
"CLASS_NAME", "LINK_TEXT", "PARTIAL_LINK_TEXT", "XPATH"
251248
"""
252-
return self.driver.find_element(getattr(By, attribute.upper()), locator)
249+
return self.driver.find_element(getattr(By, attribute.upper()), selector)
253250

254251
def find_elements(self, selector, attribute="CSS_SELECTOR"):
255252
"""find_elements returns a list of all elements matching the attribute
@@ -288,7 +285,6 @@ def _wait_for(self, method, timeout, msg):
288285
message = msg(self.driver)
289286
else:
290287
message = msg
291-
assert isinstance(message, str) # to satisfy type checking
292288
raise TimeoutException(message) from err
293289

294290
def wait_for_element(self, selector, timeout=None):
@@ -408,7 +404,7 @@ def wait_for_page(self, url=None, timeout=10):
408404
)
409405
except TimeoutException as exc:
410406
logger.exception("dash server is not loaded within %s seconds", timeout)
411-
logs = "\n".join((str(log) for log in self.get_logs()))
407+
logs = "\n".join((str(log) for log in self.get_logs())) # type: ignore[reportOptionalIterable]
412408
logger.debug(logs)
413409
html = self.find_element("body").get_property("innerHTML")
414410
raise DashAppLoadingError(
@@ -502,17 +498,16 @@ def _get_chrome(self):
502498
options.add_argument("--disable-gpu")
503499
options.add_argument("--remote-debugging-port=9222")
504500

505-
assert isinstance(self._remote_url, str) # to satisfy type checking
506501
chrome = (
507-
webdriver.Remote(command_executor=self._remote_url, options=options)
502+
webdriver.Remote(command_executor=self._remote_url, options=options) # type: ignore[reportAttributeAccessIssue]
508503
if self._remote
509504
else webdriver.Chrome(options=options)
510505
)
511506

512507
# https://bugs.chromium.org/p/chromium/issues/detail?id=696481
513508
if self._headless:
514509
# pylint: disable=protected-access
515-
chrome.command_executor._commands["send_command"] = ( # type: ignore[reportAttributeAccessIssue]
510+
chrome.command_executor._commands["send_command"] = ( # type: ignore[reportArgumentType]
516511
"POST",
517512
"/session/$sessionId/chromium/send_command",
518513
)
@@ -605,7 +600,7 @@ def click_at_coord_fractions(self, elem_or_selector, fx, fy):
605600
elem, elem.size["width"] * fx, elem.size["height"] * fy
606601
).click().perform()
607602

608-
def get_logs(self) -> List:
603+
def get_logs(self):
609604
"""Return a list of `SEVERE` level logs after last reset time stamps
610605
(default to 0, resettable by `reset_log_timestamp`.
611606
@@ -617,8 +612,8 @@ def get_logs(self) -> List:
617612
for entry in self.driver.get_log("browser")
618613
if entry["timestamp"] > self._last_ts
619614
]
620-
warnings.warn("get_logs always returns '[]' with webdrivers other than Chrome")
621-
return []
615+
warnings.warn("get_logs always return None with webdrivers other than Chrome")
616+
return None
622617

623618
def reset_log_timestamp(self):
624619
"""reset_log_timestamp only work with chrome webdriver."""

dash/testing/dash_page.py

+36-106
Original file line numberDiff line numberDiff line change
@@ -1,94 +1,47 @@
1-
from typing import Any, Dict, List, Optional, Protocol, cast, runtime_checkable
1+
# type: ignore[reportAttributeAccessIssue]
2+
# Ignore attribute access issues when type checking because mixin
3+
# class depends on other class lineage to supply things. We could use
4+
# a protocol definition here instead…
25

36
from bs4 import BeautifulSoup
47

5-
# directive needed because protocol classes are so brief
6-
# pylint: disable=too-few-public-methods
7-
8-
9-
@runtime_checkable
10-
class WebElement(Protocol):
11-
"""Protocol for WebElement-like objects with get_attribute."""
12-
13-
def get_attribute(self, name: str) -> str:
14-
...
15-
16-
17-
@runtime_checkable
18-
class WebDriver(Protocol):
19-
"""Protocol for WebDriver-like objects with execute_script."""
20-
21-
def execute_script(self, script: str, *args: Any) -> Any:
22-
...
23-
248

259
class DashPageMixin:
26-
"""Mixin class for Dash page with DOM access methods.
27-
28-
This mixin is intended to be used with a class that provides:
29-
1. A 'driver' attribute with execute_script method
30-
2. A 'find_element' method that returns elements with get_attribute method
31-
32-
The mixin provides properties like dash_entry_locator and methods to
33-
interact with the Dash application's DOM and state.
34-
"""
35-
36-
driver: WebDriver # Expected to be provided by the parent class
37-
38-
def find_element(self, locator: str) -> WebElement:
39-
"""Find an element by locator.
40-
41-
This is expected to be implemented by the parent class.
42-
"""
43-
raise NotImplementedError(
44-
"find_element must be implemented by the parent class"
10+
def _get_dash_dom_by_attribute(self, attr):
11+
return BeautifulSoup(
12+
self.find_element(self.dash_entry_locator).get_attribute(attr), "lxml"
4513
)
4614

4715
@property
48-
def dash_entry_locator(self) -> str:
49-
"""CSS selector for Dash app entry point."""
50-
return "#react-entry-point"
51-
52-
@property
53-
def devtools_error_count_locator(self) -> str:
54-
"""CSS selector for devtools error count."""
16+
def devtools_error_count_locator(self):
5517
return ".test-devtools-error-count"
5618

57-
def _get_dash_dom_by_attribute(self, attr: str) -> BeautifulSoup:
58-
"""Get BeautifulSoup representation of element's attribute."""
59-
element = self.find_element(self.dash_entry_locator)
60-
return BeautifulSoup(element.get_attribute(attr), "lxml")
19+
@property
20+
def dash_entry_locator(self):
21+
return "#react-entry-point"
6122

6223
@property
63-
def dash_outerhtml_dom(self) -> BeautifulSoup:
64-
"""Get BeautifulSoup representation of outerHTML."""
24+
def dash_outerhtml_dom(self):
6525
return self._get_dash_dom_by_attribute("outerHTML")
6626

6727
@property
68-
def dash_innerhtml_dom(self) -> BeautifulSoup:
69-
"""Get BeautifulSoup representation of innerHTML."""
28+
def dash_innerhtml_dom(self):
7029
return self._get_dash_dom_by_attribute("innerHTML")
7130

7231
@property
73-
def redux_state_paths(self) -> Dict[str, Any]:
74-
"""Get Redux state paths."""
75-
return cast(
76-
dict[str, Any],
77-
self.driver.execute_script(
78-
"""
32+
def redux_state_paths(self):
33+
return self.driver.execute_script(
34+
"""
7935
var p = window.store.getState().paths;
8036
return {strs: p.strs, objs: p.objs}
8137
"""
82-
),
8338
)
8439

8540
@property
86-
def redux_state_rqs(self) -> List[Dict[str, Any]]:
87-
"""Get Redux state request queue."""
88-
return cast(
89-
list[dict[str, Any]],
90-
self.driver.execute_script(
91-
"""
41+
def redux_state_rqs(self):
42+
return self.driver.execute_script(
43+
"""
44+
9245
// Check for legacy `pendingCallbacks` store prop (compatibility for Dash matrix testing)
9346
var pendingCallbacks = window.store.getState().pendingCallbacks;
9447
if (pendingCallbacks) {
@@ -108,62 +61,39 @@ def redux_state_rqs(self) -> List[Dict[str, Any]]:
10861
10962
return Array.prototype.concat.apply([], Object.values(callbacksState));
11063
"""
111-
),
11264
)
11365

11466
@property
115-
def redux_state_is_loading(self) -> bool:
116-
"""Check if Redux state is loading."""
117-
return cast(
118-
bool,
119-
self.driver.execute_script(
120-
"""
67+
def redux_state_is_loading(self):
68+
return self.driver.execute_script(
69+
"""
12170
return window.store.getState().isLoading;
12271
"""
123-
),
12472
)
12573

12674
@property
127-
def window_store(self) -> Optional[Any]:
128-
"""Get window.store object."""
75+
def window_store(self):
12976
return self.driver.execute_script("return window.store")
13077

131-
def _wait_for_callbacks(self) -> bool:
132-
"""Check if callbacks are complete."""
133-
# Access properties directly
134-
window_store = self.window_store
135-
redux_state_rqs = self.redux_state_rqs
136-
return (not window_store) or (redux_state_rqs == [])
137-
138-
def get_local_storage(self, store_id: str = "local") -> Optional[Dict[str, Any]]:
139-
"""Get item from localStorage."""
140-
return cast(
141-
Optional[dict[str, Any]],
142-
self.driver.execute_script(
143-
f"return JSON.parse(window.localStorage.getItem('{store_id}'));"
144-
),
78+
def _wait_for_callbacks(self):
79+
return (not self.window_store) or self.redux_state_rqs == []
80+
81+
def get_local_storage(self, store_id="local"):
82+
return self.driver.execute_script(
83+
f"return JSON.parse(window.localStorage.getItem('{store_id}'));"
14584
)
14685

147-
def get_session_storage(
148-
self, session_id: str = "session"
149-
) -> Optional[Dict[str, Any]]:
150-
"""Get item from sessionStorage."""
151-
return cast(
152-
Optional[dict[str, Any]],
153-
self.driver.execute_script(
154-
f"return JSON.parse(window.sessionStorage.getItem('{session_id}'));"
155-
),
86+
def get_session_storage(self, session_id="session"):
87+
return self.driver.execute_script(
88+
f"return JSON.parse(window.sessionStorage.getItem('{session_id}'));"
15689
)
15790

158-
def clear_local_storage(self) -> None:
159-
"""Clear localStorage."""
91+
def clear_local_storage(self):
16092
self.driver.execute_script("window.localStorage.clear()")
16193

162-
def clear_session_storage(self) -> None:
163-
"""Clear sessionStorage."""
94+
def clear_session_storage(self):
16495
self.driver.execute_script("window.sessionStorage.clear()")
16596

166-
def clear_storage(self) -> None:
167-
"""Clear both localStorage and sessionStorage."""
97+
def clear_storage(self):
16898
self.clear_local_storage()
16999
self.clear_session_storage()

0 commit comments

Comments
 (0)