1717)
1818from selenium .webdriver .common .action_chains import ActionChains
1919from selenium .webdriver .common .by import By
20+ from selenium .webdriver .remote .webdriver import WebDriver
2021from selenium .webdriver .support import expected_conditions
2122from selenium .webdriver .support .wait import WebDriverWait
2223
@@ -397,14 +398,10 @@ def start_chrome(**chrome_args):
397398 )
398399
399400
400- @pytest .fixture (scope = "function" )
401- def browser (request , live_server ):
402- window_size = request .config .getoption ("window_size" )
403- window_width , window_height = map (int , window_size .split ("x" , 1 ))
404-
405- driver_type = request .config .getoption ("selenium_driver" )
406- headless = not request .config .getoption ("no_headless" )
407- slow_network = request .config .getoption ("slow_network" )
401+ def _start_driver (config ):
402+ window_size = config .getoption ("window_size" )
403+ driver_type = config .getoption ("selenium_driver" )
404+ headless = not config .getoption ("no_headless" )
408405 if driver_type == "chrome" :
409406 options = webdriver .ChromeOptions ()
410407 options .add_argument ("no-sandbox" )
@@ -413,26 +410,85 @@ def browser(request, live_server):
413410 options .add_argument (f"window-size={ window_size } " )
414411 if headless :
415412 options .add_argument ("headless" )
416- chrome_path = request . config .getoption ("chrome_path" )
413+ chrome_path = config .getoption ("chrome_path" )
417414 if chrome_path :
418415 options .binary_location = chrome_path
419- chromedriver_path = request . config .getoption ("chromedriver_path" )
416+ chromedriver_path = config .getoption ("chromedriver_path" )
420417 chrome_args = {"options" : options }
421418 if chromedriver_path :
422419 chrome_args ["executable_path" ] = chromedriver_path
423420
424- driver = start_chrome (** chrome_args )
425- if slow_network :
426- driver .set_network_conditions (
427- offline = False ,
428- latency = 400 * 2 , # additional latency (ms)
429- throughput = 500 * 1024 , # maximal throughput
430- )
421+ return start_chrome (** chrome_args )
431422 elif driver_type == "firefox" :
432- driver = webdriver .Firefox ()
423+ return webdriver .Firefox ()
433424 else :
434425 raise pytest .UsageError ("--driver must be specified" )
435426
427+
428+ def _quit_driver (driver ):
429+ try :
430+ driver .quit ()
431+ except Exception :
432+ pass
433+
434+
435+ def _reset_chrome (driver , origin ):
436+ # A new tab drops sessionStorage, per-tab CDP emulation overrides, and any
437+ # JS or requests the previous test's page still has in flight, which must
438+ # stop before the database is flushed.
439+ previous_handles = driver .window_handles
440+ driver .switch_to .new_window ("tab" )
441+ fresh_handle = driver .current_window_handle
442+ for handle in previous_handles :
443+ driver .switch_to .window (handle )
444+ driver .close ()
445+ driver .switch_to .window (fresh_handle )
446+
447+ # The HTTP cache stays warm: the app cannot read it, and a cold cache
448+ # would give back part of what skipping the relaunch saves.
449+ driver .execute_cdp_cmd ("Network.clearBrowserCookies" , {})
450+ driver .execute_cdp_cmd ("Storage.clearDataForOrigin" , {"origin" : origin , "storageTypes" : "all" })
451+
452+
453+ class _SessionDriver :
454+ driver : WebDriver | None = None
455+
456+
457+ @pytest .fixture (scope = "session" )
458+ def _session_driver ():
459+ # Session scope is per xdist worker, so each worker owns one Chrome.
460+ holder = _SessionDriver ()
461+ yield holder
462+ if holder .driver is not None :
463+ _quit_driver (holder .driver )
464+
465+
466+ @pytest .fixture (scope = "function" )
467+ def browser (request , live_server , _session_driver ):
468+ window_size = request .config .getoption ("window_size" )
469+ window_width , window_height = map (int , window_size .split ("x" , 1 ))
470+ reuse = request .config .getoption ("selenium_driver" ) == "chrome"
471+
472+ driver = _session_driver .driver if reuse else None
473+ if driver is not None :
474+ try :
475+ driver .window_handles
476+ except WebDriverException :
477+ logger .warning ("selenium.reused-browser-dead" , exc_info = True )
478+ _quit_driver (driver )
479+ driver = None
480+ if driver is None :
481+ driver = _start_driver (request .config )
482+ if reuse :
483+ _session_driver .driver = driver
484+
485+ if request .config .getoption ("slow_network" ):
486+ driver .set_network_conditions (
487+ offline = False ,
488+ latency = 400 * 2 , # additional latency (ms)
489+ throughput = 500 * 1024 , # maximal throughput
490+ )
491+
436492 driver .set_window_size (window_width , window_height )
437493
438494 request .node ._driver = driver
@@ -462,11 +518,19 @@ def browser(request, live_server):
462518 sys .stderr .write ("[browser console] " )
463519 sys .stderr .write (repr (entry ))
464520 sys .stderr .write ("\n " )
465- # Teardown Selenium.
521+
522+ if not reuse :
523+ _quit_driver (driver )
524+ return
525+
466526 try :
467- driver . quit ( )
527+ _reset_chrome ( driver , live_server . url )
468528 except Exception :
469- pass
529+ # A browser that cannot be reset could leak state into the next test,
530+ # so replace it instead.
531+ logger .warning ("selenium.reset-failed" , exc_info = True )
532+ _session_driver .driver = None
533+ _quit_driver (driver )
470534
471535
472536@pytest .hookimpl (tryfirst = True , hookwrapper = True )
0 commit comments