# 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/. from marionette_driver import ( By, Wait ) from marionette_driver.errors import NoSuchElementException import firefox_puppeteer.errors as errors from firefox_puppeteer.api.security import Security from firefox_puppeteer.ui.base import UIBaseLib, DOMElement class TabBar(UIBaseLib): """Wraps the tabs toolbar DOM element inside a browser window.""" # Properties for visual elements of the tabs toolbar # @property def menupanel(self): """A :class:`MenuPanel` instance which represents the menu panel at the far right side of the tabs toolbar. :returns: :class:`MenuPanel` instance. """ return MenuPanel(self.marionette, self.window) @property def newtab_button(self): """The DOM element which represents the new tab button. :returns: Reference to the new tab button. """ return self.toolbar.find_element(By.ANON_ATTRIBUTE, {'anonid': 'tabs-newtab-button'}) @property def tabs(self): """List of all the :class:`Tab` instances of the current browser window. :returns: List of :class:`Tab` instances. """ tabs = self.toolbar.find_elements(By.TAG_NAME, 'tab') return [Tab(self.marionette, self.window, tab) for tab in tabs] @property def toolbar(self): """The DOM element which represents the tab toolbar. :returns: Reference to the tabs toolbar. """ return self.element # Properties for helpers when working with the tabs toolbar # @property def selected_index(self): """The index of the currently selected tab. :return: Index of the selected tab. """ return int(self.toolbar.get_property('selectedIndex')) @property def selected_tab(self): """A :class:`Tab` instance of the currently selected tab. :returns: :class:`Tab` instance. """ return self.tabs[self.selected_index] # Methods for helpers when working with the tabs toolbar # def close_all_tabs(self, exceptions=None): """Forces closing of all open tabs. There is an optional `exceptions` list, which can be used to exclude specific tabs from being closed. :param exceptions: Optional, list of :class:`Tab` instances not to close. """ # Get handles from tab exceptions, and find those which can be closed for tab in self.tabs: if tab not in exceptions: tab.close(force=True) def close_tab(self, tab=None, trigger='menu', force=False): """Closes the tab by using the specified trigger. By default the currently selected tab will be closed. If another :class:`Tab` is specified, that one will be closed instead. Also when the tab is closed, a :func:`switch_to` call is automatically performed, so that the new selected tab becomes active. :param tab: Optional, the :class:`Tab` instance to close. Defaults to the currently selected tab. :param trigger: Optional, method to close the current tab. This can be a string with one of `menu` or `shortcut`, or a callback which gets triggered with the :class:`Tab` as parameter. Defaults to `menu`. :param force: Optional, forces the closing of the window by using the Gecko API. Defaults to `False`. """ tab = tab or self.selected_tab tab.close(trigger, force) def open_tab(self, trigger='menu'): """Opens a new tab in the current browser window. If the tab opens in the foreground, a call to :func:`switch_to` will automatically be performed. But if it opens in the background, the current tab will keep its focus. :param trigger: Optional, method to open the new tab. This can be a string with one of `menu`, `button` or `shortcut`, or a callback which gets triggered with the current :class:`Tab` as parameter. Defaults to `menu`. :returns: :class:`Tab` instance for the opened tab. """ start_handles = self.marionette.window_handles # Prepare action which triggers the opening of the browser window if callable(trigger): trigger(self.selected_tab) elif trigger == 'button': self.window.tabbar.newtab_button.click() elif trigger == 'menu': self.window.menubar.select_by_id('file-menu', 'menu_newNavigatorTab') elif trigger == 'shortcut': self.window.send_shortcut(self.window.localize_entity('tabCmd.commandkey'), accel=True) # elif - need to add other cases else: raise ValueError('Unknown opening method: "%s"' % trigger) # TODO: Needs to be replaced with event handling code (bug 1121705) Wait(self.marionette).until( lambda mn: len(mn.window_handles) == len(start_handles) + 1, message='No new tab has been opened.') handles = self.marionette.window_handles [new_handle] = list(set(handles) - set(start_handles)) [new_tab] = [tab for tab in self.tabs if tab.handle == new_handle] # if the new tab is the currently selected tab, switch to it if new_tab == self.selected_tab: new_tab.switch_to() return new_tab def switch_to(self, target): """Switches the context to the specified tab. :param target: The tab to switch to. `target` can be an index, a :class:`Tab` instance, or a callback that returns True in the context of the desired tab. :returns: Instance of the selected :class:`Tab`. """ start_handle = self.marionette.current_window_handle if isinstance(target, int): return self.tabs[target].switch_to() elif isinstance(target, Tab): return target.switch_to() if callable(target): for tab in self.tabs: tab.switch_to() if target(tab): return tab self.marionette.switch_to_window(start_handle) raise errors.UnknownTabError("No tab found for '{}'".format(target)) raise ValueError("The 'target' parameter must either be an index or a callable") @staticmethod def get_handle_for_tab(marionette, tab_element): """Retrieves the marionette handle for the given :class:`Tab` instance. :param marionette: An instance of the Marionette client. :param tab_element: The DOM element corresponding to a tab inside the tabs toolbar. :returns: `handle` of the tab. """ # TODO: This introduces coupling with marionette's window handles # implementation. To avoid this, the capacity to get the XUL # element corresponding to the active window according to # marionette or a similar ability should be added to marionette. handle = marionette.execute_script(""" let win = arguments[0].linkedBrowser; if (!win) { return null; } return win.outerWindowID.toString(); """, script_args=[tab_element]) return handle class Tab(UIBaseLib): """Wraps a tab DOM element.""" def __init__(self, marionette, window, element): super(Tab, self).__init__(marionette, window, element) self._security = Security(self.marionette) self._handle = None # Properties for visual elements of tabs # @property def close_button(self): """The DOM element which represents the tab close button. :returns: Reference to the tab close button. """ return self.tab_element.find_element(By.ANON_ATTRIBUTE, {'anonid': 'close-button'}) @property def tab_element(self): """The inner tab DOM element. :returns: Tab DOM element. """ return self.element # Properties for backend values @property def location(self): """Returns the current URL :returns: Current URL """ self.switch_to() return self.marionette.execute_script(""" return arguments[0].linkedBrowser.currentURI.spec; """, script_args=[self.tab_element]) @property def certificate(self): """The SSL certificate assiciated with the loaded web page. :returns: Certificate details as JSON blob. """ self.switch_to() return self._security.get_certificate_for_page(self.tab_element) # Properties for helpers when working with tabs # @property def handle(self): """The `handle` of the content window. :returns: content window `handle`. """ # If no handle has been set yet, wait until it is available if not self._handle: self._handle = Wait(self.marionette).until( lambda mn: TabBar.get_handle_for_tab(mn, self.element), message='Tab handle could not be found.') return self._handle @property def selected(self): """Checks if the tab is selected. :return: `True` if the tab is selected. """ return self.marionette.execute_script(""" return arguments[0].hasAttribute('selected'); """, script_args=[self.tab_element]) # Methods for helpers when working with tabs # def __eq__(self, other): return self.handle == other.handle def close(self, trigger='menu', force=False): """Closes the tab by using the specified trigger. When the tab is closed a :func:`switch_to` call is automatically performed, so that the new selected tab becomes active. :param trigger: Optional, method in how to close the tab. This can be a string with one of `button`, `menu` or `shortcut`, or a callback which gets triggered with the current :class:`Tab` as parameter. Defaults to `menu`. :param force: Optional, forces the closing of the window by using the Gecko API. Defaults to `False`. """ handle = self.handle start_handles = self.marionette.window_handles self.switch_to() if force: self.marionette.close() elif callable(trigger): trigger(self) elif trigger == 'button': self.close_button.click() elif trigger == 'menu': self.window.menubar.select_by_id('file-menu', 'menu_close') elif trigger == 'shortcut': self.window.send_shortcut(self.window.localize_entity('closeCmd.key'), accel=True) else: raise ValueError('Unknown closing method: "%s"' % trigger) Wait(self.marionette).until( lambda _: len(self.window.tabbar.tabs) == len(start_handles) - 1, message='Tab with handle "%s" has not been closed.' % handle) # Ensure to switch to the window handle which represents the new selected tab self.window.tabbar.selected_tab.switch_to() def select(self): """Selects the tab and sets the focus to it.""" self.tab_element.click() self.switch_to() # Bug 1121705: Maybe we have to wait for TabSelect event Wait(self.marionette).until( lambda _: self.selected, message='Tab with handle "%s" could not be selected.' % self.handle) def switch_to(self): """Switches the context of Marionette to this tab. Please keep in mind that calling this method will not select the tab. Use the :func:`~Tab.select` method instead. """ self.marionette.switch_to_window(self.handle) class MenuPanel(UIBaseLib): @property def popup(self): """ :returns: The :class:`MenuPanelElement`. """ popup = self.marionette.find_element(By.ID, 'PanelUI-popup') return self.MenuPanelElement(popup) class MenuPanelElement(DOMElement): """Wraps the menu panel.""" _buttons = None @property def buttons(self): """ :returns: A list of all the clickable buttons in the menu panel. """ if not self._buttons: self._buttons = (self.find_element(By.ID, 'PanelUI-multiView') .find_element(By.ANON_ATTRIBUTE, {'anonid': 'viewContainer'}) .find_elements(By.TAG_NAME, 'toolbarbutton')) return self._buttons def click(self, target=None): """ Overrides HTMLElement.click to provide a target to click. :param target: The label associated with the button to click on, e.g., `New Private Window`. """ if not target: return DOMElement.click(self) for button in self.buttons: if button.get_attribute('label') == target: return button.click() raise NoSuchElementException('Could not find "{}"" in the ' 'menu panel UI'.format(target))