diff options
Diffstat (limited to 'testing/marionette/puppeteer/firefox/firefox_puppeteer/ui/browser')
5 files changed, 1408 insertions, 0 deletions
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) |