3636from playwright .sync_api import Error as PlaywrightError
3737from playwright .sync_api import Locator
3838from playwright .sync_api import Page
39+ from playwright .sync_api import TimeoutError
3940
4041from .locator import SmartLocator
41- from wait_for import TimedOutError
42+ from wait_for import TimedOutError , wait_for
4243
4344from .exceptions import LocatorNotImplemented
4445from .exceptions import NoSuchElementException
@@ -1628,6 +1629,119 @@ def _on_new_page(self, page: Page):
16281629 self .current .logger .info ("New page opened / popup detected: %s" , page .url )
16291630 self ._wrap_page (page )
16301631
1632+ def expect_new_page (self , timeout : float = 5.0 ):
1633+ """Context manager to wait for a new page and return the wrapped Browser instance.
1634+
1635+ This method uses Playwright's native `expect_page()` to wait for a new page
1636+ to be created. Use this when you know an action (like clicking a link with
1637+ target="_blank" or a button that calls window.open()) will open a new page.
1638+
1639+ The context manager automatically waits for the page to load (domcontentloaded).
1640+
1641+ Args:
1642+ timeout: Maximum time to wait for the new page in seconds (default: 5.0)
1643+
1644+ Yields:
1645+ Browser: The wrapped Browser instance for the new page
1646+
1647+ Example:
1648+ .. code-block:: python
1649+
1650+ # Get Browser directly - returns actual Browser instance
1651+ with window_manager.expect_new_page() as new_browser:
1652+ browser.click("a[target='_blank']")
1653+ # new_browser is the wrapped Browser instance, ready to use
1654+ assert new_browser.title == "New Page"
1655+ new_browser.element("#submit").click()
1656+ window_manager.close_browser(new_browser)
1657+
1658+ # Or with a longer timeout
1659+ with window_manager.expect_new_page(timeout=10.0) as popup_browser:
1660+ browser.click("#open-popup-button")
1661+ popup_browser.element("#confirm").click()
1662+ """
1663+
1664+ class _BrowserWaiter :
1665+ """Simple context manager that waits for page and returns wrapped Browser."""
1666+
1667+ def __init__ (self , window_manager : "WindowManager" , timeout_ms : int ) -> None :
1668+ self .window_manager = window_manager
1669+ self .context = window_manager ._context
1670+ self .timeout_ms = timeout_ms
1671+ self .page_info : Optional [Any ] = None
1672+ self ._browser : Optional [Browser ] = None
1673+ self ._initial_browsers : Optional [Set [Browser ]] = None
1674+
1675+ def __enter__ (self ) -> "_BrowserWaiter" :
1676+ # Capture initial browsers before setting up expectation
1677+ self ._initial_browsers = set (self .window_manager .all_browsers )
1678+ self .page_info = self .context .expect_page (timeout = self .timeout_ms )
1679+ self .page_info .__enter__ ()
1680+ return self
1681+
1682+ def __exit__ (self , * args : Any ) -> None :
1683+ # Exit Playwright's context manager to get the Page/Browser
1684+ if not self .page_info :
1685+ # If page_info was never set, something went wrong in __enter__
1686+ raise RuntimeError (
1687+ "expect_new_page context manager was not properly initialized. "
1688+ "This should not happen."
1689+ )
1690+
1691+ new_page = None
1692+ self .page_info .__exit__ (* args )
1693+ try :
1694+ new_page = self .page_info .value
1695+ except AttributeError :
1696+ # value might not be available in some edge cases
1697+ pass
1698+
1699+ if new_page :
1700+ # Wait for page to load and wrap it
1701+ try :
1702+ new_page .wait_for_load_state ("domcontentloaded" , timeout = 5000 )
1703+ except TimeoutError :
1704+ self .logger .warning (
1705+ "Timed out waiting for new page to load (domcontentloaded)."
1706+ )
1707+ self ._browser = self .window_manager ._wrap_page (new_page )
1708+ else :
1709+ # expect_page succeeded but value is None.This can happen if the event handler already wrapped the page
1710+ # Try to find it by comparing browser sets
1711+ if self ._initial_browsers is None :
1712+ raise RuntimeError (
1713+ "expect_new_page context manager was not properly initialized. "
1714+ "This should not happen."
1715+ )
1716+ current_browsers = set (self .window_manager .all_browsers )
1717+
1718+ if new_browsers := current_browsers - self ._initial_browsers :
1719+ self ._browser = next (iter (new_browsers ))
1720+ else :
1721+ # No new page found - this shouldn't happen if expect_page succeeded
1722+ raise RuntimeError (
1723+ "expect_page succeeded but no new page was found. "
1724+ "This may indicate a timing issue or the page was closed immediately."
1725+ )
1726+
1727+ def __getattr__ (self , name : str ) -> Any :
1728+ """Delegate all attribute access to the wrapped Browser."""
1729+ if self ._browser is None :
1730+ raise RuntimeError (
1731+ "Browser is not available yet. Access it after the 'with' block exits."
1732+ )
1733+ return getattr (self ._browser , name )
1734+
1735+ def __eq__ (self , other : Any ) -> bool :
1736+ """Compare by page identity for 'in' checks to work."""
1737+ if self ._browser is None :
1738+ return False
1739+ if isinstance (other , Browser ):
1740+ return self ._browser .page is other .page
1741+ return False
1742+
1743+ return _BrowserWaiter (self , int (timeout * 1000 ))
1744+
16311745 @property
16321746 def all_browsers (self ) -> List [Browser ]:
16331747 """Get all managed Browser instances with automatic cleanup.
@@ -1636,6 +1750,12 @@ def all_browsers(self) -> List[Browser]:
16361750 automatically performs cleanup by removing any Browser instances associated with
16371751 closed pages, ensuring the returned list only contains valid, active browsers.
16381752
1753+ Note:
1754+ When you know a click will open a new page, use ``expect_new_page()`` context manager
1755+ for reliable detection.
1756+
1757+ Returns:
1758+ List[Browser]: A list of all currently active widgetastic Browser instances
16391759
16401760 Example:
16411761 .. code-block:: python
@@ -1647,25 +1767,82 @@ def all_browsers(self) -> List[Browser]:
16471767 # Iterate through all browsers
16481768 for i, browser in enumerate(browsers):
16491769 print(f"Window {i}: {browser.title} - {browser.url}")
1770+
1771+ # For reliable new page detection, use expect_new_page
1772+ with window_manager.expect_new_page() as new_browser:
1773+ browser.click("a[target='_blank']")
1774+ # New page is now available in all_browsers
16501775 """
1651- current_pages = self ._context .pages
16521776 # Clean up browsers for pages that are no longer in the context or are closed
16531777 for page in list (self ._browsers .keys ()):
16541778 try :
16551779 # Check if page is still in context and not closed
1656- if page not in current_pages or page .is_closed ():
1780+ if page .is_closed ():
16571781 del self ._browsers [page ]
16581782 except Exception :
1659- # If we can't check the page state, assume it's closed and remove it
1783+ # If we can't check the page state, assume it's closed
16601784 if page in self ._browsers :
16611785 del self ._browsers [page ]
16621786
16631787 # Ensure all current pages are wrapped
1664- for page in current_pages :
1665- if not page .is_closed ():
1788+ for page in self ._context .pages :
1789+ if not page .is_closed () and page not in self ._browsers :
1790+ self ._wrap_page (page )
1791+
1792+ # Quick check for new pages that might be in transition
1793+ initial_count = len ([p for p in self ._context .pages if not p .is_closed ()])
1794+ initial_wrapped_count = len ([p for p in self ._browsers .keys () if not p .is_closed ()])
1795+
1796+ def _check_for_new_pages ():
1797+ """Check if new pages have appeared."""
1798+ current_pages = [p for p in self ._context .pages if not p .is_closed ()]
1799+ current_wrapped = [p for p in self ._browsers .keys () if not p .is_closed ()]
1800+
1801+ # Wrap any new pages found
1802+ for page in current_pages :
1803+ if page not in self ._browsers :
1804+ self ._wrap_page (page )
1805+
1806+ # Check if we detected a new page
1807+ return (
1808+ len (current_pages ) > initial_count # New page in context.pages
1809+ or len (current_wrapped ) > initial_wrapped_count # Event handler wrapped a new page
1810+ )
1811+
1812+ # Use wait_for with a short timeout for best-effort detection
1813+ # Note: Tests should use expect_new_page() for reliable detection
1814+ try :
1815+ wait_for (
1816+ _check_for_new_pages ,
1817+ num_sec = 1.0 , # Short timeout - expect_new_page() should be used for reliability
1818+ delay = 0.05 ,
1819+ message = "Checking for new pages" ,
1820+ silent_failure = True ,
1821+ very_quiet = True ,
1822+ )
1823+ except TimedOutError :
1824+ pass
1825+
1826+ # Final wrap - ensure all pages in context are wrapped
1827+ for page in self ._context .pages :
1828+ if not page .is_closed () and page not in self ._browsers :
16661829 self ._wrap_page (page )
16671830
1668- return list (self ._browsers .values ())
1831+ # Return all active browsers
1832+ active_browsers = []
1833+ for page , browser in self ._browsers .items ():
1834+ try :
1835+ if not page .is_closed () and not browser .is_browser_closed :
1836+ active_browsers .append (browser )
1837+ except Exception :
1838+ # If we can't check page state, try browser state
1839+ try :
1840+ if not browser .is_browser_closed :
1841+ active_browsers .append (browser )
1842+ except Exception :
1843+ active_browsers .append (browser )
1844+
1845+ return active_browsers
16691846
16701847 @property
16711848 def all_pages (self ) -> List [Page ]:
@@ -1737,7 +1914,7 @@ def switch_to(self, browser_or_page: Union[Browser, Page]):
17371914 current browser for subsequent operations.
17381915
17391916 Args:
1740- browser_or_page: Browser instance or Playwright Page to switch to
1917+ browser_or_page: Browser instance or Playwright Page to switch to.
17411918
17421919 Raises:
17431920 NoSuchElementException: If the specified page doesn't exist in the context
@@ -1753,12 +1930,23 @@ def switch_to(self, browser_or_page: Union[Browser, Page]):
17531930 all_pages = window_manager.all_pages
17541931 window_manager.switch_to(all_pages[0])
17551932
1933+ # Switch using browser from expect_new_page
1934+ with window_manager.expect_new_page() as new_browser:
1935+ browser.click("a[target='_blank']")
1936+ window_manager.switch_to(new_browser)
1937+
17561938 # Verify the switch
17571939 print(f"Now on: {window_manager.current.title}")
17581940 """
1759- target_page = (
1760- browser_or_page .page if isinstance (browser_or_page , Browser ) else browser_or_page
1761- )
1941+ if isinstance (browser_or_page , Page ):
1942+ target_page = browser_or_page
1943+ elif hasattr (browser_or_page , "page" ):
1944+ # This works for Browser and Browser-like proxies (like _BrowserWaiter)
1945+ target_page = browser_or_page .page
1946+ else :
1947+ raise TypeError (
1948+ f"Expected a Browser, Page, or Browser-like object, got { type (browser_or_page )} "
1949+ )
17621950
17631951 if target_page not in self .all_pages :
17641952 raise NoSuchElementException ("The specified Page handle does not exist." )
@@ -1801,7 +1989,16 @@ def close_browser(self, browser_or_page: Optional[Union[Browser, Page]] = None):
18011989 print(f"{remaining} tabs still open")
18021990 """
18031991 target_browser = browser_or_page or self .current
1804- target_page = target_browser .page if isinstance (target_browser , Browser ) else target_browser
1992+
1993+ if isinstance (target_browser , Page ):
1994+ target_page = target_browser
1995+ elif hasattr (target_browser , "page" ):
1996+ # This works for Browser and Browser-like proxies (like _BrowserWaiter)
1997+ target_page = target_browser .page
1998+ else :
1999+ raise TypeError (
2000+ f"Expected a Browser, Page, or Browser-like object, got { type (target_browser )} "
2001+ )
18052002
18062003 # Check if page is already closed
18072004 try :
0 commit comments