diff options
author | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
---|---|---|
committer | Matt A. Tobin <mattatobin@localhost.localdomain> | 2018-02-02 04:16:08 -0500 |
commit | 5f8de423f190bbb79a62f804151bc24824fa32d8 (patch) | |
tree | 10027f336435511475e392454359edea8e25895d /testing/marionette/puppeteer/firefox/firefox_puppeteer/ui | |
parent | 49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff) | |
download | UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip |
Add m-esr52 at 52.6.0
Diffstat (limited to 'testing/marionette/puppeteer/firefox/firefox_puppeteer/ui')
19 files changed, 2837 insertions, 0 deletions
diff --git a/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/__init__.py b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/__init__.py diff --git a/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/about_window/__init__.py b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/about_window/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/about_window/__init__.py diff --git a/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/about_window/deck.py b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/about_window/deck.py new file mode 100644 index 000000000..9d8d90603 --- /dev/null +++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/about_window/deck.py @@ -0,0 +1,174 @@ +# 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 + +from firefox_puppeteer.ui.base import UIBaseLib +from firefox_puppeteer.ui.deck import Panel + + +class Deck(UIBaseLib): + + def _create_panel_for_id(self, panel_id): + """Creates an instance of :class:`Panel` for the specified panel id. + + :param panel_id: The ID of the panel to create an instance of. + + :returns: :class:`Panel` instance + """ + mapping = {'apply': ApplyPanel, + 'checkForUpdates': CheckForUpdatesPanel, + 'checkingForUpdates': CheckingForUpdatesPanel, + 'downloadAndInstall': DownloadAndInstallPanel, + 'downloadFailed': DownloadFailedPanel, + 'downloading': DownloadingPanel, + 'noUpdatesFound': NoUpdatesFoundPanel, + } + + panel = self.element.find_element(By.ID, panel_id) + return mapping.get(panel_id, Panel)(self.marionette, self.window, panel) + + # Properties for visual elements of the deck # + + @property + def apply(self): + """The :class:`ApplyPanel` instance for the apply panel. + + :returns: :class:`ApplyPanel` instance. + """ + return self._create_panel_for_id('apply') + + @property + def check_for_updates(self): + """The :class:`CheckForUpdatesPanel` instance for the check for updates panel. + + :returns: :class:`CheckForUpdatesPanel` instance. + """ + return self._create_panel_for_id('checkForUpdates') + + @property + def checking_for_updates(self): + """The :class:`CheckingForUpdatesPanel` instance for the checking for updates panel. + + :returns: :class:`CheckingForUpdatesPanel` instance. + """ + return self._create_panel_for_id('checkingForUpdates') + + @property + def download_and_install(self): + """The :class:`DownloadAndInstallPanel` instance for the download and install panel. + + :returns: :class:`DownloadAndInstallPanel` instance. + """ + return self._create_panel_for_id('downloadAndInstall') + + @property + def download_failed(self): + """The :class:`DownloadFailedPanel` instance for the download failed panel. + + :returns: :class:`DownloadFailedPanel` instance. + """ + return self._create_panel_for_id('downloadFailed') + + @property + def downloading(self): + """The :class:`DownloadingPanel` instance for the downloading panel. + + :returns: :class:`DownloadingPanel` instance. + """ + return self._create_panel_for_id('downloading') + + @property + def no_updates_found(self): + """The :class:`NoUpdatesFoundPanel` instance for the no updates found panel. + + :returns: :class:`NoUpdatesFoundPanel` instance. + """ + return self._create_panel_for_id('noUpdatesFound') + + @property + def panels(self): + """List of all the :class:`Panel` instances of the current deck. + + :returns: List of :class:`Panel` instances. + """ + panels = self.marionette.execute_script(""" + let deck = arguments[0]; + let panels = []; + + for (let index = 0; index < deck.children.length; index++) { + if (deck.children[index].id) { + panels.push(deck.children[index].id); + } + } + + return panels; + """, script_args=[self.element]) + + return [self._create_panel_for_id(panel) for panel in panels] + + @property + def selected_index(self): + """The index of the currently selected panel. + + :return: Index of the selected panel. + """ + return int(self.element.get_property('selectedIndex')) + + @property + def selected_panel(self): + """A :class:`Panel` instance of the currently selected panel. + + :returns: :class:`Panel` instance. + """ + return self.panels[self.selected_index] + + +class ApplyPanel(Panel): + + @property + def button(self): + """The DOM element which represents the Update button. + + :returns: Reference to the button element. + """ + return self.element.find_element(By.ID, 'updateButton') + + +class CheckForUpdatesPanel(Panel): + + @property + def button(self): + """The DOM element which represents the Check for Updates button. + + :returns: Reference to the button element. + """ + return self.element.find_element(By.ID, 'checkForUpdatesButton') + + +class CheckingForUpdatesPanel(Panel): + pass + + +class DownloadAndInstallPanel(Panel): + + @property + def button(self): + """The DOM element which represents the Download button. + + :returns: Reference to the button element. + """ + return self.element.find_element(By.ID, 'downloadAndInstallButton') + + +class DownloadFailedPanel(Panel): + pass + + +class DownloadingPanel(Panel): + pass + + +class NoUpdatesFoundPanel(Panel): + pass diff --git a/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/about_window/window.py b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/about_window/window.py new file mode 100644 index 000000000..25037a471 --- /dev/null +++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/about_window/window.py @@ -0,0 +1,32 @@ +# 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 + +from firefox_puppeteer.ui.about_window.deck import Deck +from firefox_puppeteer.ui.windows import BaseWindow, Windows + + +class AboutWindow(BaseWindow): + """Representation of the About window.""" + window_type = 'Browser:About' + + dtds = [ + 'chrome://branding/locale/brand.dtd', + 'chrome://browser/locale/aboutDialog.dtd', + ] + + @property + def deck(self): + """The :class:`Deck` instance which represents the deck. + + :returns: Reference to the deck. + """ + self.switch_to() + + deck = self.window_element.find_element(By.ID, 'updateDeck') + return Deck(self.marionette, self, deck) + + +Windows.register_window(AboutWindow.window_type, AboutWindow) diff --git a/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/base.py b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/base.py new file mode 100644 index 000000000..622568df9 --- /dev/null +++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/base.py @@ -0,0 +1,54 @@ +# 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.marionette import HTMLElement + +from firefox_puppeteer.base import BaseLib +from firefox_puppeteer.ui.windows import BaseWindow + + +class UIBaseLib(BaseLib): + """A base class for all UI element wrapper classes inside a chrome window.""" + + def __init__(self, marionette, window, element): + super(UIBaseLib, self).__init__(marionette) + + assert isinstance(window, BaseWindow) + assert isinstance(element, HTMLElement) + + self._window = window + self._element = element + + @property + def element(self): + """Returns the reference to the underlying DOM element. + + :returns: Reference to the DOM element + """ + return self._element + + @property + def window(self): + """Returns the reference to the chrome window. + + :returns: :class:`BaseWindow` instance of the chrome window. + """ + return self._window + + +class DOMElement(HTMLElement): + """ + Class that inherits from HTMLElement and provides a way for subclasses to + expose new api's. + """ + + def __new__(cls, element): + instance = object.__new__(cls) + instance.__dict__ = element.__dict__.copy() + setattr(instance, 'inner', element) + + return instance + + def __init__(self, element): + pass diff --git a/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/browser/__init__.py b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/browser/__init__.py new file mode 100644 index 000000000..c580d191c --- /dev/null +++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/browser/__init__.py @@ -0,0 +1,3 @@ +# 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/. diff --git a/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/browser/notifications.py b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/browser/notifications.py new file mode 100644 index 000000000..2cf67ce7f --- /dev/null +++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/browser/notifications.py @@ -0,0 +1,116 @@ +# 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 abc import ABCMeta + +from marionette_driver import By + +from firefox_puppeteer.ui.base import UIBaseLib + + +class BaseNotification(UIBaseLib): + """Abstract base class for any kind of notification.""" + + __metaclass__ = ABCMeta + + @property + def close_button(self): + """Provide access to the close button. + + :returns: The close button. + """ + return self.element.find_element(By.ANON_ATTRIBUTE, + {'anonid': 'closebutton'}) + + @property + def label(self): + """Provide access to the notification label. + + :returns: The notification label. + """ + return self.element.get_attribute('label') + + @property + def origin(self): + """Provide access to the notification origin. + + :returns: The notification origin. + """ + return self.element.get_attribute('origin') + + def close(self, force=False): + """Close the notification. + + :param force: Optional, if True force close the notification. + Defaults to False. + """ + if force: + self.marionette.execute_script('arguments[0].click()', + script_args=[self.close_button]) + else: + self.close_button.click() + + self.window.wait_for_notification(None) + + +class AddOnInstallBlockedNotification(BaseNotification): + """Add-on install blocked notification.""" + + @property + def allow_button(self): + """Provide access to the allow button. + + :returns: The allow button. + """ + return self.element.find_element( + By.ANON_ATTRIBUTE, {'anonid': 'button'}).find_element( + By.ANON_ATTRIBUTE, {'anonid': 'button'}) + + +class AddOnInstallConfirmationNotification(BaseNotification): + """Add-on install confirmation notification.""" + + @property + def addon_name(self): + """Provide access to the add-on name. + + :returns: The add-on name. + """ + label = self.element.find_element( + By.CSS_SELECTOR, '#addon-install-confirmation-content label') + return label.get_attribute('value') + + def cancel_button(self): + """Provide access to the cancel button. + + :returns: The cancel button. + """ + return self.element.find_element( + By.ID, 'addon-install-confirmation-cancel') + + def install_button(self): + """Provide access to the install button. + + :returns: The install button. + """ + return self.element.find_element( + By.ID, 'addon-install-confirmation-accept') + + +class AddOnInstallCompleteNotification(BaseNotification): + """Add-on install complete notification.""" + + pass + + +class AddOnInstallFailedNotification(BaseNotification): + """Add-on install failed notification.""" + + pass + + +class AddOnProgressNotification(BaseNotification): + """Add-on progress notification.""" + + pass diff --git a/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/browser/tabbar.py b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/browser/tabbar.py new file mode 100644 index 000000000..4fec98d99 --- /dev/null +++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/browser/tabbar.py @@ -0,0 +1,388 @@ +# 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)) diff --git a/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/browser/toolbars.py b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/browser/toolbars.py new file mode 100644 index 000000000..d490e488f --- /dev/null +++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/browser/toolbars.py @@ -0,0 +1,641 @@ +# 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, keys, Wait + +from firefox_puppeteer.ui.base import UIBaseLib + + +class NavBar(UIBaseLib): + """Provides access to the DOM elements contained in the + navigation bar as well as the location bar.""" + + def __init__(self, *args, **kwargs): + super(NavBar, self).__init__(*args, **kwargs) + + self._locationbar = None + + @property + def back_button(self): + """Provides access to the DOM element back button in the navbar. + + :returns: Reference to the back button. + """ + return self.marionette.find_element(By.ID, 'back-button') + + @property + def forward_button(self): + """Provides access to the DOM element forward button in the navbar. + + :returns: Reference to the forward button. + """ + return self.marionette.find_element(By.ID, 'forward-button') + + @property + def home_button(self): + """Provides access to the DOM element home button in the navbar. + + :returns: Reference to the home button element + """ + return self.marionette.find_element(By.ID, 'home-button') + + @property + def locationbar(self): + """Provides access to the DOM elements contained in the + locationbar. + + See the :class:`LocationBar` reference. + """ + if not self._locationbar: + urlbar = self.marionette.find_element(By.ID, 'urlbar') + self._locationbar = LocationBar(self.marionette, self.window, urlbar) + + return self._locationbar + + @property + def menu_button(self): + """Provides access to the DOM element menu button in the navbar. + + :returns: Reference to the menu button element. + """ + return self.marionette.find_element(By.ID, 'PanelUI-menu-button') + + @property + def toolbar(self): + """The DOM element which represents the navigation toolbar. + + :returns: Reference to the navigation toolbar. + """ + return self.element + + +class LocationBar(UIBaseLib): + """Provides access to and methods for the DOM elements contained in the + locationbar (the text area of the ui that typically displays the current url).""" + + def __init__(self, *args, **kwargs): + super(LocationBar, self).__init__(*args, **kwargs) + + self._autocomplete_results = None + self._identity_popup = None + + @property + def autocomplete_results(self): + """Provides access to and methods for the location bar + autocomplete results. + + See the :class:`AutocompleteResults` reference.""" + if not self._autocomplete_results: + popup = self.marionette.find_element(By.ID, 'PopupAutoCompleteRichResult') + self._autocomplete_results = AutocompleteResults(self.marionette, + self.window, popup) + + return self._autocomplete_results + + def clear(self): + """Clears the contents of the url bar (via the DELETE shortcut).""" + self.focus('shortcut') + self.urlbar.send_keys(keys.Keys.DELETE) + Wait(self.marionette).until( + lambda _: self.value == '', + message='Contents of location bar could not be cleared.') + + def close_context_menu(self): + """Closes the Location Bar context menu by a key event.""" + # TODO: This method should be implemented via the menu API. + self.contextmenu.send_keys(keys.Keys.ESCAPE) + + @property + def connection_icon(self): + """ Provides access to the urlbar connection icon. + + :returns: Reference to the connection icon element. + """ + return self.marionette.find_element(By.ID, 'connection-icon') + + @property + def contextmenu(self): + """Provides access to the urlbar context menu. + + :returns: Reference to the urlbar context menu. + """ + # TODO: This method should be implemented via the menu API. + parent = self.urlbar.find_element(By.ANON_ATTRIBUTE, {'anonid': 'textbox-input-box'}) + return parent.find_element(By.ANON_ATTRIBUTE, {'anonid': 'input-box-contextmenu'}) + + @property + def focused(self): + """Checks the focus state of the location bar. + + :returns: `True` if focused, otherwise `False` + """ + return self.urlbar.get_attribute('focused') == 'true' + + @property + def identity_icon(self): + """ Provides access to the urlbar identity icon. + + :returns: Reference to the identity icon element. + """ + return self.marionette.find_element(By.ID, 'identity-icon') + + def focus(self, event='click'): + """Focus the location bar according to the provided event. + + :param eventt: The event to synthesize in order to focus the urlbar + (one of `click` or `shortcut`). + """ + if event == 'click': + self.urlbar.click() + elif event == 'shortcut': + cmd_key = self.window.localize_entity('openCmd.commandkey') + self.window.send_shortcut(cmd_key, accel=True) + else: + raise ValueError("An unknown event type was passed: %s" % event) + + Wait(self.marionette).until( + lambda _: self.focused, + message='Location bar has not be focused.') + + def get_contextmenu_entry(self, action): + """Retrieves the urlbar context menu entry corresponding + to the given action. + + :param action: The action corresponding to the retrieved value. + :returns: Reference to the urlbar contextmenu entry. + """ + # TODO: This method should be implemented via the menu API. + entries = self.contextmenu.find_elements(By.CSS_SELECTOR, 'menuitem') + filter_on = 'cmd_%s' % action + found = [e for e in entries if e.get_attribute('cmd') == filter_on] + return found[0] if len(found) else None + + @property + def history_drop_marker(self): + """Provides access to the history drop marker. + + :returns: Reference to the history drop marker. + """ + return self.urlbar.find_element(By.ANON_ATTRIBUTE, {'anonid': 'historydropmarker'}) + + @property + def identity_box(self): + """The DOM element which represents the identity box. + + :returns: Reference to the identity box. + """ + return self.marionette.find_element(By.ID, 'identity-box') + + @property + def identity_country_label(self): + """The DOM element which represents the identity icon country label. + + :returns: Reference to the identity icon country label. + """ + return self.marionette.find_element(By.ID, 'identity-icon-country-label') + + @property + def identity_organization_label(self): + """The DOM element which represents the identity icon label. + + :returns: Reference to the identity icon label. + """ + return self.marionette.find_element(By.ID, 'identity-icon-label') + + @property + def identity_popup(self): + """Provides utility members for accessing and manipulating the + identity popup. + + See the :class:`IdentityPopup` reference. + """ + if not self._identity_popup: + popup = self.marionette.find_element(By.ID, 'identity-popup') + self._identity_popup = IdentityPopup(self.marionette, + self.window, popup) + + return self._identity_popup + + def load_url(self, url): + """Load the specified url in the location bar by synthesized + keystrokes. + + :param url: The url to load. + """ + self.clear() + self.focus('shortcut') + self.urlbar.send_keys(url + keys.Keys.ENTER) + + @property + def notification_popup(self): + """Provides access to the DOM element notification popup. + + :returns: Reference to the notification popup. + """ + return self.marionette.find_element(By.ID, "notification-popup") + + def open_identity_popup(self): + """Open the identity popup.""" + self.identity_box.click() + Wait(self.marionette).until( + lambda _: self.identity_popup.is_open, + message='Identity popup has not been opened.') + + @property + def reload_button(self): + """Provides access to the DOM element reload button. + + :returns: Reference to the reload button. + """ + return self.marionette.find_element(By.ID, 'urlbar-reload-button') + + def reload_url(self, trigger='button', force=False): + """Reload the currently open page. + + :param trigger: The event type to use to cause the reload (one of + `shortcut`, `shortcut2`, or `button`). + :param force: Whether to cause a forced reload. + """ + # TODO: The force parameter is ignored for the moment. Use + # mouse event modifiers or actions when they're ready. + # Bug 1097705 tracks this feature in marionette. + if trigger == 'button': + self.reload_button.click() + elif trigger == 'shortcut': + cmd_key = self.window.localize_entity('reloadCmd.commandkey') + self.window.send_shortcut(cmd_key) + elif trigger == 'shortcut2': + self.window.send_shortcut(keys.Keys.F5) + + @property + def stop_button(self): + """Provides access to the DOM element stop button. + + :returns: Reference to the stop button. + """ + return self.marionette.find_element(By.ID, 'urlbar-stop-button') + + @property + def urlbar(self): + """Provides access to the DOM element urlbar. + + :returns: Reference to the url bar. + """ + return self.marionette.find_element(By.ID, 'urlbar') + + @property + def urlbar_input(self): + """Provides access to the urlbar input element. + + :returns: Reference to the urlbar input. + """ + return self.urlbar.find_element(By.ANON_ATTRIBUTE, {'anonid': 'input'}) + + @property + def value(self): + """Provides access to the currently displayed value of the urlbar. + + :returns: The urlbar value. + """ + return self.urlbar.get_property('value') + + +class AutocompleteResults(UIBaseLib): + """Wraps DOM elements and methods for interacting with autocomplete results.""" + + def close(self, force=False): + """Closes the urlbar autocomplete popup. + + :param force: If true, the popup is closed by its own hide function, + otherwise a key event is sent to close the popup. + """ + if not self.is_open: + return + + if force: + self.marionette.execute_script(""" + arguments[0].hidePopup(); + """, script_args=[self.element]) + else: + self.element.send_keys(keys.Keys.ESCAPE) + + Wait(self.marionette).until( + lambda _: not self.is_open, + message='Autocomplete popup has not been closed.') + + def get_matching_text(self, result, match_type): + """Returns an array of strings of the matching text within an autocomplete + result in the urlbar. + + :param result: The result to inspect for matches. + :param match_type: The type of match to search for (one of `title` or `url`). + """ + + if match_type not in ('title', 'url'): + raise ValueError('match_type provided must be one of' + '"title" or "url", not %s' % match_type) + + # Search for nodes of the given type with emphasized text + emphasized_nodes = result.find_elements( + By.ANON_ATTRIBUTE, + {'class': 'ac-emphasize-text ac-emphasize-text-%s' % match_type} + ) + + return [node.get_property('textContent') for node in emphasized_nodes] + + @property + def visible_results(self): + """Supplies the list of visible autocomplete result nodes. + + :returns: The list of visible results. + """ + match_count = self.element.get_property('_matchCount') + + return self.marionette.execute_script(""" + let rv = []; + let node = arguments[0]; + let count = arguments[1]; + + for (let i = 0; i < count; ++i) { + rv.push(node.getItemAtIndex(i)); + } + + return rv; + """, script_args=[self.results, match_count]) + + @property + def is_open(self): + """Returns whether this popup is currently open. + + :returns: True when the popup is open, otherwise false. + """ + return self.element.get_property('state') == 'open' + + @property + def is_complete(self): + """Returns when this popup is open and autocomplete results are complete. + + :returns: True, when autocomplete results have been populated. + """ + return self.marionette.execute_script(""" + Components.utils.import("resource://gre/modules/Services.jsm"); + + let win = Services.focus.activeWindow; + if (win) { + return win.gURLBar.controller.searchStatus >= + Components.interfaces.nsIAutoCompleteController.STATUS_COMPLETE_NO_MATCH; + } + + return null; + """) + + @property + def results(self): + """ + :returns: The autocomplete result container node. + """ + return self.element.find_element(By.ANON_ATTRIBUTE, + {'anonid': 'richlistbox'}) + + @property + def selected_index(self): + """Provides the index of the selected item in the autocomplete list. + + :returns: The index. + """ + return self.results.get_property('selectedIndex') + + +class IdentityPopup(UIBaseLib): + """Wraps DOM elements and methods for interacting with the identity popup.""" + + def __init__(self, *args, **kwargs): + super(IdentityPopup, self).__init__(*args, **kwargs) + + self._view = None + + @property + def is_open(self): + """Returns whether this popup is currently open. + + :returns: True when the popup is open, otherwise false. + """ + return self.element.get_property('state') == 'open' + + def close(self, force=False): + """Closes the identity popup by hitting the escape key. + + :param force: Optional, If `True` force close the popup. + Defaults to `False` + """ + if not self.is_open: + return + + if force: + self.marionette.execute_script(""" + arguments[0].hidePopup(); + """, script_args=[self.element]) + else: + self.element.send_keys(keys.Keys.ESCAPE) + + Wait(self.marionette).until( + lambda _: not self.is_open, + message='Identity popup has not been closed.') + + @property + def view(self): + """Provides utility members for accessing and manipulating the + identity popup's multi view. + + See the :class:`IdentityPopupMultiView` reference. + """ + if not self._view: + view = self.marionette.find_element(By.ID, 'identity-popup-multiView') + self._view = IdentityPopupMultiView(self.marionette, self.window, view) + + return self._view + + +class IdentityPopupMultiView(UIBaseLib): + + def _create_view_for_id(self, view_id): + """Creates an instance of :class:`IdentityPopupView` for the specified view id. + + :param view_id: The ID of the view to create an instance of. + + :returns: :class:`IdentityPopupView` instance + """ + mapping = {'identity-popup-mainView': IdentityPopupMainView, + 'identity-popup-securityView': IdentityPopupSecurityView, + } + + view = self.marionette.find_element(By.ID, view_id) + return mapping.get(view_id, IdentityPopupView)(self.marionette, self.window, view) + + @property + def main(self): + """The DOM element which represents the main view. + + :returns: Reference to the main view. + """ + return self._create_view_for_id('identity-popup-mainView') + + @property + def security(self): + """The DOM element which represents the security view. + + :returns: Reference to the security view. + """ + return self._create_view_for_id('identity-popup-securityView') + + +class IdentityPopupView(UIBaseLib): + + @property + def selected(self): + """Checks if the view is selected. + + :return: `True` if the view is selected. + """ + return self.element.get_attribute('current') == 'true' + + +class IdentityPopupMainView(IdentityPopupView): + + @property + def selected(self): + """Checks if the view is selected. + + :return: `True` if the view is selected. + """ + return self.marionette.execute_script(""" + return arguments[0].panelMultiView.getAttribute('viewtype') == 'main'; + """, script_args=[self.element]) + + @property + def expander(self): + """The DOM element which represents the expander button for the security content. + + :returns: Reference to the identity popup expander button. + """ + return self.element.find_element(By.CLASS_NAME, 'identity-popup-expander') + + @property + def host(self): + """The DOM element which represents the identity-popup content host. + + :returns: Reference to the identity-popup content host. + """ + return self.element.find_element(By.CLASS_NAME, 'identity-popup-headline host') + + @property + def insecure_connection_label(self): + """The DOM element which represents the identity popup insecure connection label. + + :returns: Reference to the identity-popup insecure connection label. + """ + return self.element.find_element(By.CLASS_NAME, 'identity-popup-connection-not-secure') + + @property + def internal_connection_label(self): + """The DOM element which represents the identity popup internal connection label. + + :returns: Reference to the identity-popup internal connection label. + """ + return self.element.find_element(By.CSS_SELECTOR, 'description[when-connection=chrome]') + + @property + def permissions(self): + """The DOM element which represents the identity-popup permissions content. + + :returns: Reference to the identity-popup permissions. + """ + return self.element.find_element(By.ID, 'identity-popup-permissions-content') + + @property + def secure_connection_label(self): + """The DOM element which represents the identity popup secure connection label. + + :returns: Reference to the identity-popup secure connection label. + """ + return self.element.find_element(By.CLASS_NAME, 'identity-popup-connection-secure') + + +class IdentityPopupSecurityView(IdentityPopupView): + + @property + def disable_mixed_content_blocking_button(self): + """The DOM element which represents the disable mixed content blocking button. + + :returns: Reference to the disable mixed content blocking button. + """ + return self.element.find_element(By.CSS_SELECTOR, + 'button[when-mixedcontent=active-blocked]') + + @property + def enable_mixed_content_blocking_button(self): + """The DOM element which represents the enable mixed content blocking button. + + :returns: Reference to the enable mixed content blocking button. + """ + return self.element.find_element(By.CSS_SELECTOR, + 'button[when-mixedcontent=active-loaded]') + + @property + def host(self): + """The DOM element which represents the identity-popup content host. + + :returns: Reference to the identity-popup content host. + """ + return self.element.find_element(By.CLASS_NAME, 'identity-popup-headline host') + + @property + def insecure_connection_label(self): + """The DOM element which represents the identity popup insecure connection label. + + :returns: Reference to the identity-popup insecure connection label. + """ + return self.element.find_element(By.CLASS_NAME, 'identity-popup-connection-not-secure') + + @property + def more_info_button(self): + """The DOM element which represents the identity-popup more info button. + + :returns: Reference to the identity-popup more info button. + """ + label = self.window.localize_entity('identity.moreInfoLinkText2') + + return self.element.find_element(By.CSS_SELECTOR, u'button[label="{}"]'.format(label)) + + @property + def owner(self): + """The DOM element which represents the identity-popup content owner. + + :returns: Reference to the identity-popup content owner. + """ + return self.element.find_element(By.ID, 'identity-popup-content-owner') + + @property + def owner_location(self): + """The DOM element which represents the identity-popup content supplemental. + + :returns: Reference to the identity-popup content supplemental. + """ + return self.element.find_element(By.ID, 'identity-popup-content-supplemental') + + @property + def secure_connection_label(self): + """The DOM element which represents the identity popup secure connection label. + + :returns: Reference to the identity-popup secure connection label. + """ + return self.element.find_element(By.CLASS_NAME, 'identity-popup-connection-secure') + + @property + def verifier(self): + """The DOM element which represents the identity-popup content verifier. + + :returns: Reference to the identity-popup content verifier. + """ + return self.element.find_element(By.ID, 'identity-popup-content-verifier') diff --git a/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/browser/window.py b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/browser/window.py new file mode 100644 index 000000000..728a2fd20 --- /dev/null +++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/browser/window.py @@ -0,0 +1,260 @@ +# 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 + +from firefox_puppeteer.ui.about_window.window import AboutWindow +from firefox_puppeteer.ui.browser.notifications import ( + AddOnInstallBlockedNotification, + AddOnInstallConfirmationNotification, + AddOnInstallCompleteNotification, + AddOnInstallFailedNotification, + AddOnProgressNotification, + BaseNotification) +from firefox_puppeteer.ui.browser.tabbar import TabBar +from firefox_puppeteer.ui.browser.toolbars import NavBar +from firefox_puppeteer.ui.pageinfo.window import PageInfoWindow +from firefox_puppeteer.ui.windows import BaseWindow, Windows + + +class BrowserWindow(BaseWindow): + """Representation of a browser window.""" + + window_type = 'navigator:browser' + + dtds = [ + 'chrome://branding/locale/brand.dtd', + 'chrome://browser/locale/aboutPrivateBrowsing.dtd', + 'chrome://browser/locale/browser.dtd', + 'chrome://browser/locale/netError.dtd', + ] + + properties = [ + 'chrome://branding/locale/brand.properties', + 'chrome://branding/locale/browserconfig.properties', + 'chrome://browser/locale/browser.properties', + 'chrome://browser/locale/preferences/preferences.properties', + 'chrome://global/locale/browser.properties', + ] + + def __init__(self, *args, **kwargs): + super(BrowserWindow, self).__init__(*args, **kwargs) + + self._navbar = None + self._tabbar = None + + @property + def default_homepage(self): + """The default homepage as used by the current locale. + + :returns: The default homepage for the current locale. + """ + return self.marionette.get_pref('browser.startup.homepage', + value_type='nsIPrefLocalizedString') + + @property + def is_private(self): + """Returns True if this is a Private Browsing window.""" + self.switch_to() + + with self.marionette.using_context('chrome'): + return self.marionette.execute_script(""" + Components.utils.import("resource://gre/modules/PrivateBrowsingUtils.jsm"); + + let chromeWindow = arguments[0].ownerDocument.defaultView; + return PrivateBrowsingUtils.isWindowPrivate(chromeWindow); + """, script_args=[self.window_element]) + + @property + def navbar(self): + """Provides access to the navigation bar. This is the toolbar containing + the back, forward and home buttons. It also contains the location bar. + + See the :class:`~ui.browser.toolbars.NavBar` reference. + """ + self.switch_to() + + if not self._navbar: + navbar = self.window_element.find_element(By.ID, 'nav-bar') + self._navbar = NavBar(self.marionette, self, navbar) + + return self._navbar + + @property + def notification(self): + """Provides access to the currently displayed notification.""" + + notifications_map = { + 'addon-install-blocked-notification': AddOnInstallBlockedNotification, + 'addon-install-confirmation-notification': AddOnInstallConfirmationNotification, + 'addon-install-complete-notification': AddOnInstallCompleteNotification, + 'addon-install-failed-notification': AddOnInstallFailedNotification, + 'addon-progress-notification': AddOnProgressNotification, + } + + try: + notification = self.window_element.find_element( + By.CSS_SELECTOR, '#notification-popup popupnotification') + + notification_id = notification.get_attribute('id') + return notifications_map.get(notification_id, BaseNotification)( + self.marionette, self, notification) + + except NoSuchElementException: + return None # no notification is displayed + + def wait_for_notification(self, notification_class=BaseNotification, + timeout=5): + """Waits for the specified notification to be displayed. + + :param notification_class: Optional, the notification class to wait for. + If `None` is specified it will wait for any notification to be closed. + Defaults to `BaseNotification`. + :param timeout: Optional, how long to wait for the expected notification. + Defaults to 5 seconds. + """ + wait = Wait(self.marionette, timeout=timeout) + + if notification_class: + if notification_class is BaseNotification: + message = 'No notification was shown.' + else: + message = '{0} was not shown.'.format(notification_class.__name__) + wait.until( + lambda _: isinstance(self.notification, notification_class), + message=message) + else: + message = 'Unexpected notification shown.' + wait.until( + lambda _: self.notification is None, + message='Unexpected notification shown.') + + @property + def tabbar(self): + """Provides access to the tab bar. + + See the :class:`~ui.browser.tabbar.TabBar` reference. + """ + self.switch_to() + + if not self._tabbar: + tabbrowser = self.window_element.find_element(By.ID, 'tabbrowser-tabs') + self._tabbar = TabBar(self.marionette, self, tabbrowser) + + return self._tabbar + + def close(self, trigger='menu', force=False): + """Closes the current browser window by using the specified trigger. + + :param trigger: Optional, method to close the current browser window. This can + be a string with one of `menu` or `shortcut`, or a callback which gets triggered + with the current :class:`BrowserWindow` as parameter. Defaults to `menu`. + + :param force: Optional, forces the closing of the window by using the Gecko API. + Defaults to `False`. + """ + def callback(win): + # Prepare action which triggers the opening of the browser window + if callable(trigger): + trigger(win) + elif trigger == 'menu': + self.menubar.select_by_id('file-menu', 'menu_closeWindow') + elif trigger == 'shortcut': + win.send_shortcut(win.localize_entity('closeCmd.key'), + accel=True, shift=True) + else: + raise ValueError('Unknown closing method: "%s"' % trigger) + + BaseWindow.close(self, callback, force) + + def get_final_url(self, url): + """Loads the page at `url` and returns the resulting url. + + This function enables testing redirects. + + :param url: The url to test. + :returns: The resulting loaded url. + """ + with self.marionette.using_context('content'): + self.marionette.navigate(url) + return self.marionette.get_url() + + def open_browser(self, trigger='menu', is_private=False): + """Opens a new browser window by using the specified trigger. + + :param trigger: Optional, method in how to open the new browser window. This can + be a string with one of `menu` or `shortcut`, or a callback which gets triggered + with the current :class:`BrowserWindow` as parameter. Defaults to `menu`. + + :param is_private: Optional, if True the new window will be a private browsing one. + + :returns: :class:`BrowserWindow` instance for the new browser window. + """ + def callback(win): + # Prepare action which triggers the opening of the browser window + if callable(trigger): + trigger(win) + elif trigger == 'menu': + menu_id = 'menu_newPrivateWindow' if is_private else 'menu_newNavigator' + self.menubar.select_by_id('file-menu', menu_id) + elif trigger == 'shortcut': + cmd_key = 'privateBrowsingCmd.commandkey' if is_private else 'newNavigatorCmd.key' + win.send_shortcut(win.localize_entity(cmd_key), + accel=True, shift=is_private) + else: + raise ValueError('Unknown opening method: "%s"' % trigger) + + return BaseWindow.open_window(self, callback, BrowserWindow) + + def open_about_window(self, trigger='menu'): + """Opens the about window by using the specified trigger. + + :param trigger: Optional, method in how to open the new browser window. This can + either the string `menu` or a callback which gets triggered + with the current :class:`BrowserWindow` as parameter. Defaults to `menu`. + + :returns: :class:`AboutWindow` instance of the opened window. + """ + def callback(win): + # Prepare action which triggers the opening of the browser window + if callable(trigger): + trigger(win) + elif trigger == 'menu': + self.menubar.select_by_id('helpMenu', 'aboutName') + else: + raise ValueError('Unknown opening method: "%s"' % trigger) + + return BaseWindow.open_window(self, callback, AboutWindow) + + def open_page_info_window(self, trigger='menu'): + """Opens the page info window by using the specified trigger. + + :param trigger: Optional, method in how to open the new browser window. This can + be a string with one of `menu` or `shortcut`, or a callback which gets triggered + with the current :class:`BrowserWindow` as parameter. Defaults to `menu`. + + :returns: :class:`PageInfoWindow` instance of the opened window. + """ + def callback(win): + # Prepare action which triggers the opening of the browser window + if callable(trigger): + trigger(win) + elif trigger == 'menu': + self.menubar.select_by_id('tools-menu', 'menu_pageInfo') + elif trigger == 'shortcut': + if win.marionette.session_capabilities['platformName'] == 'windows_nt': + raise ValueError('Page info shortcut not available on Windows.') + win.send_shortcut(win.localize_entity('pageInfoCmd.commandkey'), + accel=True) + elif trigger == 'context_menu': + # TODO: Add once we can do right clicks + pass + else: + raise ValueError('Unknown opening method: "%s"' % trigger) + + return BaseWindow.open_window(self, callback, PageInfoWindow) + + +Windows.register_window(BrowserWindow.window_type, BrowserWindow) diff --git a/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/deck.py b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/deck.py new file mode 100644 index 000000000..acc6d2458 --- /dev/null +++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/deck.py @@ -0,0 +1,17 @@ +# 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 firefox_puppeteer.ui.base import UIBaseLib + + +class Panel(UIBaseLib): + + def __eq__(self, other): + return self.element.get_attribute('id') == other.element.get_attribute('id') + + def __ne__(self, other): + return self.element.get_attribute('id') != other.element.get_attribute('id') + + def __str__(self): + return self.element.get_attribute('id') diff --git a/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/menu.py b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/menu.py new file mode 100644 index 000000000..8251daa01 --- /dev/null +++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/menu.py @@ -0,0 +1,110 @@ +# 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 +from marionette_driver.errors import NoSuchElementException + +from firefox_puppeteer.base import BaseLib +from firefox_puppeteer.ui.base import DOMElement + + +class MenuBar(BaseLib): + """Wraps the menubar DOM element inside a browser window.""" + + @property + def menus(self): + """A list of :class:`MenuElement` instances corresponding to + the top level menus in the menubar. + + :returns: A list of :class:`MenuElement` instances. + """ + menus = (self.marionette.find_element(By.ID, 'main-menubar') + .find_elements(By.TAG_NAME, 'menu')) + return [self.MenuElement(menu) for menu in menus] + + def get_menu(self, label): + """Get a :class:`MenuElement` instance corresponding to the specified label. + + :param label: The label of the menu, e.g., **File** or **View**. + :returns: A :class:`MenuElement` instance. + """ + menu = [m for m in self.menus if m.get_attribute('label') == label] + + if not menu: + raise NoSuchElementException('Could not find a menu with ' + 'label "{}"'.format(label)) + + return menu[0] + + def get_menu_by_id(self, menu_id): + """Get a :class:`MenuElement` instance corresponding to the specified + ID. + + :param menu_id: The ID of the menu, e.g., **file-menu** or **view-menu**. + :returns: A :class:`MenuElement` instance. + """ + menu = [m for m in self.menus if m.get_attribute('id') == menu_id] + + if not menu: + raise NoSuchElementException('Could not find a menu with ' + 'id "{}"'.format(menu_id)) + + return menu[0] + + def select(self, label, item): + """Select an item in a menu. + + :param label: The label of the menu, e.g., **File** or **View**. + :param item: The label of the item in the menu, e.g., **New Tab**. + """ + return self.get_menu(label).select(item) + + def select_by_id(self, menu_id, item_id): + """Select an item in a menu. + + :param menu_id: The ID of the menu, e.g. **file-menu** or **view-menu**. + :param item_id: The ID of the item in the menu, e.g. **menu_newNavigatorTab**. + """ + return self.get_menu_by_id(menu_id).select_by_id(item_id) + + class MenuElement(DOMElement): + """Wraps a menu element DOM element.""" + + @property + def items(self): + """A list of menuitem DOM elements within this :class:`MenuElement` instance. + + :returns: A list of items in the menu. + """ + return (self.find_element(By.TAG_NAME, 'menupopup') + .find_elements(By.TAG_NAME, 'menuitem')) + + def select(self, label): + """Click on a menu item within this menu. + + :param label: The label of the menu item, e.g., **New Tab**. + """ + item = [l for l in self.items if l.get_attribute('label') == label] + + if not item: + message = ("Item labeled '{}' not found in the '{}' menu" + .format(label, self.get_attribute('label'))) + raise NoSuchElementException(message) + + return item[0].click() + + def select_by_id(self, menu_item_id): + """Click on a menu item within this menu. + + :param menu_item_id: The ID of the menu item, e.g. **menu_newNavigatorTab**. + """ + item = [l for l in self.items if l.get_attribute('id') == + menu_item_id] + + if not item: + message = ("Item with ID '{}' not found in the '{}' menu" + .format(menu_item_id, self.get_attribute('id'))) + raise NoSuchElementException(message) + + return item[0].click() diff --git a/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/pageinfo/__init__.py b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/pageinfo/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/pageinfo/__init__.py diff --git a/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/pageinfo/deck.py b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/pageinfo/deck.py new file mode 100644 index 000000000..0f2a2167a --- /dev/null +++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/pageinfo/deck.py @@ -0,0 +1,204 @@ +# 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 firefox_puppeteer.ui.base import UIBaseLib +from firefox_puppeteer.ui.deck import Panel + + +class Deck(UIBaseLib): + + def _create_panel_for_id(self, panel_id): + """Creates an instance of :class:`Panel` for the specified panel id. + + :param panel_id: The ID of the panel to create an instance of. + + :returns: :class:`Panel` instance + """ + mapping = {'feedPanel': FeedPanel, + 'generalPanel': GeneralPanel, + 'mediaPanel': MediaPanel, + 'permPanel': PermissionsPanel, + 'securityPanel': SecurityPanel + } + + panel = self.element.find_element(By.ID, panel_id) + return mapping.get(panel_id, Panel)(self.marionette, self.window, panel) + + # Properties for visual elements of the deck # + + @property + def feed(self): + """The :class:`FeedPanel` instance for the feed panel. + + :returns: :class:`FeedPanel` instance. + """ + return self._create_panel_for_id('feedPanel') + + @property + def general(self): + """The :class:`GeneralPanel` instance for the general panel. + + :returns: :class:`GeneralPanel` instance. + """ + return self._create_panel_for_id('generalPanel') + + @property + def media(self): + """The :class:`MediaPanel` instance for the media panel. + + :returns: :class:`MediaPanel` instance. + """ + return self._create_panel_for_id('mediaPanel') + + @property + def panels(self): + """List of all the :class:`Panel` instances of the current deck. + + :returns: List of :class:`Panel` instances. + """ + panels = self.marionette.execute_script(""" + let deck = arguments[0]; + let panels = []; + + for (let index = 0; index < deck.children.length; index++) { + if (deck.children[index].id) { + panels.push(deck.children[index].id); + } + } + + return panels; + """, script_args=[self.element]) + + return [self._create_panel_for_id(panel) for panel in panels] + + @property + def permissions(self): + """The :class:`PermissionsPanel` instance for the permissions panel. + + :returns: :class:`PermissionsPanel` instance. + """ + return self._create_panel_for_id('permPanel') + + @property + def security(self): + """The :class:`SecurityPanel` instance for the security panel. + + :returns: :class:`SecurityPanel` instance. + """ + return self._create_panel_for_id('securityPanel') + + # Properties for helpers when working with the deck # + + @property + def selected_index(self): + """The index of the currently selected panel. + + :return: Index of the selected panel. + """ + return int(self.element.get_property('selectedIndex')) + + @property + def selected_panel(self): + """A :class:`Panel` instance of the currently selected panel. + + :returns: :class:`Panel` instance. + """ + return self.panels[self.selected_index] + + # Methods for helpers when working with the deck # + + def select(self, panel): + """Selects the specified panel via the tab element. + + :param panel: The panel to select. + + :returns: :class:`Panel` instance of the selected panel. + """ + panel.tab.click() + Wait(self.marionette).until( + lambda _: self.selected_panel == panel, + message='Panel with ID "%s" could not be selected.' % panel) + + return panel + + +class PageInfoPanel(Panel): + + @property + def tab(self): + """The DOM element which represents the corresponding tab element at the top. + + :returns: Reference to the tab element. + """ + name = self.element.get_property('id').split('Panel')[0] + return self.window.window_element.find_element(By.ID, name + 'Tab') + + +class FeedPanel(PageInfoPanel): + pass + + +class GeneralPanel(PageInfoPanel): + pass + + +class MediaPanel(PageInfoPanel): + pass + + +class PermissionsPanel(PageInfoPanel): + pass + + +class SecurityPanel(PageInfoPanel): + + @property + def domain(self): + """The DOM element which represents the domain textbox. + + :returns: Reference to the textbox element. + """ + return self.element.find_element(By.ID, 'security-identity-domain-value') + + @property + def owner(self): + """The DOM element which represents the owner textbox. + + :returns: Reference to the textbox element. + """ + return self.element.find_element(By.ID, 'security-identity-owner-value') + + @property + def verifier(self): + """The DOM element which represents the verifier textbox. + + :returns: Reference to the textbox element. + """ + return self.element.find_element(By.ID, 'security-identity-verifier-value') + + @property + def view_certificate(self): + """The DOM element which represents the view certificate button. + + :returns: Reference to the button element. + """ + return self.element.find_element(By.ID, 'security-view-cert') + + @property + def view_cookies(self): + """The DOM element which represents the view cookies button. + + :returns: Reference to the button element. + """ + return self.element.find_element(By.ID, 'security-view-cookies') + + @property + def view_passwords(self): + """The DOM element which represents the view passwords button. + + :returns: Reference to the button element. + """ + return self.element.find_element(By.ID, 'security-view-password') diff --git a/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/pageinfo/window.py b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/pageinfo/window.py new file mode 100644 index 000000000..070f39f79 --- /dev/null +++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/pageinfo/window.py @@ -0,0 +1,61 @@ +# 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 + +from firefox_puppeteer.ui.pageinfo.deck import Deck +from firefox_puppeteer.ui.windows import BaseWindow, Windows + + +class PageInfoWindow(BaseWindow): + """Representation of a page info window.""" + + window_type = 'Browser:page-info' + + dtds = [ + 'chrome://browser/locale/pageInfo.dtd', + ] + + properties = [ + 'chrome://browser/locale/browser.properties', + 'chrome://browser/locale/pageInfo.properties', + 'chrome://pippki/locale/pippki.properties', + ] + + @property + def deck(self): + """The :class:`Deck` instance which represents the deck. + + :returns: Reference to the deck. + """ + deck = self.window_element.find_element(By.ID, 'mainDeck') + return Deck(self.marionette, self, deck) + + def close(self, trigger='shortcut', force=False): + """Closes the current page info window by using the specified trigger. + + :param trigger: Optional, method to close the current window. This can + be a string with one of `menu` (OS X only) or `shortcut`, or a callback + which gets triggered with the current :class:`PageInfoWindow` as parameter. + Defaults to `shortcut`. + + :param force: Optional, forces the closing of the window by using the Gecko API. + Defaults to `False`. + """ + def callback(win): + # Prepare action which triggers the opening of the browser window + if callable(trigger): + trigger(win) + elif trigger == 'menu': + self.menubar.select_by_id('file-menu', 'menu_close') + elif trigger == 'shortcut': + win.send_shortcut(win.localize_entity('closeWindow.key'), + accel=True) + else: + raise ValueError('Unknown closing method: "%s"' % trigger) + + super(PageInfoWindow, self).close(callback, force) + + +Windows.register_window(PageInfoWindow.window_type, PageInfoWindow) diff --git a/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/update_wizard/__init__.py b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/update_wizard/__init__.py new file mode 100644 index 000000000..020996694 --- /dev/null +++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/update_wizard/__init__.py @@ -0,0 +1,5 @@ +# 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 dialog import UpdateWizardDialog diff --git a/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/update_wizard/dialog.py b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/update_wizard/dialog.py new file mode 100644 index 000000000..19435b211 --- /dev/null +++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/update_wizard/dialog.py @@ -0,0 +1,46 @@ +# 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 firefox_puppeteer.ui.update_wizard.wizard import Wizard +from firefox_puppeteer.ui.windows import BaseWindow, Windows + + +# Bug 1143020 - Subclass from BaseDialog ui class with possible wizard mixin +class UpdateWizardDialog(BaseWindow): + """Representation of the old Software Update Wizard Dialog.""" + window_type = 'Update:Wizard' + + dtds = [ + 'chrome://branding/locale/brand.dtd', + 'chrome://mozapps/locale/update/updates.dtd', + ] + + properties = [ + 'chrome://branding/locale/brand.properties', + 'chrome://mozapps/locale/update/updates.properties', + ] + + @property + def wizard(self): + """The :class:`Wizard` instance which represents the wizard. + + :returns: Reference to the wizard. + """ + # The deck is also the root element + wizard = self.marionette.find_element(By.ID, 'updates') + return Wizard(self.marionette, self, wizard) + + def select_next_page(self): + """Clicks on "Next" button, and waits for the next page to show up.""" + current_panel = self.wizard.selected_panel + + self.wizard.next_button.click() + Wait(self.marionette).until( + lambda _: self.wizard.selected_panel != current_panel, + message='Next panel has not been selected.') + + +Windows.register_window(UpdateWizardDialog.window_type, UpdateWizardDialog) diff --git a/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/update_wizard/wizard.py b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/update_wizard/wizard.py new file mode 100644 index 000000000..9687ac917 --- /dev/null +++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/update_wizard/wizard.py @@ -0,0 +1,291 @@ +# 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 firefox_puppeteer.ui.base import UIBaseLib +from firefox_puppeteer.ui.deck import Panel + + +class Wizard(UIBaseLib): + + def __init__(self, *args, **kwargs): + super(Wizard, self).__init__(*args, **kwargs) + + Wait(self.marionette).until( + lambda _: self.selected_panel, + message='No panel has been selected by default.') + + def _create_panel_for_id(self, panel_id): + """Creates an instance of :class:`Panel` for the specified panel id. + + :param panel_id: The ID of the panel to create an instance of. + + :returns: :class:`Panel` instance + """ + mapping = {'checking': CheckingPanel, + 'downloading': DownloadingPanel, + 'dummy': DummyPanel, + 'errorpatching': ErrorPatchingPanel, + 'errors': ErrorPanel, + 'errorextra': ErrorExtraPanel, + 'finished': FinishedPanel, + 'finishedBackground': FinishedBackgroundPanel, + 'manualUpdate': ManualUpdatePanel, + 'noupdatesfound': NoUpdatesFoundPanel, + 'updatesfoundbasic': UpdatesFoundBasicPanel, + + # TODO: Remove once we no longer support version Firefox 45.0ESR + 'incompatibleCheck': IncompatibleCheckPanel, + 'incompatibleList': IncompatibleListPanel, + } + + panel = self.element.find_element(By.ID, panel_id) + return mapping.get(panel_id, Panel)(self.marionette, self.window, panel) + + # Properties for visual buttons of the wizard # + + @property + def _buttons(self): + return self.element.find_element(By.ANON_ATTRIBUTE, {'anonid': 'Buttons'}) + + @property + def cancel_button(self): + return self._buttons.find_element(By.ANON_ATTRIBUTE, {'dlgtype': 'cancel'}) + + @property + def extra1_button(self): + return self._buttons.find_element(By.ANON_ATTRIBUTE, {'dlgtype': 'extra1'}) + + @property + def extra2_button(self): + return self._buttons.find_element(By.ANON_ATTRIBUTE, {'dlgtype': 'extra2'}) + + @property + def previous_button(self): + return self._buttons.find_element(By.ANON_ATTRIBUTE, {'dlgtype': 'back'}) + + @property + def finish_button(self): + return self._buttons.find_element(By.ANON_ATTRIBUTE, {'dlgtype': 'finish'}) + + @property + def next_button(self): + return self._buttons.find_element(By.ANON_ATTRIBUTE, {'dlgtype': 'next'}) + + # Properties for visual panels of the wizard # + + @property + def checking(self): + """The checking for updates panel. + + :returns: :class:`CheckingPanel` instance. + """ + return self._create_panel_for_id('checking') + + @property + def downloading(self): + """The downloading panel. + + :returns: :class:`DownloadingPanel` instance. + """ + return self._create_panel_for_id('downloading') + + @property + def dummy(self): + """The dummy panel. + + :returns: :class:`DummyPanel` instance. + """ + return self._create_panel_for_id('dummy') + + @property + def error_patching(self): + """The error patching panel. + + :returns: :class:`ErrorPatchingPanel` instance. + """ + return self._create_panel_for_id('errorpatching') + + @property + def error(self): + """The errors panel. + + :returns: :class:`ErrorPanel` instance. + """ + return self._create_panel_for_id('errors') + + @property + def error_extra(self): + """The error extra panel. + + :returns: :class:`ErrorExtraPanel` instance. + """ + return self._create_panel_for_id('errorextra') + + @property + def finished(self): + """The finished panel. + + :returns: :class:`FinishedPanel` instance. + """ + return self._create_panel_for_id('finished') + + @property + def finished_background(self): + """The finished background panel. + + :returns: :class:`FinishedBackgroundPanel` instance. + """ + return self._create_panel_for_id('finishedBackground') + + @property + def incompatible_check(self): + """The incompatible check panel. + + :returns: :class:`IncompatibleCheckPanel` instance. + """ + return self._create_panel_for_id('incompatibleCheck') + + @property + def incompatible_list(self): + """The incompatible list panel. + + :returns: :class:`IncompatibleListPanel` instance. + """ + return self._create_panel_for_id('incompatibleList') + + @property + def manual_update(self): + """The manual update panel. + + :returns: :class:`ManualUpdatePanel` instance. + """ + return self._create_panel_for_id('manualUpdate') + + @property + def no_updates_found(self): + """The no updates found panel. + + :returns: :class:`NoUpdatesFoundPanel` instance. + """ + return self._create_panel_for_id('noupdatesfound') + + @property + def updates_found_basic(self): + """The updates found panel. + + :returns: :class:`UpdatesFoundPanel` instance. + """ + return self._create_panel_for_id('updatesfoundbasic') + + @property + def panels(self): + """List of all the available :class:`Panel` instances. + + :returns: List of :class:`Panel` instances. + """ + panels = self.marionette.execute_script(""" + let wizard = arguments[0]; + let panels = []; + + for (let index = 0; index < wizard.children.length; index++) { + if (wizard.children[index].id) { + panels.push(wizard.children[index].id); + } + } + + return panels; + """, script_args=[self.element]) + + return [self._create_panel_for_id(panel) for panel in panels] + + @property + def selected_index(self): + """The index of the currently selected panel. + + :return: Index of the selected panel. + """ + return int(self.element.get_property('pageIndex')) + + @property + def selected_panel(self): + """A :class:`Panel` instance of the currently selected panel. + + :returns: :class:`Panel` instance. + """ + return self._create_panel_for_id(self.element.get_attribute('currentpageid')) + + +class CheckingPanel(Panel): + + @property + def progress(self): + """The DOM element which represents the progress meter. + + :returns: Reference to the progress element. + """ + return self.element.find_element(By.ID, 'checkingProgress') + + +class DownloadingPanel(Panel): + + @property + def progress(self): + """The DOM element which represents the progress meter. + + :returns: Reference to the progress element. + """ + return self.element.find_element(By.ID, 'downloadProgress') + + +class DummyPanel(Panel): + pass + + +class ErrorPatchingPanel(Panel): + pass + + +class ErrorPanel(Panel): + pass + + +class ErrorExtraPanel(Panel): + pass + + +class FinishedPanel(Panel): + pass + + +class FinishedBackgroundPanel(Panel): + pass + + +class IncompatibleCheckPanel(Panel): + + @property + def progress(self): + """The DOM element which represents the progress meter. + + :returns: Reference to the progress element. + """ + return self.element.find_element(By.ID, 'incompatibleCheckProgress') + + +class IncompatibleListPanel(Panel): + pass + + +class ManualUpdatePanel(Panel): + pass + + +class NoUpdatesFoundPanel(Panel): + pass + + +class UpdatesFoundBasicPanel(Panel): + pass diff --git a/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/windows.py b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/windows.py new file mode 100644 index 000000000..9248a3935 --- /dev/null +++ b/testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/windows.py @@ -0,0 +1,435 @@ +# 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 NoSuchWindowException +from marionette_driver.keys import Keys + +import firefox_puppeteer.errors as errors + +from firefox_puppeteer.api.l10n import L10n +from firefox_puppeteer.base import BaseLib +from firefox_puppeteer.decorators import use_class_as_property + + +class Windows(BaseLib): + + # Used for registering the different windows with this class to avoid + # circular dependencies with BaseWindow + windows_map = {} + + @property + def all(self): + """Retrieves a list of all open chrome windows. + + :returns: List of :class:`BaseWindow` instances corresponding to the + windows in `marionette.chrome_window_handles`. + """ + return [self.create_window_instance(handle) for handle in + self.marionette.chrome_window_handles] + + @property + def current(self): + """Retrieves the currently selected chrome window. + + :returns: The :class:`BaseWindow` for the currently active window. + """ + return self.create_window_instance(self.marionette.current_chrome_window_handle) + + @property + def focused_chrome_window_handle(self): + """Returns the currently focused chrome window handle. + + :returns: The `window handle` of the focused chrome window. + """ + def get_active_handle(mn): + with self.marionette.using_context('chrome'): + return self.marionette.execute_script(""" + Components.utils.import("resource://gre/modules/Services.jsm"); + + let win = Services.focus.activeWindow; + if (win) { + return win.QueryInterface(Components.interfaces.nsIInterfaceRequestor) + .getInterface(Components.interfaces.nsIDOMWindowUtils) + .outerWindowID.toString(); + } + + return null; + """) + + # In case of `None` being returned no window is currently active. This can happen + # when a focus change action is currently happening. So lets wait until it is done. + return Wait(self.marionette).until(get_active_handle, + message='No focused window has been found.') + + def close(self, handle): + """Closes the chrome window with the given handle. + + :param handle: The handle of the chrome window. + """ + self.switch_to(handle) + + # TODO: Maybe needs to wait as handled via an observer + return self.marionette.close_chrome_window() + + def close_all(self, exceptions=None): + """Closes all open chrome windows. + + There is an optional `exceptions` list, which can be used to exclude + specific chrome windows from being closed. + + :param exceptions: Optional, list of :class:`BaseWindow` instances not to close. + """ + windows_to_keep = exceptions or [] + + # Get handles of windows_to_keep + handles_to_keep = [entry.handle for entry in windows_to_keep] + + # Find handles to close and close them all + handles_to_close = set(self.marionette.chrome_window_handles) - set(handles_to_keep) + for handle in handles_to_close: + self.close(handle) + + def create_window_instance(self, handle, expected_class=None): + """Creates a :class:`BaseWindow` instance for the given chrome window. + + :param handle: The handle of the chrome window. + :param expected_class: Optional, check for the correct window class. + """ + current_handle = self.marionette.current_chrome_window_handle + window = None + + with self.marionette.using_context('chrome'): + try: + # Retrieve window type to determine the type of chrome window + if handle != self.marionette.current_chrome_window_handle: + self.switch_to(handle) + window_type = self.marionette.get_window_type() + finally: + # Ensure to switch back to the original window + if handle != current_handle: + self.switch_to(current_handle) + + if window_type in self.windows_map: + window = self.windows_map[window_type](self.marionette, handle) + else: + window = BaseWindow(self.marionette, handle) + + if expected_class is not None and type(window) is not expected_class: + raise errors.UnexpectedWindowTypeError('Expected window "%s" but got "%s"' % + (expected_class, type(window))) + + # Before continuing ensure the chrome window has been completed loading + Wait(self.marionette).until( + lambda _: self.loaded(handle), + message='Chrome window with handle "%s" did not finish loading.' % handle) + + return window + + def focus(self, handle): + """Focuses the chrome window with the given handle. + + :param handle: The handle of the chrome window. + """ + self.switch_to(handle) + + with self.marionette.using_context('chrome'): + self.marionette.execute_script(""" window.focus(); """) + + Wait(self.marionette).until( + lambda _: handle == self.focused_chrome_window_handle, + message='Focus has not been set to chrome window handle "%s".' % handle) + + def loaded(self, handle): + """Check if the chrome window with the given handle has been completed loading. + + :param handle: The handle of the chrome window. + + :returns: True, if the chrome window has been loaded. + """ + with self.marionette.using_context('chrome'): + return self.marionette.execute_script(""" + Components.utils.import("resource://gre/modules/Services.jsm"); + + let win = Services.wm.getOuterWindowWithId(Number(arguments[0])); + return win.document.readyState == 'complete'; + """, script_args=[handle]) + + def switch_to(self, target): + """Switches context to the specified chrome window. + + :param target: The window to switch to. `target` can be a `handle` or a + callback that returns True in the context of the desired + window. + + :returns: Instance of the selected :class:`BaseWindow`. + """ + target_handle = None + + if target in self.marionette.chrome_window_handles: + target_handle = target + elif callable(target): + current_handle = self.marionette.current_chrome_window_handle + + # switches context if callback for a chrome window returns `True`. + for handle in self.marionette.chrome_window_handles: + self.marionette.switch_to_window(handle) + window = self.create_window_instance(handle) + if target(window): + target_handle = handle + break + + # if no handle has been found switch back to original window + if not target_handle: + self.marionette.switch_to_window(current_handle) + + if target_handle is None: + raise NoSuchWindowException("No window found for '{}'" + .format(target)) + + # only switch if necessary + if target_handle != self.marionette.current_chrome_window_handle: + self.marionette.switch_to_window(target_handle) + + return self.create_window_instance(target_handle) + + @classmethod + def register_window(cls, window_type, window_class): + """Registers a chrome window with this class so that this class may in + turn create the appropriate window instance later on. + + :param window_type: The type of window. + :param window_class: The constructor of the window + """ + cls.windows_map[window_type] = window_class + + +class BaseWindow(BaseLib): + """Base class for any kind of chrome window.""" + + # l10n class attributes will be set by each window class individually + dtds = [] + properties = [] + + def __init__(self, marionette, window_handle): + super(BaseWindow, self).__init__(marionette) + + self._l10n = L10n(self.marionette) + self._windows = Windows(self.marionette) + + if window_handle not in self.marionette.chrome_window_handles: + raise errors.UnknownWindowError('Window with handle "%s" does not exist' % + window_handle) + self._handle = window_handle + + def __eq__(self, other): + return self.handle == other.handle + + @property + def closed(self): + """Returns closed state of the chrome window. + + :returns: True if the window has been closed. + """ + return self.handle not in self.marionette.chrome_window_handles + + @property + def focused(self): + """Returns `True` if the chrome window is focused. + + :returns: True if the window is focused. + """ + self.switch_to() + + return self.handle == self._windows.focused_chrome_window_handle + + @property + def handle(self): + """Returns the `window handle` of the chrome window. + + :returns: `window handle`. + """ + return self._handle + + @property + def loaded(self): + """Checks if the window has been fully loaded. + + :returns: True, if the window is loaded. + """ + self._windows.loaded(self.handle) + + @use_class_as_property('ui.menu.MenuBar') + def menubar(self): + """Provides access to the menu bar, for example, the **File** menu. + + See the :class:`~ui.menu.MenuBar` reference. + """ + + @property + def window_element(self): + """Returns the inner DOM window element. + + :returns: DOM window element. + """ + self.switch_to() + + return self.marionette.find_element(By.CSS_SELECTOR, ':root') + + def close(self, callback=None, force=False): + """Closes the current chrome window. + + If this is the last remaining window, the Marionette session is ended. + + :param callback: Optional, function to trigger the window to open. It is + triggered with the current :class:`BaseWindow` as parameter. + Defaults to `window.open()`. + + :param force: Optional, forces the closing of the window by using the Gecko API. + Defaults to `False`. + """ + self.switch_to() + + # Bug 1121698 + # For more stable tests register an observer topic first + prev_win_count = len(self.marionette.chrome_window_handles) + + handle = self.handle + if force or callback is None: + self._windows.close(handle) + else: + callback(self) + + # Bug 1121698 + # Observer code should let us ditch this wait code + Wait(self.marionette).until( + lambda m: len(m.chrome_window_handles) == prev_win_count - 1, + message='Chrome window with handle "%s" has not been closed.' % handle) + + def focus(self): + """Sets the focus to the current chrome window.""" + return self._windows.focus(self.handle) + + def localize_entity(self, entity_id): + """Returns the localized string for the specified DTD entity id. + + :param entity_id: The id to retrieve the value from. + + :returns: The localized string for the requested entity. + + :raises MarionetteException: When entity id is not found. + """ + return self._l10n.localize_entity(self.dtds, entity_id) + + def localize_property(self, property_id): + """Returns the localized string for the specified property id. + + :param property_id: The id to retrieve the value from. + + :returns: The localized string for the requested property. + + :raises MarionetteException: When property id is not found. + """ + return self._l10n.localize_property(self.properties, property_id) + + def open_window(self, callback=None, expected_window_class=None, focus=True): + """Opens a new top-level chrome window. + + :param callback: Optional, function to trigger the window to open. It is + triggered with the current :class:`BaseWindow` as parameter. + Defaults to `window.open()`. + :param expected_class: Optional, check for the correct window class. + :param focus: Optional, if true, focus the new window. + Defaults to `True`. + """ + # Bug 1121698 + # For more stable tests register an observer topic first + start_handles = self.marionette.chrome_window_handles + + self.switch_to() + with self.marionette.using_context('chrome'): + if callback is not None: + callback(self) + else: + self.marionette.execute_script(""" window.open(); """) + + # TODO: Needs to be replaced with observer handling code (bug 1121698) + def window_opened(mn): + return len(mn.chrome_window_handles) == len(start_handles) + 1 + Wait(self.marionette).until( + window_opened, + message='No new chrome window has been opened.') + + handles = self.marionette.chrome_window_handles + [new_handle] = list(set(handles) - set(start_handles)) + + assert new_handle is not None + + window = self._windows.create_window_instance(new_handle, expected_window_class) + + if focus: + window.focus() + + return window + + def send_shortcut(self, command_key, **kwargs): + """Sends a keyboard shortcut to the window. + + :param command_key: The key (usually a letter) to be pressed. + + :param accel: Optional, If `True`, the `Accel` modifier key is pressed. + This key differs between OS X (`Meta`) and Linux/Windows (`Ctrl`). Defaults to `False`. + + :param alt: Optional, If `True`, the `Alt` modifier key is pressed. Defaults to `False`. + + :param ctrl: Optional, If `True`, the `Ctrl` modifier key is pressed. Defaults to `False`. + + :param meta: Optional, If `True`, the `Meta` modifier key is pressed. Defaults to `False`. + + :param shift: Optional, If `True`, the `Shift` modifier key is pressed. + Defaults to `False`. + """ + + platform = self.marionette.session_capabilities['platformName'] + + keymap = { + 'accel': Keys.META if platform == 'darwin' else Keys.CONTROL, + 'alt': Keys.ALT, + 'cmd': Keys.COMMAND, + 'ctrl': Keys.CONTROL, + 'meta': Keys.META, + 'shift': Keys.SHIFT, + } + + # Append all to press modifier keys + keys = [] + for modifier in kwargs: + if modifier not in keymap: + raise KeyError('"%s" is not a known modifier' % modifier) + + if kwargs[modifier] is True: + keys.append(keymap[modifier]) + + # Bug 1125209 - Only lower-case command keys should be sent + keys.append(command_key.lower()) + + self.switch_to() + self.window_element.send_keys(*keys) + + def switch_to(self, focus=False): + """Switches the context to this chrome window. + + By default it will not focus the window. If that behavior is wanted, the + `focus` parameter can be used. + + :param focus: If `True`, the chrome window will be focused. + + :returns: Current window as :class:`BaseWindow` instance. + """ + if focus: + self._windows.focus(self.handle) + else: + self._windows.switch_to(self.handle) + + return self |