Skip to content

Commit 45c4717

Browse files
committed
use same browser for all tests during session
1 parent bd1bb13 commit 45c4717

2 files changed

Lines changed: 259 additions & 6 deletions

File tree

components/dash-core-components/tests/conftest.py

Lines changed: 183 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,197 @@ def start_server(self, app, **kwargs):
1818
self.server_url = self.server.url
1919

2020

21-
@pytest.fixture
22-
def dash_dcc(request, dash_thread_server, tmpdir):
23-
with DashCoreComponentsComposite(
24-
dash_thread_server,
21+
class _ReusableDashCoreComponentsComposite(DashCoreComponentsMixin):
22+
"""DCC composite that reuses an existing browser instance."""
23+
24+
def __init__(self, server, browser_instance):
25+
self.server = server
26+
self._browser_instance = browser_instance
27+
self._driver = browser_instance._driver
28+
self._browser = browser_instance._browser
29+
self._headless = browser_instance._headless
30+
self._wait_timeout = browser_instance._wait_timeout
31+
self._percy_run = browser_instance._percy_run
32+
self._percy_finalize = browser_instance._percy_finalize
33+
self._pause = browser_instance._pause
34+
self._wd_wait = browser_instance._wd_wait
35+
self._download_path = browser_instance._download_path
36+
self._last_ts = 0
37+
self._url = ""
38+
self._window_idx = 0
39+
40+
def __getattr__(self, name):
41+
# Delegate any missing attributes/methods to the browser instance
42+
return getattr(self._browser_instance, name)
43+
44+
@property
45+
def driver(self):
46+
return self._driver
47+
48+
@property
49+
def wait_timeout(self):
50+
return self._wait_timeout
51+
52+
def start_server(self, app, **kwargs):
53+
"""start the local server with app"""
54+
self.server(app, **kwargs)
55+
self.server_url = self.server.url
56+
57+
@property
58+
def server_url(self):
59+
return self._url
60+
61+
@server_url.setter
62+
def server_url(self, value):
63+
self._url = value
64+
self.wait_for_page()
65+
66+
def wait_for_page(self, url=None, timeout=10):
67+
from selenium.common.exceptions import TimeoutException
68+
from dash.testing.errors import DashAppLoadingError
69+
70+
self.driver.get(self._url if url is None else url)
71+
try:
72+
self.wait_for_element_by_css_selector("#react-entry-point", timeout=timeout)
73+
except TimeoutException as exc:
74+
raise DashAppLoadingError("Dash app failed to load") from exc
75+
76+
def wait_for_element_by_css_selector(self, selector, timeout=None):
77+
from selenium.webdriver.support.wait import WebDriverWait
78+
from selenium.webdriver.support import expected_conditions as EC
79+
from selenium.webdriver.common.by import By
80+
81+
wait = WebDriverWait(self.driver, timeout or self._wait_timeout)
82+
return wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, selector)))
83+
84+
def wait_for_element_by_id(self, element_id, timeout=None):
85+
from selenium.webdriver.support.wait import WebDriverWait
86+
from selenium.webdriver.support import expected_conditions as EC
87+
from selenium.webdriver.common.by import By
88+
89+
wait = WebDriverWait(self.driver, timeout or self._wait_timeout)
90+
return wait.until(EC.presence_of_element_located((By.ID, element_id)))
91+
92+
def find_element(self, selector, attribute="CSS_SELECTOR"):
93+
from selenium.webdriver.common.by import By
94+
95+
return self.driver.find_element(getattr(By, attribute.upper()), selector)
96+
97+
def find_elements(self, selector, attribute="CSS_SELECTOR"):
98+
from selenium.webdriver.common.by import By
99+
100+
return self.driver.find_elements(getattr(By, attribute.upper()), selector)
101+
102+
def wait_for_element(self, selector, timeout=None):
103+
return self.wait_for_element_by_css_selector(selector, timeout)
104+
105+
def wait_for_text_to_equal(self, selector, text, timeout=None):
106+
from dash.testing.wait import text_to_equal
107+
108+
return self._wait_for(
109+
text_to_equal(selector, text, timeout or self._wait_timeout), timeout
110+
)
111+
112+
def _wait_for(self, method, timeout):
113+
from selenium.webdriver.support.wait import WebDriverWait
114+
from selenium.common.exceptions import TimeoutException
115+
116+
wait = WebDriverWait(self.driver, timeout or self._wait_timeout)
117+
try:
118+
return wait.until(method)
119+
except TimeoutException:
120+
raise
121+
122+
def wait_for_style_to_equal(self, selector, style, val, timeout=None):
123+
from dash.testing.wait import style_to_equal
124+
125+
return self._wait_for(style_to_equal(selector, style, val), timeout)
126+
127+
def percy_snapshot(
128+
self, name="", wait_for_callbacks=False, convert_canvases=False, widths=None
129+
):
130+
# Delegate to browser instance's percy_snapshot
131+
self._browser_instance.percy_snapshot(
132+
name, wait_for_callbacks, convert_canvases, widths
133+
)
134+
135+
def clear_input(self, elem_or_selector):
136+
from selenium.webdriver.common.keys import Keys
137+
from selenium.webdriver.common.action_chains import ActionChains
138+
139+
elem = (
140+
self.find_element(elem_or_selector)
141+
if isinstance(elem_or_selector, str)
142+
else elem_or_selector
143+
)
144+
(
145+
ActionChains(self.driver)
146+
.move_to_element(elem)
147+
.pause(0.2)
148+
.click(elem)
149+
.send_keys(Keys.END)
150+
.key_down(Keys.SHIFT)
151+
.send_keys(Keys.HOME)
152+
.key_up(Keys.SHIFT)
153+
.send_keys(Keys.DELETE)
154+
).perform()
155+
156+
def clear_storage(self):
157+
self.driver.execute_script("window.localStorage.clear()")
158+
self.driver.execute_script("window.sessionStorage.clear()")
159+
160+
def get_logs(self):
161+
if self._browser == "chrome":
162+
return [
163+
entry
164+
for entry in self.driver.get_log("browser")
165+
if entry["timestamp"] > self._last_ts
166+
]
167+
return None
168+
169+
def _reset_browser_state(self):
170+
try:
171+
self.driver.delete_all_cookies()
172+
except Exception:
173+
pass
174+
try:
175+
self.driver.get("about:blank")
176+
self.clear_storage()
177+
except Exception:
178+
pass
179+
180+
def __enter__(self):
181+
self._reset_browser_state()
182+
return self
183+
184+
def __exit__(self, exc_type, exc_val, traceback):
185+
pass
186+
187+
188+
@pytest.fixture(scope="session")
189+
def _dcc_browser_session(request, tmp_path_factory):
190+
"""Session-scoped browser instance for DCC tests."""
191+
download_path = tmp_path_factory.mktemp("download")
192+
browser = Browser(
25193
browser=request.config.getoption("webdriver"),
26194
remote=request.config.getoption("remote"),
27195
remote_url=request.config.getoption("remote_url"),
28196
headless=request.config.getoption("headless"),
29197
options=request.config.hook.pytest_setup_options(),
30-
download_path=tmpdir.mkdir("download").strpath,
198+
download_path=str(download_path),
31199
percy_assets_root=request.config.getoption("percy_assets"),
32200
percy_finalize=request.config.getoption("nopercyfinalize"),
33201
pause=request.config.getoption("pause"),
202+
)
203+
yield browser
204+
browser.__exit__(None, None, None)
205+
206+
207+
@pytest.fixture
208+
def dash_dcc(request, dash_thread_server, _dcc_browser_session):
209+
with _ReusableDashCoreComponentsComposite(
210+
dash_thread_server,
211+
browser_instance=_dcc_browser_session,
34212
) as dc:
35213
yield dc
36214

