foxpuppet/windows/browser/window.py (142 lines of code) (raw):

# This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this file, # You can obtain one at http://mozilla.org/MPL/2.0/. """Contains BrowserWindow object representing the Firefox browser.""" from selenium.common.exceptions import NoSuchElementException from selenium.webdriver.common.by import By from foxpuppet import expected from foxpuppet.windows import BaseWindow from foxpuppet.windows.browser.navbar import NavBar from foxpuppet.windows.browser.notifications import BaseNotification from foxpuppet.windows.browser.bookmarks.bookmark import Bookmark from foxpuppet.windows.browser.panel_ui.panel_ui import PanelUI from selenium.webdriver.remote.webelement import WebElement from typing import Any, Optional, Union, TypeVar, Type T = TypeVar("T", bound="BaseNotification") P = TypeVar("P", bound="PanelUI") class BrowserWindow(BaseWindow): """Representation of a browser window.""" _bookmark_locator = (By.ID, "main-window") _file_menu_button_locator = (By.ID, "file-menu") _file_menu_private_window_locator = (By.ID, "menu_newPrivateWindow") _file_menu_new_window_button_locator = (By.ID, "menu_newNavigator") _nav_bar_locator = (By.ID, "nav-bar") _notification_locator = (By.CSS_SELECTOR, "#notification-popup popupnotification") _panel_ui_locator = (By.ID, "PanelUI-menu-button") _app_menu_notification_locator = ( By.CSS_SELECTOR, "#appMenu-notification-popup popupnotification", ) _app_menu_panel_ui_locator = ( By.CSS_SELECTOR, "#appMenu-mainView .panel-subview-body toolbarbutton", ) _tab_browser_locator = (By.ID, "tabbrowser-tabs") @property def navbar(self) -> NavBar: """Provide access to the Navigation Bar. Returns: :py:class:`NavBar`: FoxPuppet NavBar object. """ window = BaseWindow(self.selenium, self.selenium.current_window_handle) with self.selenium.context(self.selenium.CONTEXT_CHROME): el: WebElement = self.selenium.find_element(*self._nav_bar_locator) return NavBar(window, el) @property def notification(self) -> BaseNotification | Any: """Provide access to the currently displayed notification. Returns: :py:class:`BaseNotification`: FoxPuppet BaseNotification object. """ with self.selenium.context(self.selenium.CONTEXT_CHROME): try: root = self.selenium.find_element(*self._notification_locator) return BaseNotification.create(self, root) except NoSuchElementException: pass try: notifications = self.selenium.find_elements( *self._app_menu_notification_locator ) root = next(n for n in notifications if n.is_displayed()) return BaseNotification.create(self, root) except StopIteration: pass return None # no notification is displayed @property def bookmark(self) -> Bookmark: """Provide access to the currently displayed bookmark. Returns: :py:class:`BaseBookmark`: FoxPuppet BasicBookmark object. """ with self.selenium.context(self.selenium.CONTEXT_CHROME): root = self.selenium.find_element(*self._bookmark_locator) return Bookmark.create(self, root) @property def panel(self) -> PanelUI | Any: panel_root = None with self.selenium.context(self.selenium.CONTEXT_CHROME): root = self.selenium.find_element(*self._panel_ui_locator) panel_root = PanelUI.create(self, root) panel_items = self.selenium.find_elements(*self._app_menu_panel_ui_locator) for item in panel_items: _id = item.get_property("id") from foxpuppet.windows.browser.panel_ui.panel_ui import PANEL_ITEMS if _id in PANEL_ITEMS and item.is_displayed(): panel_root = PANEL_ITEMS[_id].create(self, item) # type: ignore return panel_root def wait_for_notification( self, notification_class: Optional[Type[T]] = BaseNotification, # type: ignore ) -> Optional[T]: """Wait for the specified notification to be displayed. Args: notification_class (:py:class:`BaseNotification`, optional): The notification class to wait for. If `None` is specified it will wait for any notification to be closed. Defaults to `BaseNotification`. Returns: Optional[:py:class:`BaseNotification`]: Firefox notification or None. """ if notification_class: if notification_class is BaseNotification: message = "No notification was shown." else: message = "{0} was not shown.".format(notification_class.__name__) self.wait.until( lambda _: isinstance(self.notification, notification_class), message=message, ) return self.notification # type: ignore else: self.wait.until( lambda _: self.notification is None, message="Unexpected notification shown.", ) return None def wait_for_bookmark(self) -> Bookmark: """Wait for the bookmark panel to be displayed. Returns: Optional[Bookmark]: The Bookmark object if found, or None if not found. """ with self.selenium.context(self.selenium.CONTEXT_CHROME): message = "Bookmark panel was not shown." self.wait.until( lambda _: self.bookmark is not None, message=message, ) return self.bookmark def wait_for_panel( self, panel_ui_class: Optional[Type[P]] = PanelUI # type: ignore ) -> Optional[P]: """Wait for the specified PanelUI item to be displayed. Args: panel_ui_class (:py:class:`PanelUI`, optional): The PanelUI subclass to wait for. If `None` is specified, it will wait for any panel UI to be displayed. Defaults to `PanelUI`. Returns: Optional[:py:class:`PanelUI`]: The displayed PanelUI or `None` if not found. """ if panel_ui_class: if panel_ui_class is PanelUI: message = "No panel UI was shown." else: message = f"{panel_ui_class.__name__} was not shown." self.wait.until( lambda _: isinstance(self.panel, panel_ui_class), message=message, ) return self.panel # type: ignore else: self.wait.until( lambda _: self.panel is None, message="Unexpected panel UI was shown.", ) return None @property def is_private(self) -> bool | Any: """Property that checks if the specified window is private or not. Returns: bool: True if this is a Private Browsing window. """ self.switch_to() with self.selenium.context(self.selenium.CONTEXT_CHROME): return self.selenium.execute_async_script( """ let callback = arguments[arguments.length - 1]; (async () => { try { const mod = await import("resource://gre/modules/PrivateBrowsingUtils.sys.mjs"); let chromeWindow = arguments[0].ownerDocument.defaultView; callback(mod.PrivateBrowsingUtils.isWindowPrivate(chromeWindow)); } catch (e) { callback({error: e.message}); } })(); """, self.document_element, ) def open_window(self, private: bool = False) -> Union["BrowserWindow", Any]: """Open a new browser window. Args: private (bool): Optional parameter to open a private browsing window. Defaults to False. Returns: :py:class:`BrowserWindow`: Opened window. """ handles_before: list[str] = self.selenium.window_handles self.switch_to() with self.selenium.context(self.selenium.CONTEXT_CHROME): # Opens private or non-private window self.selenium.find_element(*self._file_menu_button_locator).click() if private: self.selenium.find_element( *self._file_menu_private_window_locator ).click() else: self.selenium.find_element( *self._file_menu_new_window_button_locator ).click() return self.wait.until( expected.new_browser_window_is_opened(self.selenium, handles_before), message="No new browser window opened", )