1717import glob
1818import subprocess
1919import json
20+ import time
21+ import os
2022
2123class WebTest :
2224
@@ -25,13 +27,23 @@ class WebTest:
2527 def __init__ (self , cap = None ):
2628 self .read_ini ()
2729 self .driver = get_driver (cap )
28- # Change the window size to make sure all elements are visible
29- current_size = self .driver .get_window_size ()
30- new_height = 5000
31- self .driver .set_window_size (current_size ['width' ], new_height )
3230 self .browser = False
3331 if 'browserName' in self .driver .capabilities :
3432 self .browser = self .driver .capabilities ['browserName' ].lower ()
33+
34+ # Ensure a consistent, generous viewport in CI to avoid responsive layout issues
35+ try :
36+ if os .getenv ("GITHUB_ACTIONS" ) == "true" :
37+ self .driver .set_window_rect (x = 0 , y = 0 , width = 1920 , height = 1080 )
38+ actual_size = self .driver .get_window_size ()
39+ if actual_size ['width' ] < 1000 : # If still too small, try alternative method
40+ self .driver .execute_script ("window.resizeTo(1920, 1080);" )
41+ else :
42+ current_size = self .driver .get_window_size ()
43+ self .driver .set_window_size (current_size ['width' ], 5000 )
44+ except Exception as e :
45+ print (f" - Warning: Could not set window size: { e } " )
46+
3547 self .load ()
3648
3749 def read_ini (self ):
@@ -48,27 +60,44 @@ def read_ini(self):
4860 def load (self ):
4961 print (" - loading site" )
5062 self .go (SITE_URL )
63+ # In headless mode, maximize_window() doesn't work and can cause issues
64+ # Set window size explicitly instead
5165 try :
52- self .driver .maximize_window ()
66+ if os .getenv ("GITHUB_ACTIONS" ) == "true" :
67+ self .driver .set_window_rect (x = 0 , y = 0 , width = 1920 , height = 1080 )
68+ self .driver .execute_script ("window.resizeTo(1920, 1080);" )
69+ else :
70+ self .driver .maximize_window ()
5371 except Exception :
54- print (" - Could not maximize browser :(" )
72+ print (" - Could not set window size :(" )
5573 if self .browser == 'safari' :
5674 try :
5775 self .driver .set_window_size (1920 ,1080 )
5876 except Exception :
5977 print (" - Could not maximize Safari" )
6078
79+ def save_debug_artifacts (self , name ):
80+ if os .getenv ("GITHUB_ACTIONS" ) != "true" :
81+ return
82+ os .makedirs (f"artifacts/{ name } " , exist_ok = True )
83+ with open (f"artifacts/{ name } /page_dump.html" , "w" , encoding = "utf-8" ) as f :
84+ f .write (self .driver .page_source )
85+ self .driver .save_screenshot (f"artifacts/{ name } /page_screenshot.png" )
86+ logs = self .driver .get_log ("browser" )
87+ with open (f"artifacts/{ name } /console_logs.json" , "w" , encoding = "utf-8" ) as f :
88+ json .dump (logs , f , indent = 2 )
89+
6190 def mod_active (self , name ):
6291 # debug self.modules
6392 echo = " - modules enabled: "
6493 for mod in self .modules :
6594 echo += mod + " "
66- print (echo )
95+ print (echo )
6796 if name in self .modules :
6897 return True
6998 print (" - module not enabled: %s" % name )
7099 return False
71-
100+
72101 def single_server (self ):
73102 if self .servers <= 1 :
74103 return True
@@ -77,6 +106,7 @@ def single_server(self):
77106
78107 def go (self , url ):
79108 self .driver .get (url )
109+ self .wait_for_page_ready ()
80110
81111 def login (self , user , password ):
82112 print (" - logging in" )
@@ -95,7 +125,7 @@ def confirm_alert(self):
95125 WebDriverWait (self .driver , 3 ).until (exp_cond .alert_is_present (), 'timed out' )
96126 alert = self .driver .switch_to .alert
97127 alert .accept ()
98-
128+
99129 def logout_no_save (self ):
100130 print (" - logging out" )
101131 self .driver .find_element (By .CLASS_NAME , 'logout_link' ).click ()
@@ -128,7 +158,7 @@ def by_class(self, class_name):
128158 print (" - finding element by class {0}" .format (class_name ))
129159 return self .driver .find_element (By .CLASS_NAME , class_name )
130160
131- def wait_for_element_by_class (self , class_name , timeout = 10 ):
161+ def wait_for_element_by_class (self , class_name , timeout = 60 ):
132162 """Wait for an element to be present and visible by class name"""
133163 print (" - waiting for element by class {0}" .format (class_name ))
134164 WebDriverWait (self .driver , timeout ).until (
@@ -143,7 +173,7 @@ def wait_for_element_by_class(self, class_name, timeout=10):
143173 def by_xpath (self , element_xpath ):
144174 print (" - finding element by xpath {0}" .format (element_xpath ))
145175 return self .driver .find_element (By .XPATH , element_xpath )
146-
176+
147177 def element_exists (self , class_name ):
148178 print (" - checking if element exists by class {0}" .format (class_name ))
149179 try :
@@ -157,63 +187,49 @@ def wait(self, el_type=By.TAG_NAME, el_value="body", timeout=60):
157187 element = WebDriverWait (self .driver , timeout ).until (
158188 exp_cond .presence_of_element_located ((el_type , el_value )))
159189
160- def wait_on_class (self , class_name , timeout = 30 ):
190+ def wait_on_class (self , class_name , timeout = 60 ):
161191 self .wait (By .CLASS_NAME , class_name )
162192
163193 def wait_with_folder_list (self ):
164194 self .wait (By .CLASS_NAME , "main" )
165195
166- def wait_on_sys_message (self , timeout = 30 ):
196+ def wait_on_sys_message (self , timeout = 60 ):
167197 wait = WebDriverWait (self .driver , timeout )
168198 element = wait .until (wait_for_non_empty_text ((By .CLASS_NAME , "sys_messages" ))
169199)
170-
171- def wait_for_navigation_to_complete (self , timeout = 30 ):
200+
201+ def wait_for_navigation_to_complete (self , timeout = 60 ):
172202 print (" - waiting for the navigation to complete..." )
173203 # Wait for the main content to be updated and any loading indicators to disappear
174204 try :
175- # Wait for any loading indicators to disappear
176- WebDriverWait (self .driver , 5 ).until_not (
177- lambda driver : len (driver .find_elements (By .ID , "loading_indicator" )) > 0
205+ WebDriverWait (self .driver , timeout ).until (
206+ lambda driver : driver .execute_script ("return window.routingToast === null;" )
207+ )
208+ WebDriverWait (self .driver , timeout ).until (
209+ lambda driver : driver .execute_script ("return document.getElementById('nprogress') === null;" )
178210 )
179211 except :
180- # Loading icon might not be present, continue
212+ print ( " - routing toast or nprogress check failed, continuing..." )
181213 pass
182-
183- # Wait for the main content area to be stable
214+
215+ def wait_for_page_ready (self , timeout = 60 ):
216+ """Wait for document readiness and idle network to reduce flakiness after navigation."""
184217 try :
218+ # Wait for DOM ready
185219 WebDriverWait (self .driver , timeout ).until (
186- lambda driver : driver .execute_script ("""
187- return new Promise((resolve) => {
188- let lastContent = '';
189- let stableCount = 0;
190- const checkStability = () => {
191- const mainContent = document.querySelector('main')?.innerHTML || '';
192- if (mainContent === lastContent) {
193- stableCount++;
194- if (stableCount >= 3) {
195- resolve(true);
196- return;
197- }
198- } else {
199- stableCount = 0;
200- lastContent = mainContent;
201- }
202- setTimeout(checkStability, 100);
203- };
204- checkStability();
205- });
206- """ )
220+ lambda d : d .execute_script ("return document.readyState" ) == "complete"
207221 )
208- except :
209- # Fallback: just wait for the main element to be present
210- print (" - fallback: waiting for main element" )
211- WebDriverWait (self .driver , timeout ).until (
212- exp_cond .presence_of_element_located ((By .TAG_NAME , "main" ))
222+ except Exception :
223+ print (" - document.readyState wait failed, continuing..." )
224+ # Best-effort small delay to allow post-load JS to attach handlers in CI
225+ try :
226+ WebDriverWait (self .driver , 5 ).until (
227+ lambda d : d .execute_script (
228+ "return !!(window.requestIdleCallback || window.setTimeout)"
229+ )
213230 )
214- # Additional wait for any dynamic content
215- import time
216- time .sleep (1 )
231+ except Exception :
232+ pass
217233
218234 def wait_for_settings_to_expand (self ):
219235 print (" - waiting for the settings section to expand..." )
@@ -229,13 +245,13 @@ def wait_for_settings_to_expand(self):
229245 return
230246 except :
231247 pass
232-
248+
233249 # Click to expand
234250 settings_button .click ()
235-
251+
236252 # Wait for the settings to be displayed
237253 try :
238- WebDriverWait (self .driver , 10 ).until (lambda x : self .by_class ('settings' ).is_displayed ())
254+ WebDriverWait (self .driver , 60 ).until (lambda x : self .by_class ('settings' ).is_displayed ())
239255 print (" - settings expanded successfully" )
240256 except :
241257 print (" - settings expansion timeout, continuing anyway" )
@@ -249,13 +265,13 @@ def click_when_clickable(self, el):
249265 print (" - waiting for element to be clickable" )
250266 try :
251267 # Scroll element into view
252- self .driver .execute_script ("arguments[0].scrollIntoView({block: 'center'});" , el )
253-
268+ self .driver .execute_script ("arguments[0].scrollIntoView({block: 'center', behavior: 'instant' });" , el )
269+
254270 # Wait for element to be clickable
255- WebDriverWait (self .driver , 10 ).until (
271+ WebDriverWait (self .driver , 60 ).until (
256272 exp_cond .element_to_be_clickable (el )
257273 )
258-
274+
259275 # Try regular click first
260276 try :
261277 el .click ()
@@ -265,7 +281,7 @@ def click_when_clickable(self, el):
265281 print (" - trying JavaScript click as fallback" )
266282 # Use JavaScript click as fallback
267283 self .driver .execute_script ("arguments[0].click();" , el )
268-
284+
269285 except Exception as e :
270286 print (f" - click_when_clickable failed: { e } " )
271287 # Final fallback: try JavaScript click without waiting
@@ -282,11 +298,8 @@ def safe_click(self, element):
282298 for attempt in range (max_attempts ):
283299 try :
284300 # Scroll element into view
285- self .driver .execute_script ("arguments[0].scrollIntoView({block: 'center'});" , element )
286- # Wait a moment for any animations
287- import time
288- time .sleep (0.5 )
289- # Try to click
301+ self .driver .execute_script ("arguments[0].scrollIntoView({block: 'center', behavior: 'instant'});" , element )
302+ WebDriverWait (self .driver , 60 ).until (exp_cond .element_to_be_clickable (element ))
290303 element .click ()
291304 return
292305 except Exception as e :
0 commit comments