@@ -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
0 commit comments