1010from .config import get_global_config
1111from .hook import pytest_wrapper
1212from .runner import (
13+ BrowserWorkerChromeRunner ,
14+ BrowserWorkerFirefoxRunner ,
15+ BrowserWorkerSafariRunner ,
1316 NodeRunner ,
1417 PlaywrightChromeRunner ,
1518 PlaywrightFirefoxRunner ,
@@ -72,6 +75,41 @@ def _playwright_browsers(request):
7275 browser .close ()
7376
7477
78+ @functools .cache
79+ def _runner_map () -> dict [tuple [str , str , str ], type [_BrowserBaseRunner ]]:
80+ def runner_key (runner : type [_BrowserBaseRunner ]) -> tuple [str , str , str ]:
81+ worker = "worker" if getattr (runner , "_worker" , False ) else "main"
82+ return (runner .runner , runner .browser , worker )
83+
84+ runners = [
85+ SeleniumFirefoxRunner ,
86+ SeleniumChromeRunner ,
87+ SeleniumSafariRunner ,
88+ PlaywrightFirefoxRunner ,
89+ PlaywrightChromeRunner ,
90+ BrowserWorkerChromeRunner ,
91+ BrowserWorkerFirefoxRunner ,
92+ BrowserWorkerSafariRunner ,
93+ NodeRunner ,
94+ ]
95+ return {runner_key (rt ): rt for rt in runners }
96+
97+
98+ def _get_runner_cls (
99+ runtime : str , runner_type : str , is_worker : bool
100+ ) -> type [_BrowserBaseRunner ]:
101+ if runtime == "node" :
102+ runner_type = "node"
103+ thread = "worker" if is_worker else "main"
104+
105+ runner_cls = _runner_map ().get ((runner_type , runtime , thread ))
106+ if runner_cls is None :
107+ raise AssertionError (
108+ f"Unknown runner or browser: { runner_type } / { runtime } / { thread } "
109+ )
110+ return runner_cls
111+
112+
75113@contextlib .contextmanager
76114def selenium_common (
77115 request ,
@@ -80,6 +118,7 @@ def selenium_common(
80118 load_pyodide = True ,
81119 browsers = None ,
82120 jspi = False ,
121+ worker = False ,
83122):
84123 """Returns an initialized selenium object.
85124
@@ -88,21 +127,7 @@ def selenium_common(
88127 """
89128
90129 server_hostname , server_port , server_log = web_server_main
91- runner_type = request .config .option .runner .lower ()
92-
93- runner_set : dict [tuple [str , str ], type [_BrowserBaseRunner ]] = {
94- ("selenium" , "firefox" ): SeleniumFirefoxRunner ,
95- ("selenium" , "chrome" ): SeleniumChromeRunner ,
96- ("selenium" , "safari" ): SeleniumSafariRunner ,
97- ("selenium" , "node" ): NodeRunner ,
98- ("playwright" , "firefox" ): PlaywrightFirefoxRunner ,
99- ("playwright" , "chrome" ): PlaywrightChromeRunner ,
100- ("playwright" , "node" ): NodeRunner ,
101- }
102-
103- runner_cls = runner_set .get ((runner_type , runtime ))
104- if runner_cls is None :
105- raise AssertionError (f"Unknown runner or browser: { runner_type } / { runtime } " )
130+ runner_cls = _get_runner_cls (runtime , request .config .option .runner .lower (), worker )
106131
107132 dist_dir = Path (os .getcwd (), request .config .getoption ("--dist-dir" ))
108133 runner = runner_cls (
@@ -278,20 +303,78 @@ def selenium(request, selenium_module_scope):
278303 yield selenium
279304
280305
306+ # selenium instance cached at the module level
307+ @pytest .fixture (scope = "module" )
308+ def selenium_worker_module_scope (
309+ request , runtime , web_server_main , playwright_browsers
310+ ):
311+ if runtime == "node" :
312+ pytest .skip ("selenium_worker has no support in node" )
313+
314+ with selenium_common (
315+ request , runtime , web_server_main , browsers = playwright_browsers , worker = True
316+ ) as selenium :
317+ yield selenium
318+
319+
320+ # Hypothesis is unhappy with function scope fixtures. Instead, use the
321+ # module scope fixture `selenium_module_scope` and use:
322+ # `with selenium_context_manager(selenium_module_scope) as selenium`
323+ @contextlib .contextmanager
324+ def selenium_worker_context_manager (selenium_worker_module_scope ):
325+ try :
326+ selenium_worker_module_scope .clean_logs ()
327+ yield selenium_worker_module_scope
328+ finally :
329+ try :
330+ print (selenium_worker_module_scope .logs )
331+ except ValueError :
332+ # For reasons I don't entirely understand, it is possible for
333+ # selenium to be closed before this is executed. In that case, just
334+ # skip printing the logs and we can exit cleanly.
335+ pass
336+
337+
338+ @pytest .fixture
339+ def selenium_worker (request , selenium_worker_module_scope ):
340+ with selenium_worker_context_manager (
341+ selenium_worker_module_scope
342+ ) as selenium , set_webdriver_script_timeout (
343+ selenium , script_timeout = parse_driver_timeout (request .node )
344+ ):
345+ yield selenium
346+
347+
281348@pytest .fixture
282349def selenium_jspi (request , runtime , web_server_main , playwright_browsers ):
283350 yield from selenium_jspi_inner (
284351 request , runtime , web_server_main , playwright_browsers
285352 )
286353
287354
288- def selenium_jspi_inner (request , runtime , web_server_main , playwright_browsers ):
355+ @pytest .fixture
356+ def selenium_jspi_worker (request , runtime , web_server_main , playwright_browsers ):
357+ if runtime == "node" :
358+ pytest .skip ("selenium_worker has no support in node" )
359+ yield from selenium_jspi_inner (
360+ request , runtime , web_server_main , playwright_browsers , worker = True
361+ )
362+
363+
364+ def selenium_jspi_inner (
365+ request , runtime , web_server_main , playwright_browsers , worker = False
366+ ):
289367 if runtime in ["firefox" , "safari" ]:
290368 pytest .skip (f"jspi not supported in { runtime } " )
291369 if request .config .option .runner .lower () == "playwright" :
292370 pytest .skip ("jspi not supported with playwright" )
293371 with selenium_common (
294- request , runtime , web_server_main , browsers = playwright_browsers , jspi = True
372+ request ,
373+ runtime ,
374+ web_server_main ,
375+ browsers = playwright_browsers ,
376+ jspi = True ,
377+ worker = worker ,
295378 ) as selenium , set_webdriver_script_timeout (
296379 selenium , script_timeout = parse_driver_timeout (request .node )
297380 ):
0 commit comments