@@ -24,6 +24,7 @@ def __init__(self, page: Page, loop: AbstractEventLoop):
2424 self .client = None
2525 self .page = page
2626 self .loop = loop
27+ self .autoswitch_to_new_tab = True # Can be disabled via alumnium:options
2728 self .supported_tools = {
2829 ClickTool ,
2930 DragAndDropTool ,
@@ -33,6 +34,7 @@ def __init__(self, page: Page, loop: AbstractEventLoop):
3334 UploadTool ,
3435 }
3536 self ._run_async (self ._enable_target_auto_attach ())
37+ self ._run_async (self ._setup_page_tracking (page ))
3638
3739 @property
3840 def platform (self ) -> str :
@@ -327,6 +329,11 @@ async def _wait_for_page_to_load(self):
327329
328330 @asynccontextmanager
329331 async def _autoswitch_to_new_tab (self ):
332+ # If auto-switch is disabled, just yield without waiting for new pages
333+ if not self .autoswitch_to_new_tab :
334+ yield
335+ return
336+
330337 try :
331338 async with self .page .context .expect_page (timeout = PlaywrightDriver .NEW_TAB_TIMEOUT ) as new_page_info :
332339 yield
@@ -373,6 +380,29 @@ async def _enable_target_auto_attach(self):
373380 except Exception as e :
374381 logger .debug (f"Could not enable Target.setAutoAttach: { e } " )
375382
383+ async def _setup_page_tracking (self , initial_page : Page ):
384+ """Set up tracking for all pages in the context."""
385+ self ._pages : list [Page ] = [initial_page ]
386+ self ._attach_page_listeners (initial_page )
387+
388+ def _attach_page_listeners (self , page : Page ):
389+ """Attach popup and close listeners to a page."""
390+ # Use sync handler to avoid deadlock - async handler would block via _run_async
391+ page .on ("popup" , self ._on_popup_sync )
392+ page .on ("close" , self ._on_page_close )
393+
394+ def _on_popup_sync (self , popup : Page ):
395+ """Handle new popup/tab opened from a page (sync to avoid deadlock)."""
396+ logger .debug (f"New popup opened: { popup .url } " )
397+ self ._pages .append (popup )
398+ self ._attach_page_listeners (popup ) # Chain: new page also listens for popups
399+
400+ def _on_page_close (self , popup : Page ):
401+ """Handle page closed."""
402+ if popup in self ._pages :
403+ logger .debug (f"Page closed: { popup .url } " )
404+ self ._pages .remove (popup )
405+
376406 def _get_all_frame_ids (self , frame_info : dict ) -> list [str ]:
377407 """Recursively collect all frame IDs from CDP frame tree."""
378408 frame_ids = [frame_info ["frame" ]["id" ]]
@@ -572,6 +602,40 @@ def search_frame(frame_info: dict) -> str | None:
572602
573603 return search_frame (cdp_frame_tree ["frameTree" ])
574604
605+ def switch_to_next_tab (self ):
606+ self ._run_async (self ._switch_to_next_tab ())
607+
608+ async def _switch_to_next_tab (self ):
609+ # Brief wait to allow popup handlers to complete
610+ await self .page .wait_for_timeout (100 )
611+ if len (self ._pages ) <= 1 :
612+ return # Only one tab, nothing to switch
613+
614+ current_index = self ._pages .index (self .page )
615+ next_index = (current_index + 1 ) % len (self ._pages ) # Wrap to first
616+
617+ self .page = self ._pages [next_index ]
618+ self .client = None # Reset CDP client for new page
619+ await self .page .wait_for_load_state ()
620+ logger .debug (f"Switched to next tab: { self .page .url } " )
621+
622+ def switch_to_previous_tab (self ):
623+ self ._run_async (self ._switch_to_previous_tab ())
624+
625+ async def _switch_to_previous_tab (self ):
626+ # Brief wait to allow popup handlers to complete
627+ await self .page .wait_for_timeout (100 )
628+ if len (self ._pages ) <= 1 :
629+ return # Only one tab, nothing to switch
630+
631+ current_index = self ._pages .index (self .page )
632+ prev_index = (current_index - 1 ) % len (self ._pages ) # Wrap to last
633+
634+ self .page = self ._pages [prev_index ]
635+ self .client = None # Reset CDP client for new page
636+ await self .page .wait_for_load_state ()
637+ logger .debug (f"Switched to previous tab: { self .page .url } " )
638+
575639 def _run_async (self , coro ):
576640 future = run_coroutine_threadsafe (coro , self .loop )
577641 return future .result ()
0 commit comments