dash/testing/plugin.py

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,8 +186,83 @@ def dash_br(request, tmpdir) -> Browser: # type: ignore[reportInvalidTypeForm]
186186
yield browser
187187

188188

189+
@pytest.fixture(scope="session")
190+
def _dash_browser_session(request, tmp_path_factory):
191+
"""Session-scoped browser instance, reused across all tests."""
192+
if not _installed:
193+
yield None
194+
return
195+
196+
download_path = tmp_path_factory.mktemp("download")
197+
browser = Browser(
198+
browser=request.config.getoption("webdriver"),
199+
remote=request.config.getoption("remote"),
200+
remote_url=request.config.getoption("remote_url"),
201+
headless=request.config.getoption("headless"),
202+
options=request.config.hook.pytest_setup_options(),
203+
download_path=str(download_path),
204+
percy_assets_root=request.config.getoption("percy_assets"),
205+
percy_finalize=request.config.getoption("nopercyfinalize"),
206+
pause=request.config.getoption("pause"),
207+
)
208+
yield browser
209+
browser.__exit__(None, None, None)
210+
211+
212+
class _ReusableDashComposite(DashComposite):
213+
"""DashComposite that reuses an existing browser instance."""
214+
215+
def __init__(self, server, browser_instance, **kwargs):
216+
# Skip Browser.__init__, just set up the server
217+
self.server = server
218+
self._driver = browser_instance._driver
219+
self._browser = browser_instance._browser
220+
self._headless = browser_instance._headless
221+
self._wait_timeout = browser_instance._wait_timeout
222+
self._percy_run = browser_instance._percy_run
223+
self._percy_finalize = browser_instance._percy_finalize
224+
self._pause = browser_instance._pause
225+
self._wd_wait = browser_instance._wd_wait
226+
self._download_path = browser_instance._download_path
227+
self._last_ts = 0
228+
self._url = ""
229+
self._window_idx = 0
230+
231+
def _reset_browser_state(self):
232+
"""Clear browser state between tests."""
233+
try:
234+
self.driver.delete_all_cookies()
235+
except Exception:
236+
pass
237+
try:
238+
# Navigate to blank page first to ensure we can clear storage
239+
self.driver.get("about:blank")
240+
self.clear_storage()
241+
except Exception:
242+
pass
243+
244+
def __enter__(self):
245+
self._reset_browser_state()
246+
return self
247+
248+
def __exit__(self, exc_type, exc_val, traceback):
249+
# Don't quit the browser - it's shared
250+
pass
251+
252+
253+
@pytest.fixture
254+
def dash_duo(request, dash_thread_server, _dash_browser_session) -> DashComposite: # type: ignore[reportInvalidTypeForm]
255+
"""Dash test fixture with reusable browser (session-scoped)."""
256+
with _ReusableDashComposite(
257+
server=dash_thread_server,
258+
browser_instance=_dash_browser_session,
259+
) as dc:
260+
yield dc
261+
262+
189263
@pytest.fixture
190-
def dash_duo(request, dash_thread_server, tmpdir) -> DashComposite: # type: ignore[reportInvalidTypeForm]
264+
def dash_duo_fresh_browser(request, dash_thread_server, tmpdir) -> DashComposite: # type: ignore[reportInvalidTypeForm]
265+
"""Dash test fixture with a fresh browser instance (for tests that need isolation)."""
191266
with DashComposite(
192267
server=dash_thread_server,
193268
browser=request.config.getoption("webdriver"),

0 commit comments

Comments
 (0)