fixtures.py (9896B)
1 import os 2 3 import pytest 4 import pytest_asyncio 5 from tests.support.helpers import deep_update 6 7 from .chrome_handler import using_chrome_handler 8 from .context import using_context 9 from .helpers import ( 10 Browser, 11 Geckodriver, 12 create_custom_profile, 13 get_pref, 14 get_profile_folder, 15 read_user_preferences, 16 set_pref, 17 ) 18 19 20 def pytest_collection_modifyitems(items): 21 """Auto-apply markers for specific API usage by fixtures""" 22 for item in items: 23 if "use_pref" in getattr(item, "fixturenames", ()): 24 item.add_marker("allow_system_access") 25 26 27 def pytest_configure(config): 28 # register the allow_system_access marker 29 config.addinivalue_line( 30 "markers", "allow_system_access: Mark test to allow system access" 31 ) 32 33 34 @pytest.fixture(scope="module") 35 def browser(configuration, firefox_options): 36 """Start a Firefox instance without using geckodriver. 37 38 geckodriver will automatically use the --remote-allow-hosts and 39 --remote.allow.origins command line arguments. 40 41 Starting Firefox without geckodriver allows to set those command line arguments 42 as needed. The fixture method returns the browser instance that should be used 43 to connect to a RemoteAgent supported protocol (WebDriver BiDi). 44 """ 45 current_browser = None 46 47 def _browser( 48 clone_profile=True, 49 extra_args=None, 50 extra_prefs=None, 51 use_bidi=False, 52 use_marionette=False, 53 ): 54 nonlocal current_browser 55 56 webdriver_args = configuration["webdriver"]["args"] 57 log_level = None 58 truncate_enabled = True 59 60 if "-vv" or "-vvv" in webdriver_args: 61 log_level = "Trace" 62 63 if "-vvv" in webdriver_args: 64 truncate_enabled = False 65 elif "-v" in webdriver_args: 66 log_level = "Debug" 67 68 # If the requested preferences and arguments match the ones for the 69 # already started firefox, we can reuse the current firefox instance, 70 # return the instance immediately. 71 if current_browser: 72 if ( 73 current_browser.extra_args == extra_args 74 and current_browser.extra_prefs == extra_prefs 75 and current_browser.is_running 76 and current_browser.use_bidi == use_bidi 77 and current_browser.use_marionette == use_marionette 78 and current_browser.log_level == log_level 79 and current_browser.truncate_enabled == truncate_enabled 80 ): 81 return current_browser 82 83 # Otherwise, if firefox is already started, terminate it because we need 84 # to create a new instance for the provided preferences. 85 current_browser.quit() 86 87 binary = configuration["browser"]["binary"] 88 env = configuration["browser"]["env"] 89 90 profile_path = get_profile_folder(firefox_options) 91 default_prefs = read_user_preferences(profile_path) 92 profile = create_custom_profile( 93 profile_path, default_prefs, clone=clone_profile 94 ) 95 96 current_browser = Browser( 97 binary, 98 profile, 99 extra_args=extra_args, 100 extra_prefs=extra_prefs, 101 env=env, 102 log_level=log_level, 103 truncate_enabled=truncate_enabled, 104 use_bidi=use_bidi, 105 use_marionette=use_marionette, 106 ) 107 current_browser.start() 108 return current_browser 109 110 yield _browser 111 112 # Stop firefox at the end of the test module. 113 if current_browser is not None: 114 current_browser.quit() 115 current_browser = None 116 117 118 @pytest.fixture 119 def default_capabilities(request): 120 """Default capabilities to use for a new WebDriver session for Mozilla specific tests.""" 121 122 # Get the value from the overwritten original fixture 123 capabilities = request.getfixturevalue("default_capabilities") 124 125 allow_system_access = any( 126 marker.name == "allow_system_access" for marker in request.node.own_markers 127 ) 128 129 if allow_system_access: 130 deep_update( 131 capabilities, 132 { 133 "moz:firefoxOptions": { 134 "args": [ 135 "--remote-allow-system-access", 136 ] 137 } 138 }, 139 ) 140 141 return capabilities 142 143 144 @pytest.fixture 145 def default_chrome_handler(current_session): 146 manifest_path = os.path.join( 147 os.path.abspath(os.path.dirname(__file__)), "chrome-assets", "chrome.manifest" 148 ) 149 entries = [["content", "marionette-chrome", "chrome/"]] 150 151 with using_chrome_handler(current_session, manifest_path, entries): 152 yield "chrome://marionette-chrome/content/" 153 154 155 @pytest.fixture 156 def default_preferences(profile_folder): 157 return read_user_preferences(profile_folder) 158 159 160 @pytest.fixture 161 def new_chrome_window(current_session): 162 opened_chrome_windows = [] 163 164 def _new_chrome_window(url, focus=True): 165 # Bug 1944570: Replace with BiDi once scripts can be evaluated 166 # in the parent process. 167 with using_context(current_session, "chrome"): 168 new_window = current_session.execute_async_script( 169 """ 170 const { NavigableManager } = ChromeUtils.importESModule( 171 "chrome://remote/content/shared/NavigableManager.sys.mjs" 172 ); 173 174 let [url, focus, resolve] = arguments; 175 176 function waitForEvent(target, type, args) { 177 return new Promise(resolve => { 178 let params = Object.assign({once: true}, args); 179 target.addEventListener(type, event => { 180 dump(`** Received DOM event ${event.type} for ${event.target}\n`); 181 resolve(); 182 }, params); 183 }); 184 } 185 186 function waitForFocus(win) { 187 return Promise.all([ 188 waitForEvent(win, "activate"), 189 waitForEvent(win, "focus", {capture: true}), 190 ]); 191 } 192 193 const isLoaded = window => 194 window?.document.readyState === "complete" && 195 !window?.document.isUncommittedInitialDocument; 196 197 (async function() { 198 // Open a window, wait for it to receive focus 199 let newWindow = window.openDialog(url, null, "chrome,centerscreen"); 200 let focused = waitForFocus(newWindow); 201 202 newWindow.focus(); 203 await focused; 204 205 // The new window shouldn't get focused. As such set the 206 // focus back to the opening window. 207 if (!focus && Services.focus.activeWindow != window) { 208 let focused = waitForFocus(window); 209 window.focus(); 210 await focused; 211 } 212 213 // Wait for the new window to be finished loading 214 if (isLoaded(newWindow)) { 215 resolve(newWindow); 216 } else { 217 const onLoad = () => { 218 if (isLoaded(newWindow)) { 219 newWindow.removeEventListener("load", onLoad); 220 resolve(newWindow); 221 } else { 222 dump(`** Target window not loaded yet. Waiting for the next "load" event\n`); 223 } 224 }; 225 newWindow.addEventListener("load", onLoad); 226 } 227 })(); 228 """, 229 args=[url, focus], 230 ) 231 232 # Append opened chrome window to automatic closing on teardown 233 opened_chrome_windows.append(new_window) 234 return new_window 235 236 yield _new_chrome_window 237 238 with using_context(current_session, "chrome"): 239 for win in opened_chrome_windows: 240 try: 241 current_session.window_handle = win.id 242 current_session.execute_script("arguments[0].close()", args=[win]) 243 except Exception: 244 pass 245 246 current_session.window_handle = current_session.handles[0] 247 248 249 @pytest.fixture(name="create_custom_profile") 250 def fixture_create_custom_profile(default_preferences, profile_folder): 251 profile = None 252 253 def _create_custom_profile(clone=True): 254 profile = create_custom_profile( 255 profile_folder, default_preferences, clone=clone 256 ) 257 258 return profile 259 260 yield _create_custom_profile 261 262 # if profile is not None: 263 if profile: 264 profile.cleanup() 265 266 267 @pytest.fixture(scope="session") 268 def firefox_options(configuration): 269 return configuration["capabilities"]["moz:firefoxOptions"] 270 271 272 @pytest_asyncio.fixture 273 async def geckodriver(configuration): 274 """Start a geckodriver instance directly.""" 275 driver = None 276 277 def _geckodriver(config=None, hostname=None, extra_args=None, extra_env=None): 278 nonlocal driver 279 280 if config is None: 281 config = configuration 282 283 driver = Geckodriver(config, hostname, extra_args, extra_env) 284 driver.start() 285 286 return driver 287 288 yield _geckodriver 289 290 if driver is not None: 291 await driver.stop() 292 293 294 @pytest.fixture 295 def profile_folder(firefox_options): 296 return get_profile_folder(firefox_options) 297 298 299 @pytest.fixture 300 def use_pref(session): 301 """Set a specific pref value.""" 302 reset_values = {} 303 304 def _use_pref(pref, value): 305 if pref not in reset_values: 306 reset_values[pref] = get_pref(session, pref) 307 308 set_pref(session, pref, value) 309 310 yield _use_pref 311 312 for pref, reset_value in reset_values.items(): 313 set_pref(session, pref, reset_value)