diff options
Diffstat (limited to 'testing/firefox-ui/tests/functional')
11 files changed, 1071 insertions, 0 deletions
diff --git a/testing/firefox-ui/tests/functional/manifest.ini b/testing/firefox-ui/tests/functional/manifest.ini new file mode 100644 index 0000000000..8afe842897 --- /dev/null +++ b/testing/firefox-ui/tests/functional/manifest.ini @@ -0,0 +1,3 @@ +[include:safebrowsing/manifest.ini] +[include:security/manifest.ini] +[include:sessionstore/manifest.ini] diff --git a/testing/firefox-ui/tests/functional/safebrowsing/manifest.ini b/testing/firefox-ui/tests/functional/safebrowsing/manifest.ini new file mode 100644 index 0000000000..97116af83a --- /dev/null +++ b/testing/firefox-ui/tests/functional/safebrowsing/manifest.ini @@ -0,0 +1,7 @@ +[DEFAULT] +tags = remote + +[test_initial_download.py] +skip-if = debug || asan || (cc_type == "clang" && os == 'win') || (os == 'win' && bits == 64 && !debug && processor == "x86_64") # the GAPI key isn't available in debug or asan builds, bug 1526450 +[test_notification.py] +[test_warning_pages.py] diff --git a/testing/firefox-ui/tests/functional/safebrowsing/test_initial_download.py b/testing/firefox-ui/tests/functional/safebrowsing/test_initial_download.py new file mode 100644 index 0000000000..e77c9d95c7 --- /dev/null +++ b/testing/firefox-ui/tests/functional/safebrowsing/test_initial_download.py @@ -0,0 +1,144 @@ +# 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 __future__ import absolute_import + +import os + +from functools import reduce + +from marionette_driver import Wait +from marionette_harness import MarionetteTestCase + + +class TestSafeBrowsingInitialDownload(MarionetteTestCase): + + v2_file_extensions = [ + "vlpset", + "sbstore", + ] + + v4_file_extensions = [ + "vlpset", + "metadata", + ] + + prefs_download_lists = [ + "urlclassifier.blockedTable", + "urlclassifier.downloadAllowTable", + "urlclassifier.downloadBlockTable", + "urlclassifier.malwareTable", + "urlclassifier.phishTable", + "urlclassifier.trackingTable", + "urlclassifier.trackingWhitelistTable", + ] + + prefs_provider_update_time = { + # Force an immediate download of the safebrowsing files + "browser.safebrowsing.provider.mozilla.nextupdatetime": 1, + } + + prefs_safebrowsing = { + "browser.safebrowsing.debug": True, + "browser.safebrowsing.blockedURIs.enabled": True, + "browser.safebrowsing.downloads.enabled": True, + "browser.safebrowsing.phishing.enabled": True, + "browser.safebrowsing.malware.enabled": True, + "privacy.trackingprotection.enabled": True, + "privacy.trackingprotection.pbmode.enabled": True, + } + + def get_safebrowsing_files(self, is_v4): + files = [] + + if is_v4: + my_file_extensions = self.v4_file_extensions + else: # v2 + my_file_extensions = self.v2_file_extensions + + for pref_name in self.prefs_download_lists: + base_names = self.marionette.get_pref(pref_name).split(",") + + # moztest- lists are not saved to disk + # pylint --py3k: W1639 + base_names = list( + filter(lambda x: not x.startswith("moztest-"), base_names) + ) + + for ext in my_file_extensions: + files.extend( + [ + "{name}.{ext}".format(name=f, ext=ext) + for f in base_names + if f and f.endswith("-proto") == is_v4 + ] + ) + + return set(sorted(files)) + + def setUp(self): + super(TestSafeBrowsingInitialDownload, self).setUp() + + self.safebrowsing_v2_files = self.get_safebrowsing_files(False) + if any( + f.startswith("goog-") or f.startswith("googpub-") + for f in self.safebrowsing_v2_files + ): + self.prefs_provider_update_time.update( + { + "browser.safebrowsing.provider.google.nextupdatetime": 1, + } + ) + + self.safebrowsing_v4_files = self.get_safebrowsing_files(True) + if any( + f.startswith("goog-") or f.startswith("googpub-") + for f in self.safebrowsing_v4_files + ): + self.prefs_provider_update_time.update( + { + "browser.safebrowsing.provider.google4.nextupdatetime": 1, + } + ) + + # Force the preferences for the new profile + enforce_prefs = self.prefs_safebrowsing + enforce_prefs.update(self.prefs_provider_update_time) + self.marionette.enforce_gecko_prefs(enforce_prefs) + + self.safebrowsing_path = os.path.join( + self.marionette.instance.profile.profile, "safebrowsing" + ) + + def tearDown(self): + try: + # Restart with a fresh profile + self.marionette.restart(clean=True) + finally: + super(TestSafeBrowsingInitialDownload, self).tearDown() + + def test_safe_browsing_initial_download(self): + def check_downloaded(_): + return reduce( + lambda state, pref: state and int(self.marionette.get_pref(pref)) != 1, + list(self.prefs_provider_update_time), + True, + ) + + try: + Wait(self.marionette, timeout=170).until( + check_downloaded, + message="Not all safebrowsing files have been downloaded", + ) + finally: + files_on_disk_toplevel = os.listdir(self.safebrowsing_path) + for f in self.safebrowsing_v2_files: + self.assertIn(f, files_on_disk_toplevel) + + if len(self.safebrowsing_v4_files) > 0: + files_on_disk_google4 = os.listdir( + os.path.join(self.safebrowsing_path, "google4") + ) + for f in self.safebrowsing_v4_files: + self.assertIn(f, files_on_disk_google4) diff --git a/testing/firefox-ui/tests/functional/safebrowsing/test_notification.py b/testing/firefox-ui/tests/functional/safebrowsing/test_notification.py new file mode 100644 index 0000000000..7a19becba1 --- /dev/null +++ b/testing/firefox-ui/tests/functional/safebrowsing/test_notification.py @@ -0,0 +1,128 @@ +# 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 __future__ import absolute_import + +import time + +from marionette_driver import By, expected, Wait +from marionette_harness import MarionetteTestCase, WindowManagerMixin + + +class TestSafeBrowsingNotificationBar(WindowManagerMixin, MarionetteTestCase): + def setUp(self): + super(TestSafeBrowsingNotificationBar, self).setUp() + + self.test_data = [ + # Unwanted software URL + {"unsafe_page": "https://www.itisatrap.org/firefox/unwanted.html"}, + # Phishing URL info + {"unsafe_page": "https://www.itisatrap.org/firefox/its-a-trap.html"}, + # Malware URL object + {"unsafe_page": "https://www.itisatrap.org/firefox/its-an-attack.html"}, + ] + + self.default_homepage = self.marionette.get_pref("browser.startup.homepage") + + self.marionette.set_pref("browser.safebrowsing.phishing.enabled", True) + self.marionette.set_pref("browser.safebrowsing.malware.enabled", True) + + # Give the browser a little time, because SafeBrowsing.jsm takes a while + # between start up and adding the example urls to the db. + # hg.mozilla.org/mozilla-central/file/46aebcd9481e/browser/base/content/browser.js#l1194 + time.sleep(3) + + # Run this test in a new tab. + new_tab = self.open_tab() + self.marionette.switch_to_window(new_tab) + + def tearDown(self): + try: + self.marionette.clear_pref("browser.safebrowsing.phishing.enabled") + self.marionette.clear_pref("browser.safebrowsing.malware.enabled") + + self.remove_permission("https://www.itisatrap.org", "safe-browsing") + self.close_all_tabs() + finally: + super(TestSafeBrowsingNotificationBar, self).tearDown() + + def test_notification_bar(self): + for item in self.test_data: + unsafe_page = item["unsafe_page"] + + # Return to the unsafe page + # Check "ignore warning" link then notification bar's "get me out" button + self.marionette.navigate(unsafe_page) + # Wait for the DOM to receive events for about:blocked + time.sleep(1) + self.check_ignore_warning_link(unsafe_page) + self.check_get_me_out_of_here_button() + + # Return to the unsafe page + # Check "ignore warning" link then notification bar's "X" button + self.marionette.navigate(unsafe_page) + # Wait for the DOM to receive events for about:blocked + time.sleep(1) + self.check_ignore_warning_link(unsafe_page) + self.check_x_button() + + def get_final_url(self, url): + self.marionette.navigate(url) + return self.marionette.get_url() + + def remove_permission(self, host, permission): + with self.marionette.using_context("chrome"): + self.marionette.execute_script( + """ + Components.utils.import("resource://gre/modules/Services.jsm"); + let uri = Services.io.newURI(arguments[0], null, null); + let principal = Services.scriptSecurityManager.createContentPrincipal(uri, {}); + Services.perms.removeFromPrincipal(principal, arguments[1]); + """, + script_args=[host, permission], + ) + + def check_ignore_warning_link(self, unsafe_page): + button = self.marionette.find_element(By.ID, "seeDetailsButton") + button.click() + time.sleep(1) + link = self.marionette.find_element(By.ID, "ignore_warning_link") + link.click() + + Wait(self.marionette, timeout=self.marionette.timeout.page_load).until( + expected.element_present(By.ID, "main-feature"), + message='Expected target element "#main-feature" has not been found', + ) + self.assertEquals(self.marionette.get_url(), self.get_final_url(unsafe_page)) + + # Clean up here since the permission gets set in this function + self.remove_permission("https://www.itisatrap.org", "safe-browsing") + + def check_get_me_out_of_here_button(self): + with self.marionette.using_context("chrome"): + button = self.marionette.find_element( + By.ID, "tabbrowser-tabbox" + ).find_element(By.CSS_SELECTOR, 'button[label="Get me out of here!"]') + button.click() + + Wait(self.marionette, timeout=self.marionette.timeout.page_load).until( + lambda mn: self.default_homepage in mn.get_url(), + message="The default home page has not been loaded", + ) + + def check_x_button(self): + with self.marionette.using_context("chrome"): + button = ( + self.marionette.find_element(By.ID, "tabbrowser-tabbox") + .find_element( + By.CSS_SELECTOR, "notification[value=blocked-badware-page]" + ) + .find_element(By.CSS_SELECTOR, ".messageCloseButton") + ) + button.click() + + Wait(self.marionette, timeout=self.marionette.timeout.page_load).until( + expected.element_stale(button), + message="The notification bar has not been closed", + ) diff --git a/testing/firefox-ui/tests/functional/safebrowsing/test_warning_pages.py b/testing/firefox-ui/tests/functional/safebrowsing/test_warning_pages.py new file mode 100644 index 0000000000..883d90c7a6 --- /dev/null +++ b/testing/firefox-ui/tests/functional/safebrowsing/test_warning_pages.py @@ -0,0 +1,142 @@ +# 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 __future__ import absolute_import + +import time + +from marionette_driver import By, expected, Wait +from marionette_harness import MarionetteTestCase, WindowManagerMixin + + +class TestSafeBrowsingWarningPages(WindowManagerMixin, MarionetteTestCase): + def setUp(self): + super(TestSafeBrowsingWarningPages, self).setUp() + + self.urls = [ + # Unwanted software URL + "https://www.itisatrap.org/firefox/unwanted.html", + # Phishing URL + "https://www.itisatrap.org/firefox/its-a-trap.html", + # Malware URL + "https://www.itisatrap.org/firefox/its-an-attack.html", + ] + + self.default_homepage = self.marionette.get_pref("browser.startup.homepage") + self.support_page = self.marionette.absolute_url("support.html?topic=") + + self.marionette.set_pref("app.support.baseURL", self.support_page) + self.marionette.set_pref("browser.safebrowsing.phishing.enabled", True) + self.marionette.set_pref("browser.safebrowsing.malware.enabled", True) + + # Give the browser a little time, because SafeBrowsing.jsm takes a + # while between start up and adding the example urls to the db. + # hg.mozilla.org/mozilla-central/file/46aebcd9481e/browser/base/content/browser.js#l1194 + time.sleep(3) + + # Run this test in a new tab. + new_tab = self.open_tab() + self.marionette.switch_to_window(new_tab) + + def tearDown(self): + try: + self.marionette.clear_pref("app.support.baseURL") + self.marionette.clear_pref("browser.safebrowsing.malware.enabled") + self.marionette.clear_pref("browser.safebrowsing.phishing.enabled") + + self.remove_permission("https://www.itisatrap.org", "safe-browsing") + self.close_all_tabs() + finally: + super(TestSafeBrowsingWarningPages, self).tearDown() + + def test_warning_pages(self): + for unsafe_page in self.urls: + # Load a test page, then test the get me out button + self.marionette.navigate(unsafe_page) + # Wait for the DOM to receive events for about:blocked + time.sleep(1) + self.check_get_me_out_of_here_button(unsafe_page) + + # Load the test page again, then test the report button + self.marionette.navigate(unsafe_page) + # Wait for the DOM to receive events for about:blocked + time.sleep(1) + self.check_report_link(unsafe_page) + + # Load the test page again, then test the ignore warning button + self.marionette.navigate(unsafe_page) + # Wait for the DOM to receive events for about:blocked + time.sleep(1) + self.check_ignore_warning_button(unsafe_page) + + def get_final_url(self, url): + self.marionette.navigate(url) + return self.marionette.get_url() + + def remove_permission(self, host, permission): + with self.marionette.using_context("chrome"): + self.marionette.execute_script( + """ + Components.utils.import("resource://gre/modules/Services.jsm"); + let uri = Services.io.newURI(arguments[0], null, null); + let principal = Services.scriptSecurityManager.createContentPrincipal(uri, {}); + Services.perms.removeFromPrincipal(principal, arguments[1]); + """, + script_args=[host, permission], + ) + + def check_get_me_out_of_here_button(self, unsafe_page): + button = self.marionette.find_element(By.ID, "goBackButton") + button.click() + + Wait(self.marionette, timeout=self.marionette.timeout.page_load).until( + lambda mn: self.default_homepage in mn.get_url() + ) + + def check_report_link(self, unsafe_page): + # Get the URL of the support site for phishing and malware. This may result in a redirect. + with self.marionette.using_context("chrome"): + url = self.marionette.execute_script( + """ + Components.utils.import("resource://gre/modules/Services.jsm"); + return Services.urlFormatter.formatURLPref("app.support.baseURL") + + "phishing-malware"; + """ + ) + + button = self.marionette.find_element(By.ID, "seeDetailsButton") + button.click() + link = self.marionette.find_element(By.ID, "firefox_support") + link.click() + + # Wait for the button to become stale, whereby a longer timeout is needed + # here to not fail in case of slow connections. + Wait(self.marionette, timeout=self.marionette.timeout.page_load).until( + expected.element_stale(button) + ) + + # Wait for page load to be completed, so we can verify the URL even if a redirect happens. + # TODO: Bug 1140470: use replacement for mozmill's waitforPageLoad + expected_url = self.get_final_url(url) + Wait(self.marionette, timeout=self.marionette.timeout.page_load).until( + lambda mn: expected_url == mn.get_url(), + message="The expected URL '{}' has not been loaded".format(expected_url), + ) + + topic = self.marionette.find_element(By.ID, "topic") + self.assertEquals(topic.text, "phishing-malware") + + def check_ignore_warning_button(self, unsafe_page): + button = self.marionette.find_element(By.ID, "seeDetailsButton") + button.click() + link = self.marionette.find_element(By.ID, "ignore_warning_link") + link.click() + + Wait(self.marionette, timeout=self.marionette.timeout.page_load).until( + expected.element_present(By.ID, "main-feature") + ) + self.assertEquals(self.marionette.get_url(), self.get_final_url(unsafe_page)) + + # Clean up by removing safe browsing permission for unsafe page + self.remove_permission("https://www.itisatrap.org", "safe-browsing") diff --git a/testing/firefox-ui/tests/functional/security/manifest.ini b/testing/firefox-ui/tests/functional/security/manifest.ini new file mode 100644 index 0000000000..e0993b122a --- /dev/null +++ b/testing/firefox-ui/tests/functional/security/manifest.ini @@ -0,0 +1,4 @@ +[DEFAULT] +tags = remote + +[test_ssl_status_after_restart.py] diff --git a/testing/firefox-ui/tests/functional/security/test_ssl_status_after_restart.py b/testing/firefox-ui/tests/functional/security/test_ssl_status_after_restart.py new file mode 100644 index 0000000000..5941748ad3 --- /dev/null +++ b/testing/firefox-ui/tests/functional/security/test_ssl_status_after_restart.py @@ -0,0 +1,58 @@ +# 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 __future__ import absolute_import + +from marionette_driver import By, Wait +from marionette_harness import MarionetteTestCase, WindowManagerMixin + + +class TestSSLStatusAfterRestart(WindowManagerMixin, MarionetteTestCase): + def setUp(self): + super(TestSSLStatusAfterRestart, self).setUp() + self.marionette.set_context("chrome") + + self.test_url = "https://sha512.badssl.com/" + + # Set browser to restore previous session + self.marionette.set_pref("browser.startup.page", 3) + # Disable rcwn to make cache behavior deterministic + self.marionette.set_pref("network.http.rcwn.enable", False) + + def tearDown(self): + self.marionette.clear_pref("browser.startup.page") + self.marionette.clear_pref("network.http.rcwn.enable") + + super(TestSSLStatusAfterRestart, self).tearDown() + + def test_ssl_status_after_restart(self): + with self.marionette.using_context("content"): + self.marionette.navigate(self.test_url) + self.verify_certificate_status(self.test_url) + + self.marionette.restart(in_app=True) + + self.verify_certificate_status(self.test_url) + + def verify_certificate_status(self, url): + with self.marionette.using_context("content"): + Wait(self.marionette).until( + lambda _: self.marionette.get_url() == url, + message="Expected URL loaded", + ) + + identity_box = self.marionette.find_element(By.ID, "identity-box") + self.assertEqual(identity_box.get_attribute("pageproxystate"), "valid") + + class_list = self.marionette.execute_script( + """ + const names = []; + for (const name of arguments[0].classList) { + names.push(name); + } + return names; + """, + script_args=[identity_box], + ) + self.assertIn("verifiedDomain", class_list) diff --git a/testing/firefox-ui/tests/functional/sessionstore/manifest.ini b/testing/firefox-ui/tests/functional/sessionstore/manifest.ini new file mode 100644 index 0000000000..09e94968ee --- /dev/null +++ b/testing/firefox-ui/tests/functional/sessionstore/manifest.ini @@ -0,0 +1,6 @@ +[DEFAULT] +tags = local + +[test_restore_windows_after_restart_and_quit.py] +[test_restore_windows_after_windows_shutdown.py] +skip-if = os != "win" diff --git a/testing/firefox-ui/tests/functional/sessionstore/session_store_test_case.py b/testing/firefox-ui/tests/functional/sessionstore/session_store_test_case.py new file mode 100644 index 0000000000..658f80c512 --- /dev/null +++ b/testing/firefox-ui/tests/functional/sessionstore/session_store_test_case.py @@ -0,0 +1,400 @@ +# 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 __future__ import absolute_import + +from marionette_driver.keys import Keys +from marionette_harness import MarionetteTestCase, WindowManagerMixin + + +class SessionStoreTestCase(WindowManagerMixin, MarionetteTestCase): + def setUp( + self, + startup_page=1, + include_private=True, + no_auto_updates=True, + win_register_restart=False, + ): + super(SessionStoreTestCase, self).setUp() + self.marionette.set_context("chrome") + + platform = self.marionette.session_capabilities["platformName"] + self.accelKey = Keys.META if platform == "mac" else Keys.CONTROL + + # Each list element represents a window of tabs loaded at + # some testing URL + self.test_windows = set( + [ + # Window 1. Note the comma after the absolute_url call - + # this is Python's way of declaring a 1 item tuple. + (self.marionette.absolute_url("layout/mozilla.html"),), + # Window 2 + ( + self.marionette.absolute_url("layout/mozilla_organizations.html"), + self.marionette.absolute_url("layout/mozilla_community.html"), + ), + # Window 3 + ( + self.marionette.absolute_url("layout/mozilla_governance.html"), + self.marionette.absolute_url("layout/mozilla_grants.html"), + ), + ] + ) + + self.private_windows = set( + [ + ( + self.marionette.absolute_url("layout/mozilla_mission.html"), + self.marionette.absolute_url("layout/mozilla_organizations.html"), + ), + ( + self.marionette.absolute_url("layout/mozilla_projects.html"), + self.marionette.absolute_url("layout/mozilla_mission.html"), + ), + ] + ) + + self.marionette.enforce_gecko_prefs( + { + # Set browser restore previous session pref, + # depending on what the test requires. + "browser.startup.page": startup_page, + # Make the content load right away instead of waiting for + # the user to click on the background tabs + "browser.sessionstore.restore_on_demand": False, + # Avoid race conditions by having the content process never + # send us session updates unless the parent has explicitly asked + # for them via the TabStateFlusher. + "browser.sessionstore.debug.no_auto_updates": no_auto_updates, + # Whether to enable the register application restart mechanism. + "toolkit.winRegisterApplicationRestart": win_register_restart, + } + ) + + self.all_windows = self.test_windows.copy() + self.open_windows(self.test_windows) + + if include_private: + self.all_windows.update(self.private_windows) + self.open_windows(self.private_windows, is_private=True) + + def tearDown(self): + try: + # Create a fresh profile for subsequent tests. + self.marionette.restart(clean=True) + finally: + super(SessionStoreTestCase, self).tearDown() + + def open_windows(self, window_sets, is_private=False): + """Open a set of windows with tabs pointing at some URLs. + + @param window_sets (list) + A set of URL tuples. Each tuple within window_sets + represents a window, and each URL in the URL + tuples represents what will be loaded in a tab. + + Note that if is_private is False, then the first + URL tuple will be opened in the current window, and + subequent tuples will be opened in new windows. + + Example: + + set( + (self.marionette.absolute_url('layout/mozilla_1.html'), + self.marionette.absolute_url('layout/mozilla_2.html')), + + (self.marionette.absolute_url('layout/mozilla_3.html'), + self.marionette.absolute_url('layout/mozilla_4.html')), + ) + + This would take the currently open window, and load + mozilla_1.html and mozilla_2.html in new tabs. It would + then open a new, second window, and load tabs at + mozilla_3.html and mozilla_4.html. + @param is_private (boolean, optional) + Whether or not any new windows should be a private browsing + windows. + """ + if is_private: + win = self.open_window(private=True) + self.marionette.switch_to_window(win) + else: + win = self.marionette.current_chrome_window_handle + + for index, urls in enumerate(window_sets): + if index > 0: + win = self.open_window(private=is_private) + self.marionette.switch_to_window(win) + self.open_tabs(win, urls) + + def open_tabs(self, win, urls): + """Open a set of URLs inside a window in new tabs. + + @param win (browser window) + The browser window to load the tabs in. + @param urls (tuple) + A tuple of URLs to load in this window. The + first URL will be loaded in the currently selected + browser tab. Subsequent URLs will be loaded in + new tabs. + """ + # If there are any remaining URLs for this window, + # open some new tabs and navigate to them. + with self.marionette.using_context("content"): + if isinstance(urls, str): + self.marionette.navigate(urls) + else: + for index, url in enumerate(urls): + if index > 0: + tab = self.open_tab() + self.marionette.switch_to_window(tab) + self.marionette.navigate(url) + + def get_urls_for_window(self, win): + orig_handle = self.marionette.current_chrome_window_handle + + try: + with self.marionette.using_context("chrome"): + self.marionette.switch_to_window(win) + return self.marionette.execute_script( + """ + return gBrowser.tabs.map(tab => { + return tab.linkedBrowser.currentURI.spec; + }); + """ + ) + finally: + self.marionette.switch_to_window(orig_handle) + + def convert_open_windows_to_set(self): + # There's no guarantee that Marionette will return us an + # iterator for the opened windows that will match the + # order within our window list. Instead, we'll convert + # the list of URLs within each open window to a set of + # tuples that will allow us to do a direct comparison + # while allowing the windows to be in any order. + opened_windows = set() + for win in self.marionette.chrome_window_handles: + urls = tuple(self.get_urls_for_window(win)) + opened_windows.add(urls) + + return opened_windows + + def simulate_os_shutdown(self): + """Simulate an OS shutdown. + + :raises: Exception: if not supported on the current platform + :raises: WindowsError: if a Windows API call failed + """ + if self.marionette.session_capabilities["platformName"] != "windows": + raise Exception("Unsupported platform for simulate_os_shutdown") + + self._shutdown_with_windows_restart_manager(self.marionette.process_id) + + def _shutdown_with_windows_restart_manager(self, pid): + """Shut down a process using the Windows Restart Manager. + + When Windows shuts down, it uses a protocol including the + WM_QUERYENDSESSION and WM_ENDSESSION messages to give + applications a chance to shut down safely. The best way to + simulate this is via the Restart Manager, which allows a process + (such as an installer) to use the same mechanism to shut down + any other processes which are using registered resources. + + This function starts a Restart Manager session, registers the + process as a resource, and shuts down the process. + + :param pid: The process id (int) of the process to shutdown + + :raises: WindowsError: if a Windows API call fails + """ + import ctypes + from ctypes import Structure, POINTER, WINFUNCTYPE, windll, pointer, WinError + from ctypes.wintypes import HANDLE, DWORD, BOOL, WCHAR, UINT, ULONG, LPCWSTR + + # set up Windows SDK types + OpenProcess = windll.kernel32.OpenProcess + OpenProcess.restype = HANDLE + OpenProcess.argtypes = [ + DWORD, # dwDesiredAccess + BOOL, # bInheritHandle + DWORD, + ] # dwProcessId + PROCESS_QUERY_INFORMATION = 0x0400 + + class FILETIME(Structure): + _fields_ = [("dwLowDateTime", DWORD), ("dwHighDateTime", DWORD)] + + LPFILETIME = POINTER(FILETIME) + + GetProcessTimes = windll.kernel32.GetProcessTimes + GetProcessTimes.restype = BOOL + GetProcessTimes.argtypes = [ + HANDLE, # hProcess + LPFILETIME, # lpCreationTime + LPFILETIME, # lpExitTime + LPFILETIME, # lpKernelTime + LPFILETIME, + ] # lpUserTime + + ERROR_SUCCESS = 0 + + class RM_UNIQUE_PROCESS(Structure): + _fields_ = [("dwProcessId", DWORD), ("ProcessStartTime", FILETIME)] + + RmStartSession = windll.rstrtmgr.RmStartSession + RmStartSession.restype = DWORD + RmStartSession.argtypes = [ + POINTER(DWORD), # pSessionHandle + DWORD, # dwSessionFlags + POINTER(WCHAR), + ] # strSessionKey + + class GUID(ctypes.Structure): + _fields_ = [ + ("Data1", ctypes.c_ulong), + ("Data2", ctypes.c_ushort), + ("Data3", ctypes.c_ushort), + ("Data4", ctypes.c_ubyte * 8), + ] + + CCH_RM_SESSION_KEY = ctypes.sizeof(GUID) * 2 + + RmRegisterResources = windll.rstrtmgr.RmRegisterResources + RmRegisterResources.restype = DWORD + RmRegisterResources.argtypes = [ + DWORD, # dwSessionHandle + UINT, # nFiles + POINTER(LPCWSTR), # rgsFilenames + UINT, # nApplications + POINTER(RM_UNIQUE_PROCESS), # rgApplications + UINT, # nServices + POINTER(LPCWSTR), + ] # rgsServiceNames + + RM_WRITE_STATUS_CALLBACK = WINFUNCTYPE(None, UINT) + RmShutdown = windll.rstrtmgr.RmShutdown + RmShutdown.restype = DWORD + RmShutdown.argtypes = [ + DWORD, # dwSessionHandle + ULONG, # lActionFlags + RM_WRITE_STATUS_CALLBACK, + ] # fnStatus + + RmEndSession = windll.rstrtmgr.RmEndSession + RmEndSession.restype = DWORD + RmEndSession.argtypes = [DWORD] # dwSessionHandle + + # Get the info needed to uniquely identify the process + hProc = OpenProcess(PROCESS_QUERY_INFORMATION, False, pid) + if not hProc: + raise WinError() + + creationTime = FILETIME() + exitTime = FILETIME() + kernelTime = FILETIME() + userTime = FILETIME() + if not GetProcessTimes( + hProc, + pointer(creationTime), + pointer(exitTime), + pointer(kernelTime), + pointer(userTime), + ): + raise WinError() + + # Start the Restart Manager Session + dwSessionHandle = DWORD() + sessionKeyType = WCHAR * (CCH_RM_SESSION_KEY + 1) + sessionKey = sessionKeyType() + if RmStartSession(pointer(dwSessionHandle), 0, sessionKey) != ERROR_SUCCESS: + raise WinError() + + try: + UProcs_count = 1 + UProcsArrayType = RM_UNIQUE_PROCESS * UProcs_count + UProcs = UProcsArrayType(RM_UNIQUE_PROCESS(pid, creationTime)) + + # Register the process as a resource + if ( + RmRegisterResources( + dwSessionHandle, 0, None, UProcs_count, UProcs, 0, None + ) + != ERROR_SUCCESS + ): + raise WinError() + + # Shut down all processes using registered resources + if ( + RmShutdown( + dwSessionHandle, 0, ctypes.cast(None, RM_WRITE_STATUS_CALLBACK) + ) + != ERROR_SUCCESS + ): + raise WinError() + + finally: + RmEndSession(dwSessionHandle) + + def windows_shutdown_with_variety(self, restart_by_os, expect_restore): + """Test restoring windows after Windows shutdown. + + Opens a set of windows, both standard and private, with + some number of tabs in them. Once the tabs have loaded, shuts down + the browser with the Windows Restart Manager and restarts the browser. + + This specifically exercises the Windows synchronous shutdown mechanism, + which terminates the process in response to the Restart Manager's + WM_ENDSESSION message. + + If restart_by_os is True, the -os-restarted arg is passed when restarting, + simulating being automatically restarted by the Restart Manager. + + If expect_restore is True, this ensures that the standard tabs have been + restored, and that the private ones have not. Otherwise it ensures that + no tabs and windows have been restored. + """ + current_windows_set = self.convert_open_windows_to_set() + self.assertEqual( + current_windows_set, + self.all_windows, + msg="Not all requested windows have been opened. Expected {}, got {}.".format( + self.all_windows, current_windows_set + ), + ) + + self.marionette.quit(in_app=True, callback=lambda: self.simulate_os_shutdown()) + + saved_args = self.marionette.instance.app_args + try: + if restart_by_os: + self.marionette.instance.app_args = ["-os-restarted"] + + self.marionette.start_session() + self.marionette.set_context("chrome") + finally: + self.marionette.instance.app_args = saved_args + + current_windows_set = self.convert_open_windows_to_set() + if expect_restore: + self.assertEqual( + current_windows_set, + self.test_windows, + msg="""Non private browsing windows should have + been restored. Expected {}, got {}. + """.format( + self.test_windows, current_windows_set + ), + ) + else: + self.assertEqual( + len(self.marionette.chrome_window_handles), + 1, + msg="Windows from last session shouldn`t have been restored.", + ) + self.assertEqual( + len(self.marionette.window_handles), + 1, + msg="Tabs from last session shouldn`t have been restored.", + ) diff --git a/testing/firefox-ui/tests/functional/sessionstore/test_restore_windows_after_restart_and_quit.py b/testing/firefox-ui/tests/functional/sessionstore/test_restore_windows_after_restart_and_quit.py new file mode 100644 index 0000000000..b96e2cd3c1 --- /dev/null +++ b/testing/firefox-ui/tests/functional/sessionstore/test_restore_windows_after_restart_and_quit.py @@ -0,0 +1,112 @@ +# 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/. + +# add this directory to the path +from __future__ import absolute_import +import sys +import os + +sys.path.append(os.path.dirname(__file__)) + +from session_store_test_case import SessionStoreTestCase + + +class TestSessionStoreEnabledAllWindows(SessionStoreTestCase): + def setUp(self, include_private=True): + """Setup for the test, enabling session restore. + + :param include_private: Whether to open private windows. + """ + super(TestSessionStoreEnabledAllWindows, self).setUp( + include_private=include_private, startup_page=3 + ) + + def test_with_variety(self): + """Test opening and restoring both standard and private windows. + + Opens a set of windows, both standard and private, with + some number of tabs in them. Once the tabs have loaded, restarts + the browser, and then ensures that the standard tabs have been + restored, and that the private ones have not. + """ + current_windows_set = self.convert_open_windows_to_set() + self.assertEqual( + current_windows_set, + self.all_windows, + msg="Not all requested windows have been opened. Expected {}, got {}.".format( + self.all_windows, current_windows_set + ), + ) + + self.marionette.quit(in_app=True) + self.marionette.start_session() + self.marionette.set_context("chrome") + + current_windows_set = self.convert_open_windows_to_set() + self.assertEqual( + current_windows_set, + self.test_windows, + msg="""Non private browsing windows should have + been restored. Expected {}, got {}. + """.format( + self.test_windows, current_windows_set + ), + ) + + +class TestSessionStoreEnabledNoPrivateWindows(TestSessionStoreEnabledAllWindows): + def setUp(self): + super(TestSessionStoreEnabledNoPrivateWindows, self).setUp( + include_private=False + ) + + +class TestSessionStoreDisabled(SessionStoreTestCase): + def test_no_restore_with_quit(self): + current_windows_set = self.convert_open_windows_to_set() + self.assertEqual( + current_windows_set, + self.all_windows, + msg="Not all requested windows have been opened. Expected {}, got {}.".format( + self.all_windows, current_windows_set + ), + ) + + self.marionette.quit(in_app=True) + self.marionette.start_session() + self.marionette.set_context("chrome") + + self.assertEqual( + len(self.marionette.chrome_window_handles), + 1, + msg="Windows from last session shouldn`t have been restored.", + ) + self.assertEqual( + len(self.marionette.window_handles), + 1, + msg="Tabs from last session shouldn`t have been restored.", + ) + + def test_restore_with_restart(self): + current_windows_set = self.convert_open_windows_to_set() + self.assertEqual( + current_windows_set, + self.all_windows, + msg="Not all requested windows have been opened. Expected {}, got {}.".format( + self.all_windows, current_windows_set + ), + ) + + self.marionette.restart(in_app=True) + + current_windows_set = self.convert_open_windows_to_set() + self.assertEqual( + current_windows_set, + self.test_windows, + msg="""Non private browsing windows should have + been restored. Expected {}, got {}. + """.format( + self.test_windows, current_windows_set + ), + ) diff --git a/testing/firefox-ui/tests/functional/sessionstore/test_restore_windows_after_windows_shutdown.py b/testing/firefox-ui/tests/functional/sessionstore/test_restore_windows_after_windows_shutdown.py new file mode 100644 index 0000000000..7c5bf449e4 --- /dev/null +++ b/testing/firefox-ui/tests/functional/sessionstore/test_restore_windows_after_windows_shutdown.py @@ -0,0 +1,67 @@ +# 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/. + +# add this directory to the path +from __future__ import absolute_import +import sys +import os + +sys.path.append(os.path.dirname(__file__)) + +from session_store_test_case import SessionStoreTestCase + +# We test the following combinations with simulated Windows shutdown: +# - Start page = restore session (expect restore in all cases) +# - RAR (toolkit.winRegisterApplicationRestart) disabled +# - RAR enabled, restarted manually +# +# - Start page = home +# - RAR disabled (no restore) +# - RAR enabled: +# - restarted by OS (restore) +# - restarted manually (no restore) + + +class TestWindowsShutdown(SessionStoreTestCase): + def setUp(self): + super(TestWindowsShutdown, self).setUp(startup_page=3, no_auto_updates=False) + + def test_with_variety(self): + """Test session restore selected by user.""" + self.windows_shutdown_with_variety(restart_by_os=False, expect_restore=True) + + +class TestWindowsShutdownRegisterRestart(SessionStoreTestCase): + def setUp(self): + super(TestWindowsShutdownRegisterRestart, self).setUp( + startup_page=3, no_auto_updates=False, win_register_restart=True + ) + + def test_manual_restart(self): + """Test that restore tabs works in case of register restart failure.""" + self.windows_shutdown_with_variety(restart_by_os=False, expect_restore=True) + + +class TestWindowsShutdownNormal(SessionStoreTestCase): + def setUp(self): + super(TestWindowsShutdownNormal, self).setUp(no_auto_updates=False) + + def test_with_variety(self): + """Test that windows are not restored on a normal restart.""" + self.windows_shutdown_with_variety(restart_by_os=False, expect_restore=False) + + +class TestWindowsShutdownForcedSessionRestore(SessionStoreTestCase): + def setUp(self): + super(TestWindowsShutdownForcedSessionRestore, self).setUp( + no_auto_updates=False, win_register_restart=True + ) + + def test_os_restart(self): + """Test that register application restart restores the session.""" + self.windows_shutdown_with_variety(restart_by_os=True, expect_restore=True) + + def test_manual_restart(self): + """Test that OS shutdown is ignored on manual start.""" + self.windows_shutdown_with_variety(restart_by_os=False, expect_restore=False) |