diff options
Diffstat (limited to '')
97 files changed, 12510 insertions, 0 deletions
diff --git a/testing/marionette/harness/marionette_harness/tests/unit-tests.toml b/testing/marionette/harness/marionette_harness/tests/unit-tests.toml new file mode 100644 index 0000000000..26f6f559f0 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit-tests.toml @@ -0,0 +1,43 @@ +# The tests within this file are exclusively executed when `mach marionette-test` +# is called without specifying a test path. In case a specific test or manifest +# is provided, only that particular test or manifest is executed. Alternatively, +# by using a path prefix, any manifest file is recursively searched for under +# the specified path. +# +# Note: When adding a new top-level manifest file please also add a reference +# to the `MARIONETTE_MANIFESTS` entry in the appropriate `moz.build` file to +# allow the execution of tests via `mach test` and as part of the test package +# as well. + +[DEFAULT] +# marionette unit tests +["include:unit/unit-tests.toml"] + +# DOM tests +["include:../../../../../dom/cache/test/marionette/manifest.toml"] +["include:../../../../../dom/indexedDB/test/marionette/manifest.toml"] +["include:../../../../../dom/quota/test/marionette/manifest.toml"] +["include:../../../../../dom/workers/test/marionette/manifest.toml"] + +# browser tests +["include:../../../../../browser/components/tests/marionette/manifest.toml"] +["include:../../../../../browser/components/migration/tests/marionette/manifest.toml"] +["include:../../../../../browser/components/places/tests/marionette/manifest.toml"] +["include:../../../../../browser/components/search/test/marionette/manifest.toml"] +["include:../../../../../browser/components/sessionstore/test/marionette/manifest.toml"] + +# extensions tests +["include:../../../../../extensions/pref/autoconfig/test/marionette/manifest.toml"] + +# layout tests +["include:../../../../../layout/base/tests/marionette/manifest.toml"] + +# netwerk tests +["include:../../../../../netwerk/test/marionette/manifest.toml"] + +# toolkit tests +["include:../../../../../toolkit/components/cleardata/tests/marionette/manifest.toml"] +["include:../../../../../toolkit/xre/test/marionette/marionette.toml"] + +# update tests +["include:../../../../../toolkit/mozapps/update/tests/marionette/marionette.toml"] diff --git a/testing/marionette/harness/marionette_harness/tests/unit/data/test.html b/testing/marionette/harness/marionette_harness/tests/unit/data/test.html new file mode 100644 index 0000000000..8334cf0a2e --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/data/test.html @@ -0,0 +1,13 @@ +<!-- 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/. --> + +<!DOCTYPE html> +<html> +<head> +<title>Marionette Test</title> +</head> +<body> + <p id="file-url">Loaded via file://</p> +</body> +</html> diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_accessibility.py b/testing/marionette/harness/marionette_harness/tests/unit/test_accessibility.py new file mode 100644 index 0000000000..112a6974d1 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_accessibility.py @@ -0,0 +1,241 @@ +# 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/. + +import sys +import unittest + +from marionette_driver.by import By +from marionette_driver.errors import ( + ElementNotAccessibleException, + ElementNotInteractableException, + ElementClickInterceptedException, +) + +from marionette_harness import MarionetteTestCase + + +class TestAccessibility(MarionetteTestCase): + def setUp(self): + super(TestAccessibility, self).setUp() + with self.marionette.using_context("chrome"): + self.marionette.set_pref("dom.ipc.processCount", 1) + + def tearDown(self): + with self.marionette.using_context("chrome"): + self.marionette.clear_pref("dom.ipc.processCount") + + # Elements that are accessible with and without the accessibliity API + valid_elementIDs = [ + # Button1 is an accessible button with a valid accessible name + # computed from subtree + "button1", + # Button2 is an accessible button with a valid accessible name + # computed from aria-label + "button2", + # Button13 is an accessible button that is implemented via role="button" + # and is explorable using tabindex="0" + "button13", + # button17 is an accessible button that overrides parent's + # pointer-events:none; property with its own pointer-events:all; + "button17", + ] + + # Elements that are not accessible with the accessibility API + invalid_elementIDs = [ + # Button3 does not have an accessible object + "button3", + # Button4 does not support any accessible actions + "button4", + # Button5 does not have a correct accessibility role and may not be + # manipulated via the accessibility API + "button5", + # Button6 is missing an accessible name + "button6", + # Button7 is not currently visible via the accessibility API and may + # not be manipulated by it + "button7", + # Button8 is not currently visible via the accessibility API and may + # not be manipulated by it (in hidden subtree) + "button8", + # Button14 is accessible button but is not explorable because of lack + # of tabindex that would make it focusable. + "button14", + ] + + # Elements that are either accessible to accessibility API or not accessible + # at all + falsy_elements = [ + # Element is only visible to the accessibility API and may be + # manipulated by it + "button9", + # Element is not currently visible + "button10", + ] + + displayed_elementIDs = ["button1", "button2", "button4", "button5", "button6"] + + displayed_but_have_no_accessible_elementIDs = [ + # Button3 does not have an accessible object + "button3", + # Button 7 is hidden with aria-hidden set to true + "button7", + # Button 8 is inside an element with aria-hidden set to true + "button8", + "no_accessible_but_displayed", + ] + + disabled_elementIDs = ["button11", "no_accessible_but_disabled"] + + # Elements that are enabled but otherwise disabled or not explorable + # via the accessibility API + aria_disabled_elementIDs = ["button12"] + + # pointer-events: "none", which will return + # ElementClickInterceptedException if clicked + # when Marionette switches + # to using WebDriver conforming interaction + pointer_events_none_elementIDs = ["button15", "button16"] + + # Elements that are reporting selected state + valid_option_elementIDs = ["option1", "option2"] + + def run_element_test(self, ids, testFn): + for id in ids: + element = self.marionette.find_element(By.ID, id) + testFn(element) + + def setup_accessibility(self, enable_a11y_checks=True, navigate=True): + self.marionette.delete_session() + self.marionette.start_session({"moz:accessibilityChecks": enable_a11y_checks}) + self.assertEqual( + self.marionette.session_capabilities["moz:accessibilityChecks"], + enable_a11y_checks, + ) + + # Navigate to test_accessibility.html + if navigate: + test_accessibility = self.marionette.absolute_url("test_accessibility.html") + self.marionette.navigate(test_accessibility) + + def test_valid_click(self): + self.setup_accessibility() + # No exception should be raised + self.run_element_test(self.valid_elementIDs, lambda button: button.click()) + + def test_click_raises_element_not_accessible(self): + self.setup_accessibility() + self.run_element_test( + self.invalid_elementIDs, + lambda button: self.assertRaises( + ElementNotAccessibleException, button.click + ), + ) + self.run_element_test( + self.falsy_elements, + lambda button: self.assertRaises( + ElementNotInteractableException, button.click + ), + ) + + def test_click_raises_no_exceptions(self): + self.setup_accessibility(False, True) + # No exception should be raised + self.run_element_test(self.invalid_elementIDs, lambda button: button.click()) + # Elements are invisible + self.run_element_test( + self.falsy_elements, + lambda button: self.assertRaises( + ElementNotInteractableException, button.click + ), + ) + + def test_element_visible_but_not_visible_to_accessbility(self): + self.setup_accessibility() + # Elements are displayed but hidden from accessibility API + self.run_element_test( + self.displayed_but_have_no_accessible_elementIDs, + lambda element: self.assertRaises( + ElementNotAccessibleException, element.is_displayed + ), + ) + + def test_element_is_visible_to_accessibility(self): + self.setup_accessibility() + # No exception should be raised + self.run_element_test( + self.displayed_elementIDs, lambda element: element.is_displayed() + ) + + def test_element_is_not_enabled_to_accessbility(self): + self.setup_accessibility() + # Buttons are enabled but disabled/not-explorable via the accessibility API + self.run_element_test( + self.aria_disabled_elementIDs, + lambda element: self.assertRaises( + ElementNotAccessibleException, element.is_enabled + ), + ) + self.run_element_test( + self.pointer_events_none_elementIDs, + lambda element: self.assertRaises( + ElementNotAccessibleException, element.is_enabled + ), + ) + + # Buttons are enabled but disabled/not-explorable via + # the accessibility API and thus are not clickable via the + # accessibility API. + self.run_element_test( + self.aria_disabled_elementIDs, + lambda element: self.assertRaises( + ElementNotAccessibleException, element.click + ), + ) + # To be removed with bug 1405967 + if not self.marionette.session_capabilities["moz:webdriverClick"]: + self.run_element_test( + self.pointer_events_none_elementIDs, + lambda element: self.assertRaises( + ElementNotAccessibleException, element.click + ), + ) + + self.setup_accessibility(False, False) + self.run_element_test( + self.aria_disabled_elementIDs, lambda element: element.is_enabled() + ) + self.run_element_test( + self.pointer_events_none_elementIDs, lambda element: element.is_enabled() + ) + self.run_element_test( + self.aria_disabled_elementIDs, lambda element: element.click() + ) + # To be removed with bug 1405967 + if not self.marionette.session_capabilities["moz:webdriverClick"]: + self.run_element_test( + self.pointer_events_none_elementIDs, lambda element: element.click() + ) + + def test_element_is_enabled_to_accessibility(self): + self.setup_accessibility() + # No exception should be raised + self.run_element_test( + self.disabled_elementIDs, lambda element: element.is_enabled() + ) + + def test_send_keys_raises_no_exception(self): + self.setup_accessibility() + # Sending keys to valid input should not raise any exceptions + self.run_element_test(["input1"], lambda element: element.send_keys("a")) + + def test_is_selected_raises_no_exception(self): + self.setup_accessibility() + # No exception should be raised for valid options + self.run_element_test( + self.valid_option_elementIDs, lambda element: element.is_selected() + ) + # No exception should be raised for non-selectable elements + self.run_element_test( + self.valid_elementIDs, lambda element: element.is_selected() + ) diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_actions_key.py b/testing/marionette/harness/marionette_harness/tests/unit/test_actions_key.py new file mode 100644 index 0000000000..9f28b8eb4f --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_actions_key.py @@ -0,0 +1,71 @@ +# 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 six.moves.urllib.parse import quote + +from marionette_driver.by import By +from marionette_driver.keys import Keys +from marionette_harness import MarionetteTestCase, WindowManagerMixin + + +def inline(doc): + return "data:text/html;charset=utf-8,{}".format(quote(doc)) + + +class TestKeyActions(WindowManagerMixin, MarionetteTestCase): + def setUp(self): + super(TestKeyActions, self).setUp() + self.key_chain = self.marionette.actions.sequence("key", "keyboard_id") + + if self.marionette.session_capabilities["platformName"] == "mac": + self.mod_key = Keys.META + else: + self.mod_key = Keys.CONTROL + + test_html = self.marionette.absolute_url("keyboard.html") + self.marionette.navigate(test_html) + self.reporter_element = self.marionette.find_element(By.ID, "keyReporter") + self.reporter_element.click() + + def tearDown(self): + self.marionette.actions.release() + + super(TestKeyActions, self).tearDown() + + @property + def key_reporter_value(self): + return self.reporter_element.get_property("value") + + def test_basic_input(self): + self.key_chain.key_down("a").key_down("b").key_down("c").perform() + self.assertEqual(self.key_reporter_value, "abc") + + def test_upcase_input(self): + self.key_chain.key_down(Keys.SHIFT).key_down("a").key_up(Keys.SHIFT).key_down( + "b" + ).key_down("c").perform() + self.assertEqual(self.key_reporter_value, "Abc") + + def test_replace_input(self): + self.key_chain.key_down("a").key_down("b").key_down("c").perform() + self.assertEqual(self.key_reporter_value, "abc") + + self.key_chain.key_down(self.mod_key).key_down("a").key_up( + self.mod_key + ).key_down("x").perform() + self.assertEqual(self.key_reporter_value, "x") + + def test_clear_input(self): + self.key_chain.key_down("a").key_down("b").key_down("c").perform() + self.assertEqual(self.key_reporter_value, "abc") + + self.key_chain.key_down(self.mod_key).key_down("a").key_down("x").perform() + self.assertEqual(self.key_reporter_value, "") + + def test_input_with_wait(self): + self.key_chain.key_down("a").key_down("b").key_down("c").perform() + self.key_chain.key_down(self.mod_key).key_down("a").pause(250).key_down( + "x" + ).perform() + self.assertEqual(self.key_reporter_value, "") diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_actions_pointer.py b/testing/marionette/harness/marionette_harness/tests/unit/test_actions_pointer.py new file mode 100644 index 0000000000..1e21316c52 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_actions_pointer.py @@ -0,0 +1,134 @@ +# 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 six.moves.urllib.parse import quote + +from marionette_driver import By, errors, Wait +from marionette_driver.keys import Keys + +from marionette_harness import MarionetteTestCase + + +def inline(doc): + return "data:text/html;charset=utf-8,{}".format(quote(doc)) + + +class BaseMouseAction(MarionetteTestCase): + def setUp(self): + super(BaseMouseAction, self).setUp() + self.mouse_chain = self.marionette.actions.sequence( + "pointer", "pointer_id", {"pointerType": "mouse"} + ) + + if self.marionette.session_capabilities["platformName"] == "mac": + self.mod_key = Keys.META + else: + self.mod_key = Keys.CONTROL + + def tearDown(self): + self.marionette.actions.release() + + super(BaseMouseAction, self).tearDown() + + @property + def click_position(self): + return self.marionette.execute_script( + """ + if (window.click_x && window.click_y) { + return {x: window.click_x, y: window.click_y}; + } + """, + sandbox=None, + ) + + def get_element_center_point(self, elem): + # pylint --py3k W1619 + return { + "x": elem.rect["x"] + elem.rect["width"] / 2, + "y": elem.rect["y"] + elem.rect["height"] / 2, + } + + +class TestPointerActions(BaseMouseAction): + def test_click_action(self): + test_html = self.marionette.absolute_url("test.html") + self.marionette.navigate(test_html) + link = self.marionette.find_element(By.ID, "mozLink") + self.mouse_chain.click(element=link).perform() + self.assertEqual( + "Clicked", + self.marionette.execute_script( + "return document.getElementById('mozLink').innerHTML" + ), + ) + + def test_clicking_element_out_of_view(self): + self.marionette.navigate( + inline( + """ + <div style="position:relative;top:200vh;">foo</div> + """ + ) + ) + el = self.marionette.find_element(By.TAG_NAME, "div") + with self.assertRaises(errors.MoveTargetOutOfBoundsException): + self.mouse_chain.click(element=el).perform() + + def test_double_click_action(self): + self.marionette.navigate( + inline( + """ + <script>window.eventCount = 0;</script> + <button onclick="window.eventCount++">foobar</button> + """ + ) + ) + + el = self.marionette.find_element(By.CSS_SELECTOR, "button") + self.mouse_chain.click(el).pause(100).click(el).perform() + + event_count = self.marionette.execute_script( + "return window.eventCount", sandbox=None + ) + self.assertEqual(event_count, 2) + + def test_context_click_action(self): + test_html = self.marionette.absolute_url("clicks.html") + self.marionette.navigate(test_html) + click_el = self.marionette.find_element(By.ID, "normal") + + def context_menu_state(): + with self.marionette.using_context("chrome"): + cm_el = self.marionette.find_element(By.ID, "contentAreaContextMenu") + return cm_el.get_property("state") + + self.assertEqual("closed", context_menu_state()) + self.mouse_chain.click(element=click_el, button=2).perform() + Wait(self.marionette).until( + lambda _: context_menu_state() == "open", + message="Context menu did not open", + ) + with self.marionette.using_context("chrome"): + cm_el = self.marionette.find_element(By.ID, "contentAreaContextMenu") + self.marionette.execute_script( + "arguments[0].hidePopup()", script_args=(cm_el,) + ) + Wait(self.marionette).until( + lambda _: context_menu_state() == "closed", + message="Context menu did not close", + ) + + def test_middle_click_action(self): + test_html = self.marionette.absolute_url("clicks.html") + self.marionette.navigate(test_html) + + self.marionette.find_element(By.ID, "addbuttonlistener").click() + + el = self.marionette.find_element(By.ID, "showbutton") + self.mouse_chain.click(element=el, button=1).perform() + + Wait(self.marionette).until( + lambda _: el.get_property("innerHTML") == "1", + message="Middle-click hasn't been fired", + ) diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_actions_wheel.py b/testing/marionette/harness/marionette_harness/tests/unit/test_actions_wheel.py new file mode 100644 index 0000000000..e74d9f6423 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_actions_wheel.py @@ -0,0 +1,68 @@ +# 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_harness import MarionetteTestCase, parameterized + + +class BaseWheelAction(MarionetteTestCase): + def setUp(self): + super(BaseWheelAction, self).setUp() + + self.test_page = self.marionette.absolute_url("actions_scroll.html") + self.marionette.navigate(self.test_page) + + self.wheel_chain = self.marionette.actions.sequence("wheel", "wheel_id") + + def tearDown(self): + self.marionette.actions.release() + + super(BaseWheelAction, self).tearDown() + + def get_events(self): + return self.marionette.execute_script("return allEvents.events;", sandbox=None) + + +class TestWheelAction(BaseWheelAction): + def test_scroll_not_scrollable(self): + target = self.marionette.find_element(By.ID, "not-scrollable") + + self.wheel_chain.scroll(0, 0, 5, 10, origin=target, duration=0).perform() + + events = self.get_events() + self.assertEqual(len(events), 1) + self.assertEqual(events[0]["type"], "wheel") + self.assertEqual(events[0]["deltaX"], 5) + self.assertEqual(events[0]["deltaY"], 10) + self.assertEqual(events[0]["deltaZ"], 0) + self.assertEqual(events[0]["target"], "not-scrollable-content") + + def test_scroll_scrollable(self): + target = self.marionette.find_element(By.ID, "scrollable") + self.wheel_chain.scroll(0, 0, 5, 10, origin=target).perform() + + events = self.get_events() + self.assertEqual(len(events), 1) + self.assertEqual(events[0]["type"], "wheel") + self.assertEqual(events[0]["deltaX"], 5) + self.assertEqual(events[0]["deltaY"], 10) + self.assertEqual(events[0]["deltaZ"], 0) + self.assertEqual(events[0]["target"], "scrollable-content") + + def test_scroll_iframe_scrollable(self): + iframe = self.marionette.find_element(By.ID, "iframe") + self.marionette.switch_to_frame(iframe) + + target = self.marionette.find_element(By.ID, "iframeContent") + self.wheel_chain.scroll(0, 0, 5, 10, origin=target).perform() + + self.marionette.switch_to_frame() + + events = self.get_events() + self.assertEqual(len(events), 1) + self.assertEqual(events[0]["type"], "wheel") + self.assertEqual(events[0]["deltaX"], 5) + self.assertEqual(events[0]["deltaY"], 10) + self.assertEqual(events[0]["deltaZ"], 0) + self.assertEqual(events[0]["target"], "iframeContent") diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_addons.py b/testing/marionette/harness/marionette_harness/tests/unit/test_addons.py new file mode 100644 index 0000000000..1611739e5f --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_addons.py @@ -0,0 +1,140 @@ +# 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/. + +import os +import sys +from unittest import skipIf + +from marionette_driver.addons import Addons, AddonInstallException +from marionette_driver.errors import UnknownException +from marionette_harness import MarionetteTestCase + + +here = os.path.abspath(os.path.dirname(__file__)) + + +class TestAddons(MarionetteTestCase): + def setUp(self): + super(TestAddons, self).setUp() + + self.addons = Addons(self.marionette) + self.preinstalled_addons = self.all_addon_ids + + def tearDown(self): + self.reset_addons() + + super(TestAddons, self).tearDown() + + @property + def all_addon_ids(self): + with self.marionette.using_context("chrome"): + addons = self.marionette.execute_async_script( + """ + const [resolve] = arguments; + const { AddonManager } = ChromeUtils.import( + "resource://gre/modules/AddonManager.jsm" + ); + + async function getAllAddons() { + const addons = await AddonManager.getAllAddons(); + const ids = addons.map(x => x.id); + resolve(ids); + } + + getAllAddons(); + """ + ) + + return set(addons) + + def reset_addons(self): + with self.marionette.using_context("chrome"): + for addon in self.all_addon_ids - self.preinstalled_addons: + addon_id = self.marionette.execute_async_script( + """ + const [addonId, resolve] = arguments; + const { AddonManager } = ChromeUtils.import( + "resource://gre/modules/AddonManager.jsm" + ); + + async function uninstall() { + const addon = await AddonManager.getAddonByID(addonId); + addon.uninstall(); + resolve(addon.id); + } + + uninstall(); + """, + script_args=(addon,), + ) + self.assertEqual( + addon_id, addon, msg="Failed to uninstall {}".format(addon) + ) + + def test_temporary_install_and_remove_unsigned_addon(self): + addon_path = os.path.join(here, "webextension-unsigned.xpi") + + addon_id = self.addons.install(addon_path, temp=True) + self.assertIn(addon_id, self.all_addon_ids) + self.assertEqual(addon_id, "{d3e7c1f1-2e35-4a49-89fe-9f46eb8abf0a}") + + self.addons.uninstall(addon_id) + self.assertNotIn(addon_id, self.all_addon_ids) + + def test_temporary_install_invalid_addon(self): + addon_path = os.path.join(here, "webextension-invalid.xpi") + + with self.assertRaises(AddonInstallException): + self.addons.install(addon_path, temp=True) + self.assertNotIn("{d3e7c1f1-2e35-4a49-89fe-9f46eb8abf0a}", self.all_addon_ids) + + def test_install_and_remove_signed_addon(self): + addon_path = os.path.join(here, "webextension-signed.xpi") + + addon_id = self.addons.install(addon_path) + self.assertIn(addon_id, self.all_addon_ids) + self.assertEqual(addon_id, "{d3e7c1f1-2e35-4a49-89fe-9f46eb8abf0a}") + + self.addons.uninstall(addon_id) + self.assertNotIn(addon_id, self.all_addon_ids) + + def test_install_invalid_addon(self): + addon_path = os.path.join(here, "webextension-invalid.xpi") + + with self.assertRaises(AddonInstallException): + self.addons.install(addon_path) + self.assertNotIn("{d3e7c1f1-2e35-4a49-89fe-9f46eb8abf0a}", self.all_addon_ids) + + def test_install_unsigned_addon_fails(self): + addon_path = os.path.join(here, "webextension-unsigned.xpi") + + with self.assertRaises(AddonInstallException): + self.addons.install(addon_path) + + def test_install_nonexistent_addon(self): + addon_path = os.path.join(here, "does-not-exist.xpi") + + with self.assertRaises(AddonInstallException): + self.addons.install(addon_path) + + def test_install_with_relative_path(self): + with self.assertRaises(AddonInstallException): + self.addons.install("webextension.xpi") + + @skipIf(sys.platform != "win32", "Only makes sense on Windows") + def test_install_mixed_separator_windows(self): + # Ensure the base path has only \ + addon_path = here.replace("/", "\\") + addon_path += "/webextension-signed.xpi" + + addon_id = self.addons.install(addon_path, temp=True) + self.assertIn(addon_id, self.all_addon_ids) + self.assertEqual(addon_id, "{d3e7c1f1-2e35-4a49-89fe-9f46eb8abf0a}") + + self.addons.uninstall(addon_id) + self.assertNotIn(addon_id, self.all_addon_ids) + + def test_uninstall_nonexistent_addon(self): + with self.assertRaises(UnknownException): + self.addons.uninstall("i-do-not-exist-as-an-id") diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_capabilities.py b/testing/marionette/harness/marionette_harness/tests/unit/test_capabilities.py new file mode 100644 index 0000000000..0cdaf8343f --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_capabilities.py @@ -0,0 +1,322 @@ +# 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/. + +import os +import sys +import unittest + +import marionette_driver.errors as errors +from marionette_harness import MarionetteTestCase + + +class TestCapabilities(MarionetteTestCase): + def setUp(self): + super(TestCapabilities, self).setUp() + self.caps = self.marionette.session_capabilities + with self.marionette.using_context("chrome"): + self.appinfo = self.marionette.execute_script( + """ + return { + name: Services.appinfo.name, + version: Services.appinfo.version, + processID: Services.appinfo.processID, + buildID: Services.appinfo.appBuildID, + } + """ + ) + self.os_name = self.marionette.execute_script( + """ + let name = Services.sysinfo.getProperty("name"); + switch (name) { + case "Windows_NT": + return "windows"; + case "Darwin": + return "mac"; + default: + return name.toLowerCase(); + } + """ + ) + self.os_version = self.marionette.execute_script( + "return Services.sysinfo.getProperty('version')" + ) + + def test_mandated_capabilities(self): + self.assertIn("acceptInsecureCerts", self.caps) + self.assertIn("browserName", self.caps) + self.assertIn("browserVersion", self.caps) + self.assertIn("platformName", self.caps) + self.assertIn("proxy", self.caps) + self.assertIn("setWindowRect", self.caps) + self.assertIn("strictFileInteractability", self.caps) + self.assertIn("timeouts", self.caps) + + self.assertFalse(self.caps["acceptInsecureCerts"]) + self.assertEqual(self.caps["browserName"], self.appinfo["name"].lower()) + self.assertEqual(self.caps["browserVersion"], self.appinfo["version"]) + self.assertEqual(self.caps["platformName"], self.os_name) + self.assertEqual(self.caps["proxy"], {}) + + if self.appinfo["name"] == "Firefox": + self.assertTrue(self.caps["setWindowRect"]) + else: + self.assertFalse(self.caps["setWindowRect"]) + self.assertTrue(self.caps["strictFileInteractability"]) + self.assertDictEqual( + self.caps["timeouts"], {"implicit": 0, "pageLoad": 300000, "script": 30000} + ) + + def test_additional_capabilities(self): + self.assertIn("moz:processID", self.caps) + self.assertEqual(self.caps["moz:processID"], self.appinfo["processID"]) + self.assertEqual(self.marionette.process_id, self.appinfo["processID"]) + + self.assertIn("moz:profile", self.caps) + if self.marionette.instance is not None: + if self.caps["browserName"] == "fennec": + current_profile = ( + self.marionette.instance.runner.device.app_ctx.remote_profile + ) + else: + current_profile = self.marionette.profile_path + # Bug 1438461 - mozprofile uses lower-case letters even on case-sensitive filesystems + # Bug 1533221 - paths may differ due to file system links or aliases + self.assertEqual( + os.path.basename(self.caps["moz:profile"]).lower(), + os.path.basename(current_profile).lower(), + ) + + self.assertIn("moz:accessibilityChecks", self.caps) + self.assertFalse(self.caps["moz:accessibilityChecks"]) + + self.assertIn("moz:buildID", self.caps) + self.assertEqual(self.caps["moz:buildID"], self.appinfo["buildID"]) + + self.assertNotIn("moz:debuggerAddress", self.caps) + + self.assertIn("moz:platformVersion", self.caps) + self.assertEqual(self.caps["moz:platformVersion"], self.os_version) + + self.assertIn("moz:webdriverClick", self.caps) + self.assertTrue(self.caps["moz:webdriverClick"]) + + self.assertIn("moz:windowless", self.caps) + self.assertFalse(self.caps["moz:windowless"]) + + # No longer supported capabilities + self.assertNotIn("moz:useNonSpecCompliantPointerOrigin", self.caps) + + def test_disable_webdriver_click(self): + self.marionette.delete_session() + self.marionette.start_session({"moz:webdriverClick": False}) + caps = self.marionette.session_capabilities + self.assertFalse(caps["moz:webdriverClick"]) + + def test_no_longer_supported_capabilities(self): + self.marionette.delete_session() + with self.assertRaisesRegexp( + errors.SessionNotCreatedException, "InvalidArgumentError" + ): + self.marionette.start_session( + {"moz:useNonSpecCompliantPointerOrigin": True} + ) + + def test_valid_uuid4_when_creating_a_session(self): + self.assertNotIn( + "{", + self.marionette.session_id, + "Session ID has {{}} in it: {}".format(self.marionette.session_id), + ) + + def test_windowless_false(self): + self.marionette.delete_session() + self.marionette.start_session({"moz:windowless": False}) + caps = self.marionette.session_capabilities + self.assertFalse(caps["moz:windowless"]) + + @unittest.skipUnless(sys.platform.startswith("darwin"), "Only supported on MacOS") + def test_windowless_true(self): + self.marionette.delete_session() + self.marionette.start_session({"moz:windowless": True}) + caps = self.marionette.session_capabilities + self.assertTrue(caps["moz:windowless"]) + + +class TestCapabilityMatching(MarionetteTestCase): + def setUp(self): + MarionetteTestCase.setUp(self) + self.browser_name = self.marionette.session_capabilities["browserName"] + self.delete_session() + + def delete_session(self): + if self.marionette.session is not None: + self.marionette.delete_session() + + def test_accept_insecure_certs(self): + for value in ["", 42, {}, []]: + print(" type {}".format(type(value))) + with self.assertRaises(errors.SessionNotCreatedException): + self.marionette.start_session({"acceptInsecureCerts": value}) + + self.delete_session() + self.marionette.start_session({"acceptInsecureCerts": True}) + self.assertTrue(self.marionette.session_capabilities["acceptInsecureCerts"]) + + def test_page_load_strategy(self): + for strategy in ["none", "eager", "normal"]: + print("valid strategy {}".format(strategy)) + self.delete_session() + self.marionette.start_session({"pageLoadStrategy": strategy}) + self.assertEqual( + self.marionette.session_capabilities["pageLoadStrategy"], strategy + ) + + self.delete_session() + + for value in ["", "EAGER", True, 42, {}, []]: + print("invalid strategy {}".format(value)) + with self.assertRaisesRegexp( + errors.SessionNotCreatedException, "InvalidArgumentError" + ): + self.marionette.start_session({"pageLoadStrategy": value}) + + def test_set_window_rect(self): + with self.assertRaisesRegexp( + errors.SessionNotCreatedException, "InvalidArgumentError" + ): + self.marionette.start_session({"setWindowRect": False}) + + def test_timeouts(self): + for value in ["", 2.5, {}, []]: + print(" type {}".format(type(value))) + with self.assertRaises(errors.SessionNotCreatedException): + self.marionette.start_session({"timeouts": {"pageLoad": value}}) + + self.delete_session() + + timeouts = {"implicit": 0, "pageLoad": 2.0, "script": 2**53 - 1} + self.marionette.start_session({"timeouts": timeouts}) + self.assertIn("timeouts", self.marionette.session_capabilities) + self.assertDictEqual(self.marionette.session_capabilities["timeouts"], timeouts) + self.assertDictEqual( + self.marionette._send_message("WebDriver:GetTimeouts"), timeouts + ) + + def test_strict_file_interactability(self): + for value in ["", 2.5, {}, []]: + print(" type {}".format(type(value))) + with self.assertRaises(errors.SessionNotCreatedException): + self.marionette.start_session({"strictFileInteractability": value}) + + self.delete_session() + + self.marionette.start_session({"strictFileInteractability": True}) + self.assertIn("strictFileInteractability", self.marionette.session_capabilities) + self.assertTrue( + self.marionette.session_capabilities["strictFileInteractability"] + ) + + self.delete_session() + + self.marionette.start_session({"strictFileInteractability": False}) + self.assertIn("strictFileInteractability", self.marionette.session_capabilities) + self.assertFalse( + self.marionette.session_capabilities["strictFileInteractability"] + ) + + def test_unhandled_prompt_behavior(self): + behaviors = [ + "accept", + "accept and notify", + "dismiss", + "dismiss and notify", + "ignore", + ] + + for behavior in behaviors: + print("valid unhandled prompt behavior {}".format(behavior)) + self.delete_session() + self.marionette.start_session({"unhandledPromptBehavior": behavior}) + self.assertEqual( + self.marionette.session_capabilities["unhandledPromptBehavior"], + behavior, + ) + + # Default value + self.delete_session() + self.marionette.start_session() + self.assertEqual( + self.marionette.session_capabilities["unhandledPromptBehavior"], + "dismiss and notify", + ) + + # Invalid values + self.delete_session() + for behavior in ["", "ACCEPT", True, 42, {}, []]: + print("invalid unhandled prompt behavior {}".format(behavior)) + with self.assertRaisesRegexp( + errors.SessionNotCreatedException, "InvalidArgumentError" + ): + self.marionette.start_session({"unhandledPromptBehavior": behavior}) + + def test_web_socket_url(self): + self.marionette.start_session({"webSocketUrl": True}) + # Remote Agent is not active by default + self.assertNotIn("webSocketUrl", self.marionette.session_capabilities) + + def test_webauthn_extension_cred_blob(self): + for value in ["", 42, {}, []]: + print(" type {}".format(type(value))) + with self.assertRaises(errors.SessionNotCreatedException): + self.marionette.start_session({"webauthn:extension:credBlob": value}) + + self.delete_session() + self.marionette.start_session({"webauthn:extension:credBlob": True}) + self.assertTrue( + self.marionette.session_capabilities["webauthn:extension:credBlob"] + ) + + def test_webauthn_extension_large_blob(self): + for value in ["", 42, {}, []]: + print(" type {}".format(type(value))) + with self.assertRaises(errors.SessionNotCreatedException): + self.marionette.start_session({"webauthn:extension:largeBlob": value}) + + self.delete_session() + self.marionette.start_session({"webauthn:extension:largeBlob": True}) + self.assertTrue( + self.marionette.session_capabilities["webauthn:extension:largeBlob"] + ) + + def test_webauthn_extension_prf(self): + for value in ["", 42, {}, []]: + print(" type {}".format(type(value))) + with self.assertRaises(errors.SessionNotCreatedException): + self.marionette.start_session({"webauthn:extension:prf": value}) + + self.delete_session() + self.marionette.start_session({"webauthn:extension:prf": True}) + self.assertTrue(self.marionette.session_capabilities["webauthn:extension:prf"]) + + def test_webauthn_extension_uvm(self): + for value in ["", 42, {}, []]: + print(" type {}".format(type(value))) + with self.assertRaises(errors.SessionNotCreatedException): + self.marionette.start_session({"webauthn:extension:uvm": value}) + + self.delete_session() + self.marionette.start_session({"webauthn:extension:uvm": True}) + self.assertTrue(self.marionette.session_capabilities["webauthn:extension:uvm"]) + + def test_webauthn_virtual_authenticators(self): + for value in ["", 42, {}, []]: + print(" type {}".format(type(value))) + with self.assertRaises(errors.SessionNotCreatedException): + self.marionette.start_session({"webauthn:virtualAuthenticators": value}) + + self.delete_session() + self.marionette.start_session({"webauthn:virtualAuthenticators": True}) + self.assertTrue( + self.marionette.session_capabilities["webauthn:virtualAuthenticators"] + ) diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_checkbox.py b/testing/marionette/harness/marionette_harness/tests/unit/test_checkbox.py new file mode 100644 index 0000000000..8709d6e325 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_checkbox.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 marionette_driver.by import By + +from marionette_harness import MarionetteTestCase + + +class TestCheckbox(MarionetteTestCase): + def test_selected(self): + test_html = self.marionette.absolute_url("test.html") + self.marionette.navigate(test_html) + box = self.marionette.find_element(By.NAME, "myCheckBox") + self.assertFalse(box.is_selected()) + box.click() + self.assertTrue(box.is_selected()) diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_checkbox_chrome.py b/testing/marionette/harness/marionette_harness/tests/unit/test_checkbox_chrome.py new file mode 100644 index 0000000000..e8640d9021 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_checkbox_chrome.py @@ -0,0 +1,33 @@ +# 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.by import By + +from marionette_harness import MarionetteTestCase, WindowManagerMixin + + +class TestSelectedChrome(WindowManagerMixin, MarionetteTestCase): + def setUp(self): + super(TestSelectedChrome, self).setUp() + + self.marionette.set_context("chrome") + + new_window = self.open_chrome_window( + "chrome://remote/content/marionette/test.xhtml" + ) + self.marionette.switch_to_window(new_window) + + def tearDown(self): + try: + self.close_all_windows() + finally: + super(TestSelectedChrome, self).tearDown() + + def test_selected(self): + box = self.marionette.find_element(By.ID, "testBox") + self.assertFalse(box.is_selected()) + self.assertFalse( + self.marionette.execute_script("arguments[0].checked = true;", [box]) + ) + self.assertTrue(box.is_selected()) diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_chrome.py b/testing/marionette/harness/marionette_harness/tests/unit/test_chrome.py new file mode 100644 index 0000000000..664fbeeb37 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_chrome.py @@ -0,0 +1,31 @@ +from marionette_harness import MarionetteTestCase, WindowManagerMixin + + +class ChromeTests(WindowManagerMixin, MarionetteTestCase): + def setUp(self): + super(ChromeTests, self).setUp() + + def tearDown(self): + self.close_all_windows() + super(ChromeTests, self).tearDown() + + def test_hang_until_timeout(self): + with self.marionette.using_context("chrome"): + new_window = self.open_window() + self.marionette.switch_to_window(new_window) + + try: + try: + # Raise an exception type which should not be thrown by Marionette + # while running this test. Otherwise it would mask eg. IOError as + # thrown for a socket timeout. + raise NotImplementedError( + "Exception should not cause a hang when " + "closing the chrome window in content " + "context" + ) + finally: + self.marionette.close_chrome_window() + self.marionette.switch_to_window(self.start_window) + except NotImplementedError: + pass diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_chrome_action.py b/testing/marionette/harness/marionette_harness/tests/unit/test_chrome_action.py new file mode 100644 index 0000000000..fadabe9602 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_chrome_action.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 marionette_driver.keys import Keys + +from marionette_harness import MarionetteTestCase, WindowManagerMixin + + +class TestPointerActions(WindowManagerMixin, MarionetteTestCase): + def setUp(self): + super(TestPointerActions, self).setUp() + + self.mouse_chain = self.marionette.actions.sequence( + "pointer", "pointer_id", {"pointerType": "mouse"} + ) + self.key_chain = self.marionette.actions.sequence("key", "keyboard_id") + + if self.marionette.session_capabilities["platformName"] == "mac": + self.mod_key = Keys.META + else: + self.mod_key = Keys.CONTROL + + self.marionette.set_context("chrome") + + self.win = self.open_chrome_window( + "chrome://remote/content/marionette/test.xhtml" + ) + self.marionette.switch_to_window(self.win) + + def tearDown(self): + self.marionette.actions.release() + self.close_all_windows() + + super(TestPointerActions, self).tearDown() + + def test_click_action(self): + box = self.marionette.find_element(By.ID, "testBox") + box.get_property("localName") + self.assertFalse( + self.marionette.execute_script( + "return document.getElementById('testBox').checked" + ) + ) + self.mouse_chain.click(element=box).perform() + self.assertTrue( + self.marionette.execute_script( + "return document.getElementById('testBox').checked" + ) + ) + + def test_key_action(self): + self.marionette.find_element(By.ID, "textInput").click() + self.key_chain.send_keys("x").perform() + self.assertEqual( + self.marionette.execute_script( + "return document.getElementById('textInput').value" + ), + "testx", + ) diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_chrome_element_css.py b/testing/marionette/harness/marionette_harness/tests/unit/test_chrome_element_css.py new file mode 100644 index 0000000000..cbf326844e --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_chrome_element_css.py @@ -0,0 +1,31 @@ +# 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.by import By + +from marionette_harness import MarionetteTestCase + + +class TestChromeElementCSS(MarionetteTestCase): + def get_element_computed_style(self, element, property): + return self.marionette.execute_script( + """ + const [el, prop] = arguments; + const elStyle = window.getComputedStyle(el); + return elStyle[prop];""", + script_args=(element, property), + sandbox=None, + ) + + def test_we_can_get_css_value_on_chrome_element(self): + with self.marionette.using_context("chrome"): + identity_icon = self.marionette.find_element(By.ID, "identity-icon") + favicon_image = identity_icon.value_of_css_property("list-style-image") + self.assertIn("chrome://", favicon_image) + identity_box = self.marionette.find_element(By.ID, "identity-box") + expected_bg_colour = self.get_element_computed_style( + identity_box, "backgroundColor" + ) + actual_bg_colour = identity_box.value_of_css_property("background-color") + self.assertEqual(expected_bg_colour, actual_bg_colour) diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_cli_arguments.py b/testing/marionette/harness/marionette_harness/tests/unit/test_cli_arguments.py new file mode 100644 index 0000000000..c4bbbfad1b --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_cli_arguments.py @@ -0,0 +1,98 @@ +# 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/. + +import copy + +import requests + +from marionette_harness import MarionetteTestCase + + +class TestCommandLineArguments(MarionetteTestCase): + def setUp(self): + super(TestCommandLineArguments, self).setUp() + + self.orig_arguments = copy.copy(self.marionette.instance.app_args) + + def tearDown(self): + self.marionette.instance.app_args = self.orig_arguments + self.marionette.quit(in_app=False, clean=True) + + super(TestCommandLineArguments, self).tearDown() + + def test_debugger_address_cdp_status(self): + # By default Remote Agent is not enabled + debugger_address = self.marionette.session_capabilities.get( + "moz:debuggerAddress" + ) + self.assertIsNone(debugger_address) + + # With BiDi only enabled the capability shouldn't be returned + self.marionette.set_pref("remote.active-protocols", 1) + self.marionette.quit() + + self.marionette.instance.app_args.append("-remote-debugging-port") + self.marionette.start_session() + + debugger_address = self.marionette.session_capabilities.get( + "moz:debuggerAddress" + ) + self.assertIsNone(debugger_address) + + # Clean the profile so that the preference is definetely reset. + self.marionette.quit(in_app=False, clean=True) + + # With all protocols enabled again the capability has to be returned + self.marionette.start_session() + debugger_address = self.marionette.session_capabilities.get( + "moz:debuggerAddress" + ) + + self.assertEqual(debugger_address, "127.0.0.1:9222") + result = requests.get(url="http://{}/json/version".format(debugger_address)) + self.assertTrue(result.ok) + + def test_websocket_url(self): + # By default Remote Agent is not enabled + self.assertNotIn("webSocketUrl", self.marionette.session_capabilities) + + # With CDP only enabled the capability is still not returned + self.marionette.set_pref("remote.active-protocols", 2) + + self.marionette.quit() + self.marionette.instance.app_args.append("-remote-debugging-port") + self.marionette.start_session({"webSocketUrl": True}) + + self.assertNotIn("webSocketUrl", self.marionette.session_capabilities) + + # Clean the profile so that the preference is definetely reset. + self.marionette.quit(in_app=False, clean=True) + + # With all protocols enabled again the capability has to be returned + self.marionette.start_session({"webSocketUrl": True}) + + session_id = self.marionette.session_id + websocket_url = self.marionette.session_capabilities.get("webSocketUrl") + + self.assertEqual( + websocket_url, "ws://127.0.0.1:9222/session/{}".format(session_id) + ) + + # An issue in the command line argument handling lead to open Firefox on + # random URLs when remote-debugging-port is set to an explicit value, on macos. + # See Bug 1724251. + def test_start_page_about_blank(self): + self.marionette.quit() + self.marionette.instance.app_args.append("-remote-debugging-port=0") + self.marionette.start_session({"webSocketUrl": True}) + self.assertEqual(self.marionette.get_url(), "about:blank") + + def test_startup_timeout(self): + try: + self.marionette.quit() + with self.assertRaisesRegexp(IOError, "Process killed after 0s"): + # Use a small enough timeout which should always cause an IOError + self.marionette.start_session(timeout=0) + finally: + self.marionette.start_session() diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_click.py b/testing/marionette/harness/marionette_harness/tests/unit/test_click.py new file mode 100644 index 0000000000..5936be1e69 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_click.py @@ -0,0 +1,571 @@ +# 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/. + +import sys +from unittest import skipIf + +from six.moves.urllib.parse import quote + +from marionette_driver import By, errors +from marionette_driver.marionette import Alert + +from marionette_harness import ( + MarionetteTestCase, + WindowManagerMixin, +) + + +def inline(doc): + return "data:text/html;charset=utf-8,{}".format(quote(doc)) + + +# The <a> element in the following HTML is not interactable because it +# is hidden by an overlay when scrolled into the top of the viewport. +# It should be interactable when scrolled in at the bottom of the +# viewport. +fixed_overlay = inline( + """ +<style> +* { margin: 0; padding: 0; } +body { height: 300vh } +div, a { display: block } +div { + background-color: pink; + position: fixed; + width: 100%; + height: 40px; + top: 0; +} +a { + margin-top: 1000px; +} +</style> + +<div>overlay</div> +<a href=#>link</a> + +<script> +window.clicked = false; + +let link = document.querySelector("a"); +link.addEventListener("click", () => window.clicked = true); +</script> +""" +) + + +obscured_overlay = inline( + """ +<style> +* { margin: 0; padding: 0; } +body { height: 100vh } +#overlay { + background-color: pink; + position: absolute; + width: 100%; + height: 100%; +} +</style> + +<div id=overlay></div> +<a id=obscured href=#>link</a> + +<script> +window.clicked = false; + +let link = document.querySelector("#obscured"); +link.addEventListener("click", () => window.clicked = true); +</script> +""" +) + + +class ClickBaseTestCase(WindowManagerMixin, MarionetteTestCase): + def setUp(self): + super(ClickBaseTestCase, self).setUp() + + # Always use a blank new tab for an empty history + self.new_tab = self.open_tab() + self.marionette.switch_to_window(self.new_tab) + + def tearDown(self): + self.close_all_tabs() + + def test_click(self): + self.marionette.navigate( + inline( + """ + <button>click me</button> + <script> + window.clicks = 0; + let button = document.querySelector("button"); + button.addEventListener("click", () => window.clicks++); + </script> + """ + ) + ) + button = self.marionette.find_element(By.TAG_NAME, "button") + button.click() + self.assertEqual( + 1, self.marionette.execute_script("return window.clicks", sandbox=None) + ) + + def test_click_number_link(self): + test_html = self.marionette.absolute_url("clicks.html") + self.marionette.navigate(test_html) + self.marionette.find_element(By.LINK_TEXT, "333333").click() + self.marionette.find_element(By.ID, "testDiv") + self.assertEqual(self.marionette.title, "Marionette Test") + + def test_clicking_an_element_that_is_not_displayed_raises(self): + self.marionette.navigate( + inline( + """ + <p hidden>foo</p> + """ + ) + ) + + with self.assertRaises(errors.ElementNotInteractableException): + self.marionette.find_element(By.TAG_NAME, "p").click() + + def test_clicking_on_a_multiline_link(self): + test_html = self.marionette.absolute_url("clicks.html") + self.marionette.navigate(test_html) + self.marionette.find_element(By.ID, "overflowLink").click() + self.marionette.find_element(By.ID, "testDiv") + self.assertEqual(self.marionette.title, "Marionette Test") + + def test_click_mathml(self): + self.marionette.navigate( + inline( + """ + <math><mtext id="target">click me</mtext></math> + <script> + window.clicks = 0; + let mtext = document.getElementById("target"); + mtext.addEventListener("click", () => window.clicks++); + </script> + """ + ) + ) + mtext = self.marionette.find_element(By.ID, "target") + mtext.click() + self.assertEqual( + 1, self.marionette.execute_script("return window.clicks", sandbox=None) + ) + + def test_scroll_into_view_near_end(self): + self.marionette.navigate(fixed_overlay) + link = self.marionette.find_element(By.TAG_NAME, "a") + link.click() + self.assertTrue( + self.marionette.execute_script("return window.clicked", sandbox=None) + ) + + def test_inclusive_descendant(self): + self.marionette.navigate( + inline( + """ + <select multiple> + <option>first + <option>second + <option>third + </select>""" + ) + ) + select = self.marionette.find_element(By.TAG_NAME, "select") + + # This tests that the pointer-interactability test does not + # cause an ElementClickInterceptedException. + # + # At a <select multiple>'s in-view centre point, you might + # find a fully rendered <option>. Marionette should test that + # the paint tree at this point _contains_ <option>, not that the + # first element of the paint tree is _equal_ to <select>. + select.click() + + # Bug 1413821 - Click does not select an option on Android + if self.marionette.session_capabilities["browserName"] != "fennec": + self.assertNotEqual(select.get_property("selectedIndex"), -1) + + def test_container_is_select(self): + self.marionette.navigate( + inline( + """ + <select> + <option>foo</option> + </select>""" + ) + ) + option = self.marionette.find_element(By.TAG_NAME, "option") + option.click() + self.assertTrue(option.get_property("selected")) + + def test_container_is_button(self): + self.marionette.navigate( + inline( + """ + <button onclick="window.clicked = true;"> + <span><em>foo</em></span> + </button>""" + ) + ) + span = self.marionette.find_element(By.TAG_NAME, "span") + span.click() + self.assertTrue( + self.marionette.execute_script("return window.clicked", sandbox=None) + ) + + def test_container_element_outside_view(self): + self.marionette.navigate( + inline( + """ + <select style="margin-top: 100vh"> + <option>foo</option> + </select>""" + ) + ) + option = self.marionette.find_element(By.TAG_NAME, "option") + option.click() + self.assertTrue(option.get_property("selected")) + + def test_table_tr(self): + self.marionette.navigate( + inline( + """ + <table> + <tr><td onclick="window.clicked = true;"> + foo + </td></tr> + </table>""" + ) + ) + tr = self.marionette.find_element(By.TAG_NAME, "tr") + tr.click() + self.assertTrue( + self.marionette.execute_script("return window.clicked", sandbox=None) + ) + + +class TestLegacyClick(ClickBaseTestCase): + """Uses legacy Selenium element displayedness checks.""" + + def setUp(self): + super(TestLegacyClick, self).setUp() + + self.marionette.delete_session() + self.marionette.start_session({"moz:webdriverClick": False}) + + +class TestClick(ClickBaseTestCase): + """Uses WebDriver specification compatible element interactability checks.""" + + def setUp(self): + super(TestClick, self).setUp() + + self.marionette.delete_session() + self.marionette.start_session({"moz:webdriverClick": True}) + + def test_click_element_obscured_by_absolute_positioned_element(self): + self.marionette.navigate(obscured_overlay) + overlay = self.marionette.find_element(By.ID, "overlay") + obscured = self.marionette.find_element(By.ID, "obscured") + + overlay.click() + with self.assertRaises(errors.ElementClickInterceptedException): + obscured.click() + + def test_centre_outside_viewport_vertically(self): + self.marionette.navigate( + inline( + """ + <style> + * { margin: 0; padding: 0; } + div { + display: block; + position: absolute; + background-color: blue; + width: 200px; + height: 200px; + + /* move centre point off viewport vertically */ + top: -105px; + } + </style> + + <div onclick="window.clicked = true;"></div>""" + ) + ) + + self.marionette.find_element(By.TAG_NAME, "div").click() + self.assertTrue( + self.marionette.execute_script("return window.clicked", sandbox=None) + ) + + def test_centre_outside_viewport_horizontally(self): + self.marionette.navigate( + inline( + """ + <style> + * { margin: 0; padding: 0; } + div { + display: block; + position: absolute; + background-color: blue; + width: 200px; + height: 200px; + + /* move centre point off viewport horizontally */ + left: -105px; + } + </style> + + <div onclick="window.clicked = true;"></div>""" + ) + ) + + self.marionette.find_element(By.TAG_NAME, "div").click() + self.assertTrue( + self.marionette.execute_script("return window.clicked", sandbox=None) + ) + + def test_centre_outside_viewport(self): + self.marionette.navigate( + inline( + """ + <style> + * { margin: 0; padding: 0; } + div { + display: block; + position: absolute; + background-color: blue; + width: 200px; + height: 200px; + + /* move centre point off viewport */ + left: -105px; + top: -105px; + } + </style> + + <div onclick="window.clicked = true;"></div>""" + ) + ) + + self.marionette.find_element(By.TAG_NAME, "div").click() + self.assertTrue( + self.marionette.execute_script("return window.clicked", sandbox=None) + ) + + def test_css_transforms(self): + self.marionette.navigate( + inline( + """ + <style> + * { margin: 0; padding: 0; } + div { + display: block; + background-color: blue; + width: 200px; + height: 200px; + + transform: translateX(-105px); + } + </style> + + <div onclick="window.clicked = true;"></div>""" + ) + ) + + self.marionette.find_element(By.TAG_NAME, "div").click() + self.assertTrue( + self.marionette.execute_script("return window.clicked", sandbox=None) + ) + + def test_input_file(self): + self.marionette.navigate(inline("<input type=file>")) + with self.assertRaises(errors.InvalidArgumentException): + self.marionette.find_element(By.TAG_NAME, "input").click() + + def test_obscured_element(self): + self.marionette.navigate(obscured_overlay) + overlay = self.marionette.find_element(By.ID, "overlay") + obscured = self.marionette.find_element(By.ID, "obscured") + + overlay.click() + with self.assertRaises(errors.ElementClickInterceptedException): + obscured.click() + self.assertFalse( + self.marionette.execute_script("return window.clicked", sandbox=None) + ) + + def test_pointer_events_none(self): + self.marionette.navigate( + inline( + """ + <button style="pointer-events: none">click me</button> + <script> + window.clicked = false; + let button = document.querySelector("button"); + button.addEventListener("click", () => window.clicked = true); + </script> + """ + ) + ) + button = self.marionette.find_element(By.TAG_NAME, "button") + self.assertEqual("none", button.value_of_css_property("pointer-events")) + + with self.assertRaisesRegexp( + errors.ElementClickInterceptedException, + "does not have pointer events enabled", + ): + button.click() + self.assertFalse( + self.marionette.execute_script("return window.clicked", sandbox=None) + ) + + def test_prevent_default(self): + self.marionette.navigate( + inline( + """ + <button>click me</button> + <script> + let button = document.querySelector("button"); + button.addEventListener("click", event => event.preventDefault()); + </script> + """ + ) + ) + button = self.marionette.find_element(By.TAG_NAME, "button") + # should not time out + button.click() + + def test_stop_propagation(self): + self.marionette.navigate( + inline( + """ + <button>click me</button> + <script> + let button = document.querySelector("button"); + button.addEventListener("click", event => event.stopPropagation()); + </script> + """ + ) + ) + button = self.marionette.find_element(By.TAG_NAME, "button") + # should not time out + button.click() + + def test_stop_immediate_propagation(self): + self.marionette.navigate( + inline( + """ + <button>click me</button> + <script> + let button = document.querySelector("button"); + button.addEventListener("click", event => event.stopImmediatePropagation()); + </script> + """ + ) + ) + button = self.marionette.find_element(By.TAG_NAME, "button") + # should not time out + button.click() + + +class TestClickNavigation(WindowManagerMixin, MarionetteTestCase): + def setUp(self): + super(TestClickNavigation, self).setUp() + + # Always use a blank new tab for an empty history + self.new_tab = self.open_tab() + self.marionette.switch_to_window(self.new_tab) + + self.test_page = self.marionette.absolute_url("clicks.html") + self.marionette.navigate(self.test_page) + + def tearDown(self): + self.close_all_tabs() + + def close_notification(self): + try: + with self.marionette.using_context("chrome"): + elem = self.marionette.find_element( + By.CSS_SELECTOR, + "#notification-popup popupnotification .popup-notification-closebutton", + ) + elem.click() + except errors.NoSuchElementException: + pass + + def test_click_link_page_load(self): + self.marionette.find_element(By.LINK_TEXT, "333333").click() + self.assertNotEqual(self.marionette.get_url(), self.test_page) + self.assertEqual(self.marionette.title, "Marionette Test") + + def test_click_link_anchor(self): + self.marionette.find_element(By.ID, "anchor").click() + self.assertEqual(self.marionette.get_url(), "{}#".format(self.test_page)) + + @skipIf( + sys.platform.startswith("win"), + "Bug 1627965 - Skip on Windows for frequent failures", + ) + def test_click_link_install_addon(self): + try: + self.marionette.find_element(By.ID, "install-addon").click() + self.assertEqual(self.marionette.get_url(), self.test_page) + finally: + self.close_notification() + + def test_click_no_link(self): + self.marionette.find_element(By.ID, "links").click() + self.assertEqual(self.marionette.get_url(), self.test_page) + + def test_click_option_navigate(self): + self.marionette.find_element(By.ID, "option").click() + self.marionette.find_element(By.ID, "delay") + + def test_click_remoteness_change(self): + self.marionette.navigate("about:robots") + self.marionette.navigate(self.test_page) + self.marionette.find_element(By.ID, "anchor") + + self.marionette.navigate("about:robots") + with self.assertRaises(errors.NoSuchElementException): + self.marionette.find_element(By.ID, "anchor") + + self.marionette.go_back() + self.marionette.find_element(By.ID, "anchor") + + self.marionette.find_element(By.ID, "history-back").click() + with self.assertRaises(errors.NoSuchElementException): + self.marionette.find_element(By.ID, "anchor") + + +class TestClickCloseContext(WindowManagerMixin, MarionetteTestCase): + def setUp(self): + super(TestClickCloseContext, self).setUp() + + self.test_page = self.marionette.absolute_url("clicks.html") + + def tearDown(self): + self.close_all_tabs() + + super(TestClickCloseContext, self).tearDown() + + def test_click_close_tab(self): + new_tab = self.open_tab() + self.marionette.switch_to_window(new_tab) + + self.marionette.navigate(self.test_page) + self.marionette.find_element(By.ID, "close-window").click() + + def test_click_close_window(self): + new_tab = self.open_window() + self.marionette.switch_to_window(new_tab) + + self.marionette.navigate(self.test_page) + self.marionette.find_element(By.ID, "close-window").click() diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_click_chrome.py b/testing/marionette/harness/marionette_harness/tests/unit/test_click_chrome.py new file mode 100644 index 0000000000..1fb4ca89a3 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_click_chrome.py @@ -0,0 +1,33 @@ +# 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.by import By + +from marionette_harness import MarionetteTestCase, WindowManagerMixin + + +class TestClickChrome(WindowManagerMixin, MarionetteTestCase): + def setUp(self): + super(TestClickChrome, self).setUp() + + self.marionette.set_context("chrome") + + def tearDown(self): + self.close_all_windows() + + super(TestClickChrome, self).tearDown() + + def test_click(self): + win = self.open_chrome_window("chrome://remote/content/marionette/test.xhtml") + self.marionette.switch_to_window(win) + + def checked(): + return self.marionette.execute_script( + "return arguments[0].checked", script_args=[box] + ) + + box = self.marionette.find_element(By.ID, "testBox") + self.assertFalse(checked()) + box.click() + self.assertTrue(checked()) diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_click_scrolling.py b/testing/marionette/harness/marionette_harness/tests/unit/test_click_scrolling.py new file mode 100644 index 0000000000..ade5a21b36 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_click_scrolling.py @@ -0,0 +1,167 @@ +# 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 six.moves.urllib.parse import quote + +from marionette_driver.by import By +from marionette_driver.errors import MoveTargetOutOfBoundsException + +from marionette_harness import MarionetteTestCase + + +def inline(doc): + return "data:text/html;charset=utf-8,{}".format(quote(doc)) + + +class TestClickScrolling(MarionetteTestCase): + def test_clicking_on_anchor_scrolls_page(self): + self.marionette.navigate( + inline( + """ + <a href="#content">Link to content</a> + <div id="content" style="margin-top: 205vh;">Text</div> + """ + ) + ) + + # Focusing on to click, but not actually following, + # the link will scroll it in to view, which is a few + # pixels further than 0 + self.marionette.find_element(By.CSS_SELECTOR, "a").click() + + y_offset = self.marionette.execute_script( + """ + var pageY; + if (typeof(window.pageYOffset) == 'number') { + pageY = window.pageYOffset; + } else { + pageY = document.documentElement.scrollTop; + } + return pageY; + """ + ) + + self.assertGreater(y_offset, 300) + + def test_should_scroll_to_click_on_an_element_hidden_by_overflow(self): + test_html = self.marionette.absolute_url("click_out_of_bounds_overflow.html") + self.marionette.navigate(test_html) + + link = self.marionette.find_element(By.ID, "link") + try: + link.click() + except MoveTargetOutOfBoundsException: + self.fail("Should not be out of bounds") + + def test_should_not_scroll_elements_if_click_point_is_in_view(self): + test_html = self.marionette.absolute_url("element_outside_viewport.html") + + for s in ["top", "right", "bottom", "left"]: + for p in ["50", "30"]: + self.marionette.navigate(test_html) + scroll = self.marionette.execute_script( + "return [window.scrollX, window.scrollY];" + ) + self.marionette.find_element(By.ID, "{0}-{1}".format(s, p)).click() + self.assertEqual( + scroll, + self.marionette.execute_script( + "return [window.scrollX, window.scrollY];" + ), + ) + + def test_do_not_scroll_again_if_element_is_already_in_view(self): + self.marionette.navigate( + inline( + """ + <div style="height: 200vh;"> + <button id="button1" style="margin-top: 105vh">Button1</button> + <button id="button2" style="position: relative; top: 5em">Button2</button> + </div> + """ + ) + ) + button1 = self.marionette.find_element(By.ID, "button1") + button2 = self.marionette.find_element(By.ID, "button2") + + button2.click() + scroll_top = self.marionette.execute_script("return document.body.scrollTop;") + button1.click() + + self.assertEqual( + scroll_top, + self.marionette.execute_script("return document.body.scrollTop;"), + ) + + def test_scroll_radio_button_into_view(self): + self.marionette.navigate( + inline( + """ + <input type="radio" id="radio" style="margin-top: 105vh;"> + """ + ) + ) + self.marionette.find_element(By.ID, "radio").click() + + def test_overflow_scroll_do_not_scroll_elements_which_are_visible(self): + self.marionette.navigate( + inline( + """ + <ul style='overflow: scroll; height: 8em; line-height: 3em'> + <li></li> + <li id="desired">Text</li> + <li></li> + <li></li> + </ul> + """ + ) + ) + + list_el = self.marionette.find_element(By.TAG_NAME, "ul") + expected_y_offset = self.marionette.execute_script( + "return arguments[0].scrollTop;", script_args=(list_el,) + ) + + item = list_el.find_element(By.ID, "desired") + item.click() + + y_offset = self.marionette.execute_script( + "return arguments[0].scrollTop;", script_args=(list_el,) + ) + self.assertEqual(expected_y_offset, y_offset) + + def test_overflow_scroll_click_on_hidden_element(self): + self.marionette.navigate( + inline( + """ + Result: <span id="result"></span> + <ul style='overflow: scroll; width: 150px; height: 8em; line-height: 4em' + onclick="document.getElementById('result').innerText = event.target.id;"> + <li>line1</li> + <li>line2</li> + <li>line3</li> + <li id="line4">line4</li> + </ul> + """ + ) + ) + + self.marionette.find_element(By.ID, "line4").click() + self.assertEqual("line4", self.marionette.find_element(By.ID, "result").text) + + def test_overflow_scroll_vertically_for_click_point_outside_of_viewport(self): + self.marionette.navigate( + inline( + """ + Result: <span id="result"></span> + <div style='overflow: scroll; width: 100px; height: 100px; background-color: yellow;'> + <div id="inner" style="width: 100px; height: 300px; background-color: green;" + onclick="document.getElementById('result').innerText = event.type" ></div> + </div> + """ + ) + ) + + self.marionette.find_element(By.ID, "inner").click() + self.assertEqual("click", self.marionette.find_element(By.ID, "result").text) diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_context.py b/testing/marionette/harness/marionette_harness/tests/unit/test_context.py new file mode 100644 index 0000000000..4f2c077677 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_context.py @@ -0,0 +1,82 @@ +# 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.decorators import using_context +from marionette_driver.errors import MarionetteException +from marionette_harness import MarionetteTestCase + + +class ContextTestCase(MarionetteTestCase): + def setUp(self): + super(ContextTestCase, self).setUp() + + # shortcuts to improve readability of these tests + self.chrome = self.marionette.CONTEXT_CHROME + self.content = self.marionette.CONTEXT_CONTENT + + self.assertEqual(self.get_context(), self.content) + + test_url = self.marionette.absolute_url("empty.html") + self.marionette.navigate(test_url) + + def get_context(self): + return self.marionette._send_message("Marionette:GetContext", key="value") + + +class TestSetContext(ContextTestCase): + def test_switch_context(self): + self.marionette.set_context(self.chrome) + self.assertEqual(self.get_context(), self.chrome) + + self.marionette.set_context(self.content) + self.assertEqual(self.get_context(), self.content) + + def test_invalid_context(self): + with self.assertRaises(ValueError): + self.marionette.set_context("foobar") + + +class TestUsingContext(ContextTestCase): + def test_set_different_context_using_with_block(self): + with self.marionette.using_context(self.chrome): + self.assertEqual(self.get_context(), self.chrome) + self.assertEqual(self.get_context(), self.content) + + def test_set_same_context_using_with_block(self): + with self.marionette.using_context(self.content): + self.assertEqual(self.get_context(), self.content) + self.assertEqual(self.get_context(), self.content) + + def test_nested_with_blocks(self): + with self.marionette.using_context(self.chrome): + self.assertEqual(self.get_context(), self.chrome) + with self.marionette.using_context(self.content): + self.assertEqual(self.get_context(), self.content) + self.assertEqual(self.get_context(), self.chrome) + self.assertEqual(self.get_context(), self.content) + + def test_set_scope_while_in_with_block(self): + with self.marionette.using_context(self.chrome): + self.assertEqual(self.get_context(), self.chrome) + self.marionette.set_context(self.content) + self.assertEqual(self.get_context(), self.content) + self.assertEqual(self.get_context(), self.content) + + def test_exception_raised_while_in_with_block_is_propagated(self): + with self.assertRaises(MarionetteException): + with self.marionette.using_context(self.chrome): + raise MarionetteException + self.assertEqual(self.get_context(), self.content) + + def test_with_using_context_decorator(self): + @using_context("content") + def inner_content(m): + self.assertEqual(self.get_context(), "content") + + @using_context("chrome") + def inner_chrome(m): + self.assertEqual(self.get_context(), "chrome") + + inner_content(self.marionette) + inner_chrome(self.marionette) diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_cookies.py b/testing/marionette/harness/marionette_harness/tests/unit/test_cookies.py new file mode 100644 index 0000000000..ea51214909 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_cookies.py @@ -0,0 +1,115 @@ +# 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/. + +import calendar +import random +import time + +from marionette_driver.errors import UnsupportedOperationException +from marionette_harness import MarionetteTestCase + + +class CookieTest(MarionetteTestCase): + def setUp(self): + MarionetteTestCase.setUp(self) + test_url = self.marionette.absolute_url("test.html") + self.marionette.navigate(test_url) + self.COOKIE_A = {"name": "foo", "value": "bar", "path": "/", "secure": False} + + def tearDown(self): + self.marionette.delete_all_cookies() + MarionetteTestCase.tearDown(self) + + def test_add_cookie(self): + self.marionette.add_cookie(self.COOKIE_A) + cookie_returned = str(self.marionette.execute_script("return document.cookie")) + self.assertTrue(self.COOKIE_A["name"] in cookie_returned) + + def test_adding_a_cookie_that_expired_in_the_past(self): + cookie = self.COOKIE_A.copy() + cookie["expiry"] = calendar.timegm(time.gmtime()) - (60 * 60 * 24) + self.marionette.add_cookie(cookie) + cookies = self.marionette.get_cookies() + self.assertEqual(0, len(cookies)) + + def test_chrome_error(self): + with self.marionette.using_context("chrome"): + self.assertRaises( + UnsupportedOperationException, self.marionette.add_cookie, self.COOKIE_A + ) + self.assertRaises( + UnsupportedOperationException, + self.marionette.delete_cookie, + self.COOKIE_A, + ) + self.assertRaises( + UnsupportedOperationException, self.marionette.delete_all_cookies + ) + self.assertRaises( + UnsupportedOperationException, self.marionette.get_cookies + ) + + def test_delete_all_cookie(self): + self.marionette.add_cookie(self.COOKIE_A) + cookie_returned = str(self.marionette.execute_script("return document.cookie")) + print(cookie_returned) + self.assertTrue(self.COOKIE_A["name"] in cookie_returned) + self.marionette.delete_all_cookies() + self.assertFalse(self.marionette.get_cookies()) + + def test_delete_cookie(self): + self.marionette.add_cookie(self.COOKIE_A) + cookie_returned = str(self.marionette.execute_script("return document.cookie")) + self.assertTrue(self.COOKIE_A["name"] in cookie_returned) + self.marionette.delete_cookie("foo") + cookie_returned = str(self.marionette.execute_script("return document.cookie")) + self.assertFalse(self.COOKIE_A["name"] in cookie_returned) + + def test_should_get_cookie_by_name(self): + key = "key_{}".format(int(random.random() * 10000000)) + self.marionette.execute_script( + "document.cookie = arguments[0] + '=set';", [key] + ) + + cookie = self.marionette.get_cookie(key) + self.assertEqual("set", cookie["value"]) + + def test_get_all_cookies(self): + key1 = "key_{}".format(int(random.random() * 10000000)) + key2 = "key_{}".format(int(random.random() * 10000000)) + + cookies = self.marionette.get_cookies() + count = len(cookies) + + one = {"name": key1, "value": "value"} + two = {"name": key2, "value": "value"} + + self.marionette.add_cookie(one) + self.marionette.add_cookie(two) + + test_url = self.marionette.absolute_url("test.html") + self.marionette.navigate(test_url) + cookies = self.marionette.get_cookies() + self.assertEqual(count + 2, len(cookies)) + + def test_should_not_delete_cookies_with_a_similar_name(self): + cookieOneName = "fish" + cookie1 = {"name": cookieOneName, "value": "cod"} + cookie2 = {"name": cookieOneName + "x", "value": "earth"} + self.marionette.add_cookie(cookie1) + self.marionette.add_cookie(cookie2) + + self.marionette.delete_cookie(cookieOneName) + cookies = self.marionette.get_cookies() + + self.assertFalse(cookie1["name"] == cookies[0]["name"], msg=str(cookies)) + self.assertEqual(cookie2["name"], cookies[0]["name"], msg=str(cookies)) + + def test_we_get_required_elements_when_available(self): + self.marionette.add_cookie(self.COOKIE_A) + cookies = self.marionette.get_cookies() + + self.assertIn("name", cookies[0], "name not available") + self.assertIn("value", cookies[0], "value not available") + self.assertIn("httpOnly", cookies[0], "httpOnly not available") diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_crash.py b/testing/marionette/harness/marionette_harness/tests/unit/test_crash.py new file mode 100644 index 0000000000..b413adda0d --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_crash.py @@ -0,0 +1,211 @@ +# 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/. + +import glob +import os +import shutil +import sys + +from io import StringIO + +from marionette_driver import Wait +from marionette_driver.errors import ( + InvalidSessionIdException, + NoSuchWindowException, + TimeoutException, +) + +from marionette_harness import MarionetteTestCase, expectedFailure + +# Import runner module to monkey patch mozcrash module +from mozrunner.base import runner + + +class MockMozCrash(object): + """Mock object to replace original mozcrash methods.""" + + def __init__(self, marionette): + self.marionette = marionette + + with self.marionette.using_context("chrome"): + self.crash_reporter_enabled = self.marionette.execute_script( + """ + const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" + ); + return AppConstants.MOZ_CRASHREPORTER; + """ + ) + + def check_for_crashes(self, dump_directory, *args, **kwargs): + if self.crash_reporter_enabled: + # Workaround until bug 1376795 has been fixed + # Wait at maximum 5s for the minidump files being created + # minidump_files = glob.glob('{}/*.dmp'.format(dump_directory)) + try: + minidump_files = Wait(None, timeout=5).until( + lambda _: glob.glob("{}/*.dmp".format(dump_directory)) + ) + except TimeoutException: + minidump_files = [] + + if os.path.isdir(dump_directory): + shutil.rmtree(dump_directory) + + return len(minidump_files) + else: + return len(minidump_files) == 0 + + def log_crashes(self, logger, dump_directory, *args, **kwargs): + return self.check_for_crashes(dump_directory, *args, **kwargs) + + +class BaseCrashTestCase(MarionetteTestCase): + # Reduce the timeout for faster processing of the tests + socket_timeout = 10 + + def setUp(self): + super(BaseCrashTestCase, self).setUp() + + # Monkey patch mozcrash to avoid crash info output only for our triggered crashes. + mozcrash_mock = MockMozCrash(self.marionette) + if not mozcrash_mock.crash_reporter_enabled: + self.skipTest("Crash reporter disabled") + return + + self.mozcrash = runner.mozcrash + runner.mozcrash = mozcrash_mock + + self.crash_count = self.marionette.crashed + self.pid = self.marionette.process_id + + def tearDown(self): + # Replace mockup with original mozcrash instance + runner.mozcrash = self.mozcrash + + self.marionette.crashed = self.crash_count + + super(BaseCrashTestCase, self).tearDown() + + def crash(self, parent=True): + socket_timeout = self.marionette.client.socket_timeout + self.marionette.client.socket_timeout = self.socket_timeout + + self.marionette.set_context("content") + try: + self.marionette.navigate( + "about:crash{}".format("parent" if parent else "content") + ) + finally: + self.marionette.client.socket_timeout = socket_timeout + + +class TestCrash(BaseCrashTestCase): + def setUp(self): + if os.environ.get("MOZ_AUTOMATION"): + # Capture stdout, otherwise the Gecko output causes mozharness to fail + # the task due to "A content process has crashed" appearing in the log. + # To view stdout for debugging, use `print(self.new_out.getvalue())` + print( + "Suppressing GECKO output. To view, add `print(self.new_out.getvalue())` " + "to the end of this test." + ) + self.new_out, self.new_err = StringIO(), StringIO() + self.old_out, self.old_err = sys.stdout, sys.stderr + sys.stdout, sys.stderr = self.new_out, self.new_err + + super(TestCrash, self).setUp() + + def tearDown(self): + super(TestCrash, self).tearDown() + + if os.environ.get("MOZ_AUTOMATION"): + sys.stdout, sys.stderr = self.old_out, self.old_err + + def test_crash_chrome_process(self): + self.assertRaisesRegexp(IOError, "Process crashed", self.crash, parent=True) + + # A crash results in a non zero exit code + self.assertNotIn(self.marionette.instance.runner.returncode, (None, 0)) + + self.assertEqual(self.marionette.crashed, 1) + self.assertIsNone(self.marionette.session) + with self.assertRaisesRegexp( + InvalidSessionIdException, "Please start a session" + ): + self.marionette.get_url() + + self.marionette.start_session() + self.assertNotEqual(self.marionette.process_id, self.pid) + + self.marionette.get_url() + + def test_crash_content_process(self): + # For a content process crash and MOZ_CRASHREPORTER_SHUTDOWN set the top + # browsing context will be gone first. As such the raised NoSuchWindowException + # has to be ignored. To check for the IOError, further commands have to + # be executed until the process is gone. + with self.assertRaisesRegexp(IOError, "Content process crashed"): + self.crash(parent=False) + Wait( + self.marionette, + timeout=self.socket_timeout, + ignored_exceptions=NoSuchWindowException, + ).until( + lambda _: self.marionette.get_url(), + message="Expected IOError exception for content crash not raised.", + ) + + # A crash when loading about:crashcontent results in a SIGUSR1 exit code. + self.assertEqual(self.marionette.instance.runner.returncode, 245) + + self.assertEqual(self.marionette.crashed, 1) + self.assertIsNone(self.marionette.session) + with self.assertRaisesRegexp( + InvalidSessionIdException, "Please start a session" + ): + self.marionette.get_url() + + self.marionette.start_session() + self.assertNotEqual(self.marionette.process_id, self.pid) + self.marionette.get_url() + + @expectedFailure + def test_unexpected_crash(self): + self.crash(parent=True) + + +class TestCrashInSetUp(BaseCrashTestCase): + def setUp(self): + super(TestCrashInSetUp, self).setUp() + + self.assertRaisesRegexp(IOError, "Process crashed", self.crash, parent=True) + + # A crash results in a non zero exit code + self.assertNotIn(self.marionette.instance.runner.returncode, (None, 0)) + + self.assertEqual(self.marionette.crashed, 1) + self.assertIsNone(self.marionette.session) + + def test_crash_in_setup(self): + self.marionette.start_session() + self.assertNotEqual(self.marionette.process_id, self.pid) + + +class TestCrashInTearDown(BaseCrashTestCase): + def tearDown(self): + try: + self.assertRaisesRegexp(IOError, "Process crashed", self.crash, parent=True) + + # A crash results in a non zero exit code + self.assertNotIn(self.marionette.instance.runner.returncode, (None, 0)) + + self.assertEqual(self.marionette.crashed, 1) + self.assertIsNone(self.marionette.session) + + finally: + super(TestCrashInTearDown, self).tearDown() + + def test_crash_in_teardown(self): + pass diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_data_driven.py b/testing/marionette/harness/marionette_harness/tests/unit/test_data_driven.py new file mode 100644 index 0000000000..b7d1ecf5ff --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_data_driven.py @@ -0,0 +1,72 @@ +# 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/. + +import six + +from marionette_harness.marionette_test import ( + parameterized, + with_parameters, + MetaParameterized, + MarionetteTestCase, +) + + +@six.add_metaclass(MetaParameterized) +class Parameterizable(object): + pass + + +class TestDataDriven(MarionetteTestCase): + def test_parameterized(self): + class Test(Parameterizable): + def __init__(self): + self.parameters = [] + + @parameterized("1", "thing", named=43) + @parameterized("2", "thing2") + def test(self, thing, named=None): + self.parameters.append((thing, named)) + + self.assertFalse(hasattr(Test, "test")) + self.assertTrue(hasattr(Test, "test_1")) + self.assertTrue(hasattr(Test, "test_2")) + + test = Test() + test.test_1() + test.test_2() + + self.assertEqual(test.parameters, [("thing", 43), ("thing2", None)]) + + def test_with_parameters(self): + DATA = [("1", ("thing",), {"named": 43}), ("2", ("thing2",), {"named": None})] + + class Test(Parameterizable): + def __init__(self): + self.parameters = [] + + @with_parameters(DATA) + def test(self, thing, named=None): + self.parameters.append((thing, named)) + + self.assertFalse(hasattr(Test, "test")) + self.assertTrue(hasattr(Test, "test_1")) + self.assertTrue(hasattr(Test, "test_2")) + + test = Test() + test.test_1() + test.test_2() + + self.assertEqual(test.parameters, [("thing", 43), ("thing2", None)]) + + def test_parameterized_same_name_raises_error(self): + with self.assertRaises(KeyError): + + class Test(Parameterizable): + @parameterized("1", "thing", named=43) + @parameterized("1", "thing2") + def test(self, thing, named=None): + pass + + def test_marionette_test_case_is_parameterizable(self): + self.assertTrue(isinstance(MarionetteTestCase, MetaParameterized)) diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_date_time_value.py b/testing/marionette/harness/marionette_harness/tests/unit/test_date_time_value.py new file mode 100644 index 0000000000..7bab80ee8f --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_date_time_value.py @@ -0,0 +1,33 @@ +# 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 datetime import datetime + +from six.moves.urllib.parse import quote + +from marionette_driver.by import By +from marionette_driver.date_time_value import DateTimeValue +from marionette_harness import MarionetteTestCase + + +def inline(doc): + return "data:text/html;charset=utf-8,{}".format(quote(doc)) + + +class TestDateTime(MarionetteTestCase): + def test_set_date(self): + self.marionette.navigate(inline("<input id='date-test' type='date'/>")) + + element = self.marionette.find_element(By.ID, "date-test") + dt_value = DateTimeValue(element) + dt_value.date = datetime(1998, 6, 2) + self.assertEqual("1998-06-02", element.get_property("value")) + + def test_set_time(self): + self.marionette.navigate(inline("<input id='time-test' type='time'/>")) + + element = self.marionette.find_element(By.ID, "time-test") + dt_value = DateTimeValue(element) + dt_value.time = datetime(1998, 11, 19, 9, 8, 7) + self.assertEqual("09:08:07", element.get_property("value")) diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_element_id.py b/testing/marionette/harness/marionette_harness/tests/unit/test_element_id.py new file mode 100644 index 0000000000..c7827daa08 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_element_id.py @@ -0,0 +1,55 @@ +# 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/. + +import re +from urllib.parse import quote + +from marionette_driver.by import By +from marionette_driver.errors import NoSuchElementException, InvalidSelectorException +from marionette_driver.marionette import WebElement + +from marionette_harness import MarionetteTestCase + + +def inline(doc): + return "data:text/html;charset=utf-8,{}".format(quote(doc)) + + +id_html = inline("<p id=foo></p>") + + +class TestElementID(MarionetteTestCase): + def setUp(self): + MarionetteTestCase.setUp(self) + self.marionette.timeout.implicit = 0 + + def test_id_is_valid_uuid(self): + self.marionette.navigate(id_html) + el = self.marionette.find_element(By.TAG_NAME, "p") + uuid_regex = re.compile( + "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + ) + self.assertIsNotNone( + re.search(uuid_regex, el.id), + "UUID for the WebElement is not valid. ID is {}".format(el.id), + ) + + def test_id_identical_for_the_same_element(self): + self.marionette.navigate(id_html) + found = self.marionette.find_element(By.ID, "foo") + self.assertIsInstance(found, WebElement) + + found_again = self.marionette.find_element(By.ID, "foo") + self.assertEqual(found_again, found) + + def test_id_unique_per_session(self): + self.marionette.navigate(id_html) + found = self.marionette.find_element(By.ID, "foo") + self.assertIsInstance(found, WebElement) + + self.marionette.delete_session() + self.marionette.start_session() + + found_again = self.marionette.find_element(By.ID, "foo") + self.assertNotEqual(found_again.id, found.id) diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_element_id_chrome.py b/testing/marionette/harness/marionette_harness/tests/unit/test_element_id_chrome.py new file mode 100644 index 0000000000..6c9f01f339 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_element_id_chrome.py @@ -0,0 +1,88 @@ +# 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.by import By +from marionette_driver.errors import NoSuchElementException +from marionette_driver.marionette import WebElement + +from marionette_harness import MarionetteTestCase, parameterized, WindowManagerMixin + + +PAGE_XHTML = "chrome://remote/content/marionette/test_no_xul.xhtml" +PAGE_XUL = "chrome://remote/content/marionette/test.xhtml" + + +class TestElementIDChrome(WindowManagerMixin, MarionetteTestCase): + def setUp(self): + super(TestElementIDChrome, self).setUp() + + self.marionette.set_context("chrome") + + def tearDown(self): + self.close_all_windows() + + super(TestElementIDChrome, self).tearDown() + + @parameterized("XUL", PAGE_XUL) + @parameterized("XHTML", PAGE_XHTML) + def test_id_identical_for_the_same_element(self, chrome_url): + win = self.open_chrome_window(chrome_url) + self.marionette.switch_to_window(win) + + found_el = self.marionette.find_element(By.ID, "textInput") + self.assertEqual(WebElement, type(found_el)) + + found_el_new = self.marionette.find_element(By.ID, "textInput") + self.assertEqual(found_el_new.id, found_el.id) + + @parameterized("XUL", PAGE_XUL) + @parameterized("XHTML", PAGE_XHTML) + def test_id_unique_per_session(self, chrome_url): + win = self.open_chrome_window(chrome_url) + self.marionette.switch_to_window(win) + + found_el = self.marionette.find_element(By.ID, "textInput") + self.assertEqual(WebElement, type(found_el)) + + self.marionette.delete_session() + self.marionette.start_session() + + self.marionette.set_context("chrome") + self.marionette.switch_to_window(win) + + found_el_new = self.marionette.find_element(By.ID, "textInput") + self.assertNotEqual(found_el_new.id, found_el.id) + + @parameterized("XUL", PAGE_XUL) + @parameterized("XHTML", PAGE_XHTML) + def test_id_no_such_element_in_another_chrome_window(self, chrome_url): + original_handle = self.marionette.current_window_handle + + win = self.open_chrome_window(chrome_url) + self.marionette.switch_to_window(win) + + found_el = self.marionette.find_element(By.ID, "textInput") + self.assertEqual(WebElement, type(found_el)) + + self.marionette.switch_to_window(original_handle) + + with self.assertRaises(NoSuchElementException): + found_el.get_property("localName") + + @parameterized("XUL", PAGE_XUL) + @parameterized("XHTML", PAGE_XHTML) + def test_id_removed_when_chrome_window_is_closed(self, chrome_url): + original_handle = self.marionette.current_window_handle + + win = self.open_chrome_window(chrome_url) + self.marionette.switch_to_window(win) + + found_el = self.marionette.find_element(By.ID, "textInput") + self.assertEqual(WebElement, type(found_el)) + + self.marionette.close_chrome_window() + self.marionette.switch_to_window(original_handle) + + with self.assertRaises(NoSuchElementException): + found_el.get_property("localName") diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_element_rect.py b/testing/marionette/harness/marionette_harness/tests/unit/test_element_rect.py new file mode 100644 index 0000000000..4eea9a2c40 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_element_rect.py @@ -0,0 +1,22 @@ +# 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 six.moves.urllib.parse import quote + +from marionette_driver.by import By +from marionette_harness import MarionetteTestCase + + +def inline(doc): + return "data:text/html;charset=utf-8,{}".format(quote(doc)) + + +class TestElementSize(MarionetteTestCase): + def test_payload(self): + self.marionette.navigate(inline("""<a href="#">link</a>""")) + rect = self.marionette.find_element(By.LINK_TEXT, "link").rect + self.assertTrue(rect["x"] > 0) + self.assertTrue(rect["y"] > 0) + self.assertTrue(rect["width"] > 0) + self.assertTrue(rect["height"] > 0) diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_element_rect_chrome.py b/testing/marionette/harness/marionette_harness/tests/unit/test_element_rect_chrome.py new file mode 100644 index 0000000000..2ea46182c2 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_element_rect_chrome.py @@ -0,0 +1,30 @@ +# 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.by import By + +from marionette_harness import MarionetteTestCase, WindowManagerMixin + + +class TestElementSizeChrome(WindowManagerMixin, MarionetteTestCase): + def setUp(self): + super(TestElementSizeChrome, self).setUp() + + self.marionette.set_context("chrome") + + new_window = self.open_chrome_window( + "chrome://remote/content/marionette/test.xhtml" + ) + self.marionette.switch_to_window(new_window) + + def tearDown(self): + self.close_all_windows() + super(TestElementSizeChrome, self).tearDown() + + def test_payload(self): + rect = self.marionette.find_element(By.ID, "textInput").rect + self.assertTrue(rect["x"] > 0) + self.assertTrue(rect["y"] > 0) + self.assertTrue(rect["width"] > 0) + self.assertTrue(rect["height"] > 0) diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_element_state.py b/testing/marionette/harness/marionette_harness/tests/unit/test_element_state.py new file mode 100644 index 0000000000..3122cc42b8 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_element_state.py @@ -0,0 +1,175 @@ +# 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/. + +import types + +import six +from six.moves.urllib.parse import quote + +from marionette_driver.by import By +from marionette_harness import MarionetteTestCase + + +boolean_attributes = { + "audio": ["autoplay", "controls", "loop", "muted"], + "button": ["autofocus", "disabled", "formnovalidate"], + "details": ["open"], + "dialog": ["open"], + "fieldset": ["disabled"], + "form": ["novalidate"], + "iframe": ["allowfullscreen"], + "img": ["ismap"], + "input": [ + "autofocus", + "checked", + "disabled", + "formnovalidate", + "multiple", + "readonly", + "required", + ], + "menuitem": ["checked", "default", "disabled"], + "ol": ["reversed"], + "optgroup": ["disabled"], + "option": ["disabled", "selected"], + "script": ["async", "defer"], + "select": ["autofocus", "disabled", "multiple", "required"], + "textarea": ["autofocus", "disabled", "readonly", "required"], + "track": ["default"], + "video": ["autoplay", "controls", "loop", "muted"], +} + + +def inline(doc, doctype="html"): + if doctype == "html": + return "data:text/html;charset=utf-8,{}".format(quote(doc)) + elif doctype == "xhtml": + return "data:application/xhtml+xml,{}".format( + quote( + r"""<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> + <head> + <title>XHTML might be the future</title> + </head> + + <body> + {} + </body> +</html>""".format( + doc + ) + ) + ) + + +attribute = inline("<input foo=bar>") +input = inline("<input>") +disabled = inline("<input disabled=baz>") +check = inline("<input type=checkbox>") + + +class TestIsElementEnabled(MarionetteTestCase): + def test_is_enabled(self): + test_html = self.marionette.absolute_url("test.html") + self.marionette.navigate(test_html) + l = self.marionette.find_element(By.NAME, "myCheckBox") + self.assertTrue(l.is_enabled()) + self.marionette.execute_script("arguments[0].disabled = true;", [l]) + self.assertFalse(l.is_enabled()) + + +class TestIsElementDisplayed(MarionetteTestCase): + def test_is_displayed(self): + test_html = self.marionette.absolute_url("test.html") + self.marionette.navigate(test_html) + l = self.marionette.find_element(By.NAME, "myCheckBox") + self.assertTrue(l.is_displayed()) + self.marionette.execute_script("arguments[0].hidden = true;", [l]) + self.assertFalse(l.is_displayed()) + + +class TestGetElementAttribute(MarionetteTestCase): + def test_normal_attribute(self): + self.marionette.navigate(inline("<p style=foo>")) + el = self.marionette.find_element(By.TAG_NAME, "p") + attr = el.get_attribute("style") + self.assertIsInstance(attr, six.string_types) + self.assertEqual("foo", attr) + + def test_boolean_attributes(self): + for tag, attrs in six.iteritems(boolean_attributes): + for attr in attrs: + print("testing boolean attribute <{0} {1}>".format(tag, attr)) + doc = inline("<{0} {1}>".format(tag, attr)) + self.marionette.navigate(doc) + el = self.marionette.find_element(By.TAG_NAME, tag) + res = el.get_attribute(attr) + self.assertIsInstance(res, six.string_types) + self.assertEqual("true", res) + + def test_global_boolean_attributes(self): + self.marionette.navigate(inline("<p hidden>foo")) + el = self.marionette.find_element(By.TAG_NAME, "p") + attr = el.get_attribute("hidden") + self.assertIsInstance(attr, six.string_types) + self.assertEqual("true", attr) + + self.marionette.navigate(inline("<p>foo")) + el = self.marionette.find_element(By.TAG_NAME, "p") + attr = el.get_attribute("hidden") + self.assertIsNone(attr) + + self.marionette.navigate(inline("<p itemscope>foo")) + el = self.marionette.find_element(By.TAG_NAME, "p") + attr = el.get_attribute("itemscope") + self.assertIsInstance(attr, six.string_types) + self.assertEqual("true", attr) + + self.marionette.navigate(inline("<p>foo")) + el = self.marionette.find_element(By.TAG_NAME, "p") + attr = el.get_attribute("itemscope") + self.assertIsNone(attr) + + # TODO(ato): Test for custom elements + + def test_xhtml(self): + doc = inline('<p hidden="true">foo</p>', doctype="xhtml") + self.marionette.navigate(doc) + el = self.marionette.find_element(By.TAG_NAME, "p") + attr = el.get_attribute("hidden") + self.assertIsInstance(attr, six.string_types) + self.assertEqual("true", attr) + + +class TestGetElementProperty(MarionetteTestCase): + def test_get(self): + self.marionette.navigate(disabled) + el = self.marionette.find_element(By.TAG_NAME, "input") + prop = el.get_property("disabled") + self.assertIsInstance(prop, bool) + self.assertTrue(prop) + + def test_missing_property_returns_default(self): + self.marionette.navigate(input) + el = self.marionette.find_element(By.TAG_NAME, "input") + prop = el.get_property("checked") + self.assertIsInstance(prop, bool) + self.assertFalse(prop) + + def test_attribute_not_returned(self): + self.marionette.navigate(attribute) + el = self.marionette.find_element(By.TAG_NAME, "input") + self.assertEqual(el.get_property("foo"), None) + + def test_manipulated_element(self): + self.marionette.navigate(check) + el = self.marionette.find_element(By.TAG_NAME, "input") + self.assertEqual(el.get_property("checked"), False) + + el.click() + self.assertEqual(el.get_property("checked"), True) + + el.click() + self.assertEqual(el.get_property("checked"), False) diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_element_state_chrome.py b/testing/marionette/harness/marionette_harness/tests/unit/test_element_state_chrome.py new file mode 100644 index 0000000000..a39c907952 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_element_state_chrome.py @@ -0,0 +1,56 @@ +# 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.by import By + +from marionette_harness import MarionetteTestCase, skip, WindowManagerMixin + + +class TestElementState(WindowManagerMixin, MarionetteTestCase): + def setUp(self): + super(TestElementState, self).setUp() + + self.marionette.set_context("chrome") + + self.win = self.open_chrome_window( + "chrome://remote/content/marionette/test.xhtml" + ) + self.marionette.switch_to_window(self.win) + + def tearDown(self): + self.close_all_windows() + + super(TestElementState, self).tearDown() + + def test_is_displayed(self): + l = self.marionette.find_element(By.ID, "textInput") + self.assertTrue(l.is_displayed()) + self.marionette.execute_script("arguments[0].hidden = true;", [l]) + self.assertFalse(l.is_displayed()) + self.marionette.execute_script("arguments[0].hidden = false;", [l]) + + def test_enabled(self): + l = self.marionette.find_element(By.ID, "textInput") + self.assertTrue(l.is_enabled()) + self.marionette.execute_script("arguments[0].disabled = true;", [l]) + self.assertFalse(l.is_enabled()) + self.marionette.execute_script("arguments[0].disabled = false;", [l]) + + def test_can_get_element_rect(self): + l = self.marionette.find_element(By.ID, "textInput") + rect = l.rect + self.assertTrue(rect["x"] > 0) + self.assertTrue(rect["y"] > 0) + + def test_get_attribute(self): + el = self.marionette.execute_script( + "return window.document.getElementById('textInput');" + ) + self.assertEqual(el.get_attribute("id"), "textInput") + + def test_get_property(self): + el = self.marionette.execute_script( + "return window.document.getElementById('textInput');" + ) + self.assertEqual(el.get_property("id"), "textInput") diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_errors.py b/testing/marionette/harness/marionette_harness/tests/unit/test_errors.py new file mode 100644 index 0000000000..53984dba48 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_errors.py @@ -0,0 +1,105 @@ +# 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/. + +import sys + +import six + +from marionette_driver import errors + +from marionette_harness import marionette_test + + +def fake_cause(): + try: + raise ValueError("bar") + except ValueError: + return sys.exc_info() + + +message = "foo" +unicode_message = "\u201Cfoo" +cause = fake_cause() +stacktrace = "first\nsecond" + + +class TestErrors(marionette_test.MarionetteTestCase): + def test_defaults(self): + exc = errors.MarionetteException() + self.assertEqual(str(exc), "None") + self.assertIsNone(exc.cause) + self.assertIsNone(exc.stacktrace) + + def test_construction(self): + exc = errors.MarionetteException( + message=message, cause=cause, stacktrace=stacktrace + ) + self.assertEqual(exc.message, message) + self.assertEqual(exc.cause, cause) + self.assertEqual(exc.stacktrace, stacktrace) + + def test_str_message(self): + exc = errors.MarionetteException( + message=message, cause=cause, stacktrace=stacktrace + ) + r = str(exc) + self.assertIn(message, r) + self.assertIn(", caused by {0!r}".format(cause[0]), r) + self.assertIn("\nstacktrace:\n\tfirst\n\tsecond", r) + + def test_unicode_message(self): + exc = errors.MarionetteException( + message=unicode_message, cause=cause, stacktrace=stacktrace + ) + r = six.text_type(exc) + self.assertIn(unicode_message, r) + self.assertIn(", caused by {0!r}".format(cause[0]), r) + self.assertIn("\nstacktrace:\n\tfirst\n\tsecond", r) + + def test_unicode_message_as_str(self): + exc = errors.MarionetteException( + message=unicode_message, cause=cause, stacktrace=stacktrace + ) + r = str(exc) + self.assertIn(six.ensure_str(unicode_message, encoding="utf-8"), r) + self.assertIn(", caused by {0!r}".format(cause[0]), r) + self.assertIn("\nstacktrace:\n\tfirst\n\tsecond", r) + + def test_cause_string(self): + exc = errors.MarionetteException(cause="foo") + self.assertEqual(exc.cause, "foo") + r = str(exc) + self.assertIn(", caused by foo", r) + + def test_cause_tuple(self): + exc = errors.MarionetteException(cause=cause) + self.assertEqual(exc.cause, cause) + r = str(exc) + self.assertIn(", caused by {0!r}".format(cause[0]), r) + + +class TestLookup(marionette_test.MarionetteTestCase): + def test_by_unknown_number(self): + self.assertEqual(errors.MarionetteException, errors.lookup(123456)) + + def test_by_known_string(self): + self.assertEqual( + errors.NoSuchElementException, errors.lookup("no such element") + ) + + def test_by_unknown_string(self): + self.assertEqual(errors.MarionetteException, errors.lookup("barbera")) + + def test_by_known_unicode_string(self): + self.assertEqual( + errors.NoSuchElementException, errors.lookup("no such element") + ) + + +class TestAllErrors(marionette_test.MarionetteTestCase): + def test_properties(self): + for exc in errors.es_: + self.assertTrue( + hasattr(exc, "status"), "expected exception to have attribute `status'" + ) diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_execute_async_script.py b/testing/marionette/harness/marionette_harness/tests/unit/test_execute_async_script.py new file mode 100644 index 0000000000..49f68f7b94 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_execute_async_script.py @@ -0,0 +1,240 @@ +import os + +from marionette_driver.errors import ( + JavascriptException, + NoAlertPresentException, + ScriptTimeoutException, +) +from marionette_driver.marionette import Alert +from marionette_driver.wait import Wait + +from marionette_harness import MarionetteTestCase + + +class TestExecuteAsyncContent(MarionetteTestCase): + def setUp(self): + super(TestExecuteAsyncContent, self).setUp() + self.marionette.timeout.script = 1 + + def tearDown(self): + if self.alert_present(): + alert = self.marionette.switch_to_alert() + alert.dismiss() + self.wait_for_alert_closed() + + def alert_present(self): + try: + Alert(self.marionette).text + return True + except NoAlertPresentException: + return False + + def wait_for_alert_closed(self, timeout=None): + Wait(self.marionette, timeout=timeout).until(lambda _: not self.alert_present()) + + def test_execute_async_simple(self): + self.assertEqual( + 1, self.marionette.execute_async_script("arguments[arguments.length-1](1);") + ) + + def test_execute_async_ours(self): + self.assertEqual(1, self.marionette.execute_async_script("arguments[0](1);")) + + def test_script_timeout_error(self): + with self.assertRaisesRegexp(ScriptTimeoutException, "Timed out after 100 ms"): + self.marionette.execute_async_script("var x = 1;", script_timeout=100) + + def test_script_timeout_reset_after_timeout_error(self): + script_timeout = self.marionette.timeout.script + with self.assertRaises(ScriptTimeoutException): + self.marionette.execute_async_script("var x = 1;", script_timeout=100) + self.assertEqual(self.marionette.timeout.script, script_timeout) + + def test_script_timeout_no_timeout_error(self): + self.assertTrue( + self.marionette.execute_async_script( + """ + var callback = arguments[arguments.length - 1]; + setTimeout(function() { callback(true); }, 500); + """, + script_timeout=1000, + ) + ) + + def test_no_timeout(self): + self.marionette.timeout.script = 10 + self.assertTrue( + self.marionette.execute_async_script( + """ + var callback = arguments[arguments.length - 1]; + setTimeout(function() { callback(true); }, 500); + """ + ) + ) + + def test_execute_async_unload(self): + self.marionette.timeout.script = 5 + unload = """ + window.location.href = "about:blank"; + """ + self.assertRaises( + JavascriptException, self.marionette.execute_async_script, unload + ) + + def test_check_window(self): + self.assertTrue( + self.marionette.execute_async_script( + "arguments[0](window != null && window != undefined);" + ) + ) + + def test_same_context(self): + var1 = "testing" + self.assertEqual( + self.marionette.execute_script( + """ + this.testvar = '{}'; + return this.testvar; + """.format( + var1 + ) + ), + var1, + ) + self.assertEqual( + self.marionette.execute_async_script( + "arguments[0](this.testvar);", new_sandbox=False + ), + var1, + ) + + def test_execute_no_return(self): + self.assertEqual(self.marionette.execute_async_script("arguments[0]()"), None) + + def test_execute_js_exception(self): + try: + self.marionette.execute_async_script( + """ + let a = 1; + foo(bar); + """ + ) + self.fail() + except JavascriptException as e: + self.assertIsNotNone(e.stacktrace) + self.assertIn( + os.path.relpath(__file__.replace(".pyc", ".py")), e.stacktrace + ) + + def test_execute_async_js_exception(self): + try: + self.marionette.execute_async_script( + """ + let [resolve] = arguments; + resolve(foo()); + """ + ) + self.fail() + except JavascriptException as e: + self.assertIsNotNone(e.stacktrace) + self.assertIn( + os.path.relpath(__file__.replace(".pyc", ".py")), e.stacktrace + ) + + def test_script_finished(self): + self.assertTrue( + self.marionette.execute_async_script( + """ + arguments[0](true); + """ + ) + ) + + def test_execute_permission(self): + self.assertRaises( + JavascriptException, + self.marionette.execute_async_script, + """ +let prefs = Components.classes["@mozilla.org/preferences-service;1"] + .getService(Components.interfaces.nsIPrefBranch); +arguments[0](4); +""", + ) + + def test_sandbox_reuse(self): + # Sandboxes between `execute_script()` invocations are shared. + self.marionette.execute_async_script( + "this.foobar = [23, 42];" "arguments[0]();" + ) + self.assertEqual( + self.marionette.execute_async_script( + "arguments[0](this.foobar);", new_sandbox=False + ), + [23, 42], + ) + + def test_sandbox_refresh_arguments(self): + self.marionette.execute_async_script( + "this.foobar = [arguments[0], arguments[1]];" + "let resolve = " + "arguments[arguments.length - 1];" + "resolve();", + script_args=[23, 42], + ) + self.assertEqual( + self.marionette.execute_async_script( + "arguments[0](this.foobar);", new_sandbox=False + ), + [23, 42], + ) + + # Functions defined in higher privilege scopes, such as the privileged + # JSWindowActor child runs in, cannot be accessed from + # content. This tests that it is possible to introspect the objects on + # `arguments` without getting permission defined errors. This is made + # possible because the last argument is always the callback/complete + # function. + # + # See bug 1290966. + def test_introspection_of_arguments(self): + self.marionette.execute_async_script( + "arguments[0].cheese; __webDriverCallback();", script_args=[], sandbox=None + ) + + def test_return_value_on_alert(self): + res = self.marionette.execute_async_script("alert()") + self.assertIsNone(res) + + +class TestExecuteAsyncChrome(TestExecuteAsyncContent): + def setUp(self): + super(TestExecuteAsyncChrome, self).setUp() + self.marionette.set_context("chrome") + + def test_execute_async_unload(self): + pass + + def test_execute_permission(self): + self.assertEqual( + 5, + self.marionette.execute_async_script( + """ + var c = Components.classes; + arguments[0](5); + """ + ), + ) + + def test_execute_async_js_exception(self): + # Javascript exceptions are not propagated in chrome code + self.marionette.timeout.script = 0.2 + with self.assertRaises(ScriptTimeoutException): + self.marionette.execute_async_script( + """ + var callback = arguments[arguments.length - 1]; + setTimeout(function() { callback(foo()); }, 50); + """ + ) + + def test_return_value_on_alert(self): + pass diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_execute_isolate.py b/testing/marionette/harness/marionette_harness/tests/unit/test_execute_isolate.py new file mode 100644 index 0000000000..d60e2c062e --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_execute_isolate.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.errors import ScriptTimeoutException + +from marionette_harness import MarionetteTestCase + + +class TestExecuteIsolationContent(MarionetteTestCase): + def setUp(self): + super(TestExecuteIsolationContent, self).setUp() + self.content = True + + def test_execute_async_isolate(self): + # Results from one execute call that has timed out should not + # contaminate a future call. + multiplier = "*3" if self.content else "*1" + self.marionette.timeout.script = 0.5 + self.assertRaises( + ScriptTimeoutException, + self.marionette.execute_async_script, + ( + "setTimeout(function() {{ arguments[0](5{}); }}, 3000);".format( + multiplier + ) + ), + ) + + self.marionette.timeout.script = 6 + result = self.marionette.execute_async_script( + """ + let [resolve] = arguments; + setTimeout(function() {{ resolve(10{}); }}, 5000); + """.format( + multiplier + ) + ) + self.assertEqual(result, 30 if self.content else 10) + + +class TestExecuteIsolationChrome(TestExecuteIsolationContent): + def setUp(self): + super(TestExecuteIsolationChrome, self).setUp() + self.marionette.set_context("chrome") + self.content = False diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_execute_sandboxes.py b/testing/marionette/harness/marionette_harness/tests/unit/test_execute_sandboxes.py new file mode 100644 index 0000000000..5c089acd01 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_execute_sandboxes.py @@ -0,0 +1,86 @@ +# 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.errors import JavascriptException + +from marionette_harness import MarionetteTestCase + + +class TestExecuteSandboxes(MarionetteTestCase): + def setUp(self): + super(TestExecuteSandboxes, self).setUp() + + def test_execute_system_sandbox(self): + # Test that "system" sandbox has elevated privileges in execute_script + result = self.marionette.execute_script( + "return Components.interfaces.nsIPermissionManager.ALLOW_ACTION", + sandbox="system", + ) + self.assertEqual(result, 1) + + def test_execute_async_system_sandbox(self): + # Test that "system" sandbox has elevated privileges in + # execute_async_script. + result = self.marionette.execute_async_script( + """ + let result = Ci.nsIPermissionManager.ALLOW_ACTION; + arguments[0](result); + """, + sandbox="system", + ) + self.assertEqual(result, 1) + + def test_execute_switch_sandboxes(self): + # Test that sandboxes are retained when switching between them + # for execute_script. + self.marionette.execute_script("foo = 1", sandbox="1") + self.marionette.execute_script("foo = 2", sandbox="2") + foo = self.marionette.execute_script( + "return foo", sandbox="1", new_sandbox=False + ) + self.assertEqual(foo, 1) + foo = self.marionette.execute_script( + "return foo", sandbox="2", new_sandbox=False + ) + self.assertEqual(foo, 2) + + def test_execute_new_sandbox(self): + # test that clearing a sandbox does not affect other sandboxes + self.marionette.execute_script("foo = 1", sandbox="1") + self.marionette.execute_script("foo = 2", sandbox="2") + + # deprecate sandbox 1 by asking explicitly for a fresh one + with self.assertRaises(JavascriptException): + self.marionette.execute_script( + """ + return foo + """, + sandbox="1", + new_sandbox=True, + ) + + foo = self.marionette.execute_script( + "return foo", sandbox="2", new_sandbox=False + ) + self.assertEqual(foo, 2) + + def test_execute_async_switch_sandboxes(self): + # Test that sandboxes are retained when switching between them + # for execute_async_script. + self.marionette.execute_async_script("foo = 1; arguments[0]();", sandbox="1") + self.marionette.execute_async_script("foo = 2; arguments[0]();", sandbox="2") + foo = self.marionette.execute_async_script( + "arguments[0](foo);", sandbox="1", new_sandbox=False + ) + self.assertEqual(foo, 1) + foo = self.marionette.execute_async_script( + "arguments[0](foo);", sandbox="2", new_sandbox=False + ) + self.assertEqual(foo, 2) + + +class TestExecuteSandboxesChrome(TestExecuteSandboxes): + def setUp(self): + super(TestExecuteSandboxesChrome, self).setUp() + self.marionette.set_context("chrome") diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_execute_script.py b/testing/marionette/harness/marionette_harness/tests/unit/test_execute_script.py new file mode 100644 index 0000000000..79a6185d65 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_execute_script.py @@ -0,0 +1,569 @@ +import os + +from six.moves.urllib.parse import quote + +from marionette_driver import By, errors +from marionette_driver.marionette import Alert, WebElement +from marionette_driver.wait import Wait + +from marionette_harness import MarionetteTestCase, WindowManagerMixin + + +def inline(doc): + return "data:text/html;charset=utf-8,{}".format(quote(doc)) + + +elements = inline("<p>foo</p> <p>bar</p>") + +shadow_dom = """ + <style> + custom-checkbox-element { + display:block; width:20px; height:20px; + } + </style> + <custom-checkbox-element></custom-checkbox-element> + <script> + customElements.define('custom-checkbox-element', + class extends HTMLElement { + constructor() { + super(); + this.attachShadow({mode: '%s'}).innerHTML = ` + <div><input type="checkbox"/></div> + `; + } + }); + </script>""" + + +globals = set( + [ + "atob", + "Audio", + "btoa", + "document", + "navigator", + "URL", + "window", + ] +) + + +class TestExecuteContent(MarionetteTestCase): + def alert_present(self): + try: + Alert(self.marionette).text + return True + except errors.NoAlertPresentException: + return False + + def wait_for_alert_closed(self, timeout=None): + Wait(self.marionette, timeout=timeout).until(lambda _: not self.alert_present()) + + def tearDown(self): + if self.alert_present(): + alert = self.marionette.switch_to_alert() + alert.dismiss() + self.wait_for_alert_closed() + + def assert_is_defined(self, property, sandbox="default"): + self.assertTrue( + self.marionette.execute_script( + "return typeof arguments[0] != 'undefined'", [property], sandbox=sandbox + ), + "property {} is undefined".format(property), + ) + + def assert_is_web_element(self, element): + self.assertIsInstance(element, WebElement) + + def test_return_number(self): + self.assertEqual(1, self.marionette.execute_script("return 1")) + self.assertEqual(1.5, self.marionette.execute_script("return 1.5")) + + def test_return_boolean(self): + self.assertTrue(self.marionette.execute_script("return true")) + + def test_return_string(self): + self.assertEqual("foo", self.marionette.execute_script("return 'foo'")) + + def test_return_array(self): + self.assertEqual([1, 2], self.marionette.execute_script("return [1, 2]")) + self.assertEqual( + [1.25, 1.75], self.marionette.execute_script("return [1.25, 1.75]") + ) + self.assertEqual( + [True, False], self.marionette.execute_script("return [true, false]") + ) + self.assertEqual( + ["foo", "bar"], self.marionette.execute_script("return ['foo', 'bar']") + ) + self.assertEqual( + [1, 1.5, True, "foo"], + self.marionette.execute_script("return [1, 1.5, true, 'foo']"), + ) + self.assertEqual([1, [2]], self.marionette.execute_script("return [1, [2]]")) + + def test_return_object(self): + self.assertEqual({"foo": 1}, self.marionette.execute_script("return {foo: 1}")) + self.assertEqual( + {"foo": 1.5}, self.marionette.execute_script("return {foo: 1.5}") + ) + self.assertEqual( + {"foo": True}, self.marionette.execute_script("return {foo: true}") + ) + self.assertEqual( + {"foo": "bar"}, self.marionette.execute_script("return {foo: 'bar'}") + ) + self.assertEqual( + {"foo": [1, 2]}, self.marionette.execute_script("return {foo: [1, 2]}") + ) + self.assertEqual( + {"foo": {"bar": [1, 2]}}, + self.marionette.execute_script("return {foo: {bar: [1, 2]}}"), + ) + + def test_no_return_value(self): + self.assertIsNone(self.marionette.execute_script("true")) + + def test_argument_null(self): + self.assertIsNone( + self.marionette.execute_script( + "return arguments[0]", script_args=(None,), sandbox="default" + ) + ) + self.assertIsNone( + self.marionette.execute_script( + "return arguments[0]", script_args=(None,), sandbox="system" + ) + ) + self.assertIsNone( + self.marionette.execute_script( + "return arguments[0]", script_args=(None,), sandbox=None + ) + ) + + def test_argument_number(self): + self.assertEqual(1, self.marionette.execute_script("return arguments[0]", (1,))) + self.assertEqual( + 1.5, self.marionette.execute_script("return arguments[0]", (1.5,)) + ) + + def test_argument_boolean(self): + self.assertTrue(self.marionette.execute_script("return arguments[0]", (True,))) + + def test_argument_string(self): + self.assertEqual( + "foo", self.marionette.execute_script("return arguments[0]", ("foo",)) + ) + + def test_argument_array(self): + self.assertEqual( + [1, 2], self.marionette.execute_script("return arguments[0]", ([1, 2],)) + ) + + def test_argument_object(self): + self.assertEqual( + {"foo": 1}, + self.marionette.execute_script("return arguments[0]", ({"foo": 1},)), + ) + + def test_argument_shadow_root(self): + self.marionette.navigate(inline(shadow_dom % "open")) + elem = self.marionette.find_element(By.TAG_NAME, "custom-checkbox-element") + shadow_root = elem.shadow_root + nodeType = self.marionette.execute_script( + "return arguments[0].nodeType", script_args=(shadow_root,) + ) + self.assertEqual(nodeType, 11) + + def test_argument_web_element(self): + self.marionette.navigate(elements) + elem = self.marionette.find_element(By.TAG_NAME, "p") + nodeType = self.marionette.execute_script( + "return arguments[0].nodeType", script_args=(elem,) + ) + self.assertEqual(nodeType, 1) + + def test_default_sandbox_globals(self): + for property in globals: + self.assert_is_defined(property, sandbox="default") + + self.assert_is_defined("Components") + self.assert_is_defined("window.wrappedJSObject") + + def test_system_globals(self): + for property in globals: + self.assert_is_defined(property, sandbox="system") + + self.assert_is_defined("Components", sandbox="system") + self.assert_is_defined("window.wrappedJSObject", sandbox="system") + + def test_mutable_sandbox_globals(self): + for property in globals: + self.assert_is_defined(property, sandbox=None) + + # Components is there, but will be removed soon + self.assert_is_defined("Components", sandbox=None) + # wrappedJSObject is always there in sandboxes + self.assert_is_defined("window.wrappedJSObject", sandbox=None) + + def test_exception(self): + self.assertRaises( + errors.JavascriptException, self.marionette.execute_script, "return foo" + ) + + def test_stacktrace(self): + with self.assertRaises(errors.JavascriptException) as cm: + self.marionette.execute_script("return b") + + # by default execute_script pass the name of the python file + self.assertIn( + os.path.relpath(__file__.replace(".pyc", ".py")), cm.exception.stacktrace + ) + self.assertIn("b is not defined", str(cm.exception)) + + def test_permission(self): + for sandbox in ["default", None]: + with self.assertRaises(errors.JavascriptException): + self.marionette.execute_script( + "Components.classes['@mozilla.org/preferences-service;1']" + ) + + def test_return_web_element(self): + self.marionette.navigate(elements) + expected = self.marionette.find_element(By.TAG_NAME, "p") + actual = self.marionette.execute_script("return document.querySelector('p')") + self.assertEqual(expected, actual) + + def test_return_web_element_array(self): + self.marionette.navigate(elements) + expected = self.marionette.find_elements(By.TAG_NAME, "p") + actual = self.marionette.execute_script( + """ + let els = document.querySelectorAll('p') + return [els[0], els[1]]""" + ) + self.assertEqual(expected, actual) + + def test_return_web_element_nested_array(self): + self.marionette.navigate(elements) + expected = self.marionette.find_elements(By.TAG_NAME, "p") + actual = self.marionette.execute_script( + """ + let els = document.querySelectorAll('p') + return { els: [els[0], els[1]] }""" + ) + self.assertEqual(expected, actual["els"]) + + def test_return_web_element_nested_dict(self): + self.marionette.navigate(elements) + expected = self.marionette.find_element(By.TAG_NAME, "p") + actual = self.marionette.execute_script( + """ + let el = document.querySelector('p') + return { path: { to: { el } } }""" + ) + self.assertEqual(expected, actual["path"]["to"]["el"]) + + # Bug 938228 identifies a problem with unmarshaling NodeList + # objects from the DOM. document.querySelectorAll returns this + # construct. + def test_return_web_element_nodelist(self): + self.marionette.navigate(elements) + expected = self.marionette.find_elements(By.TAG_NAME, "p") + actual = self.marionette.execute_script("return document.querySelectorAll('p')") + self.assertEqual(expected, actual) + + def test_sandbox_reuse(self): + # Sandboxes between `execute_script()` invocations are shared. + self.marionette.execute_script("this.foobar = [23, 42];") + self.assertEqual( + self.marionette.execute_script("return this.foobar;", new_sandbox=False), + [23, 42], + ) + + def test_sandbox_refresh_arguments(self): + self.marionette.execute_script( + "this.foobar = [arguments[0], arguments[1]]", [23, 42] + ) + self.assertEqual( + self.marionette.execute_script("return this.foobar", new_sandbox=False), + [23, 42], + ) + + def test_mutable_sandbox_wrappedjsobject(self): + self.assert_is_defined("window.wrappedJSObject") + with self.assertRaises(errors.JavascriptException): + self.marionette.execute_script( + "window.wrappedJSObject.foo = 1", sandbox=None + ) + + def test_default_sandbox_wrappedjsobject(self): + self.assert_is_defined("window.wrappedJSObject", sandbox="default") + + try: + self.marionette.execute_script( + "window.wrappedJSObject.foo = 4", sandbox="default" + ) + self.assertEqual( + self.marionette.execute_script( + "return window.wrappedJSObject.foo", sandbox="default" + ), + 4, + ) + finally: + self.marionette.execute_script( + "delete window.wrappedJSObject.foo", sandbox="default" + ) + + def test_system_sandbox_wrappedjsobject(self): + self.assert_is_defined("window.wrappedJSObject", sandbox="system") + + self.marionette.execute_script( + "window.wrappedJSObject.foo = 4", sandbox="system" + ) + self.assertEqual( + self.marionette.execute_script( + "return window.wrappedJSObject.foo", sandbox="system" + ), + 4, + ) + + def test_system_dead_object(self): + self.assert_is_defined("window.wrappedJSObject", sandbox="system") + + self.marionette.execute_script( + "window.wrappedJSObject.foo = function() { return 'yo' }", sandbox="system" + ) + self.marionette.execute_script( + "dump(window.wrappedJSObject.foo)", sandbox="system" + ) + + self.marionette.execute_script( + "window.wrappedJSObject.foo = function() { return 'yolo' }", + sandbox="system", + ) + typ = self.marionette.execute_script( + "return typeof window.wrappedJSObject.foo", sandbox="system" + ) + self.assertEqual("function", typ) + obj = self.marionette.execute_script( + "return window.wrappedJSObject.foo.toString()", sandbox="system" + ) + self.assertIn("yolo", obj) + + def test_lasting_side_effects(self): + def send(script): + return self.marionette._send_message( + "WebDriver:ExecuteScript", {"script": script}, key="value" + ) + + send("window.foo = 1") + foo = send("return window.foo") + self.assertEqual(1, foo) + + for property in globals: + exists = send("return typeof {} != 'undefined'".format(property)) + self.assertTrue(exists, "property {} is undefined".format(property)) + + self.assertTrue( + send( + """ + return (typeof Components == 'undefined') || + (typeof Components.utils == 'undefined') + """ + ) + ) + self.assertTrue(send("return typeof window.wrappedJSObject == 'undefined'")) + + def test_no_callback(self): + self.assertTrue( + self.marionette.execute_script("return typeof arguments[0] == 'undefined'") + ) + + def test_window_set_timeout_is_not_cancelled(self): + def content_timeout_triggered(mn): + return mn.execute_script("return window.n", sandbox=None) > 0 + + # subsequent call to execute_script after this + # should not cancel the setTimeout event + self.marionette.navigate( + inline( + """ + <script> + window.n = 0; + setTimeout(() => ++window.n, 4000); + </script>""" + ) + ) + + # as debug builds are inherently slow, + # we need to assert the event did not already fire + self.assertEqual( + 0, + self.marionette.execute_script("return window.n", sandbox=None), + "setTimeout already fired", + ) + + # if event was cancelled, this will time out + Wait(self.marionette, timeout=8).until( + content_timeout_triggered, + message="Scheduled setTimeout event was cancelled by call to execute_script", + ) + + def test_access_chrome_objects_in_event_listeners(self): + # sandbox.window.addEventListener/removeEventListener + # is used by Marionette for installing the unloadHandler which + # is used to return an error when a document is unloaded during + # script execution. + # + # Certain web frameworks, notably Angular, override + # window.addEventListener/removeEventListener and introspects + # objects passed to them. If these objects originates from chrome + # without having been cloned, a permission denied error is thrown + # as part of the security precautions put in place by the sandbox. + + # addEventListener is called when script is injected + self.marionette.navigate( + inline( + """ + <script> + window.addEventListener = (event, listener) => listener.toString(); + </script> + """ + ) + ) + self.marionette.execute_script("", sandbox=None) + + # removeEventListener is called when sandbox is unloaded + self.marionette.navigate( + inline( + """ + <script> + window.removeEventListener = (event, listener) => listener.toString(); + </script> + """ + ) + ) + self.marionette.execute_script("", sandbox=None) + + def test_access_global_objects_from_chrome(self): + # test inspection of arguments + self.marionette.execute_script("__webDriverArguments.toString()") + + def test_toJSON(self): + foo = self.marionette.execute_script( + """ + return { + toJSON () { + return "foo"; + } + } + """, + sandbox=None, + ) + self.assertEqual("foo", foo) + + def test_unsafe_toJSON(self): + el = self.marionette.execute_script( + """ + return { + toJSON () { + return document.documentElement; + } + } + """, + sandbox=None, + ) + self.assert_is_web_element(el) + self.assertEqual(el, self.marionette.find_element(By.CSS_SELECTOR, ":root")) + + def test_comment_in_last_line(self): + self.marionette.execute_script(" // comment ") + + def test_return_value_on_alert(self): + res = self.marionette.execute_script("alert()") + self.assertIsNone(res) + + +class TestExecuteChrome(WindowManagerMixin, TestExecuteContent): + def setUp(self): + super(TestExecuteChrome, self).setUp() + + self.marionette.set_context("chrome") + win = self.open_chrome_window("chrome://remote/content/marionette/test.xhtml") + self.marionette.switch_to_window(win) + + def tearDown(self): + self.close_all_windows() + + super(TestExecuteChrome, self).tearDown() + + def test_permission(self): + self.marionette.execute_script( + "Components.classes['@mozilla.org/preferences-service;1']" + ) + + def test_unmarshal_element_collection(self): + expected = self.marionette.find_elements(By.TAG_NAME, "input") + actual = self.marionette.execute_script( + "return document.querySelectorAll('input')" + ) + self.assertTrue(len(expected) > 0) + self.assertEqual(expected, actual) + + def test_argument_shadow_root(self): + pass + + def test_argument_web_element(self): + elem = self.marionette.find_element(By.TAG_NAME, "input") + nodeType = self.marionette.execute_script( + "return arguments[0].nodeType", script_args=(elem,) + ) + self.assertEqual(nodeType, 1) + + def test_async_script_timeout(self): + with self.assertRaises(errors.ScriptTimeoutException): + self.marionette.execute_async_script( + """ + var cb = arguments[arguments.length - 1]; + setTimeout(function() { cb() }, 2500); + """, + script_timeout=100, + ) + + def test_lasting_side_effects(self): + pass + + def test_return_web_element(self): + pass + + def test_return_web_element_array(self): + pass + + def test_return_web_element_nested_array(self): + pass + + def test_return_web_element_nested_dict(self): + pass + + def test_return_web_element_nodelist(self): + pass + + def test_window_set_timeout_is_not_cancelled(self): + pass + + def test_mutable_sandbox_wrappedjsobject(self): + pass + + def test_default_sandbox_wrappedjsobject(self): + pass + + def test_system_sandbox_wrappedjsobject(self): + pass + + def test_access_chrome_objects_in_event_listeners(self): + pass + + def test_return_value_on_alert(self): + pass diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_expected.py b/testing/marionette/harness/marionette_harness/tests/unit/test_expected.py new file mode 100644 index 0000000000..4e22e31e83 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_expected.py @@ -0,0 +1,233 @@ +# 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 six.moves.urllib.parse import quote + +from marionette_driver import expected +from marionette_driver.by import By + +from marionette_harness import marionette_test + + +def inline(doc): + return "data:text/html;charset=utf-8,{}".format(quote(doc)) + + +static_element = inline("""<p>foo</p>""") +static_elements = static_element + static_element + +remove_element_by_tag_name = """var el = document.getElementsByTagName('{}')[0]; + document.getElementsByTagName("body")[0].remove(el);""" + +hidden_element = inline("<p style='display: none'>hidden</p>") + +selected_element = inline("<option selected>selected</option>") +unselected_element = inline("<option>unselected</option>") + +enabled_element = inline("<input>") +disabled_element = inline("<input disabled>") + + +def no_such_element(marionette): + return marionette.find_element(By.ID, "nosuchelement") + + +def no_such_elements(marionette): + return marionette.find_elements(By.ID, "nosuchelement") + + +def p(marionette): + return marionette.find_element(By.TAG_NAME, "p") + + +def ps(marionette): + return marionette.find_elements(By.TAG_NAME, "p") + + +class TestExpected(marionette_test.MarionetteTestCase): + def test_element_present_func(self): + self.marionette.navigate(static_element) + el = expected.element_present(p)(self.marionette) + self.assertIsNotNone(el) + + def test_element_present_locator(self): + self.marionette.navigate(static_element) + el = expected.element_present(By.TAG_NAME, "p")(self.marionette) + self.assertIsNotNone(el) + + def test_element_present_not_present(self): + r = expected.element_present(no_such_element)(self.marionette) + self.assertIsInstance(r, bool) + self.assertFalse(r) + + def test_element_not_present_func(self): + r = expected.element_not_present(no_such_element)(self.marionette) + self.assertIsInstance(r, bool) + self.assertTrue(r) + + def test_element_not_present_locator(self): + r = expected.element_not_present(By.ID, "nosuchelement")(self.marionette) + self.assertIsInstance(r, bool) + self.assertTrue(r) + + def test_element_not_present_is_present(self): + self.marionette.navigate(static_element) + r = expected.element_not_present(p)(self.marionette) + self.assertIsInstance(r, bool) + self.assertFalse(r) + + def test_element_stale(self): + self.marionette.navigate(static_element) + el = self.marionette.find_element(By.TAG_NAME, "p") + self.assertIsNotNone(el) + self.marionette.execute_script(remove_element_by_tag_name.format("p")) + r = expected.element_stale(el)(self.marionette) + self.assertTrue(r) + + def test_element_stale_is_not_stale(self): + self.marionette.navigate(static_element) + el = self.marionette.find_element(By.TAG_NAME, "p") + r = expected.element_stale(el)(self.marionette) + self.assertFalse(r) + + def test_elements_present_func(self): + self.marionette.navigate(static_elements) + els = expected.elements_present(ps)(self.marionette) + self.assertEqual(len(els), 2) + + def test_elements_present_locator(self): + self.marionette.navigate(static_elements) + els = expected.elements_present(By.TAG_NAME, "p")(self.marionette) + self.assertEqual(len(els), 2) + + def test_elements_present_not_present(self): + r = expected.elements_present(no_such_elements)(self.marionette) + self.assertEqual(r, []) + + def test_elements_not_present_func(self): + r = expected.element_not_present(no_such_elements)(self.marionette) + self.assertIsInstance(r, bool) + self.assertTrue(r) + + def test_elements_not_present_locator(self): + r = expected.element_not_present(By.ID, "nosuchelement")(self.marionette) + self.assertIsInstance(r, bool) + self.assertTrue(r) + + def test_elements_not_present_is_present(self): + self.marionette.navigate(static_elements) + r = expected.elements_not_present(ps)(self.marionette) + self.assertIsInstance(r, bool) + self.assertFalse(r) + + def test_element_displayed(self): + self.marionette.navigate(static_element) + el = self.marionette.find_element(By.TAG_NAME, "p") + visible = expected.element_displayed(el)(self.marionette) + self.assertTrue(visible) + + def test_element_displayed_locator(self): + self.marionette.navigate(static_element) + visible = expected.element_displayed(By.TAG_NAME, "p")(self.marionette) + self.assertTrue(visible) + + def test_element_displayed_when_hidden(self): + self.marionette.navigate(hidden_element) + el = self.marionette.find_element(By.TAG_NAME, "p") + visible = expected.element_displayed(el)(self.marionette) + self.assertFalse(visible) + + def test_element_displayed_when_hidden_locator(self): + self.marionette.navigate(hidden_element) + visible = expected.element_displayed(By.TAG_NAME, "p")(self.marionette) + self.assertFalse(visible) + + def test_element_displayed_when_not_present(self): + self.marionette.navigate("about:blank") + visible = expected.element_displayed(By.TAG_NAME, "p")(self.marionette) + self.assertFalse(visible) + + def test_element_displayed_when_stale_element(self): + self.marionette.navigate(static_element) + el = self.marionette.find_element(By.TAG_NAME, "p") + self.marionette.execute_script("arguments[0].remove()", [el]) + missing = expected.element_displayed(el)(self.marionette) + self.assertFalse(missing) + + def test_element_not_displayed(self): + self.marionette.navigate(hidden_element) + el = self.marionette.find_element(By.TAG_NAME, "p") + hidden = expected.element_not_displayed(el)(self.marionette) + self.assertTrue(hidden) + + def test_element_not_displayed_locator(self): + self.marionette.navigate(hidden_element) + hidden = expected.element_not_displayed(By.TAG_NAME, "p")(self.marionette) + self.assertTrue(hidden) + + def test_element_not_displayed_when_visible(self): + self.marionette.navigate(static_element) + el = self.marionette.find_element(By.TAG_NAME, "p") + hidden = expected.element_not_displayed(el)(self.marionette) + self.assertFalse(hidden) + + def test_element_not_displayed_when_visible_locator(self): + self.marionette.navigate(static_element) + hidden = expected.element_not_displayed(By.TAG_NAME, "p")(self.marionette) + self.assertFalse(hidden) + + def test_element_not_displayed_when_stale_element(self): + self.marionette.navigate(static_element) + el = self.marionette.find_element(By.TAG_NAME, "p") + self.marionette.execute_script("arguments[0].remove()", [el]) + missing = expected.element_not_displayed(el)(self.marionette) + self.assertTrue(missing) + + def test_element_selected(self): + self.marionette.navigate(selected_element) + el = self.marionette.find_element(By.TAG_NAME, "option") + selected = expected.element_selected(el)(self.marionette) + self.assertTrue(selected) + + def test_element_selected_when_not_selected(self): + self.marionette.navigate(unselected_element) + el = self.marionette.find_element(By.TAG_NAME, "option") + unselected = expected.element_selected(el)(self.marionette) + self.assertFalse(unselected) + + def test_element_not_selected(self): + self.marionette.navigate(unselected_element) + el = self.marionette.find_element(By.TAG_NAME, "option") + unselected = expected.element_not_selected(el)(self.marionette) + self.assertTrue(unselected) + + def test_element_not_selected_when_selected(self): + self.marionette.navigate(selected_element) + el = self.marionette.find_element(By.TAG_NAME, "option") + selected = expected.element_not_selected(el)(self.marionette) + self.assertFalse(selected) + + def test_element_enabled(self): + self.marionette.navigate(enabled_element) + el = self.marionette.find_element(By.TAG_NAME, "input") + enabled = expected.element_enabled(el)(self.marionette) + self.assertTrue(enabled) + + def test_element_enabled_when_disabled(self): + self.marionette.navigate(disabled_element) + el = self.marionette.find_element(By.TAG_NAME, "input") + disabled = expected.element_enabled(el)(self.marionette) + self.assertFalse(disabled) + + def test_element_not_enabled(self): + self.marionette.navigate(disabled_element) + el = self.marionette.find_element(By.TAG_NAME, "input") + disabled = expected.element_not_enabled(el)(self.marionette) + self.assertTrue(disabled) + + def test_element_not_enabled_when_enabled(self): + self.marionette.navigate(enabled_element) + el = self.marionette.find_element(By.TAG_NAME, "input") + enabled = expected.element_not_enabled(el)(self.marionette) + self.assertFalse(enabled) diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_expectedfail.py b/testing/marionette/harness/marionette_harness/tests/unit/test_expectedfail.py new file mode 100644 index 0000000000..e4d3fc499e --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_expectedfail.py @@ -0,0 +1,11 @@ +# 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_harness import MarionetteTestCase + + +class TestFail(MarionetteTestCase): + def test_fails(self): + # this test is supposed to fail! + self.assertEqual(True, False) diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_file_upload.py b/testing/marionette/harness/marionette_harness/tests/unit/test_file_upload.py new file mode 100644 index 0000000000..d2ed2a8731 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_file_upload.py @@ -0,0 +1,169 @@ +# 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/. + +import contextlib + +from tempfile import NamedTemporaryFile as tempfile + +import six +from six.moves.urllib.parse import quote + +from marionette_driver import By, errors, expected +from marionette_driver.wait import Wait +from marionette_harness import MarionetteTestCase, skip + + +single = "data:text/html,{}".format(quote("<input type=file>")) +multiple = "data:text/html,{}".format(quote("<input type=file multiple>")) +upload = lambda url: "data:text/html,{}".format( + quote( + """ + <form action='{}' method=post enctype='multipart/form-data'> + <input type=file> + <input type=submit> + </form>""".format( + url + ) + ) +) + + +class TestFileUpload(MarionetteTestCase): + def test_sets_one_file(self): + self.marionette.navigate(single) + input = self.input + + exp = None + with tempfile() as f: + input.send_keys(f.name) + exp = [f.name] + + files = self.get_file_names(input) + self.assertEqual(len(files), 1) + self.assertFileNamesEqual(files, exp) + + def test_sets_multiple_files(self): + self.marionette.navigate(multiple) + input = self.input + + exp = None + with tempfile() as a, tempfile() as b: + input.send_keys(a.name) + input.send_keys(b.name) + exp = [a.name, b.name] + + files = self.get_file_names(input) + self.assertEqual(len(files), 2) + self.assertFileNamesEqual(files, exp) + + def test_sets_multiple_indentical_files(self): + self.marionette.navigate(multiple) + input = self.input + + exp = [] + with tempfile() as f: + input.send_keys(f.name) + input.send_keys(f.name) + exp = f.name + + files = self.get_file_names(input) + self.assertEqual(len(files), 2) + self.assertFileNamesEqual(files, exp) + + def test_clear_file(self): + self.marionette.navigate(single) + input = self.input + + with tempfile() as f: + input.send_keys(f.name) + + self.assertEqual(len(self.get_files(input)), 1) + input.clear() + self.assertEqual(len(self.get_files(input)), 0) + + def test_clear_files(self): + self.marionette.navigate(multiple) + input = self.input + + with tempfile() as a, tempfile() as b: + input.send_keys(a.name) + input.send_keys(b.name) + + self.assertEqual(len(self.get_files(input)), 2) + input.clear() + self.assertEqual(len(self.get_files(input)), 0) + + def test_illegal_file(self): + self.marionette.navigate(single) + with self.assertRaisesRegexp(errors.MarionetteException, "File not found"): + self.input.send_keys("rochefort") + + def test_upload(self): + self.marionette.navigate(upload(self.marionette.absolute_url("file_upload"))) + url = self.marionette.get_url() + + with tempfile() as f: + f.write(six.ensure_binary("camembert")) + f.flush() + self.input.send_keys(f.name) + self.submit.click() + + Wait(self.marionette, timeout=self.marionette.timeout.page_load).until( + lambda m: m.get_url() != url, + message="URL didn't change after submitting a file upload", + ) + self.assertIn("multipart/form-data", self.body.text) + + def test_change_event(self): + self.marionette.navigate(single) + self.marionette.execute_script( + """ + window.changeEvs = []; + let el = arguments[arguments.length - 1]; + el.addEventListener("change", ev => window.changeEvs.push(ev)); + console.log(window.changeEvs.length); + """, + script_args=(self.input,), + sandbox=None, + ) + + with tempfile() as f: + self.input.send_keys(f.name) + + nevs = self.marionette.execute_script( + "return window.changeEvs.length", sandbox=None + ) + self.assertEqual(1, nevs) + + def find_inputs(self): + return self.marionette.find_elements(By.TAG_NAME, "input") + + @property + def input(self): + return self.find_inputs()[0] + + @property + def submit(self): + return self.find_inputs()[1] + + @property + def body(self): + return Wait(self.marionette).until( + expected.element_present(By.TAG_NAME, "body") + ) + + def get_file_names(self, el): + fl = self.get_files(el) + return [f["name"] for f in fl] + + def get_files(self, el): + return self.marionette.execute_script( + "return arguments[0].files", script_args=[el] + ) + + def assertFileNamesEqual(self, act, exp): + # File array returned from browser doesn't contain full path names, + # this cuts off the path of the expected files. + filenames = [f.rsplit("/", 0)[-1] for f in act] + self.assertListEqual(filenames, act) diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_findelement.py b/testing/marionette/harness/marionette_harness/tests/unit/test_findelement.py new file mode 100644 index 0000000000..3718d6bc6d --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_findelement.py @@ -0,0 +1,479 @@ +# 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/. + +import re + +from six.moves.urllib.parse import quote + +from marionette_driver.by import By +from marionette_driver.errors import NoSuchElementException, InvalidSelectorException +from marionette_driver.marionette import WebElement + +from marionette_harness import MarionetteTestCase, skip + + +def inline(doc, doctype="html"): + if doctype == "html": + return "data:text/html;charset=utf-8,{}".format(quote(doc)) + elif doctype == "xhtml": + return "data:application/xhtml+xml,{}".format( + quote( + r"""<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" + "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> + <head> + <title>XHTML might be the future</title> + </head> + + <body> + {} + </body> +</html>""".format( + doc + ) + ) + ) + + +id_html = inline("<p id=foo></p>", doctype="html") +id_xhtml = inline('<p id="foo"></p>', doctype="xhtml") +parent_child_html = inline("<div id=parent><p id=child></p></div>", doctype="html") +parent_child_xhtml = inline( + '<div id="parent"><p id="child"></p></div>', doctype="xhtml" +) +children_html = inline("<div><p>foo <p>bar</div>", doctype="html") +children_xhtml = inline("<div><p>foo</p> <p>bar</p></div>", doctype="xhtml") +class_html = inline("<p class='foo bar'>", doctype="html") +class_xhtml = inline('<p class="foo bar"></p>', doctype="xhtml") +name_html = inline("<p name=foo>", doctype="html") +name_xhtml = inline('<p name="foo"></p>', doctype="xhtml") + + +class TestFindElementHTML(MarionetteTestCase): + def setUp(self): + MarionetteTestCase.setUp(self) + self.marionette.timeout.implicit = 0 + + def test_id(self): + self.marionette.navigate(id_html) + expected = self.marionette.execute_script("return document.querySelector('p')") + found = self.marionette.find_element(By.ID, "foo") + self.assertIsInstance(found, WebElement) + self.assertEqual(found, expected) + + def test_child_element(self): + self.marionette.navigate(parent_child_html) + parent = self.marionette.find_element(By.ID, "parent") + child = self.marionette.find_element(By.ID, "child") + found = parent.find_element(By.TAG_NAME, "p") + self.assertEqual(found.tag_name, "p") + self.assertIsInstance(found, WebElement) + self.assertEqual(child, found) + + def test_tag_name(self): + self.marionette.navigate(children_html) + el = self.marionette.execute_script("return document.querySelector('p')") + found = self.marionette.find_element(By.TAG_NAME, "p") + self.assertIsInstance(found, WebElement) + self.assertEqual(el, found) + + def test_class_name(self): + self.marionette.navigate(class_html) + el = self.marionette.execute_script("return document.querySelector('.foo')") + found = self.marionette.find_element(By.CLASS_NAME, "foo") + self.assertIsInstance(found, WebElement) + self.assertEqual(el, found) + + def test_by_name(self): + self.marionette.navigate(name_html) + el = self.marionette.execute_script( + "return document.querySelector('[name=foo]')" + ) + found = self.marionette.find_element(By.NAME, "foo") + self.assertIsInstance(found, WebElement) + self.assertEqual(el, found) + + def test_css_selector(self): + self.marionette.navigate(children_html) + el = self.marionette.execute_script("return document.querySelector('p')") + found = self.marionette.find_element(By.CSS_SELECTOR, "p") + self.assertIsInstance(found, WebElement) + self.assertEqual(el, found) + + def test_invalid_css_selector_should_throw(self): + with self.assertRaises(InvalidSelectorException): + self.marionette.find_element(By.CSS_SELECTOR, "#") + + def test_xpath(self): + self.marionette.navigate(id_html) + el = self.marionette.execute_script("return document.querySelector('#foo')") + found = self.marionette.find_element(By.XPATH, "id('foo')") + self.assertIsInstance(found, WebElement) + self.assertEqual(el, found) + + def test_not_found(self): + self.marionette.timeout.implicit = 0 + self.assertRaises( + NoSuchElementException, + self.marionette.find_element, + By.CLASS_NAME, + "cheese", + ) + self.assertRaises( + NoSuchElementException, + self.marionette.find_element, + By.CSS_SELECTOR, + "cheese", + ) + self.assertRaises( + NoSuchElementException, self.marionette.find_element, By.ID, "cheese" + ) + self.assertRaises( + NoSuchElementException, self.marionette.find_element, By.LINK_TEXT, "cheese" + ) + self.assertRaises( + NoSuchElementException, self.marionette.find_element, By.NAME, "cheese" + ) + self.assertRaises( + NoSuchElementException, + self.marionette.find_element, + By.PARTIAL_LINK_TEXT, + "cheese", + ) + self.assertRaises( + NoSuchElementException, self.marionette.find_element, By.TAG_NAME, "cheese" + ) + self.assertRaises( + NoSuchElementException, self.marionette.find_element, By.XPATH, "cheese" + ) + + def test_not_found_implicit_wait(self): + self.marionette.timeout.implicit = 0.5 + self.assertRaises( + NoSuchElementException, + self.marionette.find_element, + By.CLASS_NAME, + "cheese", + ) + self.assertRaises( + NoSuchElementException, + self.marionette.find_element, + By.CSS_SELECTOR, + "cheese", + ) + self.assertRaises( + NoSuchElementException, self.marionette.find_element, By.ID, "cheese" + ) + self.assertRaises( + NoSuchElementException, self.marionette.find_element, By.LINK_TEXT, "cheese" + ) + self.assertRaises( + NoSuchElementException, self.marionette.find_element, By.NAME, "cheese" + ) + self.assertRaises( + NoSuchElementException, + self.marionette.find_element, + By.PARTIAL_LINK_TEXT, + "cheese", + ) + self.assertRaises( + NoSuchElementException, self.marionette.find_element, By.TAG_NAME, "cheese" + ) + self.assertRaises( + NoSuchElementException, self.marionette.find_element, By.XPATH, "cheese" + ) + + def test_not_found_from_element(self): + self.marionette.timeout.implicit = 0 + self.marionette.navigate(id_html) + el = self.marionette.find_element(By.ID, "foo") + self.assertRaises( + NoSuchElementException, el.find_element, By.CLASS_NAME, "cheese" + ) + self.assertRaises( + NoSuchElementException, el.find_element, By.CSS_SELECTOR, "cheese" + ) + self.assertRaises(NoSuchElementException, el.find_element, By.ID, "cheese") + self.assertRaises( + NoSuchElementException, el.find_element, By.LINK_TEXT, "cheese" + ) + self.assertRaises(NoSuchElementException, el.find_element, By.NAME, "cheese") + self.assertRaises( + NoSuchElementException, el.find_element, By.PARTIAL_LINK_TEXT, "cheese" + ) + self.assertRaises( + NoSuchElementException, el.find_element, By.TAG_NAME, "cheese" + ) + self.assertRaises(NoSuchElementException, el.find_element, By.XPATH, "cheese") + + def test_not_found_implicit_wait_from_element(self): + self.marionette.timeout.implicit = 0.5 + self.marionette.navigate(id_html) + el = self.marionette.find_element(By.ID, "foo") + self.assertRaises( + NoSuchElementException, el.find_element, By.CLASS_NAME, "cheese" + ) + self.assertRaises( + NoSuchElementException, el.find_element, By.CSS_SELECTOR, "cheese" + ) + self.assertRaises(NoSuchElementException, el.find_element, By.ID, "cheese") + self.assertRaises( + NoSuchElementException, el.find_element, By.LINK_TEXT, "cheese" + ) + self.assertRaises(NoSuchElementException, el.find_element, By.NAME, "cheese") + self.assertRaises( + NoSuchElementException, el.find_element, By.PARTIAL_LINK_TEXT, "cheese" + ) + self.assertRaises( + NoSuchElementException, el.find_element, By.TAG_NAME, "cheese" + ) + self.assertRaises(NoSuchElementException, el.find_element, By.XPATH, "cheese") + + def test_css_selector_scope_doesnt_start_at_rootnode(self): + self.marionette.navigate(parent_child_html) + el = self.marionette.find_element(By.ID, "child") + parent = self.marionette.find_element(By.ID, "parent") + found = parent.find_element(By.CSS_SELECTOR, "p") + self.assertEqual(el, found) + + def test_unknown_selector(self): + with self.assertRaises(InvalidSelectorException): + self.marionette.find_element("foo", "bar") + + def test_invalid_xpath_selector(self): + with self.assertRaises(InvalidSelectorException): + self.marionette.find_element(By.XPATH, "count(//input)") + with self.assertRaises(InvalidSelectorException): + parent = self.marionette.execute_script("return document.documentElement") + parent.find_element(By.XPATH, "count(//input)") + + def test_invalid_css_selector(self): + with self.assertRaises(InvalidSelectorException): + self.marionette.find_element(By.CSS_SELECTOR, "") + with self.assertRaises(InvalidSelectorException): + parent = self.marionette.execute_script("return document.documentElement") + parent.find_element(By.CSS_SELECTOR, "") + + def test_finding_active_element_returns_element(self): + self.marionette.navigate(id_html) + active = self.marionette.execute_script("return document.activeElement") + self.assertEqual(active, self.marionette.get_active_element()) + + +class TestFindElementXHTML(MarionetteTestCase): + def setUp(self): + MarionetteTestCase.setUp(self) + self.marionette.timeout.implicit = 0 + + def test_id(self): + self.marionette.navigate(id_xhtml) + expected = self.marionette.execute_script("return document.querySelector('p')") + found = self.marionette.find_element(By.ID, "foo") + self.assertIsInstance(found, WebElement) + self.assertEqual(expected, found) + + def test_child_element(self): + self.marionette.navigate(parent_child_xhtml) + parent = self.marionette.find_element(By.ID, "parent") + child = self.marionette.find_element(By.ID, "child") + found = parent.find_element(By.TAG_NAME, "p") + self.assertEqual(found.tag_name, "p") + self.assertIsInstance(found, WebElement) + self.assertEqual(child, found) + + def test_tag_name(self): + self.marionette.navigate(children_xhtml) + el = self.marionette.execute_script("return document.querySelector('p')") + found = self.marionette.find_element(By.TAG_NAME, "p") + self.assertIsInstance(found, WebElement) + self.assertEqual(el, found) + + def test_class_name(self): + self.marionette.navigate(class_xhtml) + el = self.marionette.execute_script("return document.querySelector('.foo')") + found = self.marionette.find_element(By.CLASS_NAME, "foo") + self.assertIsInstance(found, WebElement) + self.assertEqual(el, found) + + def test_by_name(self): + self.marionette.navigate(name_xhtml) + el = self.marionette.execute_script( + "return document.querySelector('[name=foo]')" + ) + found = self.marionette.find_element(By.NAME, "foo") + self.assertIsInstance(found, WebElement) + self.assertEqual(el, found) + + def test_css_selector(self): + self.marionette.navigate(children_xhtml) + el = self.marionette.execute_script("return document.querySelector('p')") + found = self.marionette.find_element(By.CSS_SELECTOR, "p") + self.assertIsInstance(found, WebElement) + self.assertEqual(el, found) + + def test_xpath(self): + self.marionette.navigate(id_xhtml) + el = self.marionette.execute_script("return document.querySelector('#foo')") + found = self.marionette.find_element(By.XPATH, "id('foo')") + self.assertIsInstance(found, WebElement) + self.assertEqual(el, found) + + def test_css_selector_scope_does_not_start_at_rootnode(self): + self.marionette.navigate(parent_child_xhtml) + el = self.marionette.find_element(By.ID, "child") + parent = self.marionette.find_element(By.ID, "parent") + found = parent.find_element(By.CSS_SELECTOR, "p") + self.assertEqual(el, found) + + def test_active_element(self): + self.marionette.navigate(id_xhtml) + active = self.marionette.execute_script("return document.activeElement") + self.assertEqual(active, self.marionette.get_active_element()) + + +class TestFindElementsHTML(MarionetteTestCase): + def setUp(self): + MarionetteTestCase.setUp(self) + self.marionette.timeout.implicit = 0 + + def assertItemsIsInstance(self, items, typ): + for item in items: + self.assertIsInstance(item, typ) + + def test_child_elements(self): + self.marionette.navigate(children_html) + parent = self.marionette.find_element(By.TAG_NAME, "div") + children = self.marionette.find_elements(By.TAG_NAME, "p") + found = parent.find_elements(By.TAG_NAME, "p") + self.assertItemsIsInstance(found, WebElement) + self.assertSequenceEqual(found, children) + + def test_tag_name(self): + self.marionette.navigate(children_html) + els = self.marionette.execute_script("return document.querySelectorAll('p')") + found = self.marionette.find_elements(By.TAG_NAME, "p") + self.assertItemsIsInstance(found, WebElement) + self.assertSequenceEqual(els, found) + + def test_class_name(self): + self.marionette.navigate(class_html) + els = self.marionette.execute_script("return document.querySelectorAll('.foo')") + found = self.marionette.find_elements(By.CLASS_NAME, "foo") + self.assertItemsIsInstance(found, WebElement) + self.assertSequenceEqual(els, found) + + def test_by_name(self): + self.marionette.navigate(name_html) + els = self.marionette.execute_script( + "return document.querySelectorAll('[name=foo]')" + ) + found = self.marionette.find_elements(By.NAME, "foo") + self.assertItemsIsInstance(found, WebElement) + self.assertSequenceEqual(els, found) + + def test_css_selector(self): + self.marionette.navigate(children_html) + els = self.marionette.execute_script("return document.querySelectorAll('p')") + found = self.marionette.find_elements(By.CSS_SELECTOR, "p") + self.assertItemsIsInstance(found, WebElement) + self.assertSequenceEqual(els, found) + + def test_invalid_css_selector_should_throw(self): + with self.assertRaises(InvalidSelectorException): + self.marionette.find_elements(By.CSS_SELECTOR, "#") + + def test_xpath(self): + self.marionette.navigate(children_html) + els = self.marionette.execute_script("return document.querySelectorAll('p')") + found = self.marionette.find_elements(By.XPATH, ".//p") + self.assertItemsIsInstance(found, WebElement) + self.assertSequenceEqual(els, found) + + def test_css_selector_scope_doesnt_start_at_rootnode(self): + self.marionette.navigate(parent_child_html) + els = self.marionette.find_elements(By.ID, "child") + parent = self.marionette.find_element(By.ID, "parent") + found = parent.find_elements(By.CSS_SELECTOR, "p") + self.assertSequenceEqual(els, found) + + def test_unknown_selector(self): + with self.assertRaises(InvalidSelectorException): + self.marionette.find_elements("foo", "bar") + + def test_invalid_xpath_selector(self): + with self.assertRaises(InvalidSelectorException): + self.marionette.find_elements(By.XPATH, "count(//input)") + with self.assertRaises(InvalidSelectorException): + parent = self.marionette.execute_script("return document.documentElement") + parent.find_elements(By.XPATH, "count(//input)") + + def test_invalid_css_selector(self): + with self.assertRaises(InvalidSelectorException): + self.marionette.find_elements(By.CSS_SELECTOR, "") + with self.assertRaises(InvalidSelectorException): + parent = self.marionette.execute_script("return document.documentElement") + parent.find_elements(By.CSS_SELECTOR, "") + + +class TestFindElementsXHTML(MarionetteTestCase): + def setUp(self): + MarionetteTestCase.setUp(self) + self.marionette.timeout.implicit = 0 + + def assertItemsIsInstance(self, items, typ): + for item in items: + self.assertIsInstance(item, typ) + + def test_child_elements(self): + self.marionette.navigate(children_xhtml) + parent = self.marionette.find_element(By.TAG_NAME, "div") + children = self.marionette.find_elements(By.TAG_NAME, "p") + found = parent.find_elements(By.TAG_NAME, "p") + self.assertItemsIsInstance(found, WebElement) + self.assertSequenceEqual(found, children) + + def test_tag_name(self): + self.marionette.navigate(children_xhtml) + els = self.marionette.execute_script("return document.querySelectorAll('p')") + found = self.marionette.find_elements(By.TAG_NAME, "p") + self.assertItemsIsInstance(found, WebElement) + self.assertSequenceEqual(els, found) + + def test_class_name(self): + self.marionette.navigate(class_xhtml) + els = self.marionette.execute_script("return document.querySelectorAll('.foo')") + found = self.marionette.find_elements(By.CLASS_NAME, "foo") + self.assertItemsIsInstance(found, WebElement) + self.assertSequenceEqual(els, found) + + def test_by_name(self): + self.marionette.navigate(name_xhtml) + els = self.marionette.execute_script( + "return document.querySelectorAll('[name=foo]')" + ) + found = self.marionette.find_elements(By.NAME, "foo") + self.assertItemsIsInstance(found, WebElement) + self.assertSequenceEqual(els, found) + + def test_css_selector(self): + self.marionette.navigate(children_xhtml) + els = self.marionette.execute_script("return document.querySelectorAll('p')") + found = self.marionette.find_elements(By.CSS_SELECTOR, "p") + self.assertItemsIsInstance(found, WebElement) + self.assertSequenceEqual(els, found) + + @skip("XHTML namespace not yet supported") + def test_xpath(self): + self.marionette.navigate(children_xhtml) + els = self.marionette.execute_script("return document.querySelectorAll('p')") + found = self.marionette.find_elements(By.XPATH, "//xhtml:p") + self.assertItemsIsInstance(found, WebElement) + self.assertSequenceEqual(els, found) + + def test_css_selector_scope_doesnt_start_at_rootnode(self): + self.marionette.navigate(parent_child_xhtml) + els = self.marionette.find_elements(By.ID, "child") + parent = self.marionette.find_element(By.ID, "parent") + found = parent.find_elements(By.CSS_SELECTOR, "p") + self.assertSequenceEqual(els, found) diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_findelement_chrome.py b/testing/marionette/harness/marionette_harness/tests/unit/test_findelement_chrome.py new file mode 100644 index 0000000000..eccbcf1195 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_findelement_chrome.py @@ -0,0 +1,169 @@ +# 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.by import By +from marionette_driver.errors import NoSuchElementException +from marionette_driver.marionette import WebElement, WEB_ELEMENT_KEY + +from marionette_harness import MarionetteTestCase, parameterized, WindowManagerMixin + + +PAGE_XHTML = "chrome://remote/content/marionette/test_no_xul.xhtml" +PAGE_XUL = "chrome://remote/content/marionette/test.xhtml" + + +class TestElementsChrome(WindowManagerMixin, MarionetteTestCase): + def setUp(self): + super(TestElementsChrome, self).setUp() + + self.marionette.set_context("chrome") + + def tearDown(self): + self.close_all_windows() + + super(TestElementsChrome, self).tearDown() + + @parameterized("XUL", PAGE_XUL) + @parameterized("XHTML", PAGE_XHTML) + def test_id(self, chrome_url): + win = self.open_chrome_window(chrome_url) + self.marionette.switch_to_window(win) + + el = self.marionette.execute_script( + "return window.document.getElementById('textInput');" + ) + found_el = self.marionette.find_element(By.ID, "textInput") + self.assertEqual(WebElement, type(found_el)) + self.assertEqual(WEB_ELEMENT_KEY, found_el.kind) + self.assertEqual(el, found_el) + + @parameterized("XUL", PAGE_XUL) + @parameterized("XHTML", PAGE_XHTML) + def test_that_we_can_find_elements_from_css_selectors(self, chrome_url): + win = self.open_chrome_window(chrome_url) + self.marionette.switch_to_window(win) + + el = self.marionette.execute_script( + "return window.document.getElementById('textInput');" + ) + found_el = self.marionette.find_element(By.CSS_SELECTOR, "#textInput") + self.assertEqual(WebElement, type(found_el)) + self.assertEqual(WEB_ELEMENT_KEY, found_el.kind) + self.assertEqual(el, found_el) + + @parameterized("XUL", PAGE_XUL) + @parameterized("XHTML", PAGE_XHTML) + def test_child_element(self, chrome_url): + win = self.open_chrome_window(chrome_url) + self.marionette.switch_to_window(win) + + el = self.marionette.find_element(By.ID, "textInput") + parent = self.marionette.find_element(By.ID, "things") + found_el = parent.find_element(By.TAG_NAME, "input") + self.assertEqual(WebElement, type(found_el)) + self.assertEqual(WEB_ELEMENT_KEY, found_el.kind) + self.assertEqual(el, found_el) + + @parameterized("XUL", PAGE_XUL) + @parameterized("XHTML", PAGE_XHTML) + def test_child_elements(self, chrome_url): + win = self.open_chrome_window(chrome_url) + self.marionette.switch_to_window(win) + + el = self.marionette.find_element(By.ID, "textInput3") + parent = self.marionette.find_element(By.ID, "things") + found_els = parent.find_elements(By.TAG_NAME, "input") + self.assertTrue(el.id in [found_el.id for found_el in found_els]) + + @parameterized("XUL", PAGE_XUL) + @parameterized("XHTML", PAGE_XHTML) + def test_tag_name(self, chrome_url): + win = self.open_chrome_window(chrome_url) + self.marionette.switch_to_window(win) + + el = self.marionette.execute_script( + "return window.document.getElementsByTagName('vbox')[0];" + ) + found_el = self.marionette.find_element(By.TAG_NAME, "vbox") + self.assertEqual("vbox", found_el.tag_name) + self.assertEqual(WebElement, type(found_el)) + self.assertEqual(WEB_ELEMENT_KEY, found_el.kind) + self.assertEqual(el, found_el) + + @parameterized("XUL", PAGE_XUL) + @parameterized("XHTML", PAGE_XHTML) + def test_class_name(self, chrome_url): + win = self.open_chrome_window(chrome_url) + self.marionette.switch_to_window(win) + + el = self.marionette.execute_script( + "return window.document.getElementsByClassName('asdf')[0];" + ) + found_el = self.marionette.find_element(By.CLASS_NAME, "asdf") + self.assertEqual(WebElement, type(found_el)) + self.assertEqual(WEB_ELEMENT_KEY, found_el.kind) + self.assertEqual(el, found_el) + + @parameterized("XUL", PAGE_XUL) + @parameterized("XHTML", PAGE_XHTML) + def test_xpath(self, chrome_url): + win = self.open_chrome_window(chrome_url) + self.marionette.switch_to_window(win) + + el = self.marionette.execute_script( + "return window.document.getElementById('testBox');" + ) + found_el = self.marionette.find_element(By.XPATH, "id('testBox')") + self.assertEqual(WebElement, type(found_el)) + self.assertEqual(WEB_ELEMENT_KEY, found_el.kind) + self.assertEqual(el, found_el) + + @parameterized("XUL", PAGE_XUL) + @parameterized("XHTML", PAGE_XHTML) + def test_not_found(self, chrome_url): + win = self.open_chrome_window(chrome_url) + self.marionette.switch_to_window(win) + + self.marionette.timeout.implicit = 1 + self.assertRaises( + NoSuchElementException, + self.marionette.find_element, + By.ID, + "I'm not on the page", + ) + self.marionette.timeout.implicit = 0 + self.assertRaises( + NoSuchElementException, + self.marionette.find_element, + By.ID, + "I'm not on the page", + ) + + @parameterized("XUL", PAGE_XUL) + @parameterized("XHTML", PAGE_XHTML) + def test_timeout(self, chrome_url): + win = self.open_chrome_window(chrome_url) + self.marionette.switch_to_window(win) + + self.assertRaises( + NoSuchElementException, self.marionette.find_element, By.ID, "myid" + ) + self.marionette.timeout.implicit = 4 + self.marionette.execute_script( + """ + window.setTimeout(function () { + var b = window.document.createXULElement('button'); + b.id = 'myid'; + document.getElementById('things').appendChild(b); + }, 1000); """ + ) + found_el = self.marionette.find_element(By.ID, "myid") + self.assertEqual(WebElement, type(found_el)) + self.assertEqual(WEB_ELEMENT_KEY, found_el.kind) + + self.marionette.execute_script( + """ + var elem = window.document.getElementById('things'); + elem.removeChild(window.document.getElementById('myid')); """ + ) diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_geckoinstance.py b/testing/marionette/harness/marionette_harness/tests/unit/test_geckoinstance.py new file mode 100644 index 0000000000..3d35217bc4 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_geckoinstance.py @@ -0,0 +1,25 @@ +# 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.geckoinstance import apps, GeckoInstance + +from marionette_harness import MarionetteTestCase + + +class TestGeckoInstance(MarionetteTestCase): + def test_create(self): + """Test that the correct gecko instance is determined.""" + for app in apps: + # If app has been specified we directly return the appropriate instance class + self.assertEqual(type(GeckoInstance.create(app=app, bin="n/a")), apps[app]) + + # Unknown applications and binaries should fail + self.assertRaises( + NotImplementedError, + GeckoInstance.create, + app="n/a", + bin=self.marionette.bin, + ) + self.assertRaises(NotImplementedError, GeckoInstance.create, bin="n/a") + self.assertRaises(NotImplementedError, GeckoInstance.create, bin=None) diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_get_computed_label.py b/testing/marionette/harness/marionette_harness/tests/unit/test_get_computed_label.py new file mode 100644 index 0000000000..07091319c9 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_get_computed_label.py @@ -0,0 +1,26 @@ +# 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 six.moves.urllib.parse import quote + +from marionette_driver import By, errors +from marionette_harness import MarionetteTestCase + + +def inline(doc): + return "data:text/html;charset=utf-8,{}".format(quote(doc)) + + +class TestGetComputedLabel(MarionetteTestCase): + def test_can_get_computed_label(self): + self.marionette.navigate(inline("<label for=b>foo<label><input id=b>")) + computed_label = self.marionette.find_element(By.ID, "b").computed_label + self.assertEqual(computed_label, "foo") + + def test_get_computed_label_no_such_element(self): + self.marionette.navigate(inline("<div id=a>")) + element = self.marionette.find_element(By.ID, "a") + element.id = "b" + with self.assertRaises(errors.NoSuchElementException): + element.computed_label diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_get_computed_role.py b/testing/marionette/harness/marionette_harness/tests/unit/test_get_computed_role.py new file mode 100644 index 0000000000..4b16a98741 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_get_computed_role.py @@ -0,0 +1,26 @@ +# 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 six.moves.urllib.parse import quote + +from marionette_driver import By, errors +from marionette_harness import MarionetteTestCase + + +def inline(doc): + return "data:text/html;charset=utf-8,{}".format(quote(doc)) + + +class TestGetComputedRole(MarionetteTestCase): + def test_can_get_computed_role(self): + self.marionette.navigate(inline("<button id=a>btn</button>")) + computed_role = self.marionette.find_element(By.ID, "a").computed_role + self.assertEqual(computed_role, "button") + + def test_get_computed_role_no_such_element(self): + self.marionette.navigate(inline("<div id=a>")) + element = self.marionette.find_element(By.ID, "a") + element.id = "b" + with self.assertRaises(errors.NoSuchElementException): + element.computed_role diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_get_current_url_chrome.py b/testing/marionette/harness/marionette_harness/tests/unit/test_get_current_url_chrome.py new file mode 100644 index 0000000000..2a2c876d03 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_get_current_url_chrome.py @@ -0,0 +1,39 @@ +# 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.errors import NoSuchWindowException + +from marionette_harness import MarionetteTestCase, WindowManagerMixin + + +class TestGetCurrentUrlChrome(WindowManagerMixin, MarionetteTestCase): + def setUp(self): + super(TestGetCurrentUrlChrome, self).setUp() + self.marionette.set_context("chrome") + + def tearDown(self): + self.close_all_windows() + super(TestGetCurrentUrlChrome, self).tearDown() + + def test_browser_window(self): + url = self.marionette.absolute_url("test.html") + + with self.marionette.using_context("content"): + self.marionette.navigate(url) + self.assertEqual(self.marionette.get_url(), url) + + chrome_url = self.marionette.execute_script("return window.location.href;") + self.assertEqual(self.marionette.get_url(), chrome_url) + + def test_no_browser_window(self): + win = self.open_chrome_window("chrome://remote/content/marionette/test.xhtml") + self.marionette.switch_to_window(win) + + chrome_url = self.marionette.execute_script("return window.location.href;") + self.assertEqual(self.marionette.get_url(), chrome_url) + + # With no tabbrowser available an exception will be thrown + with self.assertRaises(NoSuchWindowException): + with self.marionette.using_context("content"): + self.marionette.get_url() diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_get_shadow_root.py b/testing/marionette/harness/marionette_harness/tests/unit/test_get_shadow_root.py new file mode 100644 index 0000000000..b8750a6c63 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_get_shadow_root.py @@ -0,0 +1,66 @@ +# 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 six.moves.urllib.parse import quote + +from marionette_driver.by import By +from marionette_driver.errors import ( + DetachedShadowRootException, + NoSuchShadowRootException, +) +from marionette_driver.marionette import ShadowRoot +from marionette_harness import MarionetteTestCase + +checkbox_dom = """ + <style> + custom-checkbox-element { + display:block; width:20px; height:20px; + } + </style> + <custom-checkbox-element></custom-checkbox-element> + <script> + customElements.define('custom-checkbox-element', + class extends HTMLElement { + constructor() { + super(); + this.attachShadow({mode: '%s'}).innerHTML = ` + <div><input type="checkbox"/></div> + `; + } + }); + </script>""" + + +def inline(doc): + return "data:text/html;charset=utf-8,{}".format(quote(doc)) + + +class TestShadowDom(MarionetteTestCase): + def setUp(self): + super(TestShadowDom, self).setUp() + + def test_can_get_open_shadow_root(self): + self.marionette.navigate(inline(checkbox_dom % "open")) + element = self.marionette.find_element( + By.CSS_SELECTOR, "custom-checkbox-element" + ) + shadow_root = element.shadow_root + assert isinstance( + shadow_root, ShadowRoot + ), "Should have received ShadowRoot but got {}".format(shadow_root) + + def test_can_get_closed_shadow_root(self): + self.marionette.navigate(inline(checkbox_dom % "closed")) + element = self.marionette.find_element( + By.CSS_SELECTOR, "custom-checkbox-element" + ) + shadow_root = element.shadow_root + assert isinstance( + shadow_root, ShadowRoot + ), "Should have received ShadowRoot but got {}".format(shadow_root) + + def test_cannot_find_shadow_root(self): + element = self.marionette.find_element(By.CSS_SELECTOR, "style") + with self.assertRaises(NoSuchShadowRootException): + element.shadow_root diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_implicit_waits.py b/testing/marionette/harness/marionette_harness/tests/unit/test_implicit_waits.py new file mode 100644 index 0000000000..954443ac30 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_implicit_waits.py @@ -0,0 +1,26 @@ +# 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.by import By +from marionette_driver.errors import NoSuchElementException + +from marionette_harness import MarionetteTestCase + + +class TestImplicitWaits(MarionetteTestCase): + def test_implicitly_wait_for_single_element(self): + test_html = self.marionette.absolute_url("test_dynamic.html") + self.marionette.navigate(test_html) + add = self.marionette.find_element(By.ID, "adder") + self.marionette.timeout.implicit = 30 + add.click() + # all is well if this does not throw + self.marionette.find_element(By.ID, "box0") + + def test_implicit_wait_reaches_timeout(self): + test_html = self.marionette.absolute_url("test_dynamic.html") + self.marionette.navigate(test_html) + self.marionette.timeout.implicit = 3 + with self.assertRaises(NoSuchElementException): + self.marionette.find_element(By.ID, "box0") diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_localization.py b/testing/marionette/harness/marionette_harness/tests/unit/test_localization.py new file mode 100644 index 0000000000..9bf0c1ea19 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_localization.py @@ -0,0 +1,71 @@ +# 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 ( + InvalidArgumentException, + NoSuchElementException, + UnknownException, +) +from marionette_driver.localization import L10n + +from marionette_harness import MarionetteTestCase + + +class TestL10n(MarionetteTestCase): + def setUp(self): + super(TestL10n, self).setUp() + + self.l10n = L10n(self.marionette) + + def test_localize_entity(self): + dtds = ["chrome://remote/content/marionette/test_dialog.dtd"] + value = self.l10n.localize_entity(dtds, "testDialog.title") + + self.assertEqual(value, "Test Dialog") + + def test_localize_entity_invalid_arguments(self): + dtds = ["chrome://remote/content/marionette/test_dialog.dtd"] + + self.assertRaises( + NoSuchElementException, self.l10n.localize_entity, dtds, "notExistent" + ) + self.assertRaises( + InvalidArgumentException, self.l10n.localize_entity, dtds[0], "notExistent" + ) + self.assertRaises( + InvalidArgumentException, self.l10n.localize_entity, dtds, True + ) + + def test_localize_property(self): + properties = ["chrome://remote/content/marionette/test_dialog.properties"] + + value = self.l10n.localize_property(properties, "testDialog.title") + self.assertEqual(value, "Test Dialog") + + self.assertRaises( + NoSuchElementException, + self.l10n.localize_property, + properties, + "notExistent", + ) + + def test_localize_property_invalid_arguments(self): + properties = ["chrome://global/locale/filepicker.properties"] + + self.assertRaises( + NoSuchElementException, + self.l10n.localize_property, + properties, + "notExistent", + ) + self.assertRaises( + InvalidArgumentException, + self.l10n.localize_property, + properties[0], + "notExistent", + ) + self.assertRaises( + InvalidArgumentException, self.l10n.localize_property, properties, True + ) diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_marionette.py b/testing/marionette/harness/marionette_harness/tests/unit/test_marionette.py new file mode 100644 index 0000000000..790a802975 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_marionette.py @@ -0,0 +1,138 @@ +# 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/. + +import os +import socket +import time + +from marionette_driver import errors +from marionette_driver.marionette import Marionette +from marionette_harness import MarionetteTestCase, run_if_manage_instance + + +class TestMarionette(MarionetteTestCase): + def test_correct_test_name(self): + """Test that the correct test name gets set.""" + expected_test_name = "{module}.py {cls}.{func}".format( + module=__name__, + cls=self.__class__.__name__, + func=self.test_correct_test_name.__name__, + ) + + self.assertIn(expected_test_name, self.marionette.test_name) + + @run_if_manage_instance("Only runnable if Marionette manages the instance") + def test_raise_for_port_non_existing_process(self): + """Test that raise_for_port doesn't run into a timeout if instance is not running.""" + self.marionette.quit() + self.assertIsNotNone(self.marionette.instance.runner.returncode) + start_time = time.time() + self.assertRaises(socket.timeout, self.marionette.raise_for_port, timeout=5) + self.assertLess(time.time() - start_time, 5) + + @run_if_manage_instance("Only runnable if Marionette manages the instance") + def test_marionette_active_port_file(self): + active_port_file = os.path.join( + self.marionette.instance.profile.profile, "MarionetteActivePort" + ) + self.assertTrue( + os.path.exists(active_port_file), "MarionetteActivePort file written" + ) + with open(active_port_file, "r") as fp: + lines = fp.readlines() + self.assertEqual(len(lines), 1, "MarionetteActivePort file contains two lines") + self.assertEqual( + int(lines[0]), + self.marionette.port, + "MarionetteActivePort file contains port", + ) + + self.marionette.quit() + self.assertFalse( + os.path.exists(active_port_file), "MarionetteActivePort file removed" + ) + + def test_single_active_session(self): + self.assertEqual(1, self.marionette.execute_script("return 1")) + + # Use a new Marionette instance for the connection attempt, while there is + # still an active session present. + marionette = Marionette(host=self.marionette.host, port=self.marionette.port) + self.assertRaises(socket.timeout, marionette.raise_for_port, timeout=1.0) + + def test_disable_enable_new_connections(self): + # Do not re-create socket if it already exists + self.marionette._send_message("Marionette:AcceptConnections", {"value": True}) + + try: + # Disabling new connections does not affect the existing one. + self.marionette._send_message( + "Marionette:AcceptConnections", {"value": False} + ) + self.assertEqual(1, self.marionette.execute_script("return 1")) + + # Delete the current active session to allow new connection attempts. + self.marionette.delete_session() + + # Use a new Marionette instance for the connection attempt, that doesn't + # handle an instance of the application to prevent a connection lost error. + marionette = Marionette( + host=self.marionette.host, port=self.marionette.port + ) + self.assertRaises(socket.timeout, marionette.raise_for_port, timeout=1.0) + + finally: + self.marionette.quit(in_app=False) + + def test_client_socket_uses_expected_socket_timeout(self): + current_socket_timeout = self.marionette.socket_timeout + + self.assertEqual(current_socket_timeout, self.marionette.client.socket_timeout) + self.assertEqual( + current_socket_timeout, + self.marionette.client._socket_context._sock.gettimeout(), + ) + + def test_application_update_disabled(self): + # Updates of the application should always be disabled by default + with self.marionette.using_context("chrome"): + update_allowed = self.marionette.execute_script( + """ + let aus = Cc['@mozilla.org/updates/update-service;1'] + .getService(Ci.nsIApplicationUpdateService); + return aus.canCheckForUpdates; + """ + ) + + self.assertFalse(update_allowed) + + +class TestContext(MarionetteTestCase): + def setUp(self): + MarionetteTestCase.setUp(self) + self.marionette.set_context(self.marionette.CONTEXT_CONTENT) + + def get_context(self): + return self.marionette._send_message("Marionette:GetContext", key="value") + + def set_context(self, value): + return self.marionette._send_message("Marionette:SetContext", {"value": value}) + + def test_set_context(self): + self.assertEqual(self.set_context("content"), {"value": None}) + self.assertEqual(self.set_context("chrome"), {"value": None}) + + for typ in [True, 42, [], {}, None]: + with self.assertRaises(errors.InvalidArgumentException): + self.set_context(typ) + + with self.assertRaises(errors.MarionetteException): + self.set_context("foo") + + def test_get_context(self): + self.assertEqual(self.get_context(), "content") + self.set_context("chrome") + self.assertEqual(self.get_context(), "chrome") + self.set_context("content") + self.assertEqual(self.get_context(), "content") diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_modal_dialogs.py b/testing/marionette/harness/marionette_harness/tests/unit/test_modal_dialogs.py new file mode 100644 index 0000000000..e738625899 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_modal_dialogs.py @@ -0,0 +1,161 @@ +from marionette_driver.by import By +from marionette_driver.expected import element_present +from marionette_driver import errors +from marionette_driver.marionette import Alert +from marionette_driver.wait import Wait + +from marionette_harness import MarionetteTestCase, parameterized, WindowManagerMixin + + +class TestModalDialogs(WindowManagerMixin, MarionetteTestCase): + def setUp(self): + super(TestModalDialogs, self).setUp() + self.new_tab = self.open_tab() + self.marionette.switch_to_window(self.new_tab) + + self.http_auth_pref = ( + "network.auth.non-web-content-triggered-resources-http-auth-allow" + ) + + def tearDown(self): + # Ensure to close all possible remaining tab modal dialogs + try: + while True: + alert = self.marionette.switch_to_alert() + alert.dismiss() + except errors.NoAlertPresentException: + pass + + self.close_all_tabs() + self.close_all_windows() + + super(TestModalDialogs, self).tearDown() + + @property + def alert_present(self): + try: + Alert(self.marionette).text + return True + except errors.NoAlertPresentException: + return False + + def wait_for_alert(self, timeout=None): + Wait(self.marionette, timeout=timeout).until(lambda _: self.alert_present) + + def open_custom_prompt(self, modal_type, delay=0): + browsing_context_id = self.marionette.execute_script( + """ + return window.browsingContext.id; + """, + sandbox="system", + ) + + with self.marionette.using_context("chrome"): + self.marionette.execute_script( + """ + const [ modalType, browsingContextId, delay ] = arguments; + + const modalTypes = { + 1: Services.prompt.MODAL_TYPE_CONTENT, + 2: Services.prompt.MODAL_TYPE_TAB, + 3: Services.prompt.MODAL_TYPE_WINDOW, + 4: Services.prompt.MODAL_TYPE_INTERNAL_WINDOW, + } + + window.setTimeout(() => { + Services.prompt.alertBC( + BrowsingContext.get(browsingContextId), + modalTypes[modalType], + "title", + "text" + ); + }, delay); + """, + script_args=(modal_type, browsing_context_id, delay * 1000), + ) + + @parameterized("content", 1) + @parameterized("tab", 2) + @parameterized("window", 3) + @parameterized("internal_window", 4) + def test_detect_modal_type_in_current_tab_for_type(self, type): + self.open_custom_prompt(type) + self.wait_for_alert() + + self.assertTrue(self.alert_present) + + # Restart the session to ensure we still find the formerly left-open dialog. + self.marionette.delete_session() + self.marionette.start_session() + + alert = self.marionette.switch_to_alert() + alert.dismiss() + + @parameterized("content", 1) + @parameterized("tab", 2) + def test_dont_detect_content_and_tab_modal_type_in_another_tab_for_type(self, type): + self.open_custom_prompt(type, delay=0.25) + + self.marionette.switch_to_window(self.start_tab) + with self.assertRaises(errors.TimeoutException): + self.wait_for_alert(2) + + self.marionette.switch_to_window(self.new_tab) + alert = self.marionette.switch_to_alert() + alert.dismiss() + + @parameterized("window", 3) + @parameterized("internal_window", 4) + def test_detect_window_modal_type_in_another_tab_for_type(self, type): + self.open_custom_prompt(type, delay=0.25) + + self.marionette.switch_to_window(self.start_tab) + self.wait_for_alert() + + alert = self.marionette.switch_to_alert() + alert.dismiss() + + self.marionette.switch_to_window(self.new_tab) + self.assertFalse(self.alert_present) + + @parameterized("window", 3) + @parameterized("internal_window", 4) + def test_detect_window_modal_type_in_another_window_for_type(self, type): + self.new_window = self.open_window() + + self.marionette.switch_to_window(self.new_window) + + self.open_custom_prompt(type, delay=0.25) + + self.marionette.switch_to_window(self.new_tab) + with self.assertRaises(errors.TimeoutException): + self.wait_for_alert(2) + + self.marionette.switch_to_window(self.new_window) + alert = self.marionette.switch_to_alert() + alert.dismiss() + + self.marionette.switch_to_window(self.new_tab) + self.assertFalse(self.alert_present) + + def test_http_auth_dismiss(self): + with self.marionette.using_prefs({self.http_auth_pref: True}): + self.marionette.navigate(self.marionette.absolute_url("http_auth")) + self.wait_for_alert(timeout=self.marionette.timeout.page_load) + + alert = self.marionette.switch_to_alert() + alert.dismiss() + + status = Wait( + self.marionette, timeout=self.marionette.timeout.page_load + ).until(element_present(By.ID, "status")) + self.assertEqual(status.text, "restricted") + + def test_http_auth_send_keys(self): + with self.marionette.using_prefs({self.http_auth_pref: True}): + self.marionette.navigate(self.marionette.absolute_url("http_auth")) + self.wait_for_alert(timeout=self.marionette.timeout.page_load) + + alert = self.marionette.switch_to_alert() + with self.assertRaises(errors.UnsupportedOperationException): + alert.send_keys("foo") diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_navigation.py b/testing/marionette/harness/marionette_harness/tests/unit/test_navigation.py new file mode 100644 index 0000000000..ec1a8f1be0 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_navigation.py @@ -0,0 +1,901 @@ +# 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/. + +import contextlib +import os + +from six.moves.urllib.parse import quote + +from marionette_driver import By, errors, expected, Wait +from marionette_driver.keys import Keys +from marionette_driver.marionette import Alert +from marionette_harness import ( + MarionetteTestCase, + run_if_manage_instance, + WindowManagerMixin, +) + +here = os.path.abspath(os.path.dirname(__file__)) + + +BLACK_PIXEL = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==" # noqa +RED_PIXEL = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEX/TQBcNTh/AAAAAXRSTlPM0jRW/QAAAApJREFUeJxjYgAAAAYAAzY3fKgAAAAASUVORK5CYII=" # noqa + + +def inline(doc): + return "data:text/html;charset=utf-8,%s" % quote(doc) + + +def inline_image(data): + return "data:image/png;base64,%s" % data + + +class BaseNavigationTestCase(WindowManagerMixin, MarionetteTestCase): + def setUp(self): + super(BaseNavigationTestCase, self).setUp() + + file_path = os.path.join(here, "data", "test.html").replace("\\", "/") + + self.test_page_file_url = "file:///{}".format(file_path) + self.test_page_frameset = self.marionette.absolute_url("frameset.html") + self.test_page_insecure = self.fixtures.where_is("test.html", on="https") + self.test_page_not_remote = "about:robots" + self.test_page_push_state = self.marionette.absolute_url( + "navigation_pushstate.html" + ) + self.test_page_remote = self.marionette.absolute_url("test.html") + + if self.marionette.session_capabilities["platformName"] == "mac": + self.mod_key = Keys.META + else: + self.mod_key = Keys.CONTROL + + # Always use a blank new tab for an empty history + self.new_tab = self.open_tab() + self.marionette.switch_to_window(self.new_tab) + Wait(self.marionette, timeout=self.marionette.timeout.page_load).until( + lambda _: self.history_length == 1, + message="The newly opened tab doesn't have a browser history length of 1", + ) + + def tearDown(self): + self.marionette.timeout.reset() + + self.close_all_tabs() + + super(BaseNavigationTestCase, self).tearDown() + + @property + def history_length(self): + return self.marionette.execute_script("return window.history.length;") + + @property + def is_remote_tab(self): + with self.marionette.using_context("chrome"): + # TODO: DO NOT USE MOST RECENT WINDOW BUT CURRENT ONE + return self.marionette.execute_script( + """ + const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" + ); + + let win = null; + + if (AppConstants.MOZ_APP_NAME == "fennec") { + win = Services.wm.getMostRecentWindow("navigator:browser"); + } else { + const { BrowserWindowTracker } = ChromeUtils.importESModule( + "resource:///modules/BrowserWindowTracker.sys.mjs" + ); + win = BrowserWindowTracker.getTopWindow(); + } + + let tabBrowser = null; + + // Fennec + if (win.BrowserApp) { + tabBrowser = win.BrowserApp.selectedBrowser; + + // Firefox + } else if (win.gBrowser) { + tabBrowser = win.gBrowser.selectedBrowser; + + } else { + return null; + } + + return tabBrowser.isRemoteBrowser; + """ + ) + + @property + def ready_state(self): + return self.marionette.execute_script( + "return window.document.readyState;", sandbox=None + ) + + +class TestNavigate(BaseNavigationTestCase): + def test_set_location_through_execute_script(self): + # To avoid unexpected remoteness changes and a hang in any non-navigation + # command (bug 1519354) when navigating via the location bar, already + # pre-load a page which causes a remoteness change. + self.marionette.navigate(self.test_page_push_state) + + self.marionette.execute_script( + "window.location.href = arguments[0];", + script_args=(self.test_page_remote,), + sandbox=None, + ) + + Wait(self.marionette, timeout=self.marionette.timeout.page_load).until( + expected.element_present(*(By.ID, "testh1")), + message="Target element 'testh1' has not been found", + ) + + self.assertEqual(self.test_page_remote, self.marionette.get_url()) + + def test_navigate_chrome_unsupported_error(self): + with self.marionette.using_context("chrome"): + self.assertRaises( + errors.UnsupportedOperationException, + self.marionette.navigate, + "about:blank", + ) + self.assertRaises( + errors.UnsupportedOperationException, self.marionette.go_back + ) + self.assertRaises( + errors.UnsupportedOperationException, self.marionette.go_forward + ) + self.assertRaises( + errors.UnsupportedOperationException, self.marionette.refresh + ) + + def test_get_current_url_returns_top_level_browsing_context_url(self): + page_iframe = self.marionette.absolute_url("test_iframe.html") + + self.marionette.navigate(page_iframe) + self.assertEqual(page_iframe, self.marionette.get_url()) + frame = self.marionette.find_element(By.CSS_SELECTOR, "#test_iframe") + self.marionette.switch_to_frame(frame) + self.assertEqual(page_iframe, self.marionette.get_url()) + + def test_get_current_url(self): + self.marionette.navigate(self.test_page_remote) + self.assertEqual(self.test_page_remote, self.marionette.get_url()) + self.marionette.navigate("about:blank") + self.assertEqual("about:blank", self.marionette.get_url()) + + def test_navigate_in_child_frame_changes_to_top(self): + self.marionette.navigate(self.test_page_frameset) + frame = self.marionette.find_element(By.NAME, "third") + self.marionette.switch_to_frame(frame) + self.assertRaises( + errors.NoSuchElementException, + self.marionette.find_element, + By.NAME, + "third", + ) + + self.marionette.navigate(self.test_page_frameset) + self.marionette.find_element(By.NAME, "third") + + def test_invalid_url(self): + with self.assertRaises(errors.MarionetteException): + self.marionette.navigate("foo") + with self.assertRaises(errors.MarionetteException): + self.marionette.navigate("thisprotocoldoesnotexist://") + + def test_find_element_state_complete(self): + self.marionette.navigate(self.test_page_remote) + self.assertEqual("complete", self.ready_state) + self.assertTrue(self.marionette.find_element(By.ID, "mozLink")) + + def test_navigate_timeout_error_no_remoteness_change(self): + is_remote_before_timeout = self.is_remote_tab + self.marionette.timeout.page_load = 0.5 + with self.assertRaises(errors.TimeoutException): + self.marionette.navigate(self.marionette.absolute_url("slow")) + self.assertEqual(self.is_remote_tab, is_remote_before_timeout) + + def test_navigate_timeout_error_remoteness_change(self): + self.assertTrue(self.is_remote_tab) + self.marionette.navigate("about:robots") + self.assertFalse(self.is_remote_tab) + + self.marionette.timeout.page_load = 0.5 + with self.assertRaises(errors.TimeoutException): + self.marionette.navigate(self.marionette.absolute_url("slow")) + + def test_navigate_to_same_image_document_twice(self): + self.marionette.navigate(self.fixtures.where_is("black.png")) + self.assertIn("black.png", self.marionette.title) + self.marionette.navigate(self.fixtures.where_is("black.png")) + self.assertIn("black.png", self.marionette.title) + + def test_navigate_hash_change(self): + doc = inline("<p id=foo>") + self.marionette.navigate(doc) + self.marionette.execute_script("window.visited = true", sandbox=None) + self.marionette.navigate("{}#foo".format(doc)) + self.assertTrue( + self.marionette.execute_script("return window.visited", sandbox=None) + ) + + def test_navigate_hash_argument_identical(self): + test_page = "{}#foo".format(inline("<p id=foo>")) + + self.marionette.navigate(test_page) + self.marionette.find_element(By.ID, "foo") + self.marionette.navigate(test_page) + self.marionette.find_element(By.ID, "foo") + + def test_navigate_hash_argument_differnt(self): + test_page = "{}#Foo".format(inline("<p id=foo>")) + + self.marionette.navigate(test_page) + self.marionette.find_element(By.ID, "foo") + self.marionette.navigate(test_page.lower()) + self.marionette.find_element(By.ID, "foo") + + def test_navigate_history_pushstate(self): + target_page = self.marionette.absolute_url("navigation_pushstate_target.html") + + self.marionette.navigate(self.test_page_push_state) + self.marionette.find_element(By.ID, "forward").click() + + # By using pushState() the URL is updated but the target page is not loaded + # and as such the element is not displayed + self.assertEqual(self.marionette.get_url(), target_page) + with self.assertRaises(errors.NoSuchElementException): + self.marionette.find_element(By.ID, "target") + + self.marionette.go_back() + self.assertEqual(self.marionette.get_url(), self.test_page_push_state) + + # The target page still gets not loaded + self.marionette.go_forward() + self.assertEqual(self.marionette.get_url(), target_page) + with self.assertRaises(errors.NoSuchElementException): + self.marionette.find_element(By.ID, "target") + + # Navigating to a different page, and returning to the injected + # page, it will be loaded. + self.marionette.navigate(self.test_page_remote) + self.assertEqual(self.marionette.get_url(), self.test_page_remote) + + self.marionette.go_back() + self.assertEqual(self.marionette.get_url(), target_page) + self.marionette.find_element(By.ID, "target") + + self.marionette.go_back() + self.assertEqual(self.marionette.get_url(), self.test_page_push_state) + + def test_navigate_file_url(self): + self.marionette.navigate(self.test_page_file_url) + self.marionette.find_element(By.ID, "file-url") + self.marionette.navigate(self.test_page_remote) + + def test_navigate_file_url_remoteness_change(self): + self.marionette.navigate("about:robots") + self.assertFalse(self.is_remote_tab) + + self.marionette.navigate(self.test_page_file_url) + self.assertTrue(self.is_remote_tab) + self.marionette.find_element(By.ID, "file-url") + + self.marionette.navigate("about:robots") + self.assertFalse(self.is_remote_tab) + + def test_no_such_element_after_remoteness_change(self): + self.marionette.navigate(self.test_page_file_url) + self.assertTrue(self.is_remote_tab) + elem = self.marionette.find_element(By.ID, "file-url") + + self.marionette.navigate("about:robots") + self.assertFalse(self.is_remote_tab) + + with self.assertRaises(errors.StaleElementException): + elem.click() + + def test_about_blank_for_new_docshell(self): + self.assertEqual(self.marionette.get_url(), "about:blank") + + self.marionette.navigate("about:blank") + + def test_about_newtab(self): + with self.marionette.using_prefs({"browser.newtabpage.enabled": True}): + self.marionette.navigate("about:newtab") + + self.marionette.navigate(self.test_page_remote) + self.marionette.find_element(By.ID, "testDiv") + + @run_if_manage_instance("Only runnable if Marionette manages the instance") + def test_focus_after_navigation(self): + self.marionette.restart() + + self.marionette.navigate(inline("<input autofocus>")) + + # Per spec, autofocus candidates will be + # flushed by next paint, so we use rAF here to + # ensure the candidates are flushed. + self.marionette.execute_async_script( + """ + const callback = arguments[arguments.length - 1]; + window.requestAnimationFrame(function() { + window.requestAnimationFrame(callback); + }); + """ + ) + focus_el = self.marionette.find_element(By.CSS_SELECTOR, ":focus") + self.assertEqual(self.marionette.get_active_element(), focus_el) + + def test_no_hang_when_navigating_after_closing_original_tab(self): + # Close the start tab + self.marionette.switch_to_window(self.start_tab) + self.marionette.close() + + self.marionette.switch_to_window(self.new_tab) + self.marionette.navigate(self.test_page_remote) + + def test_type_to_non_remote_tab(self): + self.marionette.navigate(self.test_page_not_remote) + self.assertFalse(self.is_remote_tab) + + with self.marionette.using_context("chrome"): + urlbar = self.marionette.find_element(By.ID, "urlbar-input") + urlbar.send_keys(self.mod_key + "a") + urlbar.send_keys(self.mod_key + "x") + urlbar.send_keys("about:support" + Keys.ENTER) + + Wait(self.marionette, timeout=self.marionette.timeout.page_load).until( + lambda mn: mn.get_url() == "about:support", + message="'about:support' hasn't been loaded", + ) + self.assertFalse(self.is_remote_tab) + + def test_type_to_remote_tab(self): + self.assertTrue(self.is_remote_tab) + + with self.marionette.using_context("chrome"): + urlbar = self.marionette.find_element(By.ID, "urlbar-input") + urlbar.send_keys(self.mod_key + "a") + urlbar.send_keys(self.mod_key + "x") + urlbar.send_keys(self.test_page_remote + Keys.ENTER) + + Wait(self.marionette, timeout=self.marionette.timeout.page_load).until( + lambda mn: mn.get_url() == self.test_page_remote, + message="'{}' hasn't been loaded".format(self.test_page_remote), + ) + self.assertTrue(self.is_remote_tab) + + def test_navigate_after_deleting_session(self): + self.marionette.delete_session() + self.marionette.start_session() + + self.marionette.navigate(self.test_page_remote) + self.assertEqual(self.test_page_remote, self.marionette.get_url()) + + +class TestBackForwardNavigation(BaseNavigationTestCase): + def run_bfcache_test(self, test_pages): + # Helper method to run simple back and forward testcases. + + def check_page_status(page, expected_history_length): + if "alert_text" in page: + self.assertEqual(Alert(self.marionette).text, page["alert_text"]) + + self.assertEqual(self.marionette.get_url(), page["url"]) + self.assertEqual(self.history_length, expected_history_length) + + if "is_remote" in page: + self.assertEqual( + page["is_remote"], + self.is_remote_tab, + "'{}' doesn't match expected remoteness state: {}".format( + page["url"], page["is_remote"] + ), + ) + + if "callback" in page and callable(page["callback"]): + page["callback"]() + + for index, page in enumerate(test_pages): + if "error" in page: + with self.assertRaises(page["error"]): + self.marionette.navigate(page["url"]) + else: + self.marionette.navigate(page["url"]) + + check_page_status(page, index + 1) + + # Now going back in history for all test pages by backward iterating + # through the list (-1) and skipping the first entry at the end (-2). + for page in test_pages[-2::-1]: + if "error" in page: + with self.assertRaises(page["error"]): + self.marionette.go_back() + else: + self.marionette.go_back() + + check_page_status(page, len(test_pages)) + + # Now going forward in history by skipping the first entry. + for page in test_pages[1::]: + if "error" in page: + with self.assertRaises(page["error"]): + self.marionette.go_forward() + else: + self.marionette.go_forward() + + check_page_status(page, len(test_pages)) + + def test_no_history_items(self): + # Both methods should not raise a failure if no navigation is possible + self.marionette.go_back() + self.marionette.go_forward() + + def test_data_urls(self): + test_pages = [ + {"url": inline("<p>foobar</p>")}, + {"url": self.test_page_remote}, + {"url": inline("<p>foobar</p>")}, + ] + self.run_bfcache_test(test_pages) + + def test_same_document_hash_change(self): + test_pages = [ + {"url": "{}#23".format(self.test_page_remote)}, + {"url": self.test_page_remote}, + {"url": "{}#42".format(self.test_page_remote)}, + ] + self.run_bfcache_test(test_pages) + + def test_file_url(self): + test_pages = [ + {"url": self.test_page_remote}, + {"url": self.test_page_file_url}, + {"url": self.test_page_remote}, + ] + self.run_bfcache_test(test_pages) + + def test_frameset(self): + test_pages = [ + {"url": self.marionette.absolute_url("frameset.html")}, + {"url": self.test_page_remote}, + {"url": self.marionette.absolute_url("frameset.html")}, + ] + self.run_bfcache_test(test_pages) + + def test_frameset_after_navigating_in_frame(self): + test_element_locator = (By.ID, "email") + + self.marionette.navigate(self.test_page_remote) + self.assertEqual(self.marionette.get_url(), self.test_page_remote) + self.assertEqual(self.history_length, 1) + page = self.marionette.absolute_url("frameset.html") + self.marionette.navigate(page) + self.assertEqual(self.marionette.get_url(), page) + self.assertEqual(self.history_length, 2) + frame = self.marionette.find_element(By.ID, "fifth") + self.marionette.switch_to_frame(frame) + link = self.marionette.find_element(By.ID, "linkId") + link.click() + + # We cannot use get_url() to wait until the target page has been loaded, + # because it will return the URL of the top browsing context and doesn't + # wait for the page load to be complete. + Wait(self.marionette, timeout=self.marionette.timeout.page_load).until( + expected.element_present(*test_element_locator), + message="Target element 'email' has not been found", + ) + self.assertEqual(self.history_length, 3) + + # Go back to the frame the click navigated away from + self.marionette.go_back() + self.assertEqual(self.marionette.get_url(), page) + with self.assertRaises(errors.NoSuchElementException): + self.marionette.find_element(*test_element_locator) + + # Go back to the non-frameset page + self.marionette.switch_to_parent_frame() + self.marionette.go_back() + self.assertEqual(self.marionette.get_url(), self.test_page_remote) + + # Go forward to the frameset page + self.marionette.go_forward() + self.assertEqual(self.marionette.get_url(), page) + + # Go forward to the frame the click navigated to + # TODO: See above for automatic browser context switches. Hard to do here + frame = self.marionette.find_element(By.ID, "fifth") + self.marionette.switch_to_frame(frame) + self.marionette.go_forward() + self.marionette.find_element(*test_element_locator) + self.assertEqual(self.marionette.get_url(), page) + + def test_image_to_html_to_image(self): + test_pages = [ + {"url": self.marionette.absolute_url("black.png")}, + {"url": self.test_page_remote}, + {"url": self.marionette.absolute_url("white.png")}, + ] + self.run_bfcache_test(test_pages) + + def test_image_to_image(self): + test_pages = [ + {"url": self.marionette.absolute_url("black.png")}, + {"url": self.marionette.absolute_url("white.png")}, + {"url": inline_image(RED_PIXEL)}, + {"url": inline_image(BLACK_PIXEL)}, + {"url": self.marionette.absolute_url("black.png")}, + ] + self.run_bfcache_test(test_pages) + + def test_remoteness_change(self): + test_pages = [ + {"url": "about:robots", "is_remote": False}, + {"url": self.test_page_remote, "is_remote": True}, + {"url": "about:robots", "is_remote": False}, + ] + self.run_bfcache_test(test_pages) + + def test_non_remote_about_pages(self): + test_pages = [ + {"url": "about:preferences", "is_remote": False}, + {"url": "about:robots", "is_remote": False}, + {"url": "about:support", "is_remote": False}, + ] + self.run_bfcache_test(test_pages) + + def test_navigate_to_requested_about_page_after_error_page(self): + test_pages = [ + {"url": "about:neterror"}, + {"url": self.test_page_remote}, + {"url": "about:blocked"}, + ] + self.run_bfcache_test(test_pages) + + def test_timeout_error(self): + urls = [ + self.marionette.absolute_url("slow?delay=3"), + self.test_page_remote, + self.marionette.absolute_url("slow?delay=4"), + ] + + # First, load all pages completely to get them added to the cache + for index, url in enumerate(urls): + self.marionette.navigate(url) + self.assertEqual(url, self.marionette.get_url()) + self.assertEqual(self.history_length, index + 1) + + self.marionette.go_back() + self.assertEqual(urls[1], self.marionette.get_url()) + + # Force triggering a timeout error + self.marionette.timeout.page_load = 0.5 + with self.assertRaises(errors.TimeoutException): + self.marionette.go_back() + self.marionette.timeout.reset() + + delay = Wait(self.marionette, timeout=self.marionette.timeout.page_load).until( + expected.element_present(By.ID, "delay"), + message="Target element 'delay' has not been found after timeout in 'back'", + ) + self.assertEqual(delay.text, "3") + + self.marionette.go_forward() + self.assertEqual(urls[1], self.marionette.get_url()) + + # Force triggering a timeout error + self.marionette.timeout.page_load = 0.5 + with self.assertRaises(errors.TimeoutException): + self.marionette.go_forward() + self.marionette.timeout.reset() + + delay = Wait(self.marionette, timeout=self.marionette.timeout.page_load).until( + expected.element_present(By.ID, "delay"), + message="Target element 'delay' has not been found after timeout in 'forward'", + ) + self.assertEqual(delay.text, "4") + + def test_certificate_error(self): + test_pages = [ + { + "url": self.test_page_insecure, + "error": errors.InsecureCertificateException, + }, + {"url": self.test_page_remote}, + { + "url": self.test_page_insecure, + "error": errors.InsecureCertificateException, + }, + ] + self.run_bfcache_test(test_pages) + + +class TestRefresh(BaseNavigationTestCase): + def test_basic(self): + self.marionette.navigate(self.test_page_remote) + self.assertEqual(self.test_page_remote, self.marionette.get_url()) + + self.marionette.execute_script( + """ + let elem = window.document.createElement('div'); + elem.id = 'someDiv'; + window.document.body.appendChild(elem); + """ + ) + self.marionette.find_element(By.ID, "someDiv") + + self.marionette.refresh() + self.assertEqual(self.test_page_remote, self.marionette.get_url()) + with self.assertRaises(errors.NoSuchElementException): + self.marionette.find_element(By.ID, "someDiv") + + def test_refresh_in_child_frame_navigates_to_top(self): + self.marionette.navigate(self.test_page_frameset) + self.assertEqual(self.test_page_frameset, self.marionette.get_url()) + + frame = self.marionette.find_element(By.NAME, "third") + self.marionette.switch_to_frame(frame) + self.assertRaises( + errors.NoSuchElementException, + self.marionette.find_element, + By.NAME, + "third", + ) + + self.marionette.refresh() + self.marionette.find_element(By.NAME, "third") + + def test_file_url(self): + self.marionette.navigate(self.test_page_file_url) + self.assertEqual(self.test_page_file_url, self.marionette.get_url()) + + self.marionette.refresh() + self.assertEqual(self.test_page_file_url, self.marionette.get_url()) + + def test_image(self): + image = self.marionette.absolute_url("black.png") + + self.marionette.navigate(image) + self.assertEqual(image, self.marionette.get_url()) + + self.marionette.refresh() + self.assertEqual(image, self.marionette.get_url()) + + def test_history_pushstate(self): + target_page = self.marionette.absolute_url("navigation_pushstate_target.html") + + self.marionette.navigate(self.test_page_push_state) + self.marionette.find_element(By.ID, "forward").click() + + # By using pushState() the URL is updated but the target page is not loaded + # and as such the element is not displayed + self.assertEqual(self.marionette.get_url(), target_page) + with self.assertRaises(errors.NoSuchElementException): + self.marionette.find_element(By.ID, "target") + + # Refreshing the target page will trigger a full page load. + self.marionette.refresh() + self.assertEqual(self.marionette.get_url(), target_page) + self.marionette.find_element(By.ID, "target") + + self.marionette.go_back() + self.assertEqual(self.marionette.get_url(), self.test_page_push_state) + + def test_timeout_error(self): + slow_page = self.marionette.absolute_url("slow?delay=3") + + self.marionette.navigate(slow_page) + self.assertEqual(slow_page, self.marionette.get_url()) + + self.marionette.timeout.page_load = 0.5 + with self.assertRaises(errors.TimeoutException): + self.marionette.refresh() + self.assertEqual(slow_page, self.marionette.get_url()) + + def test_insecure_error(self): + with self.assertRaises(errors.InsecureCertificateException): + self.marionette.navigate(self.test_page_insecure) + self.assertEqual(self.marionette.get_url(), self.test_page_insecure) + + with self.assertRaises(errors.InsecureCertificateException): + self.marionette.refresh() + + +class TestTLSNavigation(BaseNavigationTestCase): + insecure_tls = {"acceptInsecureCerts": True} + secure_tls = {"acceptInsecureCerts": False} + + def setUp(self): + super(TestTLSNavigation, self).setUp() + + self.test_page_insecure = self.fixtures.where_is("test.html", on="https") + + self.marionette.delete_session() + self.capabilities = self.marionette.start_session(self.insecure_tls) + + def tearDown(self): + try: + self.marionette.delete_session() + self.marionette.start_session() + except: + pass + + super(TestTLSNavigation, self).tearDown() + + @contextlib.contextmanager + def safe_session(self): + try: + self.capabilities = self.marionette.start_session(self.secure_tls) + self.assertFalse(self.capabilities["acceptInsecureCerts"]) + # Always use a blank new tab for an empty history + self.new_tab = self.open_tab() + self.marionette.switch_to_window(self.new_tab) + Wait(self.marionette, timeout=self.marionette.timeout.page_load).until( + lambda _: self.history_length == 1, + message="The newly opened tab doesn't have a browser history length of 1", + ) + yield self.marionette + finally: + self.close_all_tabs() + self.marionette.delete_session() + + @contextlib.contextmanager + def unsafe_session(self): + try: + self.capabilities = self.marionette.start_session(self.insecure_tls) + self.assertTrue(self.capabilities["acceptInsecureCerts"]) + # Always use a blank new tab for an empty history + self.new_tab = self.open_tab() + self.marionette.switch_to_window(self.new_tab) + Wait(self.marionette, timeout=self.marionette.timeout.page_load).until( + lambda _: self.history_length == 1, + message="The newly opened tab doesn't have a browser history length of 1", + ) + yield self.marionette + finally: + self.close_all_tabs() + self.marionette.delete_session() + + def test_navigate_by_command(self): + self.marionette.navigate(self.test_page_insecure) + self.assertIn("https", self.marionette.get_url()) + + def test_navigate_by_click(self): + link_url = self.test_page_insecure + self.marionette.navigate( + inline("<a href=%s>https is the future</a>" % link_url) + ) + self.marionette.find_element(By.TAG_NAME, "a").click() + self.assertIn("https", self.marionette.get_url()) + + def test_deactivation(self): + invalid_cert_url = self.test_page_insecure + + self.marionette.delete_session() + + print("with safe session") + with self.safe_session() as session: + with self.assertRaises(errors.InsecureCertificateException): + session.navigate(invalid_cert_url) + + print("with unsafe session") + with self.unsafe_session() as session: + session.navigate(invalid_cert_url) + + print("with safe session again") + with self.safe_session() as session: + with self.assertRaises(errors.InsecureCertificateException): + session.navigate(invalid_cert_url) + + +class TestPageLoadStrategy(BaseNavigationTestCase): + def setUp(self): + super(TestPageLoadStrategy, self).setUp() + + # Test page that delays the response and as such the document to be + # loaded. It is used for testing the page load strategy "none". + self.test_page_slow = self.marionette.absolute_url("slow") + + # Similar to "slow" but additionally triggers a cross group navigation + # which triggers a replacement of the top-level browsing context. + self.test_page_slow_coop = self.marionette.absolute_url("slow-coop") + + # Test page that contains a slow loading <img> element which delays the + # "load" but not the "DOMContentLoaded" event. + self.test_page_slow_resource = self.marionette.absolute_url( + "slow_resource.html" + ) + + def tearDown(self): + self.marionette.delete_session() + self.marionette.start_session() + + super(TestPageLoadStrategy, self).tearDown() + + def test_none(self): + self.marionette.delete_session() + self.marionette.start_session({"pageLoadStrategy": "none"}) + + # Navigate will return immediately. As such wait for the target URL to + # be the current location, and the element to exist. + self.marionette.navigate(self.test_page_slow) + with self.assertRaises(errors.NoSuchElementException): + self.marionette.find_element(By.ID, "delay") + + Wait( + self.marionette, + ignored_exceptions=errors.NoSuchElementException, + timeout=self.marionette.timeout.page_load, + ).until(lambda _: self.marionette.find_element(By.ID, "delay")) + + self.assertEqual(self.marionette.get_url(), self.test_page_slow) + + def test_none_with_new_session_waits_for_page_loaded(self): + self.marionette.delete_session() + self.marionette.start_session({"pageLoadStrategy": "none"}) + + # Navigate will return immediately. + self.marionette.navigate(self.test_page_slow) + + # Make sure that when creating a new session right away it waits + # until the page has been finished loading. + self.marionette.delete_session() + self.marionette.start_session() + + self.assertEqual(self.marionette.get_url(), self.test_page_slow) + self.assertEqual(self.ready_state, "complete") + self.marionette.find_element(By.ID, "delay") + + def test_none_with_new_session_waits_for_page_loaded_remoteness_change(self): + self.marionette.delete_session() + self.marionette.start_session({"pageLoadStrategy": "none"}) + + # Navigate will return immediately. + self.marionette.navigate(self.test_page_slow_coop) + + # Make sure that when creating a new session right away it waits + # until the page has been finished loading. + self.marionette.delete_session() + self.marionette.start_session() + + self.assertEqual(self.marionette.get_url(), self.test_page_slow_coop) + self.assertEqual(self.ready_state, "complete") + self.marionette.find_element(By.ID, "delay") + + def test_eager(self): + self.marionette.delete_session() + self.marionette.start_session({"pageLoadStrategy": "eager"}) + + self.marionette.navigate(self.test_page_slow_resource) + self.assertEqual(self.ready_state, "interactive") + self.assertEqual(self.marionette.get_url(), self.test_page_slow_resource) + self.marionette.find_element(By.ID, "slow") + + def test_normal(self): + self.marionette.delete_session() + self.marionette.start_session({"pageLoadStrategy": "normal"}) + + self.marionette.navigate(self.test_page_slow_resource) + self.assertEqual(self.marionette.get_url(), self.test_page_slow_resource) + self.assertEqual(self.ready_state, "complete") + self.marionette.find_element(By.ID, "slow") + + def test_strategy_after_remoteness_change(self): + """Bug 1378191 - Reset of capabilities after listener reload.""" + self.marionette.delete_session() + self.marionette.start_session({"pageLoadStrategy": "eager"}) + + # Trigger a remoteness change which will reload the listener script + self.assertTrue( + self.is_remote_tab, "Initial tab doesn't have remoteness flag set" + ) + self.marionette.navigate("about:robots") + self.assertFalse(self.is_remote_tab, "Tab has remoteness flag set") + self.marionette.navigate(self.test_page_slow_resource) + self.assertEqual(self.ready_state, "interactive") diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_pagesource.py b/testing/marionette/harness/marionette_harness/tests/unit/test_pagesource.py new file mode 100644 index 0000000000..e3799bc0d6 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_pagesource.py @@ -0,0 +1,52 @@ +# 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 six.moves.urllib.parse import quote + +from marionette_harness import MarionetteTestCase + + +def inline(doc, mime=None, charset=None): + mime = "html" if mime is None else mime + charset = "utf-8" if (charset is None) else charset + return "data:text/{};charset={},{}".format(mime, charset, quote(doc)) + + +class TestPageSource(MarionetteTestCase): + def testShouldReturnTheSourceOfAPage(self): + test_html = inline("<body><p> Check the PageSource</body>") + self.marionette.navigate(test_html) + source = self.marionette.page_source + from_web_api = self.marionette.execute_script( + "return document.documentElement.outerHTML" + ) + self.assertTrue("<html" in source) + self.assertTrue("PageSource" in source) + self.assertEqual(source, from_web_api) + + def testShouldReturnTheSourceOfAPageWhenThereAreUnicodeChars(self): + test_html = inline( + '<head><meta http-equiv="pragma" content="no-cache"/></head><body><!-- the \u00ab section[id^="wifi-"] \u00bb selector.--></body>' + ) + self.marionette.navigate(test_html) + # if we don't throw on the next line we are good! + source = self.marionette.page_source + from_web_api = self.marionette.execute_script( + "return document.documentElement.outerHTML" + ) + self.assertEqual(source, from_web_api) + + def testShouldReturnAXMLDocumentSource(self): + test_xml = inline("<xml><foo><bar>baz</bar></foo></xml>", "xml") + self.marionette.navigate(test_xml) + source = self.marionette.page_source + from_web_api = self.marionette.execute_script( + "return document.documentElement.outerHTML" + ) + import re + + self.assertEqual( + re.sub("\s", "", source), "<xml><foo><bar>baz</bar></foo></xml>" + ) + self.assertEqual(source, from_web_api) diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_pagesource_chrome.py b/testing/marionette/harness/marionette_harness/tests/unit/test_pagesource_chrome.py new file mode 100644 index 0000000000..029be1471f --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_pagesource_chrome.py @@ -0,0 +1,26 @@ +# 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_harness import MarionetteTestCase, WindowManagerMixin + + +class TestPageSourceChrome(WindowManagerMixin, MarionetteTestCase): + def setUp(self): + super(TestPageSourceChrome, self).setUp() + self.marionette.set_context("chrome") + + new_window = self.open_chrome_window( + "chrome://remote/content/marionette/test.xhtml" + ) + self.marionette.switch_to_window(new_window) + + def tearDown(self): + self.close_all_windows() + super(TestPageSourceChrome, self).tearDown() + + def testShouldReturnXULDetails(self): + source = self.marionette.page_source + self.assertTrue( + '<input xmlns="http://www.w3.org/1999/xhtml" id="textInput"' in source + ) diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_position.py b/testing/marionette/harness/marionette_harness/tests/unit/test_position.py new file mode 100644 index 0000000000..f2a409c1a2 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_position.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 six.moves.urllib.parse import quote + +from marionette_driver.by import By + +from marionette_harness import MarionetteTestCase + + +def inline(doc): + return "data:text/html;charset=utf-8,{}".format(quote(doc)) + + +class TestPosition(MarionetteTestCase): + def test_should_get_element_position_back(self): + doc = """ + <head> + <title>Rectangles</title> + <style> + div { + position: absolute; + margin: 0; + border: 0; + padding: 0; + } + #r { + background-color: red; + left: 11px; + top: 10px; + width: 48.666666667px; + height: 49.333333333px; + } + </style> + </head> + <body> + <div id="r">r</div> + </body> + """ + self.marionette.navigate(inline(doc)) + + r2 = self.marionette.find_element(By.ID, "r") + location = r2.rect + self.assertEqual(11, location["x"]) + self.assertEqual(10, location["y"]) diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_prefs.py b/testing/marionette/harness/marionette_harness/tests/unit/test_prefs.py new file mode 100644 index 0000000000..4e4de0da54 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_prefs.py @@ -0,0 +1,213 @@ +# 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/. + +import six + +from marionette_driver import geckoinstance +from marionette_driver.errors import JavascriptException + +from marionette_harness import ( + MarionetteTestCase, + run_if_manage_instance, +) + + +class TestPreferences(MarionetteTestCase): + prefs = { + "bool": "marionette.test.bool", + "int": "marionette.test.int", + "string": "marionette.test.string", + } + + def tearDown(self): + for pref in self.prefs.values(): + self.marionette.clear_pref(pref) + + super(TestPreferences, self).tearDown() + + def test_gecko_instance_preferences(self): + required_prefs = geckoinstance.GeckoInstance.required_prefs + + for key, value in six.iteritems(required_prefs): + self.assertEqual( + self.marionette.get_pref(key), + value, + "Preference {} hasn't been set to {}".format(key, repr(value)), + ) + + def test_desktop_instance_preferences(self): + required_prefs = geckoinstance.DesktopInstance.desktop_prefs + + for key, value in six.iteritems(required_prefs): + self.assertEqual( + self.marionette.get_pref(key), + value, + "Preference {} hasn't been set to {}".format(key, value), + ) + + def test_clear_pref(self): + self.assertIsNone(self.marionette.get_pref(self.prefs["bool"])) + + self.marionette.set_pref(self.prefs["bool"], True) + self.assertTrue(self.marionette.get_pref(self.prefs["bool"])) + + self.marionette.clear_pref(self.prefs["bool"]) + self.assertIsNone(self.marionette.get_pref(self.prefs["bool"])) + + def test_get_and_set_pref(self): + # By default none of the preferences are set + self.assertIsNone(self.marionette.get_pref(self.prefs["bool"])) + self.assertIsNone(self.marionette.get_pref(self.prefs["int"])) + self.assertIsNone(self.marionette.get_pref(self.prefs["string"])) + + # Test boolean values + self.marionette.set_pref(self.prefs["bool"], True) + value = self.marionette.get_pref(self.prefs["bool"]) + self.assertTrue(value) + self.assertEqual(type(value), bool) + + # Test int values + self.marionette.set_pref(self.prefs["int"], 42) + value = self.marionette.get_pref(self.prefs["int"]) + self.assertEqual(value, 42) + self.assertEqual(type(value), int) + + # Test string values + self.marionette.set_pref(self.prefs["string"], "abc") + value = self.marionette.get_pref(self.prefs["string"]) + self.assertEqual(value, "abc") + self.assertTrue(isinstance(value, six.string_types)) + + # Test reset value + self.marionette.set_pref(self.prefs["string"], None) + self.assertIsNone(self.marionette.get_pref(self.prefs["string"])) + + def test_get_set_pref_default_branch(self): + pref_default = "marionette.test.pref_default1" + self.assertIsNone(self.marionette.get_pref(self.prefs["string"])) + + self.marionette.set_pref(pref_default, "default_value", default_branch=True) + self.assertEqual(self.marionette.get_pref(pref_default), "default_value") + self.assertEqual( + self.marionette.get_pref(pref_default, default_branch=True), "default_value" + ) + + self.marionette.set_pref(pref_default, "user_value") + self.assertEqual(self.marionette.get_pref(pref_default), "user_value") + self.assertEqual( + self.marionette.get_pref(pref_default, default_branch=True), "default_value" + ) + + self.marionette.clear_pref(pref_default) + self.assertEqual(self.marionette.get_pref(pref_default), "default_value") + + def test_get_pref_value_type(self): + # Without a given value type the properties URL will be returned only + pref_complex = "browser.menu.showCharacterEncoding" + properties_file = "chrome://browser/locale/browser.properties" + self.assertEqual( + self.marionette.get_pref(pref_complex, default_branch=True), properties_file + ) + + # Otherwise the property named like the pref will be translated + value = self.marionette.get_pref( + pref_complex, default_branch=True, value_type="nsIPrefLocalizedString" + ) + self.assertNotEqual(value, properties_file) + + def test_set_prefs(self): + # By default none of the preferences are set + self.assertIsNone(self.marionette.get_pref(self.prefs["bool"])) + self.assertIsNone(self.marionette.get_pref(self.prefs["int"])) + self.assertIsNone(self.marionette.get_pref(self.prefs["string"])) + + # Set a value on the default branch first + pref_default = "marionette.test.pref_default2" + self.assertIsNone(self.marionette.get_pref(pref_default)) + self.marionette.set_prefs({pref_default: "default_value"}, default_branch=True) + + # Set user values + prefs = { + self.prefs["bool"]: True, + self.prefs["int"]: 42, + self.prefs["string"]: "abc", + pref_default: "user_value", + } + self.marionette.set_prefs(prefs) + + self.assertTrue(self.marionette.get_pref(self.prefs["bool"])) + self.assertEqual(self.marionette.get_pref(self.prefs["int"]), 42) + self.assertEqual(self.marionette.get_pref(self.prefs["string"]), "abc") + self.assertEqual(self.marionette.get_pref(pref_default), "user_value") + self.assertEqual( + self.marionette.get_pref(pref_default, default_branch=True), "default_value" + ) + + def test_using_prefs(self): + # Test that multiple preferences can be set with "using_prefs", and that + # they are set correctly and unset correctly after leaving the context + # manager. + pref_not_existent = "marionette.test.not_existent1" + pref_default = "marionette.test.pref_default3" + + self.marionette.set_prefs( + { + self.prefs["string"]: "abc", + self.prefs["int"]: 42, + self.prefs["bool"]: False, + } + ) + self.assertFalse(self.marionette.get_pref(self.prefs["bool"])) + self.assertEqual(self.marionette.get_pref(self.prefs["int"]), 42) + self.assertEqual(self.marionette.get_pref(self.prefs["string"]), "abc") + self.assertIsNone(self.marionette.get_pref(pref_not_existent)) + + with self.marionette.using_prefs( + { + self.prefs["bool"]: True, + self.prefs["int"]: 24, + self.prefs["string"]: "def", + pref_not_existent: "existent", + } + ): + self.assertTrue(self.marionette.get_pref(self.prefs["bool"]), True) + self.assertEqual(self.marionette.get_pref(self.prefs["int"]), 24) + self.assertEqual(self.marionette.get_pref(self.prefs["string"]), "def") + self.assertEqual(self.marionette.get_pref(pref_not_existent), "existent") + + self.assertFalse(self.marionette.get_pref(self.prefs["bool"])) + self.assertEqual(self.marionette.get_pref(self.prefs["int"]), 42) + self.assertEqual(self.marionette.get_pref(self.prefs["string"]), "abc") + self.assertIsNone(self.marionette.get_pref(pref_not_existent)) + + # Using context with default branch + self.marionette.set_pref(pref_default, "default_value", default_branch=True) + self.assertEqual( + self.marionette.get_pref(pref_default, default_branch=True), "default_value" + ) + + with self.marionette.using_prefs( + {pref_default: "new_value"}, default_branch=True + ): + self.assertEqual( + self.marionette.get_pref(pref_default, default_branch=True), "new_value" + ) + + self.assertEqual( + self.marionette.get_pref(pref_default, default_branch=True), "default_value" + ) + + def test_using_prefs_exception(self): + # Test that throwing an exception inside the context manager doesn"t + # prevent the preferences from being restored at context manager exit. + self.marionette.set_pref(self.prefs["string"], "abc") + + try: + with self.marionette.using_prefs({self.prefs["string"]: "def"}): + self.assertEqual(self.marionette.get_pref(self.prefs["string"]), "def") + self.marionette.execute_script("return foo.bar.baz;") + except JavascriptException: + pass + + self.assertEqual(self.marionette.get_pref(self.prefs["string"]), "abc") diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_prefs_enforce.py b/testing/marionette/harness/marionette_harness/tests/unit/test_prefs_enforce.py new file mode 100644 index 0000000000..609bed0527 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_prefs_enforce.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/. + +import six + +from marionette_harness import MarionetteTestCase + + +class TestEnforcePreferences(MarionetteTestCase): + def setUp(self): + super(TestEnforcePreferences, self).setUp() + self.marionette.set_context("chrome") + + def tearDown(self): + self.marionette.restart(in_app=False, clean=True) + + super(TestEnforcePreferences, self).tearDown() + + def enforce_prefs(self, prefs=None): + test_prefs = { + "marionette.test.bool": True, + "marionette.test.int": 3, + "marionette.test.string": "testing", + } + + self.marionette.enforce_gecko_prefs(prefs or test_prefs) + + def test_preferences_are_set(self): + self.enforce_prefs() + self.assertTrue(self.marionette.get_pref("marionette.test.bool")) + self.assertEqual(self.marionette.get_pref("marionette.test.string"), "testing") + self.assertEqual(self.marionette.get_pref("marionette.test.int"), 3) + + def test_change_enforced_preference(self): + self.enforce_prefs() + self.assertTrue(self.marionette.get_pref("marionette.test.bool")) + + self.enforce_prefs({"marionette.test.bool": False}) + self.assertFalse(self.marionette.get_pref("marionette.test.bool")) + + def test_restart_with_clean_profile_after_enforce_prefs(self): + self.enforce_prefs() + self.assertTrue(self.marionette.get_pref("marionette.test.bool")) + + self.marionette.restart(in_app=False, clean=True) + self.assertEqual(self.marionette.get_pref("marionette.test.bool"), None) + + def test_restart_preserves_requested_capabilities(self): + self.marionette.delete_session() + self.marionette.start_session(capabilities={"test:fooBar": True}) + + self.enforce_prefs() + self.assertEqual(self.marionette.session.get("test:fooBar"), True) diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_profile_management.py b/testing/marionette/harness/marionette_harness/tests/unit/test_profile_management.py new file mode 100644 index 0000000000..1420b88157 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_profile_management.py @@ -0,0 +1,267 @@ +# coding=UTF-8 + +# 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/. + +import os +import shutil +import tempfile + +import mozprofile + +from marionette_driver import errors +from marionette_harness import MarionetteTestCase, parameterized + + +class BaseProfileManagement(MarionetteTestCase): + def setUp(self): + super(BaseProfileManagement, self).setUp() + + self.orig_profile_path = self.profile_path + + def tearDown(self): + shutil.rmtree(self.orig_profile_path, ignore_errors=True) + self.marionette.profile = None + + self.marionette.quit(in_app=False, clean=True) + + super(BaseProfileManagement, self).tearDown() + + @property + def profile(self): + return self.marionette.instance.profile + + @property + def profile_path(self): + return self.marionette.instance.profile.profile + + +class WorkspaceProfileManagement(BaseProfileManagement): + def setUp(self): + super(WorkspaceProfileManagement, self).setUp() + + # Set a new workspace for the instance, which will be used + # the next time a new profile is requested by a test. + self.workspace = tempfile.mkdtemp() + self.marionette.instance.workspace = self.workspace + + def tearDown(self): + self.marionette.instance.workspace = None + + shutil.rmtree(self.workspace, ignore_errors=True) + + super(WorkspaceProfileManagement, self).tearDown() + + +class ExternalProfileMixin(object): + def setUp(self): + super(ExternalProfileMixin, self).setUp() + + # Create external profile + tmp_dir = tempfile.mkdtemp(suffix="external") + shutil.rmtree(tmp_dir, ignore_errors=True) + + # Re-use all the required profile arguments (preferences) + profile_args = self.marionette.instance.profile_args + profile_args["profile"] = tmp_dir + self.external_profile = mozprofile.Profile(**profile_args) + + # Prevent profile from being removed during cleanup + self.external_profile.create_new = False + + def tearDown(self): + shutil.rmtree(self.external_profile.profile, ignore_errors=True) + + super(ExternalProfileMixin, self).tearDown() + + +class TestQuitRestartWithoutWorkspace(BaseProfileManagement): + @parameterized("safe", True) + @parameterized("forced", False) + def test_quit_keeps_same_profile(self, in_app): + self.marionette.quit(in_app=in_app) + self.marionette.start_session() + + self.assertEqual(self.profile_path, self.orig_profile_path) + self.assertTrue(os.path.exists(self.orig_profile_path)) + + def test_quit_clean_creates_new_profile(self): + self.marionette.quit(in_app=False, clean=True) + self.marionette.start_session() + + self.assertNotEqual(self.profile_path, self.orig_profile_path) + self.assertFalse(os.path.exists(self.orig_profile_path)) + + @parameterized("safe", True) + @parameterized("forced", False) + def test_restart_keeps_same_profile(self, in_app): + self.marionette.restart(in_app=in_app) + + self.assertEqual(self.profile_path, self.orig_profile_path) + self.assertTrue(os.path.exists(self.orig_profile_path)) + + def test_restart_clean_creates_new_profile(self): + self.marionette.restart(in_app=False, clean=True) + + self.assertNotEqual(self.profile_path, self.orig_profile_path) + self.assertFalse(os.path.exists(self.orig_profile_path)) + + +class TestQuitRestartWithWorkspace(WorkspaceProfileManagement): + @parameterized("safe", True) + @parameterized("forced", False) + def test_quit_keeps_same_profile(self, in_app): + self.marionette.quit(in_app=in_app) + self.marionette.start_session() + + self.assertEqual(self.profile_path, self.orig_profile_path) + self.assertNotIn(self.workspace, self.profile_path) + self.assertTrue(os.path.exists(self.orig_profile_path)) + + def test_quit_clean_creates_new_profile(self): + self.marionette.quit(in_app=False, clean=True) + self.marionette.start_session() + + self.assertNotEqual(self.profile_path, self.orig_profile_path) + self.assertIn(self.workspace, self.profile_path) + self.assertFalse(os.path.exists(self.orig_profile_path)) + + @parameterized("safe", True) + @parameterized("forced", False) + def test_restart_keeps_same_profile(self, in_app): + self.marionette.restart(in_app=in_app) + + self.assertEqual(self.profile_path, self.orig_profile_path) + self.assertNotIn(self.workspace, self.profile_path) + self.assertTrue(os.path.exists(self.orig_profile_path)) + + def test_restart_clean_creates_new_profile(self): + self.marionette.restart(in_app=False, clean=True) + + self.assertNotEqual(self.profile_path, self.orig_profile_path) + self.assertIn(self.workspace, self.profile_path) + self.assertFalse(os.path.exists(self.orig_profile_path)) + + +class TestSwitchProfileFailures(BaseProfileManagement): + def test_raise_for_switching_profile_while_instance_is_running(self): + with self.assertRaisesRegexp( + errors.MarionetteException, "instance is not running" + ): + self.marionette.instance.switch_profile() + + +class TestSwitchProfileWithoutWorkspace(ExternalProfileMixin, BaseProfileManagement): + def setUp(self): + super(TestSwitchProfileWithoutWorkspace, self).setUp() + + self.marionette.quit() + + def test_do_not_call_cleanup_of_profile_for_path_only(self): + # If a path to a profile has been given (eg. via the --profile command + # line argument) and the profile hasn't been created yet, switching the + # profile should not try to call `cleanup()` on a string. + self.marionette.instance._profile = self.external_profile.profile + self.marionette.instance.switch_profile() + + def test_new_random_profile_name(self): + self.marionette.instance.switch_profile() + self.marionette.start_session() + + self.assertNotEqual(self.profile_path, self.orig_profile_path) + self.assertFalse(os.path.exists(self.orig_profile_path)) + + def test_new_named_profile(self): + self.marionette.instance.switch_profile("foobar") + self.marionette.start_session() + + self.assertNotEqual(self.profile_path, self.orig_profile_path) + self.assertIn("foobar", self.profile_path) + self.assertFalse(os.path.exists(self.orig_profile_path)) + + def test_new_named_profile_unicode(self): + """Test using unicode string with 1-4 bytes encoding works.""" + self.marionette.instance.switch_profile("$¢€🍪") + self.marionette.start_session() + + self.assertNotEqual(self.profile_path, self.orig_profile_path) + self.assertIn("$¢€🍪", self.profile_path) + self.assertFalse(os.path.exists(self.orig_profile_path)) + + def test_new_named_profile_unicode_escape_characters(self): + """Test using escaped unicode string with 1-4 bytes encoding works.""" + self.marionette.instance.switch_profile("\u0024\u00A2\u20AC\u1F36A") + self.marionette.start_session() + + self.assertNotEqual(self.profile_path, self.orig_profile_path) + self.assertIn("\u0024\u00A2\u20AC\u1F36A", self.profile_path) + self.assertFalse(os.path.exists(self.orig_profile_path)) + + def test_clone_existing_profile(self): + self.marionette.instance.switch_profile(clone_from=self.external_profile) + self.marionette.start_session() + + self.assertIn( + os.path.basename(self.external_profile.profile), self.profile_path + ) + self.assertTrue(os.path.exists(self.external_profile.profile)) + + def test_replace_with_current_profile(self): + self.marionette.instance.profile = self.profile + self.marionette.start_session() + + self.assertEqual(self.profile_path, self.orig_profile_path) + self.assertTrue(os.path.exists(self.orig_profile_path)) + + def test_replace_with_external_profile(self): + self.marionette.instance.profile = self.external_profile + self.marionette.start_session() + + self.assertEqual(self.profile_path, self.external_profile.profile) + self.assertFalse(os.path.exists(self.orig_profile_path)) + + # Check that required preferences have been correctly set + self.assertFalse(self.marionette.get_pref("remote.prefs.recommended")) + + # Set a new profile and ensure the external profile has not been deleted + self.marionette.quit() + self.marionette.instance.profile = None + + self.assertNotEqual(self.profile_path, self.external_profile.profile) + self.assertTrue(os.path.exists(self.external_profile.profile)) + + +class TestSwitchProfileWithWorkspace(ExternalProfileMixin, WorkspaceProfileManagement): + def setUp(self): + super(TestSwitchProfileWithWorkspace, self).setUp() + + self.marionette.quit() + + def test_new_random_profile_name(self): + self.marionette.instance.switch_profile() + self.marionette.start_session() + + self.assertNotEqual(self.profile_path, self.orig_profile_path) + self.assertIn(self.workspace, self.profile_path) + self.assertFalse(os.path.exists(self.orig_profile_path)) + + def test_new_named_profile(self): + self.marionette.instance.switch_profile("foobar") + self.marionette.start_session() + + self.assertNotEqual(self.profile_path, self.orig_profile_path) + self.assertIn("foobar", self.profile_path) + self.assertIn(self.workspace, self.profile_path) + self.assertFalse(os.path.exists(self.orig_profile_path)) + + def test_clone_existing_profile(self): + self.marionette.instance.switch_profile(clone_from=self.external_profile) + self.marionette.start_session() + + self.assertNotEqual(self.profile_path, self.orig_profile_path) + self.assertIn(self.workspace, self.profile_path) + self.assertIn( + os.path.basename(self.external_profile.profile), self.profile_path + ) + self.assertTrue(os.path.exists(self.external_profile.profile)) diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_proxy.py b/testing/marionette/harness/marionette_harness/tests/unit/test_proxy.py new file mode 100644 index 0000000000..21a37639d9 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_proxy.py @@ -0,0 +1,159 @@ +# 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 errors + +from marionette_harness import MarionetteTestCase + + +class TestProxyCapabilities(MarionetteTestCase): + def setUp(self): + super(TestProxyCapabilities, self).setUp() + + self.marionette.delete_session() + + def tearDown(self): + if not self.marionette.session: + self.marionette.start_session() + + with self.marionette.using_context("chrome"): + self.marionette.execute_script( + """ + const { Preferences } = ChromeUtils.importESModule( + "resource://gre/modules/Preferences.sys.mjs" + ); + Preferences.resetBranch("network.proxy"); + """ + ) + + super(TestProxyCapabilities, self).tearDown() + + def test_proxy_object_in_returned_capabilities(self): + capabilities = {"proxy": {"proxyType": "system"}} + + self.marionette.start_session(capabilities) + self.assertEqual( + self.marionette.session_capabilities["proxy"], capabilities["proxy"] + ) + + def test_proxy_type_autodetect(self): + capabilities = {"proxy": {"proxyType": "autodetect"}} + + self.marionette.start_session(capabilities) + self.assertEqual( + self.marionette.session_capabilities["proxy"], capabilities["proxy"] + ) + + def test_proxy_type_direct(self): + capabilities = {"proxy": {"proxyType": "direct"}} + + self.marionette.start_session(capabilities) + self.assertEqual( + self.marionette.session_capabilities["proxy"], capabilities["proxy"] + ) + + def test_proxy_type_manual(self): + proxy_hostname = "marionette.test" + capabilities = { + "proxy": { + "proxyType": "manual", + "httpProxy": "{}:80".format(proxy_hostname), + "sslProxy": "{}:443".format(proxy_hostname), + "socksProxy": proxy_hostname, + "socksVersion": 4, + } + } + + self.marionette.start_session(capabilities) + self.assertEqual( + self.marionette.session_capabilities["proxy"], capabilities["proxy"] + ) + + def test_proxy_type_manual_socks_requires_version(self): + proxy_port = 4444 + proxy_hostname = "marionette.test" + proxy_host = "{}:{}".format(proxy_hostname, proxy_port) + capabilities = { + "proxy": { + "proxyType": "manual", + "socksProxy": proxy_host, + } + } + + with self.assertRaises(errors.SessionNotCreatedException): + self.marionette.start_session(capabilities) + + def test_proxy_type_manual_no_proxy_on(self): + capabilities = { + "proxy": { + "proxyType": "manual", + "noProxy": ["foo", "bar"], + } + } + + self.marionette.start_session(capabilities) + self.assertEqual( + self.marionette.session_capabilities["proxy"], capabilities["proxy"] + ) + + def test_proxy_type_manual_invalid_no_proxy_on(self): + capabilities = { + "proxy": { + "proxyType": "manual", + "noProxy": "foo, bar", + } + } + + with self.assertRaises(errors.SessionNotCreatedException): + self.marionette.start_session(capabilities) + + def test_proxy_type_pac(self): + pac_url = "http://marionette.test" + capabilities = {"proxy": {"proxyType": "pac", "proxyAutoconfigUrl": pac_url}} + + self.marionette.start_session(capabilities) + self.assertEqual( + self.marionette.session_capabilities["proxy"], capabilities["proxy"] + ) + + def test_proxy_type_system(self): + capabilities = {"proxy": {"proxyType": "system"}} + + self.marionette.start_session(capabilities) + self.assertEqual( + self.marionette.session_capabilities["proxy"], capabilities["proxy"] + ) + + def test_invalid_proxy_object(self): + capabilities = {"proxy": "I really should be a dictionary"} + + with self.assertRaises(errors.SessionNotCreatedException): + self.marionette.start_session(capabilities) + + def test_missing_proxy_type(self): + with self.assertRaises(errors.SessionNotCreatedException): + self.marionette.start_session({"proxy": {"proxyAutoconfigUrl": "foobar"}}) + + def test_invalid_proxy_type(self): + capabilities = {"proxy": {"proxyType": "NOPROXY"}} + + with self.assertRaises(errors.SessionNotCreatedException): + self.marionette.start_session(capabilities) + + def test_invalid_autoconfig_url_for_pac(self): + with self.assertRaises(errors.SessionNotCreatedException): + self.marionette.start_session({"proxy": {"proxyType": "pac"}}) + + with self.assertRaises(errors.SessionNotCreatedException): + self.marionette.start_session( + {"proxy": {"proxyType": "pac", "proxyAutoconfigUrl": None}} + ) + + def test_missing_socks_version_for_manual(self): + capabilities = { + "proxy": {"proxyType": "manual", "socksProxy": "marionette.test"} + } + + with self.assertRaises(errors.SessionNotCreatedException): + self.marionette.start_session(capabilities) diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_quit_restart.py b/testing/marionette/harness/marionette_harness/tests/unit/test_quit_restart.py new file mode 100644 index 0000000000..f41b374896 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_quit_restart.py @@ -0,0 +1,550 @@ +# 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/. + +import sys +import unittest +from urllib.parse import quote + +from marionette_driver import errors +from marionette_driver.by import By +from marionette_harness import MarionetteTestCase + + +def inline(doc): + return "data:text/html;charset=utf-8,{}".format(quote(doc)) + + +class TestServerQuitApplication(MarionetteTestCase): + def tearDown(self): + if self.marionette.session is None: + self.marionette.start_session() + + def quit(self, flags=None, safe_mode=False): + body = {} + if flags is not None: + body["flags"] = list( + flags, + ) + if safe_mode: + body["safeMode"] = safe_mode + + resp = self.marionette._send_message("Marionette:Quit", body) + self.marionette.session_id = None + self.marionette.session = None + self.marionette.process_id = None + self.marionette.profile = None + self.marionette.window = None + + self.assertIn("cause", resp) + + self.marionette.client.close() + self.marionette.instance.runner.wait() + + return resp["cause"] + + def test_types(self): + for typ in [42, True, "foo", []]: + print("testing type {}".format(type(typ))) + with self.assertRaises(errors.InvalidArgumentException): + self.marionette._send_message("Marionette:Quit", typ) + + with self.assertRaises(errors.InvalidArgumentException): + self.quit("foo") + + def test_undefined_default(self): + cause = self.quit() + self.assertEqual("shutdown", cause) + + def test_empty_default(self): + cause = self.quit(()) + self.assertEqual("shutdown", cause) + + def test_incompatible_quit_flags(self): + with self.assertRaises(errors.InvalidArgumentException): + self.quit(("eAttemptQuit", "eForceQuit")) + + def test_attempt_quit(self): + cause = self.quit(("eAttemptQuit",)) + self.assertEqual("shutdown", cause) + + def test_force_quit(self): + cause = self.quit(("eForceQuit",)) + self.assertEqual("shutdown", cause) + + def test_safe_mode_requires_restart(self): + with self.assertRaises(errors.InvalidArgumentException): + self.quit(("eAttemptQuit",), True) + + @unittest.skipUnless(sys.platform.startswith("darwin"), "Only supported on MacOS") + def test_silent_quit_missing_windowless_capability(self): + with self.assertRaises(errors.UnsupportedOperationException): + self.quit(("eSilently",)) + + +class TestQuitRestart(MarionetteTestCase): + def setUp(self): + MarionetteTestCase.setUp(self) + + self.pid = self.marionette.process_id + self.profile = self.marionette.profile + self.session_id = self.marionette.session_id + + # Use a preference to check that the restart was successful. If its + # value has not been forced, a restart will cause a reset of it. + self.assertNotEqual( + self.marionette.get_pref("startup.homepage_welcome_url"), "about:about" + ) + self.marionette.set_pref("startup.homepage_welcome_url", "about:about") + + def tearDown(self): + # Ensure to restart a session if none exist for clean-up + if self.marionette.session is None: + self.marionette.start_session() + + self.marionette.clear_pref("startup.homepage_welcome_url") + + MarionetteTestCase.tearDown(self) + + @property + def is_safe_mode(self): + with self.marionette.using_context("chrome"): + return self.marionette.execute_script( + """ + return Services.appinfo.inSafeMode; + """ + ) + + def shutdown(self, restart=False): + self.marionette.set_context("chrome") + self.marionette.execute_script( + """ + let flags = Ci.nsIAppStartup.eAttemptQuit; + if (arguments[0]) { + flags |= Ci.nsIAppStartup.eRestart; + } + Services.startup.quit(flags); + """, + script_args=(restart,), + ) + + def test_force_restart(self): + self.marionette.restart(in_app=False) + self.assertEqual(self.marionette.profile, self.profile) + self.assertNotEqual(self.marionette.session_id, self.session_id) + + # A forced restart will cause a new process id + self.assertNotEqual(self.marionette.process_id, self.pid) + self.assertNotEqual( + self.marionette.get_pref("startup.homepage_welcome_url"), "about:about" + ) + + def test_force_clean_restart(self): + self.marionette.restart(in_app=False, clean=True) + self.assertNotEqual(self.marionette.profile, self.profile) + self.assertNotEqual(self.marionette.session_id, self.session_id) + # A forced restart will cause a new process id + self.assertNotEqual(self.marionette.process_id, self.pid) + self.assertNotEqual( + self.marionette.get_pref("startup.homepage_welcome_url"), "about:about" + ) + + def test_force_quit(self): + self.marionette.quit(in_app=False) + + self.assertEqual(self.marionette.session, None) + with self.assertRaisesRegexp( + errors.InvalidSessionIdException, "Please start a session" + ): + self.marionette.get_url() + + def test_force_clean_quit(self): + self.marionette.quit(in_app=False, clean=True) + + self.assertEqual(self.marionette.session, None) + with self.assertRaisesRegexp( + errors.InvalidSessionIdException, "Please start a session" + ): + self.marionette.get_url() + + self.marionette.start_session() + self.assertNotEqual(self.marionette.profile, self.profile) + self.assertNotEqual(self.marionette.session_id, self.session_id) + self.assertNotEqual( + self.marionette.get_pref("startup.homepage_welcome_url"), "about:about" + ) + + def test_quit_no_in_app_and_clean(self): + # Test that in_app and clean cannot be used in combination + with self.assertRaisesRegexp( + ValueError, "cannot be triggered with the clean flag set" + ): + self.marionette.quit(in_app=True, clean=True) + + def test_restart_no_in_app_and_clean(self): + # Test that in_app and clean cannot be used in combination + with self.assertRaisesRegexp( + ValueError, "cannot be triggered with the clean flag set" + ): + self.marionette.restart(in_app=True, clean=True) + + def test_restart_preserves_requested_capabilities(self): + self.marionette.delete_session() + self.marionette.start_session(capabilities={"test:fooBar": True}) + + self.marionette.restart(in_app=False) + self.assertEqual(self.marionette.session.get("test:fooBar"), True) + + def test_restart_safe_mode(self): + try: + self.assertFalse(self.is_safe_mode, "Safe Mode is unexpectedly enabled") + self.marionette.restart(safe_mode=True) + self.assertTrue(self.is_safe_mode, "Safe Mode is not enabled") + finally: + self.marionette.quit(in_app=False, clean=True) + + def test_restart_safe_mode_requires_in_app(self): + self.assertFalse(self.is_safe_mode, "Safe Mode is unexpectedly enabled") + + with self.assertRaisesRegexp(ValueError, "in_app restart is required"): + self.marionette.restart(in_app=False, safe_mode=True) + + self.assertFalse(self.is_safe_mode, "Safe Mode is unexpectedly enabled") + self.marionette.quit(in_app=False, clean=True) + + def test_in_app_restart(self): + details = self.marionette.restart() + self.assertTrue(details["in_app"], "Expected in_app restart") + self.assertFalse(details["forced"], "Expected non-forced shutdown") + + self.assertEqual(self.marionette.profile, self.profile) + self.assertNotEqual(self.marionette.session_id, self.session_id) + + self.assertNotEqual(self.marionette.process_id, self.pid) + + self.assertNotEqual( + self.marionette.get_pref("startup.homepage_welcome_url"), "about:about" + ) + + def test_in_app_restart_component_prevents_shutdown(self): + with self.marionette.using_context("chrome"): + self.marionette.execute_script( + """ + Services.obs.addObserver(subject => { + let cancelQuit = subject.QueryInterface(Ci.nsISupportsPRBool); + cancelQuit.data = true; + }, "quit-application-requested"); + """ + ) + + details = self.marionette.restart() + self.assertTrue(details["in_app"], "Expected in_app restart") + self.assertTrue(details["forced"], "Expected forced shutdown") + + self.assertEqual(self.marionette.profile, self.profile) + self.assertNotEqual(self.marionette.session_id, self.session_id) + + self.assertNotEqual(self.marionette.process_id, self.pid) + + self.assertNotEqual( + self.marionette.get_pref("startup.homepage_welcome_url"), "about:about" + ) + + def test_in_app_restart_with_callback(self): + details = self.marionette.restart(callback=lambda: self.shutdown(restart=True)) + self.assertTrue(details["in_app"], "Expected in_app restart") + + self.assertEqual(self.marionette.profile, self.profile) + self.assertNotEqual(self.marionette.session_id, self.session_id) + + self.assertNotEqual(self.marionette.process_id, self.pid) + + self.assertNotEqual( + self.marionette.get_pref("startup.homepage_welcome_url"), "about:about" + ) + + def test_in_app_restart_with_non_callable_callback(self): + with self.assertRaisesRegexp(ValueError, "is not callable"): + self.marionette.restart(callback=4) + + self.assertEqual(self.marionette.instance.runner.returncode, None) + self.assertEqual(self.marionette.is_shutting_down, False) + + @unittest.skipIf(sys.platform.startswith("win"), "Bug 1493796") + def test_in_app_restart_with_callback_but_process_quits_instead(self): + try: + timeout_shutdown = self.marionette.shutdown_timeout + timeout_startup = self.marionette.startup_timeout + self.marionette.shutdown_timeout = 5 + self.marionette.startup_timeout = 0 + + with self.assertRaisesRegexp( + IOError, "Process unexpectedly quit without restarting" + ): + self.marionette.restart(callback=lambda: self.shutdown(restart=False)) + finally: + self.marionette.shutdown_timeout = timeout_shutdown + self.marionette.startup_timeout = timeout_startup + + @unittest.skipIf(sys.platform.startswith("win"), "Bug 1493796") + def test_in_app_restart_with_callback_missing_shutdown(self): + try: + timeout_shutdown = self.marionette.shutdown_timeout + timeout_startup = self.marionette.startup_timeout + self.marionette.shutdown_timeout = 5 + self.marionette.startup_timeout = 0 + + with self.assertRaisesRegexp( + IOError, "the connection to Marionette server is lost" + ): + self.marionette.restart(in_app=True, callback=lambda: False) + finally: + self.marionette.shutdown_timeout = timeout_shutdown + self.marionette.startup_timeout = timeout_startup + + def test_in_app_restart_preserves_requested_capabilities(self): + self.marionette.delete_session() + self.marionette.start_session(capabilities={"test:fooBar": True}) + + details = self.marionette.restart() + self.assertTrue(details["in_app"], "Expected in_app restart") + self.assertEqual(self.marionette.session.get("test:fooBar"), True) + + @unittest.skipUnless(sys.platform.startswith("darwin"), "Only supported on MacOS") + def test_in_app_silent_restart_fails_without_windowless_flag_on_mac_os(self): + self.marionette.delete_session() + self.marionette.start_session() + + with self.assertRaises(errors.UnsupportedOperationException): + self.marionette.restart(silent=True) + + @unittest.skipUnless(sys.platform.startswith("darwin"), "Only supported on MacOS") + def test_in_app_silent_restart_windowless_flag_on_mac_os(self): + self.marionette.delete_session() + self.marionette.start_session(capabilities={"moz:windowless": True}) + + self.marionette.restart(silent=True) + self.assertTrue(self.marionette.session_capabilities["moz:windowless"]) + + self.marionette.restart() + self.assertTrue(self.marionette.session_capabilities["moz:windowless"]) + + self.marionette.delete_session() + + @unittest.skipUnless(sys.platform.startswith("darwin"), "Only supported on MacOS") + def test_in_app_silent_restart_requires_in_app(self): + self.marionette.delete_session() + self.marionette.start_session(capabilities={"moz:windowless": True}) + + with self.assertRaisesRegexp(ValueError, "in_app restart is required"): + self.marionette.restart(in_app=False, silent=True) + + self.marionette.delete_session() + + @unittest.skipIf( + sys.platform.startswith("darwin"), "Not supported on other platforms than MacOS" + ) + def test_in_app_silent_restart_windowless_flag_unsupported_platforms(self): + self.marionette.delete_session() + + with self.assertRaises(errors.SessionNotCreatedException): + self.marionette.start_session(capabilities={"moz:windowless": True}) + + def test_in_app_quit(self): + details = self.marionette.quit() + self.assertTrue(details["in_app"], "Expected in_app shutdown") + self.assertFalse(details["forced"], "Expected non-forced shutdown") + self.assertEqual(self.marionette.instance.runner.returncode, 0) + + self.assertEqual(self.marionette.session, None) + with self.assertRaisesRegexp( + errors.InvalidSessionIdException, "Please start a session" + ): + self.marionette.get_url() + + self.marionette.start_session() + self.assertEqual(self.marionette.profile, self.profile) + self.assertNotEqual(self.marionette.session_id, self.session_id) + self.assertNotEqual( + self.marionette.get_pref("startup.homepage_welcome_url"), "about:about" + ) + + def test_in_app_quit_forced_because_component_prevents_shutdown(self): + with self.marionette.using_context("chrome"): + self.marionette.execute_script( + """ + Services.obs.addObserver(subject => { + let cancelQuit = subject.QueryInterface(Ci.nsISupportsPRBool); + cancelQuit.data = true; + }, "quit-application-requested"); + """ + ) + + details = self.marionette.quit() + self.assertTrue(details["in_app"], "Expected in_app shutdown") + self.assertTrue(details["forced"], "Expected forced shutdown") + self.assertEqual(self.marionette.instance.runner.returncode, 0) + + self.assertEqual(self.marionette.session, None) + with self.assertRaisesRegexp( + errors.InvalidSessionIdException, "Please start a session" + ): + self.marionette.get_url() + + self.marionette.start_session() + self.assertEqual(self.marionette.profile, self.profile) + self.assertNotEqual(self.marionette.session_id, self.session_id) + self.assertNotEqual( + self.marionette.get_pref("startup.homepage_welcome_url"), "about:about" + ) + + def test_in_app_quit_with_callback(self): + details = self.marionette.quit(callback=self.shutdown) + self.assertTrue(details["in_app"], "Expected in_app shutdown") + self.assertFalse(details["forced"], "Expected non-forced shutdown") + + self.assertEqual(self.marionette.instance.runner.returncode, 0) + self.assertEqual(self.marionette.is_shutting_down, False) + + self.assertEqual(self.marionette.session, None) + with self.assertRaisesRegexp( + errors.InvalidSessionIdException, "Please start a session" + ): + self.marionette.get_url() + + self.marionette.start_session() + self.assertEqual(self.marionette.profile, self.profile) + self.assertNotEqual(self.marionette.session_id, self.session_id) + self.assertNotEqual( + self.marionette.get_pref("startup.homepage_welcome_url"), "about:about" + ) + + def test_in_app_quit_with_non_callable_callback(self): + with self.assertRaisesRegexp(ValueError, "is not callable"): + self.marionette.quit(callback=4) + self.assertEqual(self.marionette.instance.runner.returncode, None) + self.assertEqual(self.marionette.is_shutting_down, False) + + def test_in_app_quit_forced_because_callback_does_not_shutdown(self): + try: + timeout = self.marionette.shutdown_timeout + self.marionette.shutdown_timeout = 5 + + with self.assertRaisesRegexp(IOError, "Process still running"): + self.marionette.quit(in_app=True, callback=lambda: False) + + self.assertNotEqual(self.marionette.instance.runner.returncode, None) + self.assertEqual(self.marionette.is_shutting_down, False) + finally: + self.marionette.shutdown_timeout = timeout + + self.marionette.start_session() + + def test_in_app_quit_with_callback_that_raises_an_exception(self): + def errorneous_callback(): + raise Exception("foo") + + with self.assertRaisesRegexp(Exception, "foo"): + self.marionette.quit(in_app=True, callback=errorneous_callback) + self.assertEqual(self.marionette.instance.runner.returncode, None) + self.assertEqual(self.marionette.is_shutting_down, False) + + self.assertIsNotNone(self.marionette.session) + self.marionette.current_window_handle + + def test_in_app_quit_with_dismissed_beforeunload_prompt(self): + self.marionette.navigate( + inline( + """ + <input type="text"> + <script> + window.addEventListener("beforeunload", function (event) { + event.preventDefault(); + }); + </script> + """ + ) + ) + + self.marionette.find_element(By.TAG_NAME, "input").send_keys("foo") + self.marionette.quit() + self.assertNotEqual(self.marionette.instance.runner.returncode, None) + self.marionette.start_session() + + def test_reset_context_after_quit_by_set_context(self): + # Check that we are in content context which is used by default in + # Marionette + self.assertNotIn( + "chrome://", + self.marionette.get_url(), + "Context does not default to content", + ) + + self.marionette.set_context("chrome") + self.marionette.quit() + self.assertEqual(self.marionette.session, None) + self.marionette.start_session() + self.assertNotIn( + "chrome://", + self.marionette.get_url(), + "Not in content context after quit with using_context", + ) + + def test_reset_context_after_quit_by_using_context(self): + # Check that we are in content context which is used by default in + # Marionette + self.assertNotIn( + "chrome://", + self.marionette.get_url(), + "Context does not default to content", + ) + + with self.marionette.using_context("chrome"): + self.marionette.quit() + self.assertEqual(self.marionette.session, None) + self.marionette.start_session() + self.assertNotIn( + "chrome://", + self.marionette.get_url(), + "Not in content context after quit with using_context", + ) + + def test_keep_context_after_restart_by_set_context(self): + # Check that we are in content context which is used by default in + # Marionette + self.assertNotIn( + "chrome://", self.marionette.get_url(), "Context doesn't default to content" + ) + + # restart while we are in chrome context + self.marionette.set_context("chrome") + self.marionette.restart() + + self.assertNotEqual(self.marionette.process_id, self.pid) + + self.assertIn( + "chrome://", + self.marionette.get_url(), + "Not in chrome context after a restart with set_context", + ) + + def test_keep_context_after_restart_by_using_context(self): + # Check that we are in content context which is used by default in + # Marionette + self.assertNotIn( + "chrome://", + self.marionette.get_url(), + "Context does not default to content", + ) + + # restart while we are in chrome context + with self.marionette.using_context("chrome"): + self.marionette.restart() + + self.assertNotEqual(self.marionette.process_id, self.pid) + + self.assertIn( + "chrome://", + self.marionette.get_url(), + "Not in chrome context after a restart with using_context", + ) diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_reftest.py b/testing/marionette/harness/marionette_harness/tests/unit/test_reftest.py new file mode 100644 index 0000000000..e173e5a963 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_reftest.py @@ -0,0 +1,105 @@ +# 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.errors import UnsupportedOperationException +from marionette_harness import MarionetteTestCase, skip + + +class TestReftest(MarionetteTestCase): + def setUp(self): + super(TestReftest, self).setUp() + + self.original_window = self.marionette.current_window_handle + + self.marionette.set_pref("marionette.log.truncate", False) + self.marionette.set_pref("dom.send_after_paint_to_content", True) + self.marionette.set_pref("widget.gtk.overlay-scrollbars.enabled", False) + + def tearDown(self): + try: + # make sure we've teared down any reftest context + self.marionette._send_message("reftest:teardown", {}) + except UnsupportedOperationException: + # this will throw if we aren't currently in a reftest context + pass + + self.marionette.switch_to_window(self.original_window) + + self.marionette.clear_pref("dom.send_after_paint_to_content") + self.marionette.clear_pref("marionette.log.truncate") + self.marionette.clear_pref("widget.gtk.overlay-scrollbars.enabled") + + super(TestReftest, self).tearDown() + + @skip("Bug 1648444 - Unexpected page unload when refreshing about:blank") + def test_basic(self): + self.marionette._send_message("reftest:setup", {"screenshot": "unexpected"}) + rv = self.marionette._send_message( + "reftest:run", + { + "test": "about:blank", + "references": [["about:blank", [], "=="]], + "expected": "PASS", + "timeout": 10 * 1000, + }, + ) + self.marionette._send_message("reftest:teardown", {}) + expected = { + "value": { + "extra": {}, + "message": "Testing about:blank == about:blank\n", + "stack": None, + "status": "PASS", + } + } + self.assertEqual(expected, rv) + + def test_url_comparison(self): + test_page = self.fixtures.where_is("test.html") + test_page_2 = self.fixtures.where_is("foo/../test.html") + + self.marionette._send_message("reftest:setup", {"screenshot": "unexpected"}) + rv = self.marionette._send_message( + "reftest:run", + { + "test": test_page, + "references": [[test_page_2, [], "=="]], + "expected": "PASS", + "timeout": 10 * 1000, + }, + ) + self.marionette._send_message("reftest:teardown", {}) + self.assertEqual("PASS", rv["value"]["status"]) + + def test_cache_multiple_sizes(self): + teal = self.fixtures.where_is("reftest/teal-700x700.html") + mostly_teal = self.fixtures.where_is("reftest/mostly-teal-700x700.html") + + self.marionette._send_message("reftest:setup", {"screenshot": "unexpected"}) + rv = self.marionette._send_message( + "reftest:run", + { + "test": teal, + "references": [[mostly_teal, [], "=="]], + "expected": "PASS", + "timeout": 10 * 1000, + "width": 600, + "height": 600, + }, + ) + self.assertEqual("PASS", rv["value"]["status"]) + + rv = self.marionette._send_message( + "reftest:run", + { + "test": teal, + "references": [[mostly_teal, [], "=="]], + "expected": "PASS", + "timeout": 10 * 1000, + "width": 700, + "height": 700, + }, + ) + self.assertEqual("FAIL", rv["value"]["status"]) + self.marionette._send_message("reftest:teardown", {}) diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_rendered_element.py b/testing/marionette/harness/marionette_harness/tests/unit/test_rendered_element.py new file mode 100644 index 0000000000..8c1d839a1b --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_rendered_element.py @@ -0,0 +1,31 @@ +from six.moves.urllib.parse import quote + +from marionette_driver.by import By +from marionette_harness import MarionetteTestCase + + +def inline(doc): + return "data:text/html;charset=utf-8,{}".format(quote(doc)) + + +class RenderedElementTests(MarionetteTestCase): + def test_get_computed_style_value_from_element(self): + self.marionette.navigate( + inline( + """ + <div style="color: green;" id="parent"> + <p id="green">This should be green</p> + <p id="red" style="color: red;">But this is red</p> + </div> + """ + ) + ) + + parent = self.marionette.find_element(By.ID, "parent") + self.assertEqual("rgb(0, 128, 0)", parent.value_of_css_property("color")) + + green = self.marionette.find_element(By.ID, "green") + self.assertEqual("rgb(0, 128, 0)", green.value_of_css_property("color")) + + red = self.marionette.find_element(By.ID, "red") + self.assertEqual("rgb(255, 0, 0)", red.value_of_css_property("color")) diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_report.py b/testing/marionette/harness/marionette_harness/tests/unit/test_report.py new file mode 100644 index 0000000000..a876888dee --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_report.py @@ -0,0 +1,27 @@ +# 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_harness import ( + expectedFailure, + MarionetteTestCase, + skip, + unexpectedSuccess, +) + + +class TestReport(MarionetteTestCase): + def test_pass(self): + assert True + + @skip("Skip Message") + def test_skip(self): + assert False + + @expectedFailure + def test_error(self): + raise Exception() + + @unexpectedSuccess + def test_unexpected_pass(self): + assert True diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_run_js_test.py b/testing/marionette/harness/marionette_harness/tests/unit/test_run_js_test.py new file mode 100644 index 0000000000..d180633376 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_run_js_test.py @@ -0,0 +1,10 @@ +# Any copyright is dedicated to the Public Domain. +# http://creativecommons.org/publicdomain/zero/1.0/ + +from marionette_harness import MarionetteTestCase + + +class TestRunJSTest(MarionetteTestCase): + def test_basic(self): + self.run_js_test("test_simpletest_pass.js") + self.run_js_test("test_simpletest_fail.js") diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_screen_orientation.py b/testing/marionette/harness/marionette_harness/tests/unit/test_screen_orientation.py new file mode 100644 index 0000000000..0bc641aa3e --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_screen_orientation.py @@ -0,0 +1,75 @@ +# 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 errors +from marionette_driver.wait import Wait +from marionette_harness import ( + MarionetteTestCase, + parameterized, + skip_if_desktop, +) + + +default_orientation = "portrait-primary" +unknown_orientation = "Unknown screen orientation: {}" + + +class TestScreenOrientation(MarionetteTestCase): + def setUp(self): + MarionetteTestCase.setUp(self) + + def tearDown(self): + MarionetteTestCase.tearDown(self) + + def wait_for_orientation(self, orientation, timeout=None): + Wait(self.marionette, timeout=timeout).until( + lambda _: self.marionette.orientation == orientation + ) + + @skip_if_desktop("Not supported in Firefox") + @parameterized("landscape-primary", "landscape-primary") + @parameterized("landscape-secondary", "landscape-secondary") + @parameterized("portrait-primary", "portrait-primary") + # @parameterized("portrait-secondary", "portrait-secondary") # Bug 1533084 + def test_set_orientation(self, orientation): + self.marionette.set_orientation(orientation) + self.wait_for_orientation(orientation) + + @skip_if_desktop("Not supported in Firefox") + def test_set_orientation_to_shorthand_portrait(self): + # Set orientation to something other than portrait-primary first, + # since the default is portrait-primary. + self.marionette.set_orientation("landscape-primary") + self.wait_for_orientation("landscape-primary") + + self.marionette.set_orientation("portrait") + self.wait_for_orientation("portrait-primary") + + @skip_if_desktop("Not supported in Firefox") + def test_set_orientation_to_shorthand_landscape(self): + self.marionette.set_orientation("landscape") + self.wait_for_orientation("landscape-primary") + + @skip_if_desktop("Not supported in Firefox") + def test_set_orientation_with_mixed_casing(self): + self.marionette.set_orientation("lAnDsCaPe") + self.wait_for_orientation("landscape-primary") + + @skip_if_desktop("Not supported in Firefox") + def test_set_invalid_orientation(self): + with self.assertRaisesRegexp( + errors.MarionetteException, unknown_orientation.format("cheese") + ): + self.marionette.set_orientation("cheese") + + @skip_if_desktop("Not supported in Firefox") + def test_set_null_orientation(self): + with self.assertRaisesRegexp( + errors.MarionetteException, unknown_orientation.format("null") + ): + self.marionette.set_orientation(None) + + def test_unsupported_operation_on_desktop(self): + with self.assertRaises(errors.UnsupportedOperationException): + self.marionette.set_orientation("landscape-primary") diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_screenshot.py b/testing/marionette/harness/marionette_harness/tests/unit/test_screenshot.py new file mode 100644 index 0000000000..bce712b059 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_screenshot.py @@ -0,0 +1,393 @@ +# 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/. + +import base64 +import hashlib +import imghdr +import struct +import tempfile +import unittest + +import six +from six.moves.urllib.parse import quote + +import mozinfo + +from marionette_driver import By +from marionette_driver.errors import NoSuchWindowException +from marionette_harness import ( + MarionetteTestCase, + skip, + WindowManagerMixin, +) + + +def decodebytes(s): + if six.PY3: + return base64.decodebytes(six.ensure_binary(s)) + return base64.decodestring(s) + + +def inline(doc, mime="text/html;charset=utf-8"): + return "data:{0},{1}".format(mime, quote(doc)) + + +box = inline( + "<body><div id='box'><p id='green' style='width: 50px; height: 50px; " + "background: silver;'></p></div></body>" +) +input = inline("<body><input id='text-input'></input></body>") +long = inline("<body style='height: 300vh'><p style='margin-top: 100vh'>foo</p></body>") +short = inline("<body style='height: 10vh'></body>") +svg = inline( + """ + <svg xmlns="http://www.w3.org/2000/svg" height="20" width="20"> + <rect height="20" width="20"/> + </svg>""", + mime="image/svg+xml", +) + + +class ScreenCaptureTestCase(MarionetteTestCase): + def setUp(self): + super(ScreenCaptureTestCase, self).setUp() + + self.maxDiff = None + + self._device_pixel_ratio = None + + # Ensure that each screenshot test runs on a blank page to avoid left + # over elements or focus which could interfer with taking screenshots + self.marionette.navigate("about:blank") + + @property + def device_pixel_ratio(self): + if self._device_pixel_ratio is None: + self._device_pixel_ratio = self.marionette.execute_script( + """ + return window.devicePixelRatio + """ + ) + return self._device_pixel_ratio + + @property + def document_element(self): + return self.marionette.find_element(By.CSS_SELECTOR, ":root") + + @property + def page_y_offset(self): + return self.marionette.execute_script("return window.pageYOffset") + + @property + def viewport_dimensions(self): + return self.marionette.execute_script( + "return [window.innerWidth, window.innerHeight];" + ) + + def assert_png(self, screenshot): + """Test that screenshot is a Base64 encoded PNG file.""" + if six.PY3 and not isinstance(screenshot, bytes): + screenshot = bytes(screenshot, encoding="utf-8") + image = decodebytes(screenshot) + self.assertEqual(imghdr.what("", image), "png") + + def assert_formats(self, element=None): + if element is None: + element = self.document_element + + screenshot_default = self.marionette.screenshot(element=element) + if six.PY3 and not isinstance(screenshot_default, bytes): + screenshot_default = bytes(screenshot_default, encoding="utf-8") + screenshot_image = self.marionette.screenshot(element=element, format="base64") + if six.PY3 and not isinstance(screenshot_image, bytes): + screenshot_image = bytes(screenshot_image, encoding="utf-8") + binary1 = self.marionette.screenshot(element=element, format="binary") + binary2 = self.marionette.screenshot(element=element, format="binary") + hash1 = self.marionette.screenshot(element=element, format="hash") + hash2 = self.marionette.screenshot(element=element, format="hash") + + # Valid data should have been returned + self.assert_png(screenshot_image) + self.assertEqual(imghdr.what("", binary1), "png") + self.assertEqual(screenshot_image, base64.b64encode(binary1)) + self.assertEqual(hash1, hashlib.sha256(screenshot_image).hexdigest()) + + # Different formats produce different data + self.assertNotEqual(screenshot_image, binary1) + self.assertNotEqual(screenshot_image, hash1) + self.assertNotEqual(binary1, hash1) + + # A second capture should be identical + self.assertEqual(screenshot_image, screenshot_default) + self.assertEqual(binary1, binary2) + self.assertEqual(hash1, hash2) + + def get_element_dimensions(self, element): + rect = element.rect + return rect["width"], rect["height"] + + def get_image_dimensions(self, screenshot): + if six.PY3 and not isinstance(screenshot, bytes): + screenshot = bytes(screenshot, encoding="utf-8") + self.assert_png(screenshot) + image = decodebytes(screenshot) + width, height = struct.unpack(">LL", image[16:24]) + return int(width), int(height) + + def scale(self, rect): + return ( + int(rect[0] * self.device_pixel_ratio), + int(rect[1] * self.device_pixel_ratio), + ) + + +class TestScreenCaptureChrome(WindowManagerMixin, ScreenCaptureTestCase): + def setUp(self): + super(TestScreenCaptureChrome, self).setUp() + self.marionette.set_context("chrome") + + def tearDown(self): + self.close_all_windows() + super(TestScreenCaptureChrome, self).tearDown() + + @property + def window_dimensions(self): + return tuple( + self.marionette.execute_script( + """ + let el = document.documentElement; + let rect = el.getBoundingClientRect(); + return [rect.width, rect.height]; + """ + ) + ) + + def open_dialog(self): + return self.open_chrome_window( + "chrome://remote/content/marionette/test_dialog.xhtml" + ) + + def test_capture_different_context(self): + """Check that screenshots in content and chrome are different.""" + with self.marionette.using_context("content"): + screenshot_content = self.marionette.screenshot() + screenshot_chrome = self.marionette.screenshot() + self.assertNotEqual(screenshot_content, screenshot_chrome) + + def test_capture_element(self): + dialog = self.open_dialog() + self.marionette.switch_to_window(dialog) + + # Ensure we only capture the element + el = self.marionette.find_element(By.ID, "test-list") + screenshot_element = self.marionette.screenshot(element=el) + self.assertEqual( + self.scale(self.get_element_dimensions(el)), + self.get_image_dimensions(screenshot_element), + ) + + # Ensure we do not capture the full window + screenshot_dialog = self.marionette.screenshot() + self.assertNotEqual(screenshot_dialog, screenshot_element) + + self.marionette.close_chrome_window() + self.marionette.switch_to_window(self.start_window) + + def test_capture_full_area(self): + dialog = self.open_dialog() + self.marionette.switch_to_window(dialog) + + root_dimensions = self.scale(self.get_element_dimensions(self.document_element)) + + # self.marionette.set_window_rect(width=100, height=100) + # A full capture is not the outer dimensions of the window, + # but instead the bounding box of the window's root node (documentElement). + screenshot_full = self.marionette.screenshot() + screenshot_root = self.marionette.screenshot(element=self.document_element) + + self.marionette.close_chrome_window() + self.marionette.switch_to_window(self.start_window) + + self.assert_png(screenshot_full) + self.assert_png(screenshot_root) + self.assertEqual(root_dimensions, self.get_image_dimensions(screenshot_full)) + self.assertEqual(screenshot_root, screenshot_full) + + def test_capture_window_already_closed(self): + dialog = self.open_dialog() + self.marionette.switch_to_window(dialog) + self.marionette.close_chrome_window() + + self.assertRaises(NoSuchWindowException, self.marionette.screenshot) + self.marionette.switch_to_window(self.start_window) + + def test_formats(self): + dialog = self.open_dialog() + self.marionette.switch_to_window(dialog) + + self.assert_formats() + + self.marionette.close_chrome_window() + self.marionette.switch_to_window(self.start_window) + + def test_format_unknown(self): + with self.assertRaises(ValueError): + self.marionette.screenshot(format="cheese") + + +class TestScreenCaptureContent(WindowManagerMixin, ScreenCaptureTestCase): + def setUp(self): + super(TestScreenCaptureContent, self).setUp() + self.marionette.set_context("content") + + def tearDown(self): + self.close_all_tabs() + super(TestScreenCaptureContent, self).tearDown() + + @property + def scroll_dimensions(self): + return tuple( + self.marionette.execute_script( + """ + return [ + document.documentElement.scrollWidth, + document.documentElement.scrollHeight + ]; + """ + ) + ) + + def test_capture_tab_already_closed(self): + new_tab = self.open_tab() + self.marionette.switch_to_window(new_tab) + self.marionette.close() + + self.assertRaises(NoSuchWindowException, self.marionette.screenshot) + self.marionette.switch_to_window(self.start_tab) + + @unittest.skipIf(mozinfo.info["bits"] == 32, "Bug 1582973 - Risk for OOM on 32bit") + def test_capture_vertical_bounds(self): + self.marionette.navigate(inline("<body style='margin-top: 32768px'>foo")) + screenshot = self.marionette.screenshot() + self.assert_png(screenshot) + + @unittest.skipIf(mozinfo.info["bits"] == 32, "Bug 1582973 - Risk for OOM on 32bit") + def test_capture_horizontal_bounds(self): + self.marionette.navigate(inline("<body style='margin-left: 32768px'>foo")) + screenshot = self.marionette.screenshot() + self.assert_png(screenshot) + + @unittest.skipIf(mozinfo.info["bits"] == 32, "Bug 1582973 - Risk for OOM on 32bit") + def test_capture_area_bounds(self): + self.marionette.navigate( + inline("<body style='margin-right: 21747px; margin-top: 21747px'>foo") + ) + screenshot = self.marionette.screenshot() + self.assert_png(screenshot) + + def test_capture_element(self): + self.marionette.navigate(box) + el = self.marionette.find_element(By.TAG_NAME, "div") + screenshot = self.marionette.screenshot(element=el) + self.assert_png(screenshot) + self.assertEqual( + self.scale(self.get_element_dimensions(el)), + self.get_image_dimensions(screenshot), + ) + + @skip("Bug 1213875") + def test_capture_element_scrolled_into_view(self): + self.marionette.navigate(long) + el = self.marionette.find_element(By.TAG_NAME, "p") + screenshot = self.marionette.screenshot(element=el) + self.assert_png(screenshot) + self.assertEqual( + self.scale(self.get_element_dimensions(el)), + self.get_image_dimensions(screenshot), + ) + self.assertGreater(self.page_y_offset, 0) + + def test_capture_full_html_document_element(self): + self.marionette.navigate(long) + screenshot = self.marionette.screenshot() + self.assert_png(screenshot) + self.assertEqual( + self.scale(self.scroll_dimensions), self.get_image_dimensions(screenshot) + ) + + def test_capture_full_svg_document_element(self): + self.marionette.navigate(svg) + screenshot = self.marionette.screenshot() + self.assert_png(screenshot) + self.assertEqual( + self.scale(self.scroll_dimensions), self.get_image_dimensions(screenshot) + ) + + def test_capture_viewport(self): + url = self.marionette.absolute_url("clicks.html") + self.marionette.navigate(short) + self.marionette.navigate(url) + screenshot = self.marionette.screenshot(full=False) + self.assert_png(screenshot) + self.assertEqual( + self.scale(self.viewport_dimensions), self.get_image_dimensions(screenshot) + ) + + def test_capture_viewport_after_scroll(self): + self.marionette.navigate(long) + before = self.marionette.screenshot() + el = self.marionette.find_element(By.TAG_NAME, "p") + self.marionette.execute_script( + "arguments[0].scrollIntoView()", script_args=[el] + ) + after = self.marionette.screenshot(full=False) + self.assertNotEqual(before, after) + self.assertGreater(self.page_y_offset, 0) + + def test_formats(self): + self.marionette.navigate(box) + + # Use a smaller region to speed up the test + element = self.marionette.find_element(By.TAG_NAME, "div") + self.assert_formats(element=element) + + def test_format_unknown(self): + with self.assertRaises(ValueError): + self.marionette.screenshot(format="cheese") + + def test_save_screenshot(self): + expected = self.marionette.screenshot(format="binary") + with tempfile.TemporaryFile("w+b") as fh: + self.marionette.save_screenshot(fh) + fh.flush() + fh.seek(0) + content = fh.read() + self.assertEqual(expected, content) + + def test_scroll_default(self): + self.marionette.navigate(long) + before = self.page_y_offset + el = self.marionette.find_element(By.TAG_NAME, "p") + self.marionette.screenshot(element=el, format="hash") + self.assertNotEqual(before, self.page_y_offset) + + def test_scroll(self): + self.marionette.navigate(long) + before = self.page_y_offset + el = self.marionette.find_element(By.TAG_NAME, "p") + self.marionette.screenshot(element=el, format="hash", scroll=True) + self.assertNotEqual(before, self.page_y_offset) + + def test_scroll_off(self): + self.marionette.navigate(long) + el = self.marionette.find_element(By.TAG_NAME, "p") + before = self.page_y_offset + self.marionette.screenshot(element=el, format="hash", scroll=False) + self.assertEqual(before, self.page_y_offset) + + def test_scroll_no_element(self): + self.marionette.navigate(long) + before = self.page_y_offset + self.marionette.screenshot(format="hash", scroll=True) + self.assertEqual(before, self.page_y_offset) diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_select.py b/testing/marionette/harness/marionette_harness/tests/unit/test_select.py new file mode 100644 index 0000000000..60cd94c870 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_select.py @@ -0,0 +1,218 @@ +# 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 six.moves.urllib.parse import quote + +from marionette_driver.by import By + +from marionette_harness import MarionetteTestCase + + +def inline(doc): + return "data:text/html;charset=utf-8,{}".format(quote(doc)) + + +class SelectTestCase(MarionetteTestCase): + def assertSelected(self, option_element): + self.assertTrue(option_element.is_selected(), "<option> element not selected") + self.assertTrue( + self.marionette.execute_script( + "return arguments[0].selected", + script_args=[option_element], + sandbox=None, + ), + "<option> selected attribute not updated", + ) + + def assertNotSelected(self, option_element): + self.assertFalse(option_element.is_selected(), "<option> is selected") + self.assertFalse( + self.marionette.execute_script( + "return arguments[0].selected", + script_args=[option_element], + sandbox=None, + ), + "<option> selected attribute not updated", + ) + + +class TestSelect(SelectTestCase): + def test_single(self): + self.marionette.navigate( + inline( + """ + <select> + <option>first + <option>second + </select>""" + ) + ) + select = self.marionette.find_element(By.TAG_NAME, "select") + options = self.marionette.find_elements(By.TAG_NAME, "option") + + self.assertSelected(options[0]) + options[1].click() + self.assertSelected(options[1]) + + def test_deselect_others(self): + self.marionette.navigate( + inline( + """ + <select> + <option>first + <option>second + <option>third + </select>""" + ) + ) + select = self.marionette.find_element(By.TAG_NAME, "select") + options = self.marionette.find_elements(By.TAG_NAME, "option") + + options[0].click() + self.assertSelected(options[0]) + options[1].click() + self.assertSelected(options[1]) + options[2].click() + self.assertSelected(options[2]) + options[0].click() + self.assertSelected(options[0]) + + def test_select_self(self): + self.marionette.navigate( + inline( + """ + <select> + <option>first + <option>second + </select>""" + ) + ) + select = self.marionette.find_element(By.TAG_NAME, "select") + options = self.marionette.find_elements(By.TAG_NAME, "option") + self.assertSelected(options[0]) + self.assertNotSelected(options[1]) + + options[1].click() + self.assertSelected(options[1]) + options[1].click() + self.assertSelected(options[1]) + + def test_out_of_view(self): + self.marionette.navigate( + inline( + """ + <select> + <option>1 + <option>2 + <option>3 + <option>4 + <option>5 + <option>6 + <option>7 + <option>8 + <option>9 + <option>10 + <option>11 + <option>12 + <option>13 + <option>14 + <option>15 + <option>16 + <option>17 + <option>18 + <option>19 + <option>20 + </select>""" + ) + ) + select = self.marionette.find_element(By.TAG_NAME, "select") + options = self.marionette.find_elements(By.TAG_NAME, "option") + + options[14].click() + self.assertSelected(options[14]) + + +class TestSelectMultiple(SelectTestCase): + def test_single(self): + self.marionette.navigate(inline("<select multiple> <option>first </select>")) + option = self.marionette.find_element(By.TAG_NAME, "option") + option.click() + self.assertSelected(option) + + def test_multiple(self): + self.marionette.navigate( + inline( + """ + <select multiple> + <option>first + <option>second + <option>third + </select>""" + ) + ) + select = self.marionette.find_element(By.TAG_NAME, "select") + options = select.find_elements(By.TAG_NAME, "option") + + options[1].click() + self.assertSelected(options[1]) + + options[2].click() + self.assertSelected(options[2]) + self.assertSelected(options[1]) + + def test_deselect_selected(self): + self.marionette.navigate(inline("<select multiple> <option>first </select>")) + option = self.marionette.find_element(By.TAG_NAME, "option") + option.click() + self.assertSelected(option) + option.click() + self.assertNotSelected(option) + + def test_deselect_preselected(self): + self.marionette.navigate( + inline( + """ + <select multiple> + <option selected>first + </select>""" + ) + ) + option = self.marionette.find_element(By.TAG_NAME, "option") + self.assertSelected(option) + option.click() + self.assertNotSelected(option) + + def test_out_of_view(self): + self.marionette.navigate( + inline( + """ + <select multiple> + <option>1 + <option>2 + <option>3 + <option>4 + <option>5 + <option>6 + <option>7 + <option>8 + <option>9 + <option>10 + <option>11 + <option>12 + <option>13 + <option>14 + <option>15 + <option>16 + <option>17 + <option>18 + <option>19 + <option>20 + </select>""" + ) + ) + select = self.marionette.find_element(By.TAG_NAME, "select") + options = self.marionette.find_elements(By.TAG_NAME, "option") + + options[-1].click() + self.assertSelected(options[-1]) diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_sendkeys_menupopup_chrome.py b/testing/marionette/harness/marionette_harness/tests/unit/test_sendkeys_menupopup_chrome.py new file mode 100644 index 0000000000..a921b37b85 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_sendkeys_menupopup_chrome.py @@ -0,0 +1,106 @@ +# 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 six.moves.urllib.parse import quote + +from marionette_driver import By, errors, Wait +from marionette_driver.keys import Keys + +from marionette_harness import ( + MarionetteTestCase, + WindowManagerMixin, +) + + +class TestSendkeysMenupopup(WindowManagerMixin, MarionetteTestCase): + def setUp(self): + super(TestSendkeysMenupopup, self).setUp() + + self.marionette.set_context("chrome") + new_window = self.open_chrome_window( + "chrome://remote/content/marionette/test_menupopup.xhtml" + ) + self.marionette.switch_to_window(new_window) + + self.click_el = self.marionette.find_element(By.ID, "options-button") + self.disabled_menuitem_el = self.marionette.find_element( + By.ID, "option-disabled" + ) + self.hidden_menuitem_el = self.marionette.find_element(By.ID, "option-hidden") + self.menuitem_el = self.marionette.find_element(By.ID, "option-enabled") + self.menupopup_el = self.marionette.find_element(By.ID, "options-menupopup") + self.testwindow_el = self.marionette.find_element(By.ID, "test-window") + + def context_menu_state(self): + return self.menupopup_el.get_property("state") + + def open_context_menu(self): + def attempt_open_context_menu(): + self.assertEqual(self.context_menu_state(), "closed") + self.click_el.click() + Wait(self.marionette).until( + lambda _: self.context_menu_state() == "open", + message="Context menu did not open", + ) + + try: + attempt_open_context_menu() + except errors.TimeoutException: + # If the first attempt timed out, try a second time. + # On Linux, the test will intermittently fail if we click too + # early on the button. Retrying fixes the issue. See Bug 1686769. + attempt_open_context_menu() + + def wait_for_context_menu_closed(self): + Wait(self.marionette).until( + lambda _: self.context_menu_state() == "closed", + message="Context menu did not close", + ) + + def tearDown(self): + try: + self.close_all_windows() + finally: + super(TestSendkeysMenupopup, self).tearDown() + + def test_sendkeys_menuitem(self): + # Try closing the context menu by sending ESCAPE to a visible context menu item. + self.open_context_menu() + + self.menuitem_el.send_keys(Keys.ESCAPE) + self.wait_for_context_menu_closed() + + def test_sendkeys_menupopup(self): + # Try closing the context menu by sending ESCAPE to the context menu. + self.open_context_menu() + + self.menupopup_el.send_keys(Keys.ESCAPE) + self.wait_for_context_menu_closed() + + def test_sendkeys_window(self): + # Try closing the context menu by sending ESCAPE to the main window. + self.open_context_menu() + + self.testwindow_el.send_keys(Keys.ESCAPE) + self.wait_for_context_menu_closed() + + def test_sendkeys_closed_menu(self): + # send_keys should throw for the menupopup if the contextmenu is closed. + with self.assertRaises(errors.ElementNotInteractableException): + self.menupopup_el.send_keys(Keys.ESCAPE) + + # send_keys should throw for the menuitem if the contextmenu is closed. + with self.assertRaises(errors.ElementNotInteractableException): + self.menuitem_el.send_keys(Keys.ESCAPE) + + def test_sendkeys_hidden_disabled_menuitem(self): + self.open_context_menu() + + # send_keys should throw for a disabled menuitem in an opened contextmenu. + with self.assertRaises(errors.ElementNotInteractableException): + self.disabled_menuitem_el.send_keys(Keys.ESCAPE) + + # send_keys should throw for a hidden menuitem in an opened contextmenu. + with self.assertRaises(errors.ElementNotInteractableException): + self.hidden_menuitem_el.send_keys(Keys.ESCAPE) diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_session.py b/testing/marionette/harness/marionette_harness/tests/unit/test_session.py new file mode 100644 index 0000000000..1b709ed28b --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_session.py @@ -0,0 +1,49 @@ +# 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/. + +import six + +from marionette_driver import errors + +from marionette_harness import MarionetteTestCase + + +class TestSession(MarionetteTestCase): + def setUp(self): + super(TestSession, self).setUp() + + self.marionette.delete_session() + + def test_new_session_returns_capabilities(self): + # Sends newSession + caps = self.marionette.start_session() + + # Check that session was created. This implies the server + # sent us the sessionId and status fields. + self.assertIsNotNone(self.marionette.session) + + # Required capabilities mandated by WebDriver spec + self.assertIn("browserName", caps) + self.assertIn("browserVersion", caps) + self.assertIn("platformName", caps) + + def test_get_session_id(self): + # Sends newSession + self.marionette.start_session() + + self.assertTrue(self.marionette.session_id is not None) + self.assertTrue(isinstance(self.marionette.session_id, six.text_type)) + + def test_session_already_started(self): + self.marionette.start_session() + self.assertTrue(isinstance(self.marionette.session_id, six.text_type)) + with self.assertRaises(errors.SessionNotCreatedException): + self.marionette._send_message("WebDriver:NewSession", {}) + + def test_no_session(self): + with self.assertRaises(errors.InvalidSessionIdException): + self.marionette.get_url() + + self.marionette.start_session() + self.marionette.get_url() diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_shadowroot_findelement.py b/testing/marionette/harness/marionette_harness/tests/unit/test_shadowroot_findelement.py new file mode 100644 index 0000000000..de5dfdb91a --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_shadowroot_findelement.py @@ -0,0 +1,113 @@ +from six.moves.urllib.parse import quote + +from marionette_driver.by import By +from marionette_driver.errors import ( + DetachedShadowRootException, + NoSuchElementException, + NoSuchShadowRootException, +) +from marionette_driver.marionette import WebElement, ShadowRoot + +from marionette_harness import MarionetteTestCase + + +def inline(doc): + return "data:text/html;charset=utf-8,{}".format(quote(doc)) + + +page_shadow_dom = inline( + """ + <custom-element></custom-element> + <script> + customElements.define('custom-element', + class extends HTMLElement { + constructor() { + super(); + this.attachShadow({mode: 'open'}).innerHTML = ` + <div><a href=# id=foo>full link text</a><a href=# id=bar>another link text</a></div> + `; + } + }); + </script>""" +) + + +class TestShadowDOMFindElement(MarionetteTestCase): + def setUp(self): + MarionetteTestCase.setUp(self) + self.marionette.timeout.implicit = 0 + + def test_find_element_from_shadow_root(self): + self.marionette.navigate(page_shadow_dom) + custom_element = self.marionette.find_element(By.CSS_SELECTOR, "custom-element") + shadow_root = custom_element.shadow_root + found = shadow_root.find_element(By.CSS_SELECTOR, "a") + self.assertIsInstance(found, WebElement) + + el = self.marionette.execute_script( + """ + return arguments[0].shadowRoot.querySelector('a') + """, + [custom_element], + ) + self.assertEqual(found, el) + + def test_unknown_element_from_shadow_root(self): + self.marionette.navigate(page_shadow_dom) + shadow_root = self.marionette.find_element( + By.CSS_SELECTOR, "custom-element" + ).shadow_root + with self.assertRaises(NoSuchElementException): + shadow_root.find_element(By.CSS_SELECTOR, "does not exist") + + def test_detached_shadow_root(self): + self.marionette.navigate(page_shadow_dom) + shadow_root = self.marionette.find_element( + By.CSS_SELECTOR, "custom-element" + ).shadow_root + self.marionette.refresh() + with self.assertRaises(DetachedShadowRootException): + shadow_root.find_element(By.CSS_SELECTOR, "a") + + def test_no_such_shadow_root(self): + not_existing_shadow_root = ShadowRoot(self.marionette, "foo") + with self.assertRaises(NoSuchShadowRootException): + not_existing_shadow_root.find_element(By.CSS_SELECTOR, "a") + + +class TestShadowDOMFindElements(MarionetteTestCase): + def setUp(self): + MarionetteTestCase.setUp(self) + self.marionette.timeout.implicit = 0 + + def test_find_elements_from_shadow_root(self): + self.marionette.navigate(page_shadow_dom) + custom_element = self.marionette.find_element(By.CSS_SELECTOR, "custom-element") + shadow_root = custom_element.shadow_root + found = shadow_root.find_elements(By.CSS_SELECTOR, "a") + self.assertEqual(len(found), 2) + + els = self.marionette.execute_script( + """ + return arguments[0].shadowRoot.querySelectorAll('a') + """, + [custom_element], + ) + + for i in range(len(found)): + self.assertIsInstance(found[i], WebElement) + self.assertEqual(found[i], els[i]) + + def test_detached_shadow_root(self): + self.marionette.navigate(page_shadow_dom) + shadow_root = self.marionette.find_element( + By.CSS_SELECTOR, "custom-element" + ).shadow_root + self.marionette.refresh() + with self.assertRaises(DetachedShadowRootException): + shadow_root.find_elements(By.CSS_SELECTOR, "a") + + def test_no_such_shadow_root(self): + not_existing_shadow_root = ShadowRoot(self.marionette, "foo") + with self.assertRaises(NoSuchShadowRootException): + not_existing_shadow_root.find_elements(By.CSS_SELECTOR, "a") diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_skip_setup.py b/testing/marionette/harness/marionette_harness/tests/unit/test_skip_setup.py new file mode 100644 index 0000000000..e3cac5947f --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_skip_setup.py @@ -0,0 +1,33 @@ +# 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_harness import MarionetteTestCase, SkipTest + + +class TestSetUpSkipped(MarionetteTestCase): + testVar = {"test": "SkipTest"} + + def setUp(self): + MarionetteTestCase.setUp(self) + try: + self.testVar["email"] + except KeyError: + raise SkipTest("email key not present in dict, skip ...") + + def test_assert(self): + assert True + + +class TestSetUpNotSkipped(MarionetteTestCase): + testVar = {"test": "SkipTest"} + + def setUp(self): + try: + self.testVar["test"] + except KeyError: + raise SkipTest("email key not present in dict, skip ...") + MarionetteTestCase.setUp(self) + + def test_assert(self): + assert True diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_switch_frame.py b/testing/marionette/harness/marionette_harness/tests/unit/test_switch_frame.py new file mode 100644 index 0000000000..04cb9ce2f7 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_switch_frame.py @@ -0,0 +1,96 @@ +# 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.by import By +from marionette_driver.errors import InvalidArgumentException, NoSuchFrameException + +from marionette_harness import MarionetteTestCase + + +class TestSwitchFrame(MarionetteTestCase): + def setUp(self): + super(TestSwitchFrame, self).setUp() + + test_html = self.marionette.absolute_url("frameset.html") + self.marionette.navigate(test_html) + + def test_exceptions(self): + frame = self.marionette.find_element(By.CSS_SELECTOR, ":root") + with self.assertRaises(NoSuchFrameException): + self.marionette.switch_to_frame(frame) + + with self.assertRaises(InvalidArgumentException): + self.marionette.switch_to_frame(-1) + + def test_by_frame_element(self): + frame = self.marionette.find_element(By.NAME, "third") + self.marionette.switch_to_frame(frame) + + element = self.marionette.find_element(By.ID, "email") + self.assertEqual(element.get_attribute("type"), "email") + + def test_by_index(self): + self.marionette.switch_to_frame(2) + + element = self.marionette.find_element(By.ID, "email") + self.assertEqual(element.get_attribute("type"), "email") + + def test_back_to_top_frame(self): + frame1 = self.marionette.find_element(By.ID, "sixth") + self.marionette.switch_to_frame(frame1) + self.marionette.switch_to_frame(0) + + self.marionette.find_element(By.ID, "testDiv") + + self.marionette.switch_to_frame() + frame = self.marionette.find_element(By.ID, "sixth") + self.assertEqual(frame, frame1) + + +class TestSwitchParentFrame(MarionetteTestCase): + def test_iframe(self): + frame_html = self.marionette.absolute_url("test_iframe.html") + self.marionette.navigate(frame_html) + + frame = self.marionette.find_element(By.ID, "test_iframe") + self.marionette.switch_to_frame(frame) + self.marionette.find_element(By.ID, "testDiv") + + self.marionette.switch_to_parent_frame() + + self.marionette.find_element(By.ID, "test_iframe") + + def test_frameset(self): + frame_html = self.marionette.absolute_url("frameset.html") + self.marionette.navigate(frame_html) + frame = self.marionette.find_element(By.NAME, "third") + self.marionette.switch_to_frame(frame) + + # If we don't find the following element we aren't on the right page + self.marionette.find_element(By.ID, "checky") + + self.marionette.switch_to_parent_frame() + self.marionette.find_element(By.NAME, "third") + + def test_from_default_context_is_a_noop(self): + formpage = self.marionette.absolute_url("formPage.html") + self.marionette.navigate(formpage) + self.marionette.find_element(By.ID, "checky") + + self.marionette.switch_to_parent_frame() + self.marionette.find_element(By.ID, "checky") + + def test_from_second_level(self): + frame_html = self.marionette.absolute_url("frameset.html") + self.marionette.navigate(frame_html) + frame = self.marionette.find_element(By.NAME, "fourth") + self.marionette.switch_to_frame(frame) + + second_level = self.marionette.find_element(By.NAME, "child1") + self.marionette.switch_to_frame(second_level) + self.marionette.find_element(By.NAME, "myCheckBox") + + self.marionette.switch_to_parent_frame() + + second_level = self.marionette.find_element(By.NAME, "child1") diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_switch_frame_chrome.py b/testing/marionette/harness/marionette_harness/tests/unit/test_switch_frame_chrome.py new file mode 100644 index 0000000000..369ea0c061 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_switch_frame_chrome.py @@ -0,0 +1,57 @@ +# 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 JavascriptException + +from marionette_harness import MarionetteTestCase, WindowManagerMixin + + +class TestSwitchFrameChrome(WindowManagerMixin, MarionetteTestCase): + def setUp(self): + super(TestSwitchFrameChrome, self).setUp() + self.marionette.set_context("chrome") + + new_window = self.open_chrome_window( + "chrome://remote/content/marionette/test.xhtml" + ) + self.marionette.switch_to_window(new_window) + self.assertNotEqual( + self.start_window, self.marionette.current_chrome_window_handle + ) + + def tearDown(self): + self.close_all_windows() + super(TestSwitchFrameChrome, self).tearDown() + + def test_switch_simple(self): + self.assertIn( + "test.xhtml", self.marionette.get_url(), "Initial navigation has failed" + ) + self.marionette.switch_to_frame(0) + self.assertIn( + "test.xhtml", self.marionette.get_url(), "Switching by index failed" + ) + self.marionette.find_element(By.ID, "testBox") + self.marionette.switch_to_frame() + self.assertIn( + "test.xhtml", self.marionette.get_url(), "Switching by null failed" + ) + iframe = self.marionette.find_element(By.ID, "iframe") + self.marionette.switch_to_frame(iframe) + self.assertIn( + "test.xhtml", self.marionette.get_url(), "Switching by element failed" + ) + self.marionette.find_element(By.ID, "testBox") + + def test_stack_trace(self): + self.assertIn( + "test.xhtml", self.marionette.get_url(), "Initial navigation has failed" + ) + self.marionette.switch_to_frame(0) + self.marionette.find_element(By.ID, "testBox") + try: + self.marionette.execute_async_script("foo();") + except JavascriptException as e: + self.assertIn("foo", str(e)) diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_switch_window_chrome.py b/testing/marionette/harness/marionette_harness/tests/unit/test_switch_window_chrome.py new file mode 100644 index 0000000000..0b02e45351 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_switch_window_chrome.py @@ -0,0 +1,113 @@ +# 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/. + +import os +import sys + +from unittest import skipIf + +# add this directory to the path +sys.path.append(os.path.dirname(__file__)) + +from test_switch_window_content import TestSwitchToWindowContent + + +class TestSwitchWindowChrome(TestSwitchToWindowContent): + def setUp(self): + super(TestSwitchWindowChrome, self).setUp() + + self.marionette.set_context("chrome") + + def tearDown(self): + self.close_all_windows() + + super(TestSwitchWindowChrome, self).tearDown() + + def test_switch_to_unloaded_tab(self): + # Can only run in content context + pass + + @skipIf( + sys.platform.startswith("linux"), + "Bug 1511970 - New window isn't moved to the background on Linux", + ) + def test_switch_tabs_for_new_background_window_without_focus_change(self): + # Open an additional tab in the original window so we can better check + # the selected index in thew new window to be opened. + second_tab = self.open_tab(focus=True) + self.marionette.switch_to_window(second_tab, focus=True) + second_tab_index = self.get_selected_tab_index() + self.assertNotEqual(second_tab_index, self.selected_tab_index) + + # Open a new background window, but we are interested in the tab + with self.marionette.using_context("content"): + tab_in_new_window = self.open_window() + self.assertEqual(self.marionette.current_window_handle, second_tab) + self.assertEqual( + self.marionette.current_chrome_window_handle, self.start_window + ) + self.assertEqual(self.get_selected_tab_index(), second_tab_index) + + # Switch to the tab in the new window but don't focus it + self.marionette.switch_to_window(tab_in_new_window, focus=False) + self.assertEqual(self.marionette.current_window_handle, tab_in_new_window) + self.assertNotEqual( + self.marionette.current_chrome_window_handle, self.start_window + ) + self.assertEqual(self.get_selected_tab_index(), second_tab_index) + + def test_switch_tabs_for_new_foreground_window_with_focus_change(self): + # Open an addition tab in the original window so we can better check + # the selected index in thew new window to be opened. + second_tab = self.open_tab() + self.marionette.switch_to_window(second_tab, focus=True) + second_tab_index = self.get_selected_tab_index() + self.assertNotEqual(second_tab_index, self.selected_tab_index) + + # Opens a new window, but we are interested in the tab + with self.marionette.using_context("content"): + tab_in_new_window = self.open_window(focus=True) + self.assertEqual(self.marionette.current_window_handle, second_tab) + self.assertEqual( + self.marionette.current_chrome_window_handle, self.start_window + ) + self.assertNotEqual(self.get_selected_tab_index(), second_tab_index) + + self.marionette.switch_to_window(tab_in_new_window) + self.assertEqual(self.marionette.current_window_handle, tab_in_new_window) + self.assertNotEqual( + self.marionette.current_chrome_window_handle, self.start_window + ) + self.assertNotEqual(self.get_selected_tab_index(), second_tab_index) + + self.marionette.switch_to_window(second_tab, focus=True) + self.assertEqual(self.marionette.current_window_handle, second_tab) + self.assertEqual( + self.marionette.current_chrome_window_handle, self.start_window + ) + # Bug 1335085 - The focus doesn't change even as requested so. + # self.assertEqual(self.get_selected_tab_index(), second_tab_index) + + def test_switch_tabs_for_new_foreground_window_without_focus_change(self): + # Open an addition tab in the original window so we can better check + # the selected index in thew new window to be opened. + second_tab = self.open_tab() + self.marionette.switch_to_window(second_tab, focus=True) + second_tab_index = self.get_selected_tab_index() + self.assertNotEqual(second_tab_index, self.selected_tab_index) + + self.open_window(focus=True) + self.assertEqual(self.marionette.current_window_handle, second_tab) + self.assertEqual( + self.marionette.current_chrome_window_handle, self.start_window + ) + self.assertNotEqual(self.get_selected_tab_index(), second_tab_index) + + # Switch to the second tab in the first window, but don't focus it. + self.marionette.switch_to_window(second_tab, focus=False) + self.assertEqual(self.marionette.current_window_handle, second_tab) + self.assertEqual( + self.marionette.current_chrome_window_handle, self.start_window + ) + self.assertNotEqual(self.get_selected_tab_index(), second_tab_index) diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_switch_window_content.py b/testing/marionette/harness/marionette_harness/tests/unit/test_switch_window_content.py new file mode 100644 index 0000000000..8cd7c8ed1e --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_switch_window_content.py @@ -0,0 +1,258 @@ +# This Source Code Form is subject to the terms of the Mozilla ublic +# 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/. + +import sys +from unittest import skipIf + +from six.moves.urllib.parse import quote + +from marionette_driver import By, Wait +from marionette_driver.keys import Keys + +from marionette_harness import ( + MarionetteTestCase, + WindowManagerMixin, +) + + +def inline(doc): + return "data:text/html;charset=utf-8,{}".format(quote(doc)) + + +class TestSwitchToWindowContent(WindowManagerMixin, MarionetteTestCase): + def setUp(self): + super(TestSwitchToWindowContent, self).setUp() + + if self.marionette.session_capabilities["platformName"] == "mac": + self.mod_key = Keys.META + else: + self.mod_key = Keys.CONTROL + + self.selected_tab_index = self.get_selected_tab_index() + + def tearDown(self): + self.close_all_tabs() + + super(TestSwitchToWindowContent, self).tearDown() + + def get_selected_tab_index(self): + with self.marionette.using_context("chrome"): + return self.marionette.execute_script( + """ + const { AppConstants } = ChromeUtils.importESModule( + "resource://gre/modules/AppConstants.sys.mjs" + ); + + let win = null; + + if (AppConstants.MOZ_APP_NAME == "fennec") { + win = Services.wm.getMostRecentWindow("navigator:browser"); + } else { + const { BrowserWindowTracker } = ChromeUtils.importESModule( + "resource:///modules/BrowserWindowTracker.sys.mjs" + ); + win = BrowserWindowTracker.getTopWindow(); + } + + let tabBrowser = null; + + // Fennec + if (win.BrowserApp) { + tabBrowser = win.BrowserApp; + + // Firefox + } else if (win.gBrowser) { + tabBrowser = win.gBrowser; + + } else { + return null; + } + + for (let i = 0; i < tabBrowser.tabs.length; i++) { + if (tabBrowser.tabs[i] == tabBrowser.selectedTab) { + return i; + } + } + """ + ) + + def test_switch_tabs_with_focus_change(self): + new_tab = self.open_tab(focus=True) + self.assertEqual(self.marionette.current_window_handle, self.start_tab) + self.assertNotEqual(self.get_selected_tab_index(), self.selected_tab_index) + + # Switch to new tab first because it is already selected + self.marionette.switch_to_window(new_tab) + self.assertEqual(self.marionette.current_window_handle, new_tab) + self.assertNotEqual(self.get_selected_tab_index(), self.selected_tab_index) + + # Switch to original tab by explicitely setting the focus + self.marionette.switch_to_window(self.start_tab, focus=True) + self.assertEqual(self.marionette.current_window_handle, self.start_tab) + self.assertEqual(self.get_selected_tab_index(), self.selected_tab_index) + + self.marionette.switch_to_window(new_tab) + self.marionette.close() + + self.marionette.switch_to_window(self.start_tab) + self.assertEqual(self.marionette.current_window_handle, self.start_tab) + self.assertEqual(self.get_selected_tab_index(), self.selected_tab_index) + + @skipIf( + sys.platform.startswith("linux"), + "Bug 1557232 - Original window sometimes doesn't receive focus", + ) + def test_switch_tabs_in_different_windows_with_focus_change(self): + new_tab1 = self.open_tab(focus=True) + self.assertEqual(self.marionette.current_window_handle, self.start_tab) + self.assertEqual(self.get_selected_tab_index(), 1) + + # Switch to new tab first which is already selected + self.marionette.switch_to_window(new_tab1) + self.assertEqual(self.marionette.current_window_handle, new_tab1) + self.assertEqual(self.get_selected_tab_index(), 1) + + # Open a new browser window with a single focused tab already focused + with self.marionette.using_context("content"): + new_tab2 = self.open_window(focus=True) + self.assertEqual(self.marionette.current_window_handle, new_tab1) + self.assertEqual(self.get_selected_tab_index(), 0) + + # Switch to that tab + self.marionette.switch_to_window(new_tab2) + self.assertEqual(self.marionette.current_window_handle, new_tab2) + self.assertEqual(self.get_selected_tab_index(), 0) + + # Switch back to the 2nd tab of the original window and setting the focus + self.marionette.switch_to_window(new_tab1, focus=True) + self.assertEqual(self.marionette.current_window_handle, new_tab1) + self.assertEqual(self.get_selected_tab_index(), 1) + + self.marionette.switch_to_window(new_tab2) + self.marionette.close() + + self.marionette.switch_to_window(new_tab1) + self.marionette.close() + + self.marionette.switch_to_window(self.start_tab) + self.assertEqual(self.marionette.current_window_handle, self.start_tab) + self.assertEqual(self.get_selected_tab_index(), self.selected_tab_index) + + def test_switch_tabs_without_focus_change(self): + new_tab = self.open_tab(focus=True) + self.assertEqual(self.marionette.current_window_handle, self.start_tab) + self.assertNotEqual(self.get_selected_tab_index(), self.selected_tab_index) + + # Switch to new tab first because it is already selected + self.marionette.switch_to_window(new_tab) + self.assertEqual(self.marionette.current_window_handle, new_tab) + + # Switch to original tab by explicitely not setting the focus + self.marionette.switch_to_window(self.start_tab, focus=False) + self.assertEqual(self.marionette.current_window_handle, self.start_tab) + self.assertNotEqual(self.get_selected_tab_index(), self.selected_tab_index) + + self.marionette.switch_to_window(new_tab) + self.marionette.close() + + self.marionette.switch_to_window(self.start_tab) + self.assertEqual(self.marionette.current_window_handle, self.start_tab) + self.assertEqual(self.get_selected_tab_index(), self.selected_tab_index) + + def test_switch_to_unloaded_tab(self): + first_page = inline("<p>foo") + second_page = inline("<p>bar") + + self.assertEqual(len(self.marionette.window_handles), 1) + self.marionette.navigate(first_page) + + new_tab = self.open_tab() + self.assertEqual(self.marionette.current_window_handle, self.start_tab) + self.assertEqual(self.get_selected_tab_index(), self.selected_tab_index) + + self.marionette.switch_to_window(new_tab) + self.assertEqual(self.marionette.current_window_handle, new_tab) + self.assertNotEqual(self.get_selected_tab_index(), self.selected_tab_index) + self.marionette.navigate(second_page) + + # The restart will cause the background tab to stay unloaded + self.marionette.restart(in_app=True) + self.assertEqual(len(self.marionette.window_handles), 2) + + # Refresh window handles + window_handles = self.marionette.window_handles + self.assertEqual(len(window_handles), 2) + + current_tab = self.marionette.current_window_handle + [other_tab] = filter(lambda handle: handle != current_tab, window_handles) + + Wait(self.marionette, timeout=5).until( + lambda _: self.marionette.get_url() == second_page, + message="Expected URL in the second tab has been loaded", + ) + + self.marionette.switch_to_window(other_tab) + Wait(self.marionette, timeout=5).until( + lambda _: self.marionette.get_url() == first_page, + message="Expected URL in the first tab has been loaded", + ) + + def test_switch_from_content_to_chrome_window_should_not_change_selected_tab(self): + new_tab = self.open_tab(focus=True) + + self.marionette.switch_to_window(new_tab) + self.assertEqual(self.marionette.current_window_handle, new_tab) + new_tab_index = self.get_selected_tab_index() + + self.marionette.switch_to_window(self.start_window) + self.assertEqual(self.marionette.current_window_handle, new_tab) + self.assertEqual(self.get_selected_tab_index(), new_tab_index) + + def test_switch_to_new_private_browsing_tab(self): + # Test that tabs (browsers) are correctly registered for a newly opened + # private browsing window/tab. This has to also happen without explicitely + # switching to the tab itself before using any commands in content scope. + # + # Note: Not sure why this only affects private browsing windows only. + new_tab = self.open_tab(focus=True) + self.marionette.switch_to_window(new_tab) + + def open_private_browsing_window_firefox(): + with self.marionette.using_context("content"): + self.marionette.find_element(By.ID, "startPrivateBrowsing").click() + + def open_private_browsing_tab_fennec(): + with self.marionette.using_context("content"): + self.marionette.find_element(By.ID, "newPrivateTabLink").click() + + with self.marionette.using_context("content"): + self.marionette.navigate("about:privatebrowsing") + if self.marionette.session_capabilities["browserName"] == "fennec": + new_pb_tab = self.open_tab(open_private_browsing_tab_fennec) + else: + new_pb_tab = self.open_tab(open_private_browsing_window_firefox) + + self.marionette.switch_to_window(new_pb_tab) + self.assertEqual(self.marionette.current_window_handle, new_pb_tab) + + self.marionette.execute_script(" return true; ") + + def test_switch_to_window_after_remoteness_change(self): + # Test that after a remoteness change (and a browsing context swap) + # marionette can still switch to tabs correctly. + with self.marionette.using_context("content"): + # about:robots runs in a different process and will trigger a + # remoteness change with or without fission. + self.marionette.navigate("about:robots") + + about_robots_tab = self.marionette.current_window_handle + + # Open a new tab and switch to it before trying to switch back to the + # initial tab. + tab2 = self.open_tab(focus=True) + self.marionette.switch_to_window(tab2) + self.marionette.close() + + self.marionette.switch_to_window(about_robots_tab) + self.assertEqual(self.marionette.current_window_handle, about_robots_tab) diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_teardown_context_preserved.py b/testing/marionette/harness/marionette_harness/tests/unit/test_teardown_context_preserved.py new file mode 100644 index 0000000000..59653393c8 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_teardown_context_preserved.py @@ -0,0 +1,21 @@ +# 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_harness import MarionetteTestCase, SkipTest + + +class TestTearDownContext(MarionetteTestCase): + def setUp(self): + MarionetteTestCase.setUp(self) + self.marionette.set_context(self.marionette.CONTEXT_CHROME) + + def tearDown(self): + self.assertEqual(self.get_context(), self.marionette.CONTEXT_CHROME) + MarionetteTestCase.tearDown(self) + + def get_context(self): + return self.marionette._send_message("Marionette:GetContext", key="value") + + def test_skipped_teardown_ok(self): + raise SkipTest("This should leave our teardown method in chrome context") diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_text.py b/testing/marionette/harness/marionette_harness/tests/unit/test_text.py new file mode 100644 index 0000000000..28b7bbe762 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_text.py @@ -0,0 +1,26 @@ +# 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.by import By +from marionette_harness import MarionetteTestCase + + +class TestText(MarionetteTestCase): + def test_get_text(self): + test_html = self.marionette.absolute_url("test.html") + self.marionette.navigate(test_html) + l = self.marionette.find_element(By.ID, "mozLink") + self.assertEqual("Click me!", l.text) + + def test_clear_text(self): + test_html = self.marionette.absolute_url("test.html") + self.marionette.navigate(test_html) + l = self.marionette.find_element(By.NAME, "myInput") + self.assertEqual( + "asdf", self.marionette.execute_script("return arguments[0].value;", [l]) + ) + l.clear() + self.assertEqual( + "", self.marionette.execute_script("return arguments[0].value;", [l]) + ) diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_text_chrome.py b/testing/marionette/harness/marionette_harness/tests/unit/test_text_chrome.py new file mode 100644 index 0000000000..f72384876d --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_text_chrome.py @@ -0,0 +1,35 @@ +# 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.by import By +from marionette_harness import MarionetteTestCase, WindowManagerMixin + + +class TestTextChrome(WindowManagerMixin, MarionetteTestCase): + def setUp(self): + super(TestTextChrome, self).setUp() + win = self.open_chrome_window("chrome://remote/content/marionette/test.xhtml") + self.marionette.switch_to_window(win) + + self.marionette.set_context("chrome") + + def tearDown(self): + self.close_all_windows() + + super(TestTextChrome, self).tearDown() + + def test_get_text(self): + elem = self.marionette.find_element(By.ID, "testBox") + self.assertEqual(elem.text, "box") + + def test_clear_text(self): + input = self.marionette.find_element(By.ID, "textInput3") + self.assertEqual( + "test", + self.marionette.execute_script("return arguments[0].value;", [input]), + ) + input.clear() + self.assertEqual( + "", self.marionette.execute_script("return arguments[0].value;", [input]) + ) diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_timeouts.py b/testing/marionette/harness/marionette_harness/tests/unit/test_timeouts.py new file mode 100644 index 0000000000..2a2992cc97 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_timeouts.py @@ -0,0 +1,113 @@ +# 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.by import By +from marionette_driver.errors import ( + MarionetteException, + NoSuchElementException, + ScriptTimeoutException, +) +from marionette_driver.marionette import WebElement + +from marionette_harness import MarionetteTestCase, run_if_manage_instance + + +class TestTimeouts(MarionetteTestCase): + def tearDown(self): + self.marionette.timeout.reset() + MarionetteTestCase.tearDown(self) + + def test_get_timeout_fraction(self): + self.marionette.timeout.script = 0.5 + self.assertEqual(self.marionette.timeout.script, 0.5) + + def test_page_timeout_notdefinetimeout_pass(self): + test_html = self.marionette.absolute_url("test.html") + self.marionette.navigate(test_html) + + def test_page_timeout_fail(self): + self.marionette.timeout.page_load = 0 + test_html = self.marionette.absolute_url("slow") + with self.assertRaises(MarionetteException): + self.marionette.navigate(test_html) + + def test_page_timeout_pass(self): + self.marionette.timeout.page_load = 60 + test_html = self.marionette.absolute_url("test.html") + self.marionette.navigate(test_html) + + def test_search_timeout_notfound_settimeout(self): + test_html = self.marionette.absolute_url("test.html") + self.marionette.navigate(test_html) + self.marionette.timeout.implicit = 1 + with self.assertRaises(NoSuchElementException): + self.marionette.find_element(By.ID, "I'm not on the page") + self.marionette.timeout.implicit = 0 + with self.assertRaises(NoSuchElementException): + self.marionette.find_element(By.ID, "I'm not on the page") + + def test_search_timeout_found_settimeout(self): + test_html = self.marionette.absolute_url("test.html") + self.marionette.navigate(test_html) + button = self.marionette.find_element(By.ID, "createDivButton") + button.click() + self.marionette.timeout.implicit = 8 + self.assertEqual( + WebElement, type(self.marionette.find_element(By.ID, "newDiv")) + ) + + def test_search_timeout_found(self): + test_html = self.marionette.absolute_url("test.html") + self.marionette.navigate(test_html) + button = self.marionette.find_element(By.ID, "createDivButton") + button.click() + self.assertRaises( + NoSuchElementException, self.marionette.find_element, By.ID, "newDiv" + ) + + @run_if_manage_instance("Only runnable if Marionette manages the instance") + def test_reset_timeout(self): + timeouts = [ + getattr(self.marionette.timeout, f) + for f in ( + "implicit", + "page_load", + "script", + ) + ] + + def do_check(callback): + for timeout in timeouts: + timeout = 10000 + self.assertEqual(timeout, 10000) + callback() + for timeout in timeouts: + self.assertNotEqual(timeout, 10000) + + def callback_quit(): + self.marionette.quit() + self.marionette.start_session() + + do_check(self.marionette.restart) + do_check(callback_quit) + + def test_execute_async_timeout_settimeout(self): + test_html = self.marionette.absolute_url("test.html") + self.marionette.navigate(test_html) + self.marionette.timeout.script = 1 + with self.assertRaises(ScriptTimeoutException): + self.marionette.execute_async_script("var x = 1;") + + def test_no_timeout_settimeout(self): + test_html = self.marionette.absolute_url("test.html") + self.marionette.navigate(test_html) + self.marionette.timeout.script = 1 + self.assertTrue( + self.marionette.execute_async_script( + """ + var callback = arguments[arguments.length - 1]; + setTimeout(function() { callback(true); }, 500); + """ + ) + ) diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_title.py b/testing/marionette/harness/marionette_harness/tests/unit/test_title.py new file mode 100644 index 0000000000..1a81291919 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_title.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 six.moves.urllib.parse import quote + +from marionette_harness import MarionetteTestCase + + +def inline(doc): + return "data:text/html;charset=utf-8,{}".format(quote(doc)) + + +class TestTitle(MarionetteTestCase): + def test_basic(self): + self.marionette.navigate(inline("<title>foo</title>")) + self.assertEqual(self.marionette.title, "foo") diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_title_chrome.py b/testing/marionette/harness/marionette_harness/tests/unit/test_title_chrome.py new file mode 100644 index 0000000000..31cc3c3cae --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_title_chrome.py @@ -0,0 +1,37 @@ +# 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_harness import MarionetteTestCase, WindowManagerMixin + + +class TestTitleChrome(WindowManagerMixin, MarionetteTestCase): + def setUp(self): + super(TestTitleChrome, self).setUp() + + self.marionette.set_context("chrome") + + def tearDown(self): + self.close_all_windows() + + super(TestTitleChrome, self).tearDown() + + def test_get_title_xhtml(self): + win = self.open_chrome_window( + "chrome://remote/content/marionette/test_no_xul.xhtml" + ) + self.marionette.switch_to_window(win) + + expected_title = self.marionette.execute_script("return window.document.title;") + self.assertEqual(self.marionette.title, expected_title) + + def test_get_title_xul(self): + win = self.open_chrome_window("chrome://remote/content/marionette/test.xhtml") + self.marionette.switch_to_window(win) + + expected_title = self.marionette.execute_script( + """ + return window.document.documentElement.getAttribute('title'); + """ + ) + self.assertEqual(self.marionette.title, expected_title) diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_transport.py b/testing/marionette/harness/marionette_harness/tests/unit/test_transport.py new file mode 100644 index 0000000000..8b90b9dd03 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_transport.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/. + +import json + +from marionette_driver.transport import Command, Response + +from marionette_harness import MarionetteTestCase + + +get_current_url = ("getCurrentUrl", None) +execute_script = ("executeScript", {"script": "return 42"}) + + +class TestMessageSequencing(MarionetteTestCase): + @property + def last_id(self): + return self.marionette.client.last_id + + @last_id.setter + def last_id(self, new_id): + self.marionette.client.last_id = new_id + + def send(self, name, params): + self.last_id = self.last_id + 1 + cmd = Command(self.last_id, name, params) + self.marionette.client.send(cmd) + return self.last_id + + +class MessageTestCase(MarionetteTestCase): + def assert_attr(self, obj, attr): + self.assertTrue( + hasattr(obj, attr), "object does not have attribute {}".format(attr) + ) + + +class TestCommand(MessageTestCase): + def create(self, msgid="msgid", name="name", params="params"): + return Command(msgid, name, params) + + def test_initialise(self): + cmd = self.create() + self.assert_attr(cmd, "id") + self.assert_attr(cmd, "name") + self.assert_attr(cmd, "params") + self.assertEqual("msgid", cmd.id) + self.assertEqual("name", cmd.name) + self.assertEqual("params", cmd.params) + + def test_stringify(self): + cmd = self.create() + string = str(cmd) + self.assertIn("Command", string) + self.assertIn("id=msgid", string) + self.assertIn("name=name", string) + self.assertIn("params=params", string) + + def test_to_msg(self): + cmd = self.create() + msg = json.loads(cmd.to_msg()) + self.assertEqual(msg[0], Command.TYPE) + self.assertEqual(msg[1], "msgid") + self.assertEqual(msg[2], "name") + self.assertEqual(msg[3], "params") + + def test_from_msg(self): + msg = [Command.TYPE, "msgid", "name", "params"] + cmd = Command.from_msg(msg) + self.assertEqual(msg[1], cmd.id) + self.assertEqual(msg[2], cmd.name) + self.assertEqual(msg[3], cmd.params) + + +class TestResponse(MessageTestCase): + def create(self, msgid="msgid", error="error", result="result"): + return Response(msgid, error, result) + + def test_initialise(self): + resp = self.create() + self.assert_attr(resp, "id") + self.assert_attr(resp, "error") + self.assert_attr(resp, "result") + self.assertEqual("msgid", resp.id) + self.assertEqual("error", resp.error) + self.assertEqual("result", resp.result) + + def test_stringify(self): + resp = self.create() + string = str(resp) + self.assertIn("Response", string) + self.assertIn("id=msgid", string) + self.assertIn("error=error", string) + self.assertIn("result=result", string) + + def test_to_msg(self): + resp = self.create() + msg = json.loads(resp.to_msg()) + self.assertEqual(msg[0], Response.TYPE) + self.assertEqual(msg[1], "msgid") + self.assertEqual(msg[2], "error") + self.assertEqual(msg[3], "result") + + def test_from_msg(self): + msg = [Response.TYPE, "msgid", "error", "result"] + resp = Response.from_msg(msg) + self.assertEqual(msg[1], resp.id) + self.assertEqual(msg[2], resp.error) + self.assertEqual(msg[3], resp.result) diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_typing.py b/testing/marionette/harness/marionette_harness/tests/unit/test_typing.py new file mode 100644 index 0000000000..0476927975 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_typing.py @@ -0,0 +1,374 @@ +# 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 six.moves.urllib.parse import quote + +from marionette_driver.by import By +from marionette_driver.errors import ElementNotInteractableException +from marionette_driver.keys import Keys + +from marionette_harness import MarionetteTestCase, skip + + +def inline(doc): + return "data:text/html;charset=utf-8,{}".format(quote(doc)) + + +class TypingTestCase(MarionetteTestCase): + def setUp(self): + super(TypingTestCase, self).setUp() + + if self.marionette.session_capabilities["platformName"] == "mac": + self.mod_key = Keys.META + else: + self.mod_key = Keys.CONTROL + + +class TestTypingChrome(TypingTestCase): + def setUp(self): + super(TestTypingChrome, self).setUp() + self.marionette.set_context("chrome") + + def test_cut_and_paste_shortcuts(self): + with self.marionette.using_context("content"): + test_html = self.marionette.absolute_url("keyboard.html") + self.marionette.navigate(test_html) + + key_reporter = self.marionette.find_element(By.ID, "keyReporter") + self.assertEqual("", key_reporter.get_property("value")) + key_reporter.send_keys("zyxwvutsr") + self.assertEqual("zyxwvutsr", key_reporter.get_property("value")) + + # select all and cut + key_reporter.send_keys(self.mod_key, "a") + key_reporter.send_keys(self.mod_key, "x") + self.assertEqual("", key_reporter.get_property("value")) + + url_bar = self.marionette.find_element(By.ID, "urlbar-input") + + # Clear contents first + url_bar.send_keys(self.mod_key, "a") + url_bar.send_keys(Keys.BACK_SPACE) + self.assertEqual("", url_bar.get_property("value")) + + url_bar.send_keys(self.mod_key, "v") + self.assertEqual("zyxwvutsr", url_bar.get_property("value")) + + +class TestTypingContent(TypingTestCase): + def test_should_fire_key_press_events(self): + test_html = self.marionette.absolute_url("keyboard.html") + self.marionette.navigate(test_html) + key_reporter = self.marionette.find_element(By.ID, "keyReporter") + key_reporter.send_keys("a") + result = self.marionette.find_element(By.ID, "result") + self.assertTrue("press:" in result.text) + + def test_should_fire_key_down_events(self): + test_html = self.marionette.absolute_url("keyboard.html") + self.marionette.navigate(test_html) + key_reporter = self.marionette.find_element(By.ID, "keyReporter") + key_reporter.send_keys("I") + result = self.marionette.find_element(By.ID, "result") + self.assertTrue("down" in result.text) + + def test_should_fire_key_up_events(self): + test_html = self.marionette.absolute_url("keyboard.html") + self.marionette.navigate(test_html) + + key_reporter = self.marionette.find_element(By.ID, "keyReporter") + key_reporter.send_keys("a") + result = self.marionette.find_element(By.ID, "result") + self.assertTrue("up:" in result.text) + + def test_should_type_lowercase_characters(self): + test_html = self.marionette.absolute_url("keyboard.html") + self.marionette.navigate(test_html) + + key_reporter = self.marionette.find_element(By.ID, "keyReporter") + key_reporter.send_keys("abc def") + self.assertEqual("abc def", key_reporter.get_property("value")) + + def test_should_type_uppercase_characters(self): + test_html = self.marionette.absolute_url("keyboard.html") + self.marionette.navigate(test_html) + + key_reporter = self.marionette.find_element(By.ID, "keyReporter") + key_reporter.send_keys("ABC DEF") + self.assertEqual("ABC DEF", key_reporter.get_property("value")) + + def test_cut_and_paste_shortcuts(self): + test_html = self.marionette.absolute_url("keyboard.html") + self.marionette.navigate(test_html) + + key_reporter = self.marionette.find_element(By.ID, "keyReporter") + self.assertEqual("", key_reporter.get_property("value")) + key_reporter.send_keys("zyxwvutsr") + self.assertEqual("zyxwvutsr", key_reporter.get_property("value")) + + # select all and cut + key_reporter.send_keys(self.mod_key, "a") + key_reporter.send_keys(self.mod_key, "x") + self.assertEqual("", key_reporter.get_property("value")) + + key_reporter.send_keys(self.mod_key, "v") + self.assertEqual("zyxwvutsr", key_reporter.get_property("value")) + + def test_should_type_a_quote_characters(self): + test_html = self.marionette.absolute_url("keyboard.html") + self.marionette.navigate(test_html) + + key_reporter = self.marionette.find_element(By.ID, "keyReporter") + key_reporter.send_keys('"') + self.assertEqual('"', key_reporter.get_property("value")) + + def test_should_type_an_at_character(self): + test_html = self.marionette.absolute_url("keyboard.html") + self.marionette.navigate(test_html) + + key_reporter = self.marionette.find_element(By.ID, "keyReporter") + key_reporter.send_keys("@") + self.assertEqual("@", key_reporter.get_property("value")) + + def test_should_type_a_mix_of_upper_and_lower_case_character(self): + test_html = self.marionette.absolute_url("keyboard.html") + self.marionette.navigate(test_html) + + key_reporter = self.marionette.find_element(By.ID, "keyReporter") + key_reporter.send_keys("me@eXample.com") + self.assertEqual("me@eXample.com", key_reporter.get_property("value")) + + def test_arrow_keys_are_not_printable(self): + test_html = self.marionette.absolute_url("keyboard.html") + self.marionette.navigate(test_html) + + key_reporter = self.marionette.find_element(By.ID, "keyReporter") + key_reporter.send_keys(Keys.ARROW_LEFT) + self.assertEqual("", key_reporter.get_property("value")) + + def test_will_simulate_a_key_up_when_entering_text_into_input_elements(self): + test_html = self.marionette.absolute_url("keyboard.html") + self.marionette.navigate(test_html) + + element = self.marionette.find_element(By.ID, "keyUp") + element.send_keys("I like cheese") + result = self.marionette.find_element(By.ID, "result") + self.assertEqual(result.text, "I like cheese") + + def test_will_simulate_a_key_down_when_entering_text_into_input_elements(self): + test_html = self.marionette.absolute_url("keyboard.html") + self.marionette.navigate(test_html) + + element = self.marionette.find_element(By.ID, "keyDown") + element.send_keys("I like cheese") + result = self.marionette.find_element(By.ID, "result") + # Because the key down gets the result before the input element is + # filled, we're a letter short here + self.assertEqual(result.text, "I like chees") + + def test_will_simulate_a_key_press_when_entering_text_into_input_elements(self): + test_html = self.marionette.absolute_url("keyboard.html") + self.marionette.navigate(test_html) + + element = self.marionette.find_element(By.ID, "keyPress") + element.send_keys("I like cheese") + result = self.marionette.find_element(By.ID, "result") + # Because the key down gets the result before the input element is + # filled, we're a letter short here + self.assertEqual(result.text, "I like chees") + + def test_will_simulate_a_keyup_when_entering_text_into_textareas(self): + test_html = self.marionette.absolute_url("keyboard.html") + self.marionette.navigate(test_html) + + element = self.marionette.find_element(By.ID, "keyUpArea") + element.send_keys("I like cheese") + result = self.marionette.find_element(By.ID, "result") + self.assertEqual("I like cheese", result.text) + + def test_will_simulate_a_keydown_when_entering_text_into_textareas(self): + test_html = self.marionette.absolute_url("keyboard.html") + self.marionette.navigate(test_html) + + element = self.marionette.find_element(By.ID, "keyDownArea") + element.send_keys("I like cheese") + result = self.marionette.find_element(By.ID, "result") + # Because the key down gets the result before the input element is + # filled, we're a letter short here + self.assertEqual(result.text, "I like chees") + + def test_will_simulate_a_keypress_when_entering_text_into_textareas(self): + test_html = self.marionette.absolute_url("keyboard.html") + self.marionette.navigate(test_html) + + element = self.marionette.find_element(By.ID, "keyPressArea") + element.send_keys("I like cheese") + result = self.marionette.find_element(By.ID, "result") + # Because the key down gets the result before the input element is + # filled, we're a letter short here + self.assertEqual(result.text, "I like chees") + + def test_should_report_key_code_of_arrow_keys_up_down_events(self): + test_html = self.marionette.absolute_url("keyboard.html") + self.marionette.navigate(test_html) + + result = self.marionette.find_element(By.ID, "result") + element = self.marionette.find_element(By.ID, "keyReporter") + + element.send_keys(Keys.ARROW_DOWN) + + self.assertIn("down: 40", result.text.strip()) + self.assertIn("up: 40", result.text.strip()) + + element.send_keys(Keys.ARROW_UP) + self.assertIn("down: 38", result.text.strip()) + self.assertIn("up: 38", result.text.strip()) + + element.send_keys(Keys.ARROW_LEFT) + self.assertIn("down: 37", result.text.strip()) + self.assertIn("up: 37", result.text.strip()) + + element.send_keys(Keys.ARROW_RIGHT) + self.assertIn("down: 39", result.text.strip()) + self.assertIn("up: 39", result.text.strip()) + + # And leave no rubbish/printable keys in the "keyReporter" + self.assertEqual("", element.get_property("value")) + + @skip("Reenable in Bug 1068728") + def test_numeric_shift_keys(self): + test_html = self.marionette.absolute_url("keyboard.html") + self.marionette.navigate(test_html) + + result = self.marionette.find_element(By.ID, "result") + element = self.marionette.find_element(By.ID, "keyReporter") + numeric_shifts_etc = '~!@#$%^&*()_+{}:i"<>?|END~' + element.send_keys(numeric_shifts_etc) + self.assertEqual(numeric_shifts_etc, element.get_property("value")) + self.assertIn(" up: 16", result.text.strip()) + + def test_numeric_non_shift_keys(self): + test_html = self.marionette.absolute_url("keyboard.html") + self.marionette.navigate(test_html) + element = self.marionette.find_element(By.ID, "keyReporter") + numeric_line_chars_non_shifted = "`1234567890-=[]\\,.'/42" + element.send_keys(numeric_line_chars_non_shifted) + self.assertEqual(numeric_line_chars_non_shifted, element.get_property("value")) + + def test_lowercase_alpha_keys(self): + test_html = self.marionette.absolute_url("keyboard.html") + self.marionette.navigate(test_html) + + element = self.marionette.find_element(By.ID, "keyReporter") + lower_alphas = "abcdefghijklmnopqrstuvwxyz" + element.send_keys(lower_alphas) + self.assertEqual(lower_alphas, element.get_property("value")) + + @skip("Reenable in Bug 1068735") + def test_uppercase_alpha_keys(self): + test_html = self.marionette.absolute_url("keyboard.html") + self.marionette.navigate(test_html) + + result = self.marionette.find_element(By.ID, "result") + element = self.marionette.find_element(By.ID, "keyReporter") + upper_alphas = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + element.send_keys(upper_alphas) + self.assertEqual(upper_alphas, element.get_property("value")) + self.assertIn(" up: 16", result.text.strip()) + + @skip("Reenable in Bug 1068726") + def test_all_printable_keys(self): + test_html = self.marionette.absolute_url("keyboard.html") + self.marionette.navigate(test_html) + + result = self.marionette.find_element(By.ID, "result") + element = self.marionette.find_element(By.ID, "keyReporter") + all_printable = ( + "!\"#$%&'()*+,-./0123456789:<=>?@ " + "ABCDEFGHIJKLMNOPQRSTUVWXYZ [\\]^_`" + "abcdefghijklmnopqrstuvwxyz{|}~" + ) + element.send_keys(all_printable) + + self.assertTrue(all_printable, element.get_property("value")) + self.assertIn(" up: 16", result.text.strip()) + + @skip("Reenable in Bug 1068733") + def test_special_space_keys(self): + test_html = self.marionette.absolute_url("keyboard.html") + self.marionette.navigate(test_html) + + element = self.marionette.find_element(By.ID, "keyReporter") + element.send_keys("abcd" + Keys.SPACE + "fgh" + Keys.SPACE + "ij") + self.assertEqual("abcd fgh ij", element.get_property("value")) + + def test_should_type_an_integer(self): + test_html = self.marionette.absolute_url("keyboard.html") + self.marionette.navigate(test_html) + + element = self.marionette.find_element(By.ID, "keyReporter") + element.send_keys(1234) + self.assertEqual("1234", element.get_property("value")) + + def test_should_send_keys_to_elements_without_the_value_attribute(self): + test_html = self.marionette.absolute_url("keyboard.html") + self.marionette.navigate(test_html) + + # If we don't get an error below we are good + self.marionette.find_element(By.TAG_NAME, "body").send_keys("foo") + + def test_appends_to_input_text(self): + self.marionette.navigate(inline("<input>")) + el = self.marionette.find_element(By.TAG_NAME, "input") + el.send_keys("foo") + el.send_keys("bar") + self.assertEqual("foobar", el.get_property("value")) + + def test_appends_to_textarea(self): + self.marionette.navigate(inline("<textarea></textarea>")) + textarea = self.marionette.find_element(By.TAG_NAME, "textarea") + textarea.send_keys("foo") + textarea.send_keys("bar") + self.assertEqual("foobar", textarea.get_property("value")) + + def test_send_keys_to_type_input(self): + test_html = self.marionette.absolute_url("html5/test_html_inputs.html") + self.marionette.navigate(test_html) + + num_input = self.marionette.find_element(By.ID, "number") + self.assertEqual( + "", self.marionette.execute_script("return arguments[0].value", [num_input]) + ) + num_input.send_keys("1234") + self.assertEqual( + "1234", + self.marionette.execute_script("return arguments[0].value", [num_input]), + ) + + def test_insert_keys(self): + l = self.marionette.find_element(By.ID, "change") + l.send_keys("abde") + self.assertEqual( + "abde", self.marionette.execute_script("return arguments[0].value;", [l]) + ) + + # Set caret position to the middle of the input text. + self.marionette.execute_script( + """var el = arguments[0]; + el.selectionStart = el.selectionEnd = el.value.length / 2;""", + script_args=[l], + ) + + l.send_keys("c") + self.assertEqual( + "abcde", self.marionette.execute_script("return arguments[0].value;", [l]) + ) + + +class TestTypingContentLegacy(TestTypingContent): + def setUp(self): + super(TestTypingContent, self).setUp() + + self.marionette.delete_session() + self.marionette.start_session({"moz:webdriverClick": False}) diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_unhandled_prompt_behavior.py b/testing/marionette/harness/marionette_harness/tests/unit/test_unhandled_prompt_behavior.py new file mode 100644 index 0000000000..d68c0a8468 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_unhandled_prompt_behavior.py @@ -0,0 +1,126 @@ +from marionette_driver import errors +from marionette_driver.marionette import Alert +from marionette_driver.wait import Wait +from marionette_harness import MarionetteTestCase, parameterized + + +class TestUnhandledPromptBehavior(MarionetteTestCase): + def setUp(self): + super(TestUnhandledPromptBehavior, self).setUp() + + self.marionette.delete_session() + + def tearDown(self): + # Ensure to close a possible remaining tab modal dialog + try: + alert = self.marionette.switch_to_alert() + alert.dismiss() + + Wait(self.marionette).until(lambda _: not self.alert_present) + except errors.NoAlertPresentException: + pass + + super(TestUnhandledPromptBehavior, self).tearDown() + + @property + def alert_present(self): + try: + Alert(self.marionette).text + return True + except errors.NoAlertPresentException: + return False + + def perform_user_prompt_check( + self, + prompt_type, + text, + expected_result, + expected_close=True, + expected_notify=True, + ): + if prompt_type not in ["alert", "confirm", "prompt"]: + raise TypeError("Invalid dialog type: {}".format(prompt_type)) + + # No need to call resolve() because opening a prompt stops the script + self.marionette.execute_async_script( + """ + window.return_value = null; + window.return_value = window[arguments[0]](arguments[1]); + """, + script_args=(prompt_type, text), + ) + + if expected_notify: + with self.assertRaises(errors.UnexpectedAlertOpen): + self.marionette.title + # Bug 1469752 - WebDriverError misses optional data property + # self.assertEqual(ex.data.text, text) + else: + self.marionette.title + + self.assertEqual(self.alert_present, not expected_close) + + # Close an expected left-over user prompt + if not expected_close: + alert = self.marionette.switch_to_alert() + alert.dismiss() + + else: + prompt_result = self.marionette.execute_script( + "return window.return_value", new_sandbox=False + ) + self.assertEqual(prompt_result, expected_result) + + @parameterized("alert", "alert", None) + @parameterized("confirm", "confirm", True) + @parameterized("prompt", "prompt", "") + def test_accept(self, prompt_type, result): + self.marionette.start_session({"unhandledPromptBehavior": "accept"}) + self.perform_user_prompt_check( + prompt_type, "foo {}".format(prompt_type), result, expected_notify=False + ) + + @parameterized("alert", "alert", None) + @parameterized("confirm", "confirm", True) + @parameterized("prompt", "prompt", "") + def test_accept_and_notify(self, prompt_type, result): + self.marionette.start_session({"unhandledPromptBehavior": "accept and notify"}) + self.perform_user_prompt_check( + prompt_type, "foo {}".format(prompt_type), result + ) + + @parameterized("alert", "alert", None) + @parameterized("confirm", "confirm", False) + @parameterized("prompt", "prompt", None) + def test_dismiss(self, prompt_type, result): + self.marionette.start_session({"unhandledPromptBehavior": "dismiss"}) + self.perform_user_prompt_check( + prompt_type, "foo {}".format(prompt_type), result, expected_notify=False + ) + + @parameterized("alert", "alert", None) + @parameterized("confirm", "confirm", False) + @parameterized("prompt", "prompt", None) + def test_dismiss_and_notify(self, prompt_type, result): + self.marionette.start_session({"unhandledPromptBehavior": "dismiss and notify"}) + self.perform_user_prompt_check( + prompt_type, "foo {}".format(prompt_type), result + ) + + @parameterized("alert", "alert", None) + @parameterized("confirm", "confirm", None) + @parameterized("prompt", "prompt", None) + def test_ignore(self, prompt_type, result): + self.marionette.start_session({"unhandledPromptBehavior": "ignore"}) + self.perform_user_prompt_check( + prompt_type, "foo {}".format(prompt_type), result, expected_close=False + ) + + @parameterized("alert", "alert", None) + @parameterized("confirm", "confirm", False) + @parameterized("prompt", "prompt", None) + def test_default(self, prompt_type, result): + self.marionette.start_session({}) + self.perform_user_prompt_check( + prompt_type, "foo {}".format(prompt_type), result + ) diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_visibility.py b/testing/marionette/harness/marionette_harness/tests/unit/test_visibility.py new file mode 100644 index 0000000000..8b4bc28061 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_visibility.py @@ -0,0 +1,175 @@ +# 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 six.moves.urllib.parse import quote + +from marionette_driver.by import By + +from marionette_harness import MarionetteTestCase + + +def inline(doc): + return "data:text/html;charset=utf-8,{}".format(quote(doc)) + + +def element_direction_doc(direction): + return inline( + """ + <meta name="viewport" content="initial-scale=1,width=device-width"> + <style> + .element{{ + position: absolute; + {}: -50px; + background_color: red; + width: 100px; + height: 100px; + }} + </style> + <div class='element'></div>""".format( + direction + ) + ) + + +class TestVisibility(MarionetteTestCase): + def testShouldAllowTheUserToTellIfAnElementIsDisplayedOrNot(self): + test_html = self.marionette.absolute_url("visibility.html") + self.marionette.navigate(test_html) + + self.assertTrue(self.marionette.find_element(By.ID, "displayed").is_displayed()) + self.assertFalse(self.marionette.find_element(By.ID, "none").is_displayed()) + self.assertFalse( + self.marionette.find_element(By.ID, "suppressedParagraph").is_displayed() + ) + self.assertFalse(self.marionette.find_element(By.ID, "hidden").is_displayed()) + + def testVisibilityShouldTakeIntoAccountParentVisibility(self): + test_html = self.marionette.absolute_url("visibility.html") + self.marionette.navigate(test_html) + + childDiv = self.marionette.find_element(By.ID, "hiddenchild") + hiddenLink = self.marionette.find_element(By.ID, "hiddenlink") + + self.assertFalse(childDiv.is_displayed()) + self.assertFalse(hiddenLink.is_displayed()) + + def testShouldCountElementsAsVisibleIfStylePropertyHasBeenSet(self): + test_html = self.marionette.absolute_url("visibility.html") + self.marionette.navigate(test_html) + shown = self.marionette.find_element(By.ID, "visibleSubElement") + self.assertTrue(shown.is_displayed()) + + def testShouldModifyTheVisibilityOfAnElementDynamically(self): + test_html = self.marionette.absolute_url("visibility.html") + self.marionette.navigate(test_html) + element = self.marionette.find_element(By.ID, "hideMe") + self.assertTrue(element.is_displayed()) + element.click() + self.assertFalse(element.is_displayed()) + + def testHiddenInputElementsAreNeverVisible(self): + test_html = self.marionette.absolute_url("visibility.html") + self.marionette.navigate(test_html) + + shown = self.marionette.find_element(By.NAME, "hidden") + + self.assertFalse(shown.is_displayed()) + + def test_elements_not_displayed_with_negative_transform(self): + self.marionette.navigate( + inline( + """ + <div id="y" style="transform: translateY(-200%);">hidden</div> + <div id="x" style="transform: translateX(-200%);">hidden</div> + """ + ) + ) + + element_x = self.marionette.find_element(By.ID, "x") + self.assertFalse(element_x.is_displayed()) + element_y = self.marionette.find_element(By.ID, "y") + self.assertFalse(element_y.is_displayed()) + + def test_elements_not_displayed_with_parents_having_negative_transform(self): + self.marionette.navigate( + inline( + """ + <div style="transform: translateY(-200%);"><p id="y">hidden</p></div> + <div style="transform: translateX(-200%);"><p id="x">hidden</p></div> + """ + ) + ) + + element_x = self.marionette.find_element(By.ID, "x") + self.assertFalse(element_x.is_displayed()) + element_y = self.marionette.find_element(By.ID, "y") + self.assertFalse(element_y.is_displayed()) + + def test_element_displayed_with_zero_transform(self): + self.marionette.navigate( + inline( + """ + <div style="transform: translate(0px, 0px);">not hidden</div> + """ + ) + ) + element = self.marionette.find_element(By.TAG_NAME, "div") + self.assertTrue(element.is_displayed()) + + def test_element_displayed_with_negative_transform_but_in_viewport(self): + self.marionette.navigate( + inline( + """ + <div style="margin-top: 1em; transform: translateY(-75%);">not hidden</div> + """ + ) + ) + element = self.marionette.find_element(By.TAG_NAME, "div") + self.assertTrue(element.is_displayed()) + + def testShouldSayElementIsInvisibleWhenOverflowXIsHiddenAndOutOfViewport(self): + test_html = self.marionette.absolute_url("bug814037.html") + self.marionette.navigate(test_html) + overflow_x = self.marionette.find_element(By.ID, "assertMe2") + self.assertFalse(overflow_x.is_displayed()) + + def testShouldShowElementNotVisibleWithHiddenAttribute(self): + self.marionette.navigate( + inline( + """ + <p hidden>foo</p> + """ + ) + ) + singleHidden = self.marionette.find_element(By.TAG_NAME, "p") + self.assertFalse(singleHidden.is_displayed()) + + def testShouldShowElementNotVisibleWhenParentElementHasHiddenAttribute(self): + self.marionette.navigate( + inline( + """ + <div hidden> + <p>foo</p> + </div> + """ + ) + ) + child = self.marionette.find_element(By.TAG_NAME, "p") + self.assertFalse(child.is_displayed()) + + def testShouldClickOnELementPartiallyOffLeft(self): + test_html = self.marionette.navigate(element_direction_doc("left")) + self.marionette.find_element(By.CSS_SELECTOR, ".element").click() + + def testShouldClickOnELementPartiallyOffRight(self): + test_html = self.marionette.navigate(element_direction_doc("right")) + self.marionette.find_element(By.CSS_SELECTOR, ".element").click() + + def testShouldClickOnELementPartiallyOffTop(self): + test_html = self.marionette.navigate(element_direction_doc("top")) + self.marionette.find_element(By.CSS_SELECTOR, ".element").click() + + def testShouldClickOnELementPartiallyOffBottom(self): + test_html = self.marionette.navigate(element_direction_doc("bottom")) + self.marionette.find_element(By.CSS_SELECTOR, ".element").click() diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_wait.py b/testing/marionette/harness/marionette_harness/tests/unit/test_wait.py new file mode 100644 index 0000000000..7a8f73bd27 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_wait.py @@ -0,0 +1,347 @@ +# 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/. + +import sys +import time + +import six + +from marionette_driver import errors, wait +from marionette_driver.wait import Wait + +from marionette_harness import MarionetteTestCase + + +class TickingClock(object): + def __init__(self, incr=1): + self.ticks = 0 + self.increment = incr + + def sleep(self, dur=None): + dur = dur if dur is not None else self.increment + self.ticks += dur + + @property + def now(self): + return self.ticks + + +class SequenceClock(object): + def __init__(self, times): + self.times = times + self.i = 0 + + @property + def now(self): + if len(self.times) > self.i: + self.i += 1 + return self.times[self.i - 1] + + def sleep(self, dur): + pass + + +class MockMarionette(object): + def __init__(self): + self.waited = 0 + + def exception(self, e=None, wait=1): + self.wait() + if self.waited == wait: + if e is None: + e = Exception + raise e + + def true(self, wait=1): + self.wait() + if self.waited == wait: + return True + return None + + def false(self, wait=1): + self.wait() + return False + + def none(self, wait=1): + self.wait() + return None + + def value(self, value, wait=1): + self.wait() + if self.waited == wait: + return value + return None + + def wait(self): + self.waited += 1 + + +def at_third_attempt(clock, end): + return clock.now == 2 + + +def now(clock, end): + return True + + +class SystemClockTest(MarionetteTestCase): + def setUp(self): + super(SystemClockTest, self).setUp() + self.clock = wait.SystemClock() + + def test_construction_initializes_time(self): + self.assertEqual(self.clock._time, time) + + def test_sleep(self): + start = time.time() + self.clock.sleep(0.1) + end = time.time() - start + self.assertGreater(end, 0) + + def test_time_now(self): + self.assertIsNotNone(self.clock.now) + + +class FormalWaitTest(MarionetteTestCase): + def setUp(self): + super(FormalWaitTest, self).setUp() + self.m = MockMarionette() + self.m.timeout = 123 + + def test_construction_with_custom_timeout(self): + wt = Wait(self.m, timeout=42) + self.assertEqual(wt.timeout, 42) + + def test_construction_with_custom_interval(self): + wt = Wait(self.m, interval=42) + self.assertEqual(wt.interval, 42) + + def test_construction_with_custom_clock(self): + c = TickingClock(1) + wt = Wait(self.m, clock=c) + self.assertEqual(wt.clock, c) + + def test_construction_with_custom_exception(self): + wt = Wait(self.m, ignored_exceptions=Exception) + self.assertIn(Exception, wt.exceptions) + self.assertEqual(len(wt.exceptions), 1) + + def test_construction_with_custom_exception_list(self): + exc = [Exception, ValueError] + wt = Wait(self.m, ignored_exceptions=exc) + for e in exc: + self.assertIn(e, wt.exceptions) + self.assertEqual(len(wt.exceptions), len(exc)) + + def test_construction_with_custom_exception_tuple(self): + exc = (Exception, ValueError) + wt = Wait(self.m, ignored_exceptions=exc) + for e in exc: + self.assertIn(e, wt.exceptions) + self.assertEqual(len(wt.exceptions), len(exc)) + + def test_duplicate_exceptions(self): + wt = Wait(self.m, ignored_exceptions=[Exception, Exception]) + self.assertIn(Exception, wt.exceptions) + self.assertEqual(len(wt.exceptions), 1) + + def test_default_timeout(self): + self.assertEqual(wait.DEFAULT_TIMEOUT, 5) + + def test_default_interval(self): + self.assertEqual(wait.DEFAULT_INTERVAL, 0.1) + + def test_end_property(self): + wt = Wait(self.m) + self.assertIsNotNone(wt.end) + + def test_marionette_property(self): + wt = Wait(self.m) + self.assertEqual(wt.marionette, self.m) + + def test_clock_property(self): + wt = Wait(self.m) + self.assertIsInstance(wt.clock, wait.SystemClock) + + def test_timeout_uses_default_if_marionette_timeout_is_none(self): + self.m.timeout = None + wt = Wait(self.m) + self.assertEqual(wt.timeout, wait.DEFAULT_TIMEOUT) + + +class PredicatesTest(MarionetteTestCase): + def test_until(self): + c = wait.SystemClock() + self.assertFalse(wait.until_pred(c, six.MAXSIZE)) + self.assertTrue(wait.until_pred(c, 0)) + + +class WaitUntilTest(MarionetteTestCase): + def setUp(self): + super(WaitUntilTest, self).setUp() + + self.m = MockMarionette() + self.clock = TickingClock() + self.wt = Wait(self.m, timeout=10, interval=1, clock=self.clock) + + def test_true(self): + r = self.wt.until(lambda x: x.true()) + self.assertTrue(r) + self.assertEqual(self.clock.ticks, 0) + + def test_true_within_timeout(self): + r = self.wt.until(lambda x: x.true(wait=5)) + self.assertTrue(r) + self.assertEqual(self.clock.ticks, 4) + + def test_timeout(self): + with self.assertRaises(errors.TimeoutException): + r = self.wt.until(lambda x: x.true(wait=15)) + self.assertEqual(self.clock.ticks, 10) + + def test_exception_raises_immediately(self): + with self.assertRaises(TypeError): + self.wt.until(lambda x: x.exception(e=TypeError)) + self.assertEqual(self.clock.ticks, 0) + + def test_ignored_exception(self): + self.wt.exceptions = (TypeError,) + with self.assertRaises(errors.TimeoutException): + self.wt.until(lambda x: x.exception(e=TypeError)) + + def test_ignored_exception_wrapped_in_timeoutexception(self): + self.wt.exceptions = (TypeError,) + + exc = None + try: + self.wt.until(lambda x: x.exception(e=TypeError)) + except Exception as e: + exc = e + + s = str(exc) + self.assertIsNotNone(exc) + self.assertIsInstance(exc, errors.TimeoutException) + self.assertIn(", caused by {0!r}".format(TypeError), s) + self.assertIn("self.wt.until(lambda x: x.exception(e=TypeError))", s) + + def test_ignored_exception_after_timeout_is_not_raised(self): + with self.assertRaises(errors.TimeoutException): + r = self.wt.until(lambda x: x.exception(wait=15)) + self.assertEqual(self.clock.ticks, 10) + + def test_keyboard_interrupt(self): + with self.assertRaises(KeyboardInterrupt): + self.wt.until(lambda x: x.exception(e=KeyboardInterrupt)) + + def test_system_exit(self): + with self.assertRaises(SystemExit): + self.wt.until(lambda x: x.exception(SystemExit)) + + def test_true_condition_returns_immediately(self): + r = self.wt.until(lambda x: x.true()) + self.assertIsInstance(r, bool) + self.assertTrue(r) + self.assertEqual(self.clock.ticks, 0) + + def test_value(self): + r = self.wt.until(lambda x: "foo") + self.assertEqual(r, "foo") + self.assertEqual(self.clock.ticks, 0) + + def test_custom_predicate(self): + r = self.wt.until(lambda x: x.true(wait=2), is_true=at_third_attempt) + self.assertTrue(r) + self.assertEqual(self.clock.ticks, 1) + + def test_custom_predicate_times_out(self): + with self.assertRaises(errors.TimeoutException): + self.wt.until(lambda x: x.true(wait=4), is_true=at_third_attempt) + + self.assertEqual(self.clock.ticks, 2) + + def test_timeout_elapsed_duration(self): + with self.assertRaisesRegexp( + errors.TimeoutException, "Timed out after 2.0 seconds" + ): + self.wt.until(lambda x: x.true(wait=4), is_true=at_third_attempt) + + def test_timeout_elapsed_rounding(self): + wt = Wait(self.m, clock=SequenceClock([1, 0.01, 1]), timeout=0) + with self.assertRaisesRegexp( + errors.TimeoutException, "Timed out after 1.0 seconds" + ): + wt.until(lambda x: x.true(), is_true=now) + + def test_timeout_elapsed_interval_by_delayed_condition_return(self): + def callback(mn): + self.clock.sleep(11) + return mn.false() + + with self.assertRaisesRegexp( + errors.TimeoutException, "Timed out after 11.0 seconds" + ): + self.wt.until(callback) + # With a delayed conditional return > timeout, only 1 iteration is + # possible + self.assertEqual(self.m.waited, 1) + + def test_timeout_with_delayed_condition_return(self): + def callback(mn): + self.clock.sleep(0.5) + return mn.false() + + with self.assertRaisesRegexp( + errors.TimeoutException, "Timed out after 10.0 seconds" + ): + self.wt.until(callback) + # With a delayed conditional return < interval, 10 iterations should be + # possible + self.assertEqual(self.m.waited, 10) + + def test_timeout_interval_shorter_than_delayed_condition_return(self): + def callback(mn): + self.clock.sleep(2) + return mn.false() + + with self.assertRaisesRegexp( + errors.TimeoutException, "Timed out after 10.0 seconds" + ): + self.wt.until(callback) + # With a delayed return of the conditional which takes twice that long than the interval, + # half of the iterations should be possible + self.assertEqual(self.m.waited, 5) + + def test_message(self): + self.wt.exceptions = (TypeError,) + exc = None + try: + self.wt.until(lambda x: x.exception(e=TypeError), message="hooba") + except errors.TimeoutException as e: + exc = e + + result = str(exc) + self.assertIn("seconds with message: hooba, caused by", result) + + def test_no_message(self): + self.wt.exceptions = (TypeError,) + exc = None + try: + self.wt.until(lambda x: x.exception(e=TypeError), message="") + except errors.TimeoutException as e: + exc = e + + result = str(exc) + self.assertIn("seconds, caused by", result) + + def test_message_has_none_as_its_value(self): + self.wt.exceptions = (TypeError,) + exc = None + try: + self.wt.until(False, None, None) + except errors.TimeoutException as e: + exc = e + + result = str(exc) + self.assertNotIn("with message:", result) + self.assertNotIn("secondsNone", result) diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_window_close_chrome.py b/testing/marionette/harness/marionette_harness/tests/unit/test_window_close_chrome.py new file mode 100644 index 0000000000..6f7bff3b6c --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_window_close_chrome.py @@ -0,0 +1,73 @@ +# 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_harness import MarionetteTestCase, WindowManagerMixin + + +class TestCloseWindow(WindowManagerMixin, MarionetteTestCase): + def setUp(self): + super(TestCloseWindow, self).setUp() + + self.marionette.set_context("chrome") + + def tearDown(self): + self.close_all_windows() + self.close_all_tabs() + + super(TestCloseWindow, self).tearDown() + + def test_close_chrome_window_for_browser_window(self): + new_window = self.open_window() + self.marionette.switch_to_window(new_window) + + self.assertNotIn(new_window, self.marionette.window_handles) + chrome_window_handles = self.marionette.close_chrome_window() + self.assertNotIn(new_window, chrome_window_handles) + self.assertListEqual(self.start_windows, chrome_window_handles) + self.assertNotIn(new_window, self.marionette.window_handles) + + def test_close_chrome_window_for_non_browser_window(self): + win = self.open_chrome_window("chrome://remote/content/marionette/test.xhtml") + self.marionette.switch_to_window(win) + + self.assertIn(win, self.marionette.chrome_window_handles) + self.assertNotIn(win, self.marionette.window_handles) + chrome_window_handles = self.marionette.close_chrome_window() + self.assertNotIn(win, chrome_window_handles) + self.assertListEqual(self.start_windows, chrome_window_handles) + self.assertNotIn(win, self.marionette.chrome_window_handles) + + def test_close_chrome_window_for_last_open_window(self): + self.close_all_windows() + + self.assertListEqual([], self.marionette.close_chrome_window()) + self.assertListEqual([self.start_tab], self.marionette.window_handles) + self.assertListEqual([self.start_window], self.marionette.chrome_window_handles) + self.assertIsNotNone(self.marionette.session) + + def test_close_window_for_browser_tab(self): + new_tab = self.open_tab() + self.marionette.switch_to_window(new_tab) + + window_handles = self.marionette.close() + self.assertNotIn(new_tab, window_handles) + self.assertListEqual(self.start_tabs, window_handles) + + def test_close_window_for_browser_window_with_single_tab(self): + new_window = self.open_window() + self.marionette.switch_to_window(new_window) + + self.assertEqual(len(self.start_tabs) + 1, len(self.marionette.window_handles)) + window_handles = self.marionette.close() + self.assertNotIn(new_window, window_handles) + self.assertListEqual(self.start_tabs, window_handles) + self.assertListEqual(self.start_windows, self.marionette.chrome_window_handles) + + def test_close_window_for_last_open_tab(self): + self.close_all_tabs() + + self.assertListEqual([], self.marionette.close()) + self.assertListEqual([self.start_tab], self.marionette.window_handles) + self.assertListEqual([self.start_window], self.marionette.chrome_window_handles) + self.assertIsNotNone(self.marionette.session) diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_window_close_content.py b/testing/marionette/harness/marionette_harness/tests/unit/test_window_close_content.py new file mode 100644 index 0000000000..fe883baf6b --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_window_close_content.py @@ -0,0 +1,109 @@ +# 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 six.moves.urllib.parse import quote + +from marionette_driver.by import By +from marionette_harness import MarionetteTestCase, WindowManagerMixin + + +def inline(doc): + return "data:text/html;charset=utf-8,{}".format(quote(doc)) + + +class TestCloseWindow(WindowManagerMixin, MarionetteTestCase): + def tearDown(self): + self.close_all_windows() + self.close_all_tabs() + + super(TestCloseWindow, self).tearDown() + + def test_close_chrome_window_for_browser_window(self): + with self.marionette.using_context("chrome"): + new_window = self.open_window() + self.marionette.switch_to_window(new_window) + + self.assertIn(new_window, self.marionette.chrome_window_handles) + chrome_window_handles = self.marionette.close_chrome_window() + self.assertNotIn(new_window, chrome_window_handles) + self.assertListEqual(self.start_windows, chrome_window_handles) + self.assertNotIn(new_window, self.marionette.window_handles) + + def test_close_chrome_window_for_non_browser_window(self): + new_window = self.open_chrome_window( + "chrome://remote/content/marionette/test.xhtml" + ) + self.marionette.switch_to_window(new_window) + + self.assertIn(new_window, self.marionette.chrome_window_handles) + self.assertNotIn(new_window, self.marionette.window_handles) + chrome_window_handles = self.marionette.close_chrome_window() + self.assertNotIn(new_window, chrome_window_handles) + self.assertListEqual(self.start_windows, chrome_window_handles) + self.assertNotIn(new_window, self.marionette.window_handles) + + def test_close_chrome_window_for_last_open_window(self): + self.close_all_windows() + + self.assertListEqual([], self.marionette.close_chrome_window()) + self.assertListEqual([self.start_tab], self.marionette.window_handles) + self.assertListEqual([self.start_window], self.marionette.chrome_window_handles) + self.assertIsNotNone(self.marionette.session) + + def test_close_window_for_browser_tab(self): + new_tab = self.open_tab() + self.marionette.switch_to_window(new_tab) + + window_handles = self.marionette.close() + self.assertNotIn(new_tab, window_handles) + self.assertListEqual(self.start_tabs, window_handles) + + def test_close_window_for_browser_window_with_single_tab(self): + new_tab = self.open_window() + self.marionette.switch_to_window(new_tab) + + self.assertEqual(len(self.marionette.window_handles), len(self.start_tabs) + 1) + window_handles = self.marionette.close() + self.assertNotIn(new_tab, window_handles) + self.assertListEqual(self.start_tabs, window_handles) + self.assertListEqual(self.start_windows, self.marionette.chrome_window_handles) + + def test_close_window_for_last_open_tab(self): + self.close_all_tabs() + + self.assertListEqual([], self.marionette.close()) + self.assertListEqual([self.start_tab], self.marionette.window_handles) + self.assertListEqual([self.start_window], self.marionette.chrome_window_handles) + self.assertIsNotNone(self.marionette.session) + + def test_close_browserless_tab(self): + self.close_all_tabs() + + test_page = self.marionette.absolute_url("windowHandles.html") + new_tab = self.open_tab() + self.marionette.switch_to_window(new_tab) + self.marionette.navigate(test_page) + self.marionette.switch_to_window(self.start_tab) + + with self.marionette.using_context("chrome"): + self.marionette.execute_async_script( + """ + const { BrowserWindowTracker } = ChromeUtils.importESModule( + "resource:///modules/BrowserWindowTracker.sys.mjs" + ); + + let win = BrowserWindowTracker.getTopWindow(); + win.addEventListener("TabBrowserDiscarded", ev => { + arguments[0](true); + }, { once: true}); + win.gBrowser.discardBrowser(win.gBrowser.tabs[1]); + """ + ) + + window_handles = self.marionette.window_handles + window_handles.remove(self.start_tab) + self.assertEqual(1, len(window_handles)) + self.marionette.switch_to_window(window_handles[0], focus=False) + self.marionette.close() + self.assertListEqual([self.start_tab], self.marionette.window_handles) diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_window_handles_chrome.py b/testing/marionette/harness/marionette_harness/tests/unit/test_window_handles_chrome.py new file mode 100644 index 0000000000..f723f82787 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_window_handles_chrome.py @@ -0,0 +1,253 @@ +# 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/. + +import six + +from marionette_driver import errors +from marionette_harness import MarionetteTestCase, WindowManagerMixin + + +class TestWindowHandles(WindowManagerMixin, MarionetteTestCase): + def setUp(self): + super(TestWindowHandles, self).setUp() + + self.chrome_dialog = "chrome://remote/content/marionette/test_dialog.xhtml" + + self.marionette.set_context("chrome") + + def tearDown(self): + self.close_all_windows() + self.close_all_tabs() + + super(TestWindowHandles, self).tearDown() + + def assert_window_handles(self): + try: + self.assertIsInstance( + self.marionette.current_chrome_window_handle, six.string_types + ) + self.assertIsInstance( + self.marionette.current_window_handle, six.string_types + ) + except errors.NoSuchWindowException: + pass + + for handle in self.marionette.chrome_window_handles: + self.assertIsInstance(handle, six.string_types) + + for handle in self.marionette.window_handles: + self.assertIsInstance(handle, six.string_types) + + def test_chrome_window_handles_with_scopes(self): + new_browser = self.open_window() + self.assert_window_handles() + self.assertEqual( + len(self.marionette.chrome_window_handles), len(self.start_windows) + 1 + ) + self.assertIn(new_browser, self.marionette.chrome_window_handles) + self.assertEqual( + self.marionette.current_chrome_window_handle, self.start_window + ) + + new_dialog = self.open_chrome_window(self.chrome_dialog) + self.assert_window_handles() + self.assertEqual( + len(self.marionette.chrome_window_handles), len(self.start_windows) + 2 + ) + self.assertIn(new_dialog, self.marionette.chrome_window_handles) + self.assertEqual( + self.marionette.current_chrome_window_handle, self.start_window + ) + + chrome_window_handles_in_chrome_scope = self.marionette.chrome_window_handles + window_handles_in_chrome_scope = self.marionette.window_handles + + with self.marionette.using_context("content"): + self.assertEqual( + self.marionette.chrome_window_handles, + chrome_window_handles_in_chrome_scope, + ) + self.assertEqual( + self.marionette.window_handles, window_handles_in_chrome_scope + ) + + def test_chrome_window_handles_after_opening_new_chrome_window(self): + new_window = self.open_chrome_window(self.chrome_dialog) + self.assert_window_handles() + self.assertEqual( + len(self.marionette.chrome_window_handles), len(self.start_windows) + 1 + ) + self.assertIn(new_window, self.marionette.chrome_window_handles) + self.assertEqual( + self.marionette.current_chrome_window_handle, self.start_window + ) + + # Check that the new chrome window has the correct URL loaded + self.marionette.switch_to_window(new_window) + self.assert_window_handles() + self.assertEqual(self.marionette.current_chrome_window_handle, new_window) + self.assertEqual(self.marionette.get_url(), self.chrome_dialog) + + # Close the chrome window, and carry on in our original window. + self.marionette.close_chrome_window() + self.assert_window_handles() + self.assertEqual( + len(self.marionette.chrome_window_handles), len(self.start_windows) + ) + self.assertNotIn(new_window, self.marionette.chrome_window_handles) + + self.marionette.switch_to_window(self.start_window) + self.assert_window_handles() + self.assertEqual( + self.marionette.current_chrome_window_handle, self.start_window + ) + + def test_chrome_window_handles_after_opening_new_window(self): + new_window = self.open_window() + self.assert_window_handles() + self.assertEqual( + len(self.marionette.chrome_window_handles), len(self.start_windows) + 1 + ) + self.assertIn(new_window, self.marionette.chrome_window_handles) + self.assertEqual( + self.marionette.current_chrome_window_handle, self.start_window + ) + + self.marionette.switch_to_window(new_window) + self.assert_window_handles() + self.assertEqual(self.marionette.current_chrome_window_handle, new_window) + + # Close the opened window and carry on in our original window. + self.marionette.close() + self.assert_window_handles() + self.assertEqual( + len(self.marionette.chrome_window_handles), len(self.start_windows) + ) + self.assertNotIn(new_window, self.marionette.chrome_window_handles) + + self.marionette.switch_to_window(self.start_window) + self.assert_window_handles() + self.assertEqual( + self.marionette.current_chrome_window_handle, self.start_window + ) + + def test_chrome_window_handles_after_session_created(self): + new_window = self.open_chrome_window(self.chrome_dialog) + self.assert_window_handles() + self.assertEqual( + len(self.marionette.chrome_window_handles), len(self.start_windows) + 1 + ) + self.assertIn(new_window, self.marionette.chrome_window_handles) + self.assertEqual( + self.marionette.current_chrome_window_handle, self.start_window + ) + + chrome_window_handles = self.marionette.chrome_window_handles + + self.marionette.delete_session() + self.marionette.start_session() + + self.assert_window_handles() + self.assertEqual(chrome_window_handles, self.marionette.chrome_window_handles) + + self.marionette.switch_to_window(new_window) + + def test_window_handles_after_opening_new_tab(self): + with self.marionette.using_context("content"): + new_tab = self.open_tab() + self.assert_window_handles() + self.assertEqual(len(self.marionette.window_handles), len(self.start_tabs) + 1) + self.assertIn(new_tab, self.marionette.window_handles) + self.assertEqual(self.marionette.current_window_handle, self.start_tab) + + self.marionette.switch_to_window(new_tab) + self.assert_window_handles() + self.assertEqual(self.marionette.current_window_handle, new_tab) + + self.marionette.switch_to_window(self.start_tab) + self.assert_window_handles() + self.assertEqual(self.marionette.current_window_handle, self.start_tab) + + self.marionette.switch_to_window(new_tab) + self.marionette.close() + self.assert_window_handles() + self.assertEqual(len(self.marionette.window_handles), len(self.start_tabs)) + self.assertNotIn(new_tab, self.marionette.window_handles) + + self.marionette.switch_to_window(self.start_tab) + self.assert_window_handles() + self.assertEqual(self.marionette.current_window_handle, self.start_tab) + + def test_window_handles_after_opening_new_foreground_tab(self): + with self.marionette.using_context("content"): + new_tab = self.open_tab(focus=True) + self.assert_window_handles() + self.assertEqual(len(self.marionette.window_handles), len(self.start_tabs) + 1) + self.assertIn(new_tab, self.marionette.window_handles) + self.assertEqual(self.marionette.current_window_handle, self.start_tab) + + # We still have the default tab set as our window handle. This + # get_url command should be sent immediately, and not be forever-queued. + with self.marionette.using_context("content"): + self.marionette.get_url() + + self.marionette.switch_to_window(new_tab) + self.assert_window_handles() + self.assertEqual(self.marionette.current_window_handle, new_tab) + + self.marionette.close() + self.assert_window_handles() + self.assertEqual(len(self.marionette.window_handles), len(self.start_tabs)) + self.assertNotIn(new_tab, self.marionette.window_handles) + + self.marionette.switch_to_window(self.start_tab) + self.assert_window_handles() + self.assertEqual(self.marionette.current_window_handle, self.start_tab) + + def test_window_handles_after_opening_new_chrome_window(self): + new_window = self.open_chrome_window(self.chrome_dialog) + self.assert_window_handles() + self.assertEqual(len(self.marionette.window_handles), len(self.start_tabs)) + self.assertNotIn(new_window, self.marionette.window_handles) + self.assertEqual(self.marionette.current_window_handle, self.start_tab) + + self.marionette.switch_to_window(new_window) + self.assert_window_handles() + self.assertEqual(self.marionette.get_url(), self.chrome_dialog) + + # Check that the opened dialog is not accessible via window handles + with self.assertRaises(errors.NoSuchWindowException): + self.marionette.current_window_handle + with self.assertRaises(errors.NoSuchWindowException): + self.marionette.close() + + # Close the dialog and carry on in our original tab. + self.marionette.close_chrome_window() + self.assert_window_handles() + self.assertEqual(len(self.marionette.window_handles), len(self.start_tabs)) + + self.marionette.switch_to_window(self.start_tab) + self.assert_window_handles() + self.assertEqual(self.marionette.current_window_handle, self.start_tab) + + def test_window_handles_after_closing_original_tab(self): + with self.marionette.using_context("content"): + new_tab = self.open_tab() + self.assert_window_handles() + self.assertEqual(len(self.marionette.window_handles), len(self.start_tabs) + 1) + self.assertIn(new_tab, self.marionette.window_handles) + self.assertEqual(self.marionette.current_window_handle, self.start_tab) + + self.marionette.close() + self.assert_window_handles() + self.assertEqual(len(self.marionette.window_handles), len(self.start_tabs)) + self.assertIn(new_tab, self.marionette.window_handles) + + self.marionette.switch_to_window(new_tab) + self.assert_window_handles() + self.assertEqual(self.marionette.current_window_handle, new_tab) + + def test_window_handles_after_closing_last_window(self): + self.close_all_windows() + self.assertEqual(self.marionette.close_chrome_window(), []) diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_window_handles_content.py b/testing/marionette/harness/marionette_harness/tests/unit/test_window_handles_content.py new file mode 100644 index 0000000000..e1c9cb42a0 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_window_handles_content.py @@ -0,0 +1,156 @@ +# 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/. + +import six +from six.moves.urllib.parse import quote + +from marionette_driver import errors +from marionette_harness import MarionetteTestCase, WindowManagerMixin + + +def inline(doc): + return "data:text/html;charset=utf-8,{}".format(quote(doc)) + + +class TestWindowHandles(WindowManagerMixin, MarionetteTestCase): + def setUp(self): + super(TestWindowHandles, self).setUp() + + self.chrome_dialog = "chrome://remote/content/marionette/test_dialog.xhtml" + + def tearDown(self): + self.close_all_windows() + self.close_all_tabs() + + super(TestWindowHandles, self).tearDown() + + def assert_window_handles(self): + try: + self.assertIsInstance( + self.marionette.current_window_handle, six.string_types + ) + except errors.NoSuchWindowException: + pass + + for handle in self.marionette.window_handles: + self.assertIsInstance(handle, six.string_types) + + def test_window_handles_after_opening_new_tab(self): + new_tab = self.open_tab() + self.assert_window_handles() + self.assertEqual(len(self.marionette.window_handles), len(self.start_tabs) + 1) + self.assertEqual(self.marionette.current_window_handle, self.start_tab) + + self.marionette.switch_to_window(new_tab) + self.assert_window_handles() + self.assertEqual(self.marionette.current_window_handle, new_tab) + + self.marionette.switch_to_window(self.start_tab) + self.assertEqual(self.marionette.current_window_handle, self.start_tab) + + self.marionette.switch_to_window(new_tab) + self.marionette.close() + self.assert_window_handles() + self.assertEqual(len(self.marionette.window_handles), len(self.start_tabs)) + + self.marionette.switch_to_window(self.start_tab) + self.assert_window_handles() + self.assertEqual(self.marionette.current_window_handle, self.start_tab) + + def test_window_handles_after_opening_new_browser_window(self): + new_tab = self.open_window() + self.assert_window_handles() + self.assertEqual(len(self.marionette.window_handles), len(self.start_tabs) + 1) + self.assertEqual(self.marionette.current_window_handle, self.start_tab) + + self.marionette.switch_to_window(new_tab) + self.assert_window_handles() + self.assertEqual(self.marionette.current_window_handle, new_tab) + + # Close the opened window and carry on in our original tab. + self.marionette.close() + self.assert_window_handles() + self.assertEqual(len(self.marionette.window_handles), len(self.start_tabs)) + + self.marionette.switch_to_window(self.start_tab) + self.assert_window_handles() + self.assertEqual(self.marionette.current_window_handle, self.start_tab) + + def test_window_handles_after_opening_new_non_browser_window(self): + new_window = self.open_chrome_window(self.chrome_dialog) + self.assert_window_handles() + self.assertEqual(len(self.marionette.window_handles), len(self.start_tabs)) + self.assertEqual(self.marionette.current_window_handle, self.start_tab) + self.assertNotIn(new_window, self.marionette.window_handles) + + self.marionette.switch_to_window(new_window) + self.assert_window_handles() + + # Check that the opened window is not accessible via window handles + with self.assertRaises(errors.NoSuchWindowException): + self.marionette.current_window_handle + with self.assertRaises(errors.NoSuchWindowException): + self.marionette.close() + + # Close the opened window and carry on in our original tab. + self.marionette.close_chrome_window() + self.assert_window_handles() + self.assertEqual(len(self.marionette.window_handles), len(self.start_tabs)) + + self.marionette.switch_to_window(self.start_tab) + self.assert_window_handles() + self.assertEqual(self.marionette.current_window_handle, self.start_tab) + + def test_window_handles_after_session_created(self): + new_window = self.open_chrome_window(self.chrome_dialog) + self.assert_window_handles() + self.assertEqual(len(self.marionette.window_handles), len(self.start_tabs)) + self.assertEqual(self.marionette.current_window_handle, self.start_tab) + self.assertNotIn(new_window, self.marionette.window_handles) + + window_handles = self.marionette.window_handles + + self.marionette.delete_session() + self.marionette.start_session() + + self.assert_window_handles() + self.assertEqual(window_handles, self.marionette.window_handles) + + self.marionette.switch_to_window(new_window) + + def test_window_handles_include_unloaded_tabs(self): + new_tab = self.open_tab() + self.assert_window_handles() + self.assertEqual(len(self.marionette.window_handles), len(self.start_tabs) + 1) + self.assertEqual(self.marionette.current_window_handle, self.start_tab) + + self.marionette.switch_to_window(new_tab) + self.assert_window_handles() + self.assertEqual(self.marionette.current_window_handle, new_tab) + + # The restart will cause the background tab to stay unloaded + self.marionette.restart(in_app=True) + + self.assert_window_handles() + self.assertEqual(len(self.marionette.window_handles), len(self.start_tabs) + 1) + + def test_window_handles_after_closing_original_tab(self): + new_tab = self.open_tab() + self.assert_window_handles() + self.assertEqual(len(self.marionette.window_handles), len(self.start_tabs) + 1) + self.assertEqual(self.marionette.current_window_handle, self.start_tab) + self.assertIn(new_tab, self.marionette.window_handles) + + self.marionette.close() + self.assert_window_handles() + self.assertEqual(len(self.marionette.window_handles), len(self.start_tabs)) + self.assertNotIn(self.start_tab, self.marionette.window_handles) + + self.marionette.switch_to_window(new_tab) + self.assert_window_handles() + self.assertEqual(self.marionette.current_window_handle, new_tab) + + def test_window_handles_after_closing_last_tab(self): + self.close_all_tabs() + self.assertEqual(self.marionette.close(), []) diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_window_management.py b/testing/marionette/harness/marionette_harness/tests/unit/test_window_management.py new file mode 100644 index 0000000000..33b75011b0 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_window_management.py @@ -0,0 +1,141 @@ +# 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 NoSuchWindowException + +from marionette_harness import MarionetteTestCase, WindowManagerMixin + + +class TestNoSuchWindowContent(WindowManagerMixin, MarionetteTestCase): + def setUp(self): + super(TestNoSuchWindowContent, self).setUp() + + def tearDown(self): + self.close_all_tabs() + super(TestNoSuchWindowContent, self).tearDown() + + def test_closed_chrome_window(self): + with self.marionette.using_context("chrome"): + new_window = self.open_window() + self.marionette.switch_to_window(new_window) + self.marionette.close_chrome_window() + + # When closing a browser window both handles are not available + for context in ("chrome", "content"): + with self.marionette.using_context(context): + with self.assertRaises(NoSuchWindowException): + self.marionette.current_chrome_window_handle + with self.assertRaises(NoSuchWindowException): + self.marionette.current_window_handle + + self.marionette.switch_to_window(self.start_window) + + with self.assertRaises(NoSuchWindowException): + self.marionette.switch_to_window(new_window) + + def test_closed_chrome_window_while_in_frame(self): + new_window = self.open_chrome_window( + "chrome://remote/content/marionette/test.xhtml" + ) + self.marionette.switch_to_window(new_window) + with self.marionette.using_context("chrome"): + self.marionette.switch_to_frame(0) + self.marionette.close_chrome_window() + + with self.assertRaises(NoSuchWindowException): + self.marionette.current_window_handle + with self.assertRaises(NoSuchWindowException): + self.marionette.current_chrome_window_handle + + self.marionette.switch_to_window(self.start_window) + + with self.assertRaises(NoSuchWindowException): + self.marionette.switch_to_window(new_window) + + def test_closed_tab(self): + new_tab = self.open_tab() + self.marionette.switch_to_window(new_tab) + self.marionette.close() + + # Check that only the content window is not available in both contexts + for context in ("chrome", "content"): + with self.marionette.using_context(context): + with self.assertRaises(NoSuchWindowException): + self.marionette.current_window_handle + self.marionette.current_chrome_window_handle + + self.marionette.switch_to_window(self.start_tab) + + with self.assertRaises(NoSuchWindowException): + self.marionette.switch_to_window(new_tab) + + def test_closed_tab_while_in_frame(self): + new_tab = self.open_tab() + self.marionette.switch_to_window(new_tab) + + with self.marionette.using_context("content"): + self.marionette.navigate(self.marionette.absolute_url("test_iframe.html")) + frame = self.marionette.find_element(By.ID, "test_iframe") + self.marionette.switch_to_frame(frame) + self.marionette.close() + + with self.assertRaises(NoSuchWindowException): + self.marionette.current_window_handle + self.marionette.current_chrome_window_handle + + self.marionette.switch_to_window(self.start_tab) + + with self.assertRaises(NoSuchWindowException): + self.marionette.switch_to_window(new_tab) + + +class TestNoSuchWindowChrome(TestNoSuchWindowContent): + def setUp(self): + super(TestNoSuchWindowChrome, self).setUp() + self.marionette.set_context("chrome") + + def tearDown(self): + self.close_all_windows() + super(TestNoSuchWindowChrome, self).tearDown() + + +class TestSwitchWindow(WindowManagerMixin, MarionetteTestCase): + def setUp(self): + super(TestSwitchWindow, self).setUp() + self.marionette.set_context("chrome") + + def tearDown(self): + self.close_all_windows() + super(TestSwitchWindow, self).tearDown() + + def test_switch_window_after_open_and_close(self): + with self.marionette.using_context("chrome"): + new_window = self.open_window() + self.assertEqual( + len(self.marionette.chrome_window_handles), len(self.start_windows) + 1 + ) + self.assertIn(new_window, self.marionette.chrome_window_handles) + self.assertEqual( + self.marionette.current_chrome_window_handle, self.start_window + ) + + # switch to the new chrome window and close it + self.marionette.switch_to_window(new_window) + self.assertEqual(self.marionette.current_chrome_window_handle, new_window) + self.assertNotEqual( + self.marionette.current_chrome_window_handle, self.start_window + ) + + self.marionette.close_chrome_window() + self.assertEqual( + len(self.marionette.chrome_window_handles), len(self.start_windows) + ) + self.assertNotIn(new_window, self.marionette.chrome_window_handles) + + # switch back to the original chrome window + self.marionette.switch_to_window(self.start_window) + self.assertEqual( + self.marionette.current_chrome_window_handle, self.start_window + ) diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_window_maximize.py b/testing/marionette/harness/marionette_harness/tests/unit/test_window_maximize.py new file mode 100644 index 0000000000..e9c2d8ba38 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_window_maximize.py @@ -0,0 +1,36 @@ +# 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_harness import MarionetteTestCase + + +class TestWindowMaximize(MarionetteTestCase): + def setUp(self): + MarionetteTestCase.setUp(self) + self.max = self.marionette.execute_script( + """ + return { + width: window.screen.availWidth, + height: window.screen.availHeight, + }""", + sandbox=None, + ) + + # ensure window is not maximized + self.marionette.set_window_rect( + width=self.max["width"] - 100, height=self.max["height"] - 100 + ) + actual = self.marionette.window_rect + self.assertNotEqual(actual["width"], self.max["width"]) + self.assertNotEqual(actual["height"], self.max["height"]) + + self.original_size = actual + + def tearDown(self): + self.marionette.set_window_rect( + width=self.original_size["width"], height=self.original_size["height"] + ) + + def test_maximize(self): + self.marionette.maximize_window() diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_window_rect.py b/testing/marionette/harness/marionette_harness/tests/unit/test_window_rect.py new file mode 100644 index 0000000000..284989cf5b --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_window_rect.py @@ -0,0 +1,315 @@ +# 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.errors import InvalidArgumentException +from marionette_harness import MarionetteTestCase + + +class TestWindowRect(MarionetteTestCase): + def setUp(self): + super(TestWindowRect, self).setUp() + + self.original_rect = self.marionette.window_rect + + self.max = self.marionette.execute_script( + """ + return { + width: window.screen.availWidth, + height: window.screen.availHeight, + }""", + sandbox=None, + ) + + # WebDriver spec says a resize cannot result in window being + # maximised, an error is returned if that is the case; therefore if + # the window is maximised at the start of this test, returning to + # the original size via set_window_rect size will result in error; + # so reset to original size minus 1 pixel width + start_size = { + "height": self.original_rect["height"], + "width": self.original_rect["width"], + } + if ( + start_size["width"] == self.max["width"] + and start_size["height"] == self.max["height"] + ): + start_size["width"] -= 10 + start_size["height"] -= 10 + self.marionette.set_window_rect( + height=start_size["height"], width=start_size["width"] + ) + + def tearDown(self): + x, y = self.original_rect["x"], self.original_rect["y"] + height, width = self.original_rect["height"], self.original_rect["width"] + + self.marionette.set_window_rect(x=x, y=y, height=height, width=width) + + is_fullscreen = self.marionette.execute_script( + "return document.fullscreenElement;", sandbox=None + ) + if is_fullscreen: + self.marionette.fullscreen() + + super(TestWindowRect, self).tearDown() + + def test_get_types(self): + rect = self.marionette.window_rect + self.assertIn("x", rect) + self.assertIn("y", rect) + self.assertIn("height", rect) + self.assertIn("width", rect) + self.assertIsInstance(rect["x"], int) + self.assertIsInstance(rect["y"], int) + self.assertIsInstance(rect["height"], int) + self.assertIsInstance(rect["width"], int) + + def test_set_types(self): + invalid_rects = ( + ["a", "b", "h", "w"], + [1.2, 3.4, 4.5, 5.6], + [True, False, True, False], + [[], [], [], []], + [{}, {}, {}, {}], + ) + for x, y, h, w in invalid_rects: + print("testing invalid type position ({},{})".format(x, y)) + with self.assertRaises(InvalidArgumentException): + self.marionette.set_window_rect(x=x, y=y, height=h, width=w) + + def test_setting_window_rect_with_nulls_errors(self): + with self.assertRaises(InvalidArgumentException): + self.marionette.set_window_rect(height=None, width=None, x=None, y=None) + + def test_set_position(self): + old_position = self.marionette.window_rect + wanted_position = {"x": old_position["x"] + 10, "y": old_position["y"] + 10} + + new_position = self.marionette.set_window_rect( + x=wanted_position["x"], y=wanted_position["y"] + ) + expected_position = self.marionette.window_rect + + self.assertEqual(new_position["x"], wanted_position["x"]) + self.assertEqual(new_position["y"], wanted_position["y"]) + self.assertEqual(new_position["x"], expected_position["x"]) + self.assertEqual(new_position["y"], expected_position["y"]) + + def test_set_size(self): + old_size = self.marionette.window_rect + wanted_size = { + "height": old_size["height"] - 50, + "width": old_size["width"] - 50, + } + + new_size = self.marionette.set_window_rect( + height=wanted_size["height"], width=wanted_size["width"] + ) + expected_size = self.marionette.window_rect + + self.assertEqual( + new_size["width"], + wanted_size["width"], + "New width is {0} but should be {1}".format( + new_size["width"], wanted_size["width"] + ), + ) + self.assertEqual( + new_size["height"], + wanted_size["height"], + "New height is {0} but should be {1}".format( + new_size["height"], wanted_size["height"] + ), + ) + self.assertEqual( + new_size["width"], + expected_size["width"], + "New width is {0} but should be {1}".format( + new_size["width"], expected_size["width"] + ), + ) + self.assertEqual( + new_size["height"], + expected_size["height"], + "New height is {0} but should be {1}".format( + new_size["height"], expected_size["height"] + ), + ) + + def test_set_position_and_size(self): + old_rect = self.marionette.window_rect + wanted_rect = { + "x": old_rect["x"] + 10, + "y": old_rect["y"] + 10, + "width": old_rect["width"] - 50, + "height": old_rect["height"] - 50, + } + + new_rect = self.marionette.set_window_rect( + x=wanted_rect["x"], + y=wanted_rect["y"], + width=wanted_rect["width"], + height=wanted_rect["height"], + ) + expected_rect = self.marionette.window_rect + + self.assertEqual(new_rect["x"], wanted_rect["x"]) + self.assertEqual(new_rect["y"], wanted_rect["y"]) + self.assertEqual( + new_rect["width"], + wanted_rect["width"], + "New width is {0} but should be {1}".format( + new_rect["width"], wanted_rect["width"] + ), + ) + self.assertEqual( + new_rect["height"], + wanted_rect["height"], + "New height is {0} but should be {1}".format( + new_rect["height"], wanted_rect["height"] + ), + ) + self.assertEqual(new_rect["x"], expected_rect["x"]) + self.assertEqual(new_rect["y"], expected_rect["y"]) + self.assertEqual( + new_rect["width"], + expected_rect["width"], + "New width is {0} but should be {1}".format( + new_rect["width"], expected_rect["width"] + ), + ) + self.assertEqual( + new_rect["height"], + expected_rect["height"], + "New height is {0} but should be {1}".format( + new_rect["height"], expected_rect["height"] + ), + ) + + def test_move_to_current_position(self): + old_position = self.marionette.window_rect + new_position = self.marionette.set_window_rect( + x=old_position["x"], y=old_position["y"] + ) + + self.assertEqual(new_position["x"], old_position["x"]) + self.assertEqual(new_position["y"], old_position["y"]) + + def test_move_to_current_size(self): + old_size = self.marionette.window_rect + new_size = self.marionette.set_window_rect( + height=old_size["height"], width=old_size["width"] + ) + + self.assertEqual(new_size["height"], old_size["height"]) + self.assertEqual(new_size["width"], old_size["width"]) + + def test_move_to_current_position_and_size(self): + old_position_and_size = self.marionette.window_rect + new_position_and_size = self.marionette.set_window_rect( + x=old_position_and_size["x"], + y=old_position_and_size["y"], + height=old_position_and_size["height"], + width=old_position_and_size["width"], + ) + + self.assertEqual(new_position_and_size["x"], old_position_and_size["x"]) + self.assertEqual(new_position_and_size["y"], old_position_and_size["y"]) + self.assertEqual(new_position_and_size["width"], old_position_and_size["width"]) + self.assertEqual( + new_position_and_size["height"], old_position_and_size["height"] + ) + + def test_move_to_negative_coordinates(self): + old_position = self.marionette.window_rect + print("Current position: {}".format(old_position["x"], old_position["y"])) + new_position = self.marionette.set_window_rect(x=-8, y=-8) + print( + "Position after requesting move to negative coordinates: {}, {}".format( + new_position["x"], new_position["y"] + ) + ) + + # Different systems will report more or less than (-8,-8) + # depending on the characteristics of the window manager, since + # the screenX/screenY position measures the chrome boundaries, + # including any WM decorations. + # + # This makes this hard to reliably test across different + # environments. Generally we are happy when calling + # marionette.set_window_position with negative coordinates does + # not throw. + # + # Because we have to cater to an unknown set of environments, + # the following assertions are the most common denominator that + # make this test pass, irregardless of system characteristics. + + os = self.marionette.session_capabilities["platformName"] + + # Regardless of platform, headless always supports being positioned + # off-screen. + if self.marionette.session_capabilities["moz:headless"]: + self.assertEqual(-8, new_position["x"]) + self.assertEqual(-8, new_position["y"]) + + # Certain WMs prohibit windows from being moved off-screen, + # but we don't have this information. It should be safe to + # assume a window can be moved to (0,0) or less. + elif os == "linux": + # certain WMs prohibit windows from being moved off-screen + self.assertLessEqual(new_position["x"], 0) + self.assertLessEqual(new_position["y"], 0) + + # On macOS, windows can only be moved off the screen on the + # horizontal axis. The system menu bar also blocks windows from + # being moved to (0,0). + elif os == "mac": + self.assertEqual(-8, new_position["x"]) + self.assertEqual(23, new_position["y"]) + + # It turns out that Windows is the only platform on which the + # window can be reliably positioned off-screen. + elif os == "windows": + self.assertEqual(-8, new_position["x"]) + self.assertEqual(-8, new_position["y"]) + + def test_resize_larger_than_screen(self): + new_size = self.marionette.set_window_rect( + width=self.max["width"] * 2, height=self.max["height"] * 2 + ) + actual_size = self.marionette.window_rect + + # in X the window size may be greater than the bounds of the screen + self.assertGreaterEqual(new_size["width"], self.max["width"]) + self.assertGreaterEqual(new_size["height"], self.max["height"]) + self.assertEqual(actual_size["width"], new_size["width"]) + self.assertEqual(actual_size["height"], new_size["height"]) + + def test_resize_to_available_screen_size(self): + expected_size = self.marionette.set_window_rect( + width=self.max["width"], height=self.max["height"] + ) + result_size = self.marionette.window_rect + + self.assertGreaterEqual(expected_size["width"], self.max["width"]) + self.assertGreaterEqual(expected_size["height"], self.max["height"]) + self.assertEqual(result_size["width"], expected_size["width"]) + self.assertEqual(result_size["height"], expected_size["height"]) + + def test_resize_while_fullscreen(self): + self.marionette.fullscreen() + expected_size = self.marionette.set_window_rect( + width=self.max["width"] - 100, height=self.max["height"] - 100 + ) + result_size = self.marionette.window_rect + + self.assertTrue( + self.marionette.execute_script( + "return window.fullscreenElement == null", sandbox=None + ) + ) + self.assertEqual(self.max["width"] - 100, expected_size["width"]) + self.assertEqual(self.max["height"] - 100, expected_size["height"]) + self.assertEqual(result_size["width"], expected_size["width"]) + self.assertEqual(result_size["height"], expected_size["height"]) diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_window_status_chrome.py b/testing/marionette/harness/marionette_harness/tests/unit/test_window_status_chrome.py new file mode 100644 index 0000000000..29eb574187 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_window_status_chrome.py @@ -0,0 +1,23 @@ +# 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/. + +import os +import sys + +# add this directory to the path +sys.path.append(os.path.dirname(__file__)) + +from test_window_status_content import TestNoSuchWindowContent + + +class TestNoSuchWindowChrome(TestNoSuchWindowContent): + def setUp(self): + super(TestNoSuchWindowChrome, self).setUp() + + self.marionette.set_context("chrome") + + def tearDown(self): + self.close_all_windows() + + super(TestNoSuchWindowChrome, self).tearDown() diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_window_status_content.py b/testing/marionette/harness/marionette_harness/tests/unit/test_window_status_content.py new file mode 100644 index 0000000000..1ce5e239a6 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_window_status_content.py @@ -0,0 +1,94 @@ +# 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 NoSuchWindowException + +from marionette_harness import MarionetteTestCase, WindowManagerMixin + + +class TestNoSuchWindowContent(WindowManagerMixin, MarionetteTestCase): + def setUp(self): + super(TestNoSuchWindowContent, self).setUp() + + def tearDown(self): + self.close_all_windows() + super(TestNoSuchWindowContent, self).tearDown() + + def test_closed_chrome_window(self): + with self.marionette.using_context("chrome"): + new_window = self.open_window() + self.marionette.switch_to_window(new_window) + self.marionette.close_chrome_window() + + # When closing a browser window both handles are not available + for context in ("chrome", "content"): + print("Testing handles with context {}".format(context)) + with self.marionette.using_context(context): + with self.assertRaises(NoSuchWindowException): + self.marionette.current_chrome_window_handle + with self.assertRaises(NoSuchWindowException): + self.marionette.current_window_handle + + self.marionette.switch_to_window(self.start_window) + + with self.assertRaises(NoSuchWindowException): + self.marionette.switch_to_window(new_window) + + def test_closed_chrome_window_while_in_frame(self): + new_window = self.open_chrome_window( + "chrome://remote/content/marionette/test.xhtml" + ) + self.marionette.switch_to_window(new_window) + + with self.marionette.using_context("chrome"): + self.marionette.switch_to_frame(0) + self.marionette.close_chrome_window() + + with self.assertRaises(NoSuchWindowException): + self.marionette.current_window_handle + with self.assertRaises(NoSuchWindowException): + self.marionette.current_chrome_window_handle + + self.marionette.switch_to_window(self.start_window) + + with self.assertRaises(NoSuchWindowException): + self.marionette.switch_to_window(new_window) + + def test_closed_tab(self): + new_tab = self.open_tab(focus=True) + self.marionette.switch_to_window(new_tab) + self.marionette.close() + + # Check that only the content window is not available in both contexts + for context in ("chrome", "content"): + with self.marionette.using_context(context): + with self.assertRaises(NoSuchWindowException): + self.marionette.current_window_handle + self.marionette.current_chrome_window_handle + + self.marionette.switch_to_window(self.start_tab) + + with self.assertRaises(NoSuchWindowException): + self.marionette.switch_to_window(new_tab) + + def test_closed_tab_while_in_frame(self): + new_tab = self.open_tab() + self.marionette.switch_to_window(new_tab) + + with self.marionette.using_context("content"): + self.marionette.navigate(self.marionette.absolute_url("test_iframe.html")) + frame = self.marionette.find_element(By.ID, "test_iframe") + self.marionette.switch_to_frame(frame) + + self.marionette.close() + + with self.assertRaises(NoSuchWindowException): + self.marionette.current_window_handle + self.marionette.current_chrome_window_handle + + self.marionette.switch_to_window(self.start_tab) + + with self.assertRaises(NoSuchWindowException): + self.marionette.switch_to_window(new_tab) diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_window_type_chrome.py b/testing/marionette/harness/marionette_harness/tests/unit/test_window_type_chrome.py new file mode 100644 index 0000000000..8737dd2be9 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_window_type_chrome.py @@ -0,0 +1,26 @@ +# 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_harness import MarionetteTestCase, WindowManagerMixin + + +class TestWindowTypeChrome(WindowManagerMixin, MarionetteTestCase): + def setUp(self): + super(TestWindowTypeChrome, self).setUp() + + self.marionette.set_context("chrome") + + def tearDown(self): + self.close_all_windows() + + super(TestWindowTypeChrome, self).tearDown() + + def test_get_window_type(self): + win = self.open_chrome_window("chrome://remote/content/marionette/test.xhtml") + self.marionette.switch_to_window(win) + + window_type = self.marionette.execute_script( + "return window.document.documentElement.getAttribute('windowtype');" + ) + self.assertEqual(window_type, self.marionette.get_window_type()) diff --git a/testing/marionette/harness/marionette_harness/tests/unit/test_windowless.py b/testing/marionette/harness/marionette_harness/tests/unit/test_windowless.py new file mode 100644 index 0000000000..e8e98350f7 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/test_windowless.py @@ -0,0 +1,60 @@ +# 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 errors, Wait +from marionette_harness import MarionetteTestCase + + +class TestWindowless(MarionetteTestCase): + def setUp(self): + super(TestWindowless, self).setUp() + + self.marionette.delete_session() + self.marionette.start_session({"moz:windowless": True}) + + def tearDown(self): + # Reset the browser and active WebDriver session + self.marionette.restart(in_app=True) + self.marionette.delete_session() + + super(TestWindowless, self).tearDown() + + def wait_for_first_window(self): + wait = Wait( + self.marionette, + ignored_exceptions=errors.NoSuchWindowException, + timeout=5, + ) + return wait.until(lambda _: self.marionette.window_handles) + + def test_last_chrome_window_can_be_closed(self): + with self.marionette.using_context("chrome"): + handles = self.marionette.chrome_window_handles + self.assertGreater(len(handles), 0) + self.marionette.switch_to_window(handles[0]) + self.marionette.close_chrome_window() + self.assertEqual(len(self.marionette.chrome_window_handles), 0) + + def test_last_content_window_can_be_closed(self): + handles = self.marionette.window_handles + self.assertGreater(len(handles), 0) + self.marionette.switch_to_window(handles[0]) + self.marionette.close() + self.assertEqual(len(self.marionette.window_handles), 0) + + def test_no_window_handles_after_silent_restart(self): + # Check that windows are present, but not after a silent restart + handles = self.marionette.window_handles + self.assertGreater(len(handles), 0) + + self.marionette.restart(silent=True) + with self.assertRaises(errors.TimeoutException): + self.wait_for_first_window() + + # After a normal restart a browser window will be opened again + self.marionette.restart(in_app=True) + handles = self.wait_for_first_window() + + self.assertGreater(len(handles), 0) + self.marionette.switch_to_window(handles[0]) diff --git a/testing/marionette/harness/marionette_harness/tests/unit/unit-tests.toml b/testing/marionette/harness/marionette_harness/tests/unit/unit-tests.toml new file mode 100644 index 0000000000..e8675e4897 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/unit-tests.toml @@ -0,0 +1,193 @@ +[DEFAULT] + +["test_accessibility.py"] + +["test_actions_key.py"] + +["test_actions_pointer.py"] + +["test_actions_wheel.py"] + +["test_addons.py"] + +["test_capabilities.py"] + +["test_checkbox.py"] + +["test_checkbox_chrome.py"] + +["test_chrome.py"] + +["test_chrome_action.py"] + +["test_chrome_element_css.py"] + +["test_cli_arguments.py"] +skip-if = ["!manage_instance"] + +["test_click.py"] + +["test_click_chrome.py"] + +["test_click_scrolling.py"] + +["test_context.py"] + +["test_cookies.py"] + +["test_crash.py"] +skip-if = [ + "asan", + "!manage_instance", +] + +["test_data_driven.py"] + +["test_date_time_value.py"] + +["test_element_id.py"] + +["test_element_id_chrome.py"] + +["test_element_rect.py"] + +["test_element_rect_chrome.py"] + +["test_element_state.py"] + +["test_element_state_chrome.py"] + +["test_errors.py"] + +["test_execute_async_script.py"] + +["test_execute_isolate.py"] + +["test_execute_sandboxes.py"] + +["test_execute_script.py"] + +["test_expected.py"] + +["test_expectedfail.py"] +expected = "fail" + +["test_file_upload.py"] +skip-if = ["os == 'win'"] # http://bugs.python.org/issue14574 + +["test_findelement.py"] + +["test_findelement_chrome.py"] + +["test_geckoinstance.py"] + +["test_get_computed_label.py"] + +["test_get_computed_role.py"] + +["test_get_current_url_chrome.py"] + +["test_get_shadow_root.py"] + +["test_implicit_waits.py"] + +["test_localization.py"] + +["test_marionette.py"] + +["test_modal_dialogs.py"] + +["test_navigation.py"] + +["test_pagesource.py"] + +["test_pagesource_chrome.py"] + +["test_position.py"] + +["test_prefs.py"] + +["test_prefs_enforce.py"] +skip-if = ["!manage_instance"] + +["test_profile_management.py"] +skip-if = [ + "!manage_instance", + "debug && (os == 'mac' || os == 'linux')", # Bug 1450355 +] + +["test_proxy.py"] + +["test_quit_restart.py"] +skip-if = ["!manage_instance"] + +["test_reftest.py"] +skip-if = ["os == 'mac'"] # bug 1674411 + +["test_rendered_element.py"] + +["test_report.py"] + +["test_screen_orientation.py"] + +["test_screenshot.py"] + +["test_select.py"] + +["test_sendkeys_menupopup_chrome.py"] + +["test_session.py"] + +["test_skip_setup.py"] + +["test_switch_frame.py"] + +["test_switch_frame_chrome.py"] + +["test_switch_window_chrome.py"] + +["test_switch_window_content.py"] + +["test_teardown_context_preserved.py"] + +["test_text.py"] + +["test_text_chrome.py"] + +["test_timeouts.py"] + +["test_title.py"] + +["test_title_chrome.py"] + +["test_transport.py"] + +["test_typing.py"] + +["test_unhandled_prompt_behavior.py"] + +["test_visibility.py"] + +["test_wait.py"] + +["test_window_close_chrome.py"] + +["test_window_close_content.py"] + +["test_window_handles_chrome.py"] + +["test_window_handles_content.py"] + +["test_window_maximize.py"] + +["test_window_rect.py"] +skip-if = ["os == 'linux' && os_version == '18.04' && !swgl"] # Bug 1709584 + +["test_window_status_chrome.py"] + +["test_window_status_content.py"] + +["test_window_type_chrome.py"] + +["test_windowless.py"] +run-if = ["os == 'mac'"] # only supported on MacOS diff --git a/testing/marionette/harness/marionette_harness/tests/unit/webextension-invalid.xpi b/testing/marionette/harness/marionette_harness/tests/unit/webextension-invalid.xpi Binary files differnew file mode 100644 index 0000000000..bd1177462e --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/webextension-invalid.xpi diff --git a/testing/marionette/harness/marionette_harness/tests/unit/webextension-signed.xpi b/testing/marionette/harness/marionette_harness/tests/unit/webextension-signed.xpi Binary files differnew file mode 100644 index 0000000000..5363911af1 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/webextension-signed.xpi diff --git a/testing/marionette/harness/marionette_harness/tests/unit/webextension-unsigned.xpi b/testing/marionette/harness/marionette_harness/tests/unit/webextension-unsigned.xpi Binary files differnew file mode 100644 index 0000000000..cf0fad63b5 --- /dev/null +++ b/testing/marionette/harness/marionette_harness/tests/unit/webextension-unsigned.xpi |