summaryrefslogtreecommitdiffstats
path: root/testing/marionette/harness/marionette_harness/tests
diff options
context:
space:
mode:
Diffstat (limited to 'testing/marionette/harness/marionette_harness/tests')
-rw-r--r--testing/marionette/harness/marionette_harness/tests/harness_unit/conftest.py99
-rw-r--r--testing/marionette/harness/marionette_harness/tests/harness_unit/python.toml14
-rw-r--r--testing/marionette/harness/marionette_harness/tests/harness_unit/test_httpd.py92
-rw-r--r--testing/marionette/harness/marionette_harness/tests/harness_unit/test_marionette_arguments.py80
-rw-r--r--testing/marionette/harness/marionette_harness/tests/harness_unit/test_marionette_harness.py110
-rw-r--r--testing/marionette/harness/marionette_harness/tests/harness_unit/test_marionette_runner.py541
-rw-r--r--testing/marionette/harness/marionette_harness/tests/harness_unit/test_marionette_test_result.py55
-rw-r--r--testing/marionette/harness/marionette_harness/tests/harness_unit/test_serve.py69
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit-tests.toml43
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/data/test.html13
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_accessibility.py241
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_actions_key.py71
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_actions_pointer.py134
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_actions_wheel.py68
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_addons.py140
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_capabilities.py322
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_checkbox.py17
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_checkbox_chrome.py33
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_chrome.py31
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_chrome_action.py61
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_chrome_element_css.py31
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_cli_arguments.py98
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_click.py571
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_click_chrome.py33
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_click_scrolling.py167
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_context.py82
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_cookies.py115
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_crash.py211
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_data_driven.py72
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_date_time_value.py33
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_element_id.py55
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_element_id_chrome.py88
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_element_rect.py22
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_element_rect_chrome.py30
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_element_state.py175
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_element_state_chrome.py56
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_errors.py105
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_execute_async_script.py240
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_execute_isolate.py46
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_execute_sandboxes.py86
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_execute_script.py569
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_expected.py233
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_expectedfail.py11
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_file_upload.py169
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_findelement.py479
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_findelement_chrome.py169
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_geckoinstance.py25
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_get_computed_label.py26
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_get_computed_role.py26
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_get_current_url_chrome.py39
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_get_shadow_root.py66
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_implicit_waits.py26
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_localization.py71
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_marionette.py138
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_modal_dialogs.py161
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_navigation.py901
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_pagesource.py52
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_pagesource_chrome.py26
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_position.py46
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_prefs.py213
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_prefs_enforce.py54
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_profile_management.py267
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_proxy.py159
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_quit_restart.py550
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_reftest.py105
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_rendered_element.py31
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_report.py27
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_run_js_test.py10
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_screen_orientation.py75
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_screenshot.py393
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_select.py218
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_sendkeys_menupopup_chrome.py106
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_session.py49
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_shadowroot_findelement.py113
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_skip_setup.py33
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_switch_frame.py96
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_switch_frame_chrome.py57
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_switch_window_chrome.py113
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_switch_window_content.py258
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_teardown_context_preserved.py21
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_text.py26
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_text_chrome.py35
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_timeouts.py113
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_title.py17
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_title_chrome.py37
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_transport.py110
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_typing.py374
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_unhandled_prompt_behavior.py126
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_visibility.py175
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_wait.py347
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_window_close_chrome.py73
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_window_close_content.py109
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_window_handles_chrome.py253
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_window_handles_content.py156
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_window_management.py141
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_window_maximize.py36
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_window_rect.py315
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_window_status_chrome.py23
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_window_status_content.py94
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_window_type_chrome.py26
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/test_windowless.py60
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/unit-tests.toml193
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/webextension-invalid.xpibin0 -> 295 bytes
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/webextension-signed.xpibin0 -> 4221 bytes
-rw-r--r--testing/marionette/harness/marionette_harness/tests/unit/webextension-unsigned.xpibin0 -> 310 bytes
105 files changed, 13570 insertions, 0 deletions
diff --git a/testing/marionette/harness/marionette_harness/tests/harness_unit/conftest.py b/testing/marionette/harness/marionette_harness/tests/harness_unit/conftest.py
new file mode 100644
index 0000000000..43951b2c04
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/tests/harness_unit/conftest.py
@@ -0,0 +1,99 @@
+# 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 pytest
+
+from unittest.mock import Mock, MagicMock
+
+from marionette_driver.marionette import Marionette
+
+from marionette_harness.runner.httpd import FixtureServer
+
+
+@pytest.fixture(scope="module")
+def logger():
+ """
+ Fake logger to help with mocking out other runner-related classes.
+ """
+ import mozlog
+
+ return Mock(spec=mozlog.structuredlog.StructuredLogger)
+
+
+@pytest.fixture
+def mach_parsed_kwargs(logger):
+ """
+ Parsed and verified dictionary used during simplest
+ call to mach marionette-test
+ """
+ return {
+ "adb_path": None,
+ "addons": None,
+ "address": None,
+ "app": None,
+ "app_args": [],
+ "avd": None,
+ "avd_home": None,
+ "binary": "/path/to/firefox",
+ "browsermob_port": None,
+ "browsermob_script": None,
+ "device_serial": None,
+ "emulator": False,
+ "emulator_bin": None,
+ "gecko_log": None,
+ "jsdebugger": False,
+ "log_errorsummary": None,
+ "log_html": None,
+ "log_mach": None,
+ "log_mach_buffer": None,
+ "log_mach_level": None,
+ "log_mach_verbose": None,
+ "log_raw": None,
+ "log_raw_level": None,
+ "log_tbpl": None,
+ "log_tbpl_buffer": None,
+ "log_tbpl_compact": None,
+ "log_tbpl_level": None,
+ "log_unittest": None,
+ "log_xunit": None,
+ "logger_name": "Marionette-based Tests",
+ "prefs": {},
+ "prefs_args": None,
+ "prefs_files": None,
+ "profile": None,
+ "pydebugger": None,
+ "repeat": None,
+ "run_until_failure": None,
+ "server_root": None,
+ "shuffle": False,
+ "shuffle_seed": 2276870381009474531,
+ "socket_timeout": 60.0,
+ "startup_timeout": 60,
+ "symbols_path": None,
+ "test_tags": None,
+ "tests": ["/path/to/unit-tests.toml"],
+ "testvars": None,
+ "this_chunk": None,
+ "timeout": None,
+ "total_chunks": None,
+ "verbose": None,
+ "workspace": None,
+ "logger": logger,
+ }
+
+
+@pytest.fixture
+def mock_httpd(request):
+ """Mock httpd instance"""
+ httpd = MagicMock(spec=FixtureServer)
+ return httpd
+
+
+@pytest.fixture
+def mock_marionette(request):
+ """Mock marionette instance"""
+ marionette = MagicMock(spec=dir(Marionette()))
+ if "has_crashed" in request.fixturenames:
+ marionette.check_for_crash.return_value = request.getfixturevalue("has_crashed")
+ return marionette
diff --git a/testing/marionette/harness/marionette_harness/tests/harness_unit/python.toml b/testing/marionette/harness/marionette_harness/tests/harness_unit/python.toml
new file mode 100644
index 0000000000..7ae7a32440
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/tests/harness_unit/python.toml
@@ -0,0 +1,14 @@
+[DEFAULT]
+subsuite = "marionette-harness"
+
+["test_httpd.py"]
+
+["test_marionette_arguments.py"]
+
+["test_marionette_harness.py"]
+
+["test_marionette_runner.py"]
+
+["test_marionette_test_result.py"]
+
+["test_serve.py"]
diff --git a/testing/marionette/harness/marionette_harness/tests/harness_unit/test_httpd.py b/testing/marionette/harness/marionette_harness/tests/harness_unit/test_httpd.py
new file mode 100644
index 0000000000..b62e731ff1
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/tests/harness_unit/test_httpd.py
@@ -0,0 +1,92 @@
+# 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
+import os
+import types
+
+import six
+from six.moves.urllib_request import urlopen
+
+import mozunit
+import pytest
+
+from wptserve.handlers import json_handler
+
+from marionette_harness.runner import httpd
+
+here = os.path.abspath(os.path.dirname(__file__))
+parent = os.path.dirname(here)
+default_doc_root = os.path.join(os.path.dirname(parent), "www")
+
+
+@pytest.fixture
+def server():
+ server = httpd.FixtureServer(default_doc_root)
+ yield server
+ server.stop()
+
+
+def test_ctor():
+ with pytest.raises(ValueError):
+ httpd.FixtureServer("foo")
+ httpd.FixtureServer(default_doc_root)
+
+
+def test_start_stop(server):
+ server.start()
+ server.stop()
+
+
+def test_get_url(server):
+ server.start()
+ url = server.get_url("/")
+ assert isinstance(url, six.string_types)
+ assert "http://" in url
+
+ server.stop()
+ with pytest.raises(httpd.NotAliveError):
+ server.get_url("/")
+
+
+def test_doc_root(server):
+ server.start()
+ assert isinstance(server.doc_root, six.string_types)
+ server.stop()
+ assert isinstance(server.doc_root, six.string_types)
+
+
+def test_router(server):
+ assert server.router is not None
+
+
+def test_routes(server):
+ assert server.routes is not None
+
+
+def test_is_alive(server):
+ assert server.is_alive == False
+ server.start()
+ assert server.is_alive == True
+
+
+def test_handler(server):
+ counter = 0
+
+ @json_handler
+ def handler(request, response):
+ return {"count": counter}
+
+ route = ("GET", "/httpd/test_handler", handler)
+ server.router.register(*route)
+ server.start()
+
+ url = server.get_url("/httpd/test_handler")
+ body = urlopen(url).read()
+ res = json.loads(body)
+ assert res["count"] == counter
+
+
+if __name__ == "__main__":
+ mozunit.main("-p", "no:terminalreporter", "--log-tbpl=-", "--capture", "no")
diff --git a/testing/marionette/harness/marionette_harness/tests/harness_unit/test_marionette_arguments.py b/testing/marionette/harness/marionette_harness/tests/harness_unit/test_marionette_arguments.py
new file mode 100644
index 0000000000..b640741a6f
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/tests/harness_unit/test_marionette_arguments.py
@@ -0,0 +1,80 @@
+# 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 mozunit
+import pytest
+
+from marionette_harness.runtests import MarionetteArguments, MarionetteTestRunner
+
+
+@pytest.mark.parametrize("socket_timeout", ["A", "10", "1B-", "1C2", "44.35"])
+def test_parse_arg_socket_timeout(socket_timeout):
+ argv = ["marionette", "--socket-timeout", socket_timeout]
+ parser = MarionetteArguments()
+
+ def _is_float_convertible(value):
+ try:
+ float(value)
+ return True
+ except ValueError:
+ return False
+
+ if not _is_float_convertible(socket_timeout):
+ with pytest.raises(SystemExit) as ex:
+ parser.parse_args(args=argv)
+ assert ex.value.code == 2
+ else:
+ args = parser.parse_args(args=argv)
+ assert hasattr(args, "socket_timeout") and args.socket_timeout == float(
+ socket_timeout
+ )
+
+
+@pytest.mark.parametrize(
+ "arg_name, arg_dest, arg_value, expected_value",
+ [
+ ("app-arg", "app_args", "samplevalue", ["samplevalue"]),
+ ("symbols-path", "symbols_path", "samplevalue", "samplevalue"),
+ ("gecko-log", "gecko_log", "samplevalue", "samplevalue"),
+ ("app", "app", "samplevalue", "samplevalue"),
+ ],
+)
+def test_parsing_optional_arguments(
+ mach_parsed_kwargs, arg_name, arg_dest, arg_value, expected_value
+):
+ parser = MarionetteArguments()
+ parsed_args = parser.parse_args(["--" + arg_name, arg_value])
+ result = vars(parsed_args)
+ assert result.get(arg_dest) == expected_value
+ mach_parsed_kwargs[arg_dest] = result[arg_dest]
+ runner = MarionetteTestRunner(**mach_parsed_kwargs)
+ built_kwargs = runner._build_kwargs()
+ assert built_kwargs[arg_dest] == expected_value
+
+
+@pytest.mark.parametrize(
+ "arg_name, arg_dest, arg_value, expected_value",
+ [
+ ("adb", "adb_path", "samplevalue", "samplevalue"),
+ ("avd", "avd", "samplevalue", "samplevalue"),
+ ("avd-home", "avd_home", "samplevalue", "samplevalue"),
+ ("package", "package_name", "samplevalue", "samplevalue"),
+ ],
+)
+def test_parse_opt_args_emulator(
+ mach_parsed_kwargs, arg_name, arg_dest, arg_value, expected_value
+):
+ parser = MarionetteArguments()
+ parsed_args = parser.parse_args(["--" + arg_name, arg_value])
+ result = vars(parsed_args)
+ assert result.get(arg_dest) == expected_value
+ mach_parsed_kwargs[arg_dest] = result[arg_dest]
+ mach_parsed_kwargs["emulator"] = True
+ runner = MarionetteTestRunner(**mach_parsed_kwargs)
+ built_kwargs = runner._build_kwargs()
+ assert built_kwargs[arg_dest] == expected_value
+
+
+if __name__ == "__main__":
+ mozunit.main("-p", "no:terminalreporter", "--log-tbpl=-", "--capture", "no")
diff --git a/testing/marionette/harness/marionette_harness/tests/harness_unit/test_marionette_harness.py b/testing/marionette/harness/marionette_harness/tests/harness_unit/test_marionette_harness.py
new file mode 100644
index 0000000000..b528594381
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/tests/harness_unit/test_marionette_harness.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 mozunit
+import pytest
+
+from unittest.mock import Mock, patch, sentinel
+
+import marionette_harness.marionette_test as marionette_test
+
+from marionette_harness.runtests import MarionetteTestRunner, MarionetteHarness, cli
+
+
+@pytest.fixture
+def harness_class(request):
+ """
+ Mock based on MarionetteHarness whose run method just returns a number of
+ failures according to the supplied test parameter
+ """
+ if "num_fails_crashed" in request.fixturenames:
+ num_fails_crashed = request.getfixturevalue("num_fails_crashed")
+ else:
+ num_fails_crashed = (0, 0)
+ harness_cls = Mock(spec=MarionetteHarness)
+ harness = harness_cls.return_value
+ if num_fails_crashed is None:
+ harness.run.side_effect = Exception
+ else:
+ harness.run.return_value = sum(num_fails_crashed)
+ return harness_cls
+
+
+@pytest.fixture
+def runner_class(request):
+ """
+ Mock based on MarionetteTestRunner, wherein the runner.failed,
+ runner.crashed attributes are provided by a test parameter
+ """
+ if "num_fails_crashed" in request.fixturenames:
+ failures, crashed = request.getfixturevalue("num_fails_crashed")
+ else:
+ failures = 0
+ crashed = 0
+ mock_runner_class = Mock(spec=MarionetteTestRunner)
+ runner = mock_runner_class.return_value
+ runner.failed = failures
+ runner.crashed = crashed
+ return mock_runner_class
+
+
+@pytest.mark.parametrize(
+ "num_fails_crashed,exit_code",
+ [((0, 0), 0), ((1, 0), 10), ((0, 1), 10), (None, 1)],
+)
+def test_cli_exit_code(num_fails_crashed, exit_code, harness_class):
+ with pytest.raises(SystemExit) as err:
+ cli(harness_class=harness_class)
+ assert err.value.code == exit_code
+
+
+@pytest.mark.parametrize("num_fails_crashed", [(0, 0), (1, 0), (1, 1)])
+def test_call_harness_with_parsed_args_yields_num_failures(
+ mach_parsed_kwargs, runner_class, num_fails_crashed
+):
+ with patch(
+ "marionette_harness.runtests.MarionetteHarness.parse_args"
+ ) as parse_args:
+ failed_or_crashed = MarionetteHarness(
+ runner_class, args=mach_parsed_kwargs
+ ).run()
+ parse_args.assert_not_called()
+ assert failed_or_crashed == sum(num_fails_crashed)
+
+
+def test_call_harness_with_no_args_yields_num_failures(runner_class):
+ with patch(
+ "marionette_harness.runtests.MarionetteHarness.parse_args",
+ return_value={"tests": []},
+ ) as parse_args:
+ failed_or_crashed = MarionetteHarness(runner_class).run()
+ assert parse_args.call_count == 1
+ assert failed_or_crashed == 0
+
+
+def test_args_passed_to_runner_class(mach_parsed_kwargs, runner_class):
+ arg_list = list(mach_parsed_kwargs.keys())
+ arg_list.remove("tests")
+ mach_parsed_kwargs.update([(a, getattr(sentinel, a)) for a in arg_list])
+ harness = MarionetteHarness(runner_class, args=mach_parsed_kwargs)
+ harness.process_args = Mock()
+ harness.run()
+ for arg in arg_list:
+ assert harness._runner_class.call_args[1][arg] is getattr(sentinel, arg)
+
+
+def test_harness_sets_up_default_test_handlers(mach_parsed_kwargs):
+ """
+ If the necessary TestCase is not in test_handlers,
+ tests are omitted silently
+ """
+ harness = MarionetteHarness(args=mach_parsed_kwargs)
+ mach_parsed_kwargs.pop("tests")
+ runner = harness._runner_class(**mach_parsed_kwargs)
+ assert marionette_test.MarionetteTestCase in runner.test_handlers
+
+
+if __name__ == "__main__":
+ mozunit.main("-p", "no:terminalreporter", "--log-tbpl=-", "--capture", "no")
diff --git a/testing/marionette/harness/marionette_harness/tests/harness_unit/test_marionette_runner.py b/testing/marionette/harness/marionette_harness/tests/harness_unit/test_marionette_runner.py
new file mode 100644
index 0000000000..fc1a1c70ee
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/tests/harness_unit/test_marionette_runner.py
@@ -0,0 +1,541 @@
+# 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 manifestparser
+import mozinfo
+import mozunit
+import pytest
+
+from unittest.mock import Mock, patch, mock_open, sentinel, DEFAULT
+
+from marionette_harness.runtests import MarionetteTestRunner
+
+
+@pytest.fixture
+def runner(mach_parsed_kwargs):
+ """
+ MarionetteTestRunner instance initialized with default options.
+ """
+ return MarionetteTestRunner(**mach_parsed_kwargs)
+
+
+@pytest.fixture
+def mock_runner(runner, mock_marionette, monkeypatch):
+ """
+ MarionetteTestRunner instance with mocked-out
+ self.marionette and other properties,
+ to enable testing runner.run_tests().
+ """
+ runner.driverclass = Mock(return_value=mock_marionette)
+ for attr in ["run_test", "_capabilities"]:
+ setattr(runner, attr, Mock())
+ runner._appName = "fake_app"
+ monkeypatch.setattr("marionette_harness.runner.base.mozversion", Mock())
+ return runner
+
+
+@pytest.fixture
+def build_kwargs_using(mach_parsed_kwargs):
+ """Helper function for test_build_kwargs_* functions"""
+
+ def kwarg_builder(new_items, return_socket=False):
+ mach_parsed_kwargs.update(new_items)
+ runner = MarionetteTestRunner(**mach_parsed_kwargs)
+ with patch("marionette_harness.runner.base.socket") as socket:
+ built_kwargs = runner._build_kwargs()
+ if return_socket:
+ return built_kwargs, socket
+ return built_kwargs
+
+ return kwarg_builder
+
+
+@pytest.fixture
+def expected_driver_args(runner):
+ """Helper fixture for tests of _build_kwargs
+ with binary/emulator.
+ Provides a dictionary of certain arguments
+ related to binary/emulator settings
+ which we expect to be passed to the
+ driverclass constructor. Expected values can
+ be updated in tests as needed.
+ Provides convenience methods for comparing the
+ expected arguments to the argument dictionary
+ created by _build_kwargs."""
+
+ class ExpectedDict(dict):
+ def assert_matches(self, actual):
+ for k, v in self.items():
+ assert actual[k] == v
+
+ def assert_keys_not_in(self, actual):
+ for k in self.keys():
+ assert k not in actual
+
+ expected = ExpectedDict(host=None, port=None, bin=None)
+ for attr in ["app", "app_args", "profile", "addons", "gecko_log"]:
+ expected[attr] = getattr(runner, attr)
+ return expected
+
+
+class ManifestFixture:
+ def __init__(
+ self,
+ name="mock_manifest",
+ tests=[{"path": "test_something.py", "expected": "pass"}],
+ ):
+ self.filepath = "/path/to/fake/manifest.toml"
+ self.n_disabled = len([t for t in tests if "disabled" in t])
+ self.n_enabled = len(tests) - self.n_disabled
+ mock_manifest = Mock(
+ spec=manifestparser.TestManifest, active_tests=Mock(return_value=tests)
+ )
+ self.manifest_class = Mock(return_value=mock_manifest)
+ self.__repr__ = lambda: "<ManifestFixture {}>".format(name)
+
+
+@pytest.fixture
+def manifest():
+ return ManifestFixture()
+
+
+@pytest.fixture(params=["enabled", "disabled", "enabled_disabled", "empty"])
+def manifest_with_tests(request):
+ """
+ Fixture for the contents of mock_manifest, where a manifest
+ can include enabled tests, disabled tests, both, or neither (empty)
+ """
+ included = []
+ if "enabled" in request.param:
+ included += [
+ ("test_expected_pass.py", "pass"),
+ ("test_expected_fail.py", "fail"),
+ ]
+ if "disabled" in request.param:
+ included += [
+ ("test_pass_disabled.py", "pass", "skip-if: true"),
+ ("test_fail_disabled.py", "fail", "skip-if: true"),
+ ]
+ keys = ("path", "expected", "disabled")
+ active_tests = [dict(list(zip(keys, values))) for values in included]
+
+ return ManifestFixture(request.param, active_tests)
+
+
+def test_args_passed_to_driverclass(mock_runner):
+ built_kwargs = {"arg1": "value1", "arg2": "value2"}
+ mock_runner._build_kwargs = Mock(return_value=built_kwargs)
+ with pytest.raises(IOError):
+ mock_runner.run_tests(["fake_tests.toml"])
+ assert mock_runner.driverclass.call_args[1] == built_kwargs
+
+
+def test_build_kwargs_basic_args(build_kwargs_using):
+ """Test the functionality of runner._build_kwargs:
+ make sure that basic arguments (those which should
+ always be included, irrespective of the runner's settings)
+ get passed to the call to runner.driverclass"""
+
+ basic_args = [
+ "socket_timeout",
+ "prefs",
+ "startup_timeout",
+ "verbose",
+ "symbols_path",
+ ]
+ args_dict = {a: getattr(sentinel, a) for a in basic_args}
+ # Mock an update method to work with calls to MarionetteTestRunner()
+ args_dict["prefs"].update = Mock(return_value={})
+ built_kwargs = build_kwargs_using([(a, getattr(sentinel, a)) for a in basic_args])
+ for arg in basic_args:
+ assert built_kwargs[arg] is getattr(sentinel, arg)
+
+
+@pytest.mark.parametrize("workspace", ["path/to/workspace", None])
+def test_build_kwargs_with_workspace(build_kwargs_using, workspace):
+ built_kwargs = build_kwargs_using({"workspace": workspace})
+ if workspace:
+ assert built_kwargs["workspace"] == workspace
+ else:
+ assert "workspace" not in built_kwargs
+
+
+@pytest.mark.parametrize("address", ["host:123", None])
+def test_build_kwargs_with_address(build_kwargs_using, address):
+ built_kwargs, socket = build_kwargs_using(
+ {"address": address, "binary": None, "emulator": None}, return_socket=True
+ )
+ assert "connect_to_running_emulator" not in built_kwargs
+ if address is not None:
+ host, port = address.split(":")
+ assert built_kwargs["host"] == host and built_kwargs["port"] == int(port)
+ socket.socket().connect.assert_called_with((host, int(port)))
+ assert socket.socket().close.called
+ else:
+ assert not socket.socket.called
+
+
+@pytest.mark.parametrize("address", ["host:123", None])
+@pytest.mark.parametrize("binary", ["path/to/bin", None])
+def test_build_kwargs_with_binary_or_address(
+ expected_driver_args, build_kwargs_using, binary, address
+):
+ built_kwargs = build_kwargs_using(
+ {"binary": binary, "address": address, "emulator": None}
+ )
+ if binary:
+ expected_driver_args["bin"] = binary
+ if address:
+ host, port = address.split(":")
+ expected_driver_args.update({"host": host, "port": int(port)})
+ else:
+ expected_driver_args.update({"host": "127.0.0.1", "port": 2828})
+ expected_driver_args.assert_matches(built_kwargs)
+ elif address is None:
+ expected_driver_args.assert_keys_not_in(built_kwargs)
+
+
+@pytest.mark.parametrize("address", ["host:123", None])
+@pytest.mark.parametrize("emulator", [True, False, None])
+def test_build_kwargs_with_emulator_or_address(
+ expected_driver_args, build_kwargs_using, emulator, address
+):
+ emulator_props = [
+ (a, getattr(sentinel, a)) for a in ["avd_home", "adb_path", "emulator_bin"]
+ ]
+ built_kwargs = build_kwargs_using(
+ [("emulator", emulator), ("address", address), ("binary", None)]
+ + emulator_props
+ )
+ if emulator:
+ expected_driver_args.update(emulator_props)
+ expected_driver_args["emulator_binary"] = expected_driver_args.pop(
+ "emulator_bin"
+ )
+ expected_driver_args["bin"] = True
+ if address:
+ expected_driver_args["connect_to_running_emulator"] = True
+ host, port = address.split(":")
+ expected_driver_args.update({"host": host, "port": int(port)})
+ else:
+ expected_driver_args.update({"host": "127.0.0.1", "port": 2828})
+ assert "connect_to_running_emulator" not in built_kwargs
+ expected_driver_args.assert_matches(built_kwargs)
+ elif not address:
+ expected_driver_args.assert_keys_not_in(built_kwargs)
+
+
+def test_parsing_testvars(mach_parsed_kwargs):
+ mach_parsed_kwargs.pop("tests")
+ testvars_json_loads = [
+ {"wifi": {"ssid": "blah", "keyManagement": "WPA-PSK", "psk": "foo"}},
+ {"wifi": {"PEAP": "bar"}, "device": {"stuff": "buzz"}},
+ ]
+ expected_dict = {
+ "wifi": {
+ "ssid": "blah",
+ "keyManagement": "WPA-PSK",
+ "psk": "foo",
+ "PEAP": "bar",
+ },
+ "device": {"stuff": "buzz"},
+ }
+ with patch(
+ "marionette_harness.runtests.MarionetteTestRunner._load_testvars",
+ return_value=testvars_json_loads,
+ ) as load:
+ runner = MarionetteTestRunner(**mach_parsed_kwargs)
+ assert runner.testvars == expected_dict
+ assert load.call_count == 1
+
+
+def test_load_testvars_throws_expected_errors(mach_parsed_kwargs):
+ mach_parsed_kwargs["testvars"] = ["some_bad_path.json"]
+ runner = MarionetteTestRunner(**mach_parsed_kwargs)
+ with pytest.raises(IOError) as io_exc:
+ runner._load_testvars()
+ assert "does not exist" in str(io_exc.value)
+ with patch("os.path.exists", return_value=True):
+ with patch(
+ "marionette_harness.runner.base.open",
+ mock_open(read_data="[not {valid JSON]"),
+ ):
+ with pytest.raises(Exception) as json_exc:
+ runner._load_testvars()
+ assert "not properly formatted" in str(json_exc.value)
+
+
+def _check_crash_counts(has_crashed, runner, mock_marionette):
+ if has_crashed:
+ assert mock_marionette.check_for_crash.call_count == 1
+ assert runner.crashed == 1
+ else:
+ assert runner.crashed == 0
+
+
+@pytest.mark.parametrize("has_crashed", [True, False])
+def test_increment_crash_count_in_run_test_set(runner, has_crashed, mock_marionette):
+ fake_tests = [{"filepath": i, "expected": "pass"} for i in "abc"]
+
+ with patch.multiple(runner, run_test=DEFAULT, marionette=mock_marionette):
+ runner.run_test_set(fake_tests)
+ if not has_crashed:
+ assert runner.marionette.check_for_crash.call_count == len(fake_tests)
+ _check_crash_counts(has_crashed, runner, runner.marionette)
+
+
+@pytest.mark.parametrize("has_crashed", [True, False])
+def test_record_crash(runner, has_crashed, mock_marionette):
+ with patch.object(runner, "marionette", mock_marionette):
+ assert runner.record_crash() == has_crashed
+ _check_crash_counts(has_crashed, runner, runner.marionette)
+
+
+def test_add_test_module(runner):
+ tests = ["test_something.py", "testSomething.js", "bad_test.py"]
+ assert len(runner.tests) == 0
+ for test in tests:
+ with patch("os.path.abspath", return_value=test) as abspath:
+ runner.add_test(test)
+ assert abspath.called
+ expected = {"filepath": test, "expected": "pass", "group": "default"}
+ assert expected in runner.tests
+ # add_test doesn't validate module names; 'bad_test.py' gets through
+ assert len(runner.tests) == 3
+
+
+def test_add_test_directory(runner):
+ test_dir = "path/to/tests"
+ dir_contents = [
+ (test_dir, ("subdir",), ("test_a.py", "bad_test_a.py")),
+ (test_dir + "/subdir", (), ("test_b.py", "bad_test_b.py")),
+ ]
+ tests = list(dir_contents[0][2] + dir_contents[1][2])
+ assert len(runner.tests) == 0
+ # Need to use side effect to make isdir return True for test_dir and False for tests
+ with patch("os.path.isdir", side_effect=[True] + [False for t in tests]) as isdir:
+ with patch("os.walk", return_value=dir_contents) as walk:
+ runner.add_test(test_dir)
+ assert isdir.called and walk.called
+ for test in runner.tests:
+ assert os.path.normpath(test_dir) in test["filepath"]
+ assert len(runner.tests) == 2
+
+
+@pytest.mark.parametrize("test_files_exist", [True, False])
+def test_add_test_manifest(
+ mock_runner, manifest_with_tests, monkeypatch, test_files_exist
+):
+ monkeypatch.setattr(
+ "marionette_harness.runner.base.TestManifest",
+ manifest_with_tests.manifest_class,
+ )
+ mock_runner.marionette = mock_runner.driverclass()
+ with patch(
+ "marionette_harness.runner.base.os.path.exists", return_value=test_files_exist
+ ):
+ if test_files_exist or manifest_with_tests.n_enabled == 0:
+ mock_runner.add_test(manifest_with_tests.filepath)
+ assert len(mock_runner.tests) == manifest_with_tests.n_enabled
+ assert (
+ len(mock_runner.manifest_skipped_tests)
+ == manifest_with_tests.n_disabled
+ )
+ for test in mock_runner.tests:
+ assert test["filepath"].endswith(test["expected"] + ".py")
+ else:
+ with pytest.raises(IOError):
+ mock_runner.add_test(manifest_with_tests.filepath)
+
+ assert manifest_with_tests.manifest_class().read.called
+ assert manifest_with_tests.manifest_class().active_tests.called
+
+
+def get_kwargs_passed_to_manifest(mock_runner, manifest, monkeypatch, **kwargs):
+ """Helper function for test_manifest_* tests.
+ Returns the kwargs passed to the call to manifest.active_tests."""
+ monkeypatch.setattr(
+ "marionette_harness.runner.base.TestManifest", manifest.manifest_class
+ )
+ monkeypatch.setitem(mozinfo.info, "mozinfo_key", "mozinfo_val")
+ for attr in kwargs:
+ setattr(mock_runner, attr, kwargs[attr])
+ mock_runner.marionette = mock_runner.driverclass()
+ with patch("marionette_harness.runner.base.os.path.exists", return_value=True):
+ mock_runner.add_test(manifest.filepath)
+ call_args, call_kwargs = manifest.manifest_class().active_tests.call_args
+ return call_kwargs
+
+
+def test_manifest_basic_args(mock_runner, manifest, monkeypatch):
+ kwargs = get_kwargs_passed_to_manifest(mock_runner, manifest, monkeypatch)
+ assert kwargs["exists"] is False
+ assert kwargs["disabled"] is True
+ assert kwargs["appname"] == "fake_app"
+ assert "mozinfo_key" in kwargs and kwargs["mozinfo_key"] == "mozinfo_val"
+
+
+@pytest.mark.parametrize("test_tags", (None, ["tag", "tag2"]))
+def test_manifest_with_test_tags(mock_runner, manifest, monkeypatch, test_tags):
+ kwargs = get_kwargs_passed_to_manifest(
+ mock_runner, manifest, monkeypatch, test_tags=test_tags
+ )
+ if test_tags is None:
+ assert kwargs["filters"] == []
+ else:
+ assert len(kwargs["filters"]) == 1 and kwargs["filters"][0].tags == test_tags
+
+
+def test_cleanup_with_manifest(mock_runner, manifest_with_tests, monkeypatch):
+ monkeypatch.setattr(
+ "marionette_harness.runner.base.TestManifest",
+ manifest_with_tests.manifest_class,
+ )
+ if manifest_with_tests.n_enabled > 0:
+ context = patch(
+ "marionette_harness.runner.base.os.path.exists", return_value=True
+ )
+ else:
+ context = pytest.raises(Exception)
+ with context:
+ mock_runner.run_tests([manifest_with_tests.filepath])
+ assert mock_runner.marionette is None
+ assert mock_runner.fixture_servers == {}
+
+
+def test_reset_test_stats(mock_runner):
+ def reset_successful(runner):
+ stats = [
+ "passed",
+ "failed",
+ "unexpected_successes",
+ "todo",
+ "skipped",
+ "failures",
+ ]
+ return all([((s in vars(runner)) and (not vars(runner)[s])) for s in stats])
+
+ assert reset_successful(mock_runner)
+ mock_runner.passed = 1
+ mock_runner.failed = 1
+ mock_runner.failures.append(["TEST-UNEXPECTED-FAIL"])
+ assert not reset_successful(mock_runner)
+ mock_runner.run_tests(["test_fake_thing.py"])
+ assert reset_successful(mock_runner)
+
+
+def test_initialize_test_run(mock_runner):
+ tests = ["test_fake_thing.py"]
+ mock_runner.reset_test_stats = Mock()
+ mock_runner.run_tests(tests)
+ assert mock_runner.reset_test_stats.called
+ with pytest.raises(AssertionError) as test_exc:
+ mock_runner.run_tests([])
+ assert "len(tests)" in str(test_exc.traceback[-1].statement)
+ with pytest.raises(AssertionError) as hndl_exc:
+ mock_runner.test_handlers = []
+ mock_runner.run_tests(tests)
+ assert "test_handlers" in str(hndl_exc.traceback[-1].statement)
+ assert mock_runner.reset_test_stats.call_count == 1
+
+
+def test_add_tests(mock_runner):
+ assert len(mock_runner.tests) == 0
+ fake_tests = ["test_" + i + ".py" for i in "abc"]
+ mock_runner.run_tests(fake_tests)
+ assert len(mock_runner.tests) == 3
+ for test_name, added_test in zip(fake_tests, mock_runner.tests):
+ assert added_test["filepath"].endswith(test_name)
+
+
+def test_repeat(mock_runner):
+ def update_result(test, expected):
+ mock_runner.failed += 1
+
+ fake_tests = ["test_1.py"]
+ mock_runner.repeat = 4
+ mock_runner.run_test = Mock(side_effect=update_result)
+ mock_runner.run_tests(fake_tests)
+
+ assert mock_runner.failed == 5
+ assert mock_runner.passed == 0
+ assert mock_runner.todo == 0
+
+
+def test_run_until_failure(mock_runner):
+ def update_result(test, expected):
+ mock_runner.failed += 1
+
+ fake_tests = ["test_1.py"]
+ mock_runner.run_until_failure = True
+ mock_runner.repeat = 4
+ mock_runner.run_test = Mock(side_effect=update_result)
+ mock_runner.run_tests(fake_tests)
+
+ assert mock_runner.failed == 1
+ assert mock_runner.passed == 0
+ assert mock_runner.todo == 0
+
+
+def test_catch_invalid_test_names(runner):
+ good_tests = ["test_ok.py", "test_is_ok.py"]
+ bad_tests = [
+ "bad_test.py",
+ "testbad.py",
+ "_test_bad.py",
+ "test_bad.notpy",
+ "test_bad",
+ "test.py",
+ "test_.py",
+ ]
+ with pytest.raises(Exception) as exc:
+ runner._add_tests(good_tests + bad_tests)
+ msg = str(exc.value)
+ assert "Test file names must be of the form" in msg
+ for bad_name in bad_tests:
+ assert bad_name in msg
+ for good_name in good_tests:
+ assert good_name not in msg
+
+
+@pytest.mark.parametrize("repeat", (None, 0, 42, -1))
+def test_option_repeat(mach_parsed_kwargs, repeat):
+ if repeat is not None:
+ mach_parsed_kwargs["repeat"] = repeat
+ runner = MarionetteTestRunner(**mach_parsed_kwargs)
+
+ if repeat is None:
+ assert runner.repeat == 0
+ else:
+ assert runner.repeat == repeat
+
+
+@pytest.mark.parametrize("repeat", (None, 42))
+@pytest.mark.parametrize("run_until_failure", (None, True))
+def test_option_run_until_failure(mach_parsed_kwargs, repeat, run_until_failure):
+ if run_until_failure is not None:
+ mach_parsed_kwargs["run_until_failure"] = run_until_failure
+ if repeat is not None:
+ mach_parsed_kwargs["repeat"] = repeat
+ runner = MarionetteTestRunner(**mach_parsed_kwargs)
+
+ if run_until_failure is None:
+ assert runner.run_until_failure is False
+ if repeat is None:
+ assert runner.repeat == 0
+ else:
+ assert runner.repeat == repeat
+
+ else:
+ assert runner.run_until_failure == run_until_failure
+ if repeat is None:
+ assert runner.repeat == 30
+ else:
+ assert runner.repeat == repeat
+
+
+if __name__ == "__main__":
+ mozunit.main("-p", "no:terminalreporter", "--log-tbpl=-", "--capture", "no")
diff --git a/testing/marionette/harness/marionette_harness/tests/harness_unit/test_marionette_test_result.py b/testing/marionette/harness/marionette_harness/tests/harness_unit/test_marionette_test_result.py
new file mode 100644
index 0000000000..6269b4135e
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/tests/harness_unit/test_marionette_test_result.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 mozunit
+import pytest
+
+from marionette_harness import MarionetteTestResult
+
+
+@pytest.fixture
+def empty_marionette_testcase():
+ """Testable MarionetteTestCase class"""
+ from marionette_harness import MarionetteTestCase
+
+ class EmptyTestCase(MarionetteTestCase):
+ def test_nothing(self):
+ pass
+
+ return EmptyTestCase
+
+
+@pytest.fixture
+def empty_marionette_test(mock_marionette, empty_marionette_testcase):
+ return empty_marionette_testcase(
+ lambda: mock_marionette, lambda: mock_httpd, "test_nothing"
+ )
+
+
+@pytest.mark.parametrize("has_crashed", [True, False])
+def test_crash_is_recorded_as_error(empty_marionette_test, logger, has_crashed):
+ """Number of errors is incremented by stopTest iff has_crashed is true"""
+ # collect results from the empty test
+ result = MarionetteTestResult(
+ marionette=empty_marionette_test._marionette_weakref(),
+ logger=logger,
+ verbosity=1,
+ stream=None,
+ descriptions=None,
+ )
+ result.startTest(empty_marionette_test)
+ assert len(result.errors) == 0
+ assert len(result.failures) == 0
+ assert result.testsRun == 1
+ assert result.shouldStop is False
+ result.stopTest(empty_marionette_test)
+ assert result.shouldStop == has_crashed
+ if has_crashed:
+ assert len(result.errors) == 1
+ else:
+ assert len(result.errors) == 0
+
+
+if __name__ == "__main__":
+ mozunit.main("-p", "no:terminalreporter", "--log-tbpl=-", "--capture", "no")
diff --git a/testing/marionette/harness/marionette_harness/tests/harness_unit/test_serve.py b/testing/marionette/harness/marionette_harness/tests/harness_unit/test_serve.py
new file mode 100644
index 0000000000..84e1f7ddf4
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/tests/harness_unit/test_serve.py
@@ -0,0 +1,69 @@
+# 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
+
+import mozunit
+import pytest
+
+from marionette_harness.runner import serve
+from marionette_harness.runner.serve import iter_proc, iter_url
+
+
+def teardown_function(func):
+ for server in [s for s in iter_proc(serve.servers) if s.is_alive]:
+ server.stop()
+ server.kill()
+
+
+def test_registered_servers():
+ # [(name, factory), ...]
+ assert serve.registered_servers[0][0] == "http"
+ assert serve.registered_servers[1][0] == "https"
+
+
+def test_globals():
+ assert serve.default_doc_root is not None
+ assert serve.registered_servers is not None
+ assert serve.servers is not None
+
+
+def test_start():
+ serve.start()
+ assert len(serve.servers) == 2
+ assert "http" in serve.servers
+ assert "https" in serve.servers
+ for url in iter_url(serve.servers):
+ assert isinstance(url, six.string_types)
+
+
+def test_start_with_custom_root(tmpdir_factory):
+ tdir = tmpdir_factory.mktemp("foo")
+ serve.start(str(tdir))
+ for server in iter_proc(serve.servers):
+ assert server.doc_root == tdir
+
+
+def test_iter_proc():
+ serve.start()
+ for server in iter_proc(serve.servers):
+ server.stop()
+
+
+def test_iter_url():
+ serve.start()
+ for url in iter_url(serve.servers):
+ assert isinstance(url, six.string_types)
+
+
+def test_where_is():
+ serve.start()
+ assert serve.where_is("/") == serve.servers["http"][1].get_url("/")
+ assert serve.where_is("/", on="https") == serve.servers["https"][1].get_url("/")
+
+
+if __name__ == "__main__":
+ mozunit.main("-p", "no:terminalreporter", "--log-tbpl=-", "--capture", "no")
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
new file mode 100644
index 0000000000..bd1177462e
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/tests/unit/webextension-invalid.xpi
Binary files differ
diff --git a/testing/marionette/harness/marionette_harness/tests/unit/webextension-signed.xpi b/testing/marionette/harness/marionette_harness/tests/unit/webextension-signed.xpi
new file mode 100644
index 0000000000..5363911af1
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/tests/unit/webextension-signed.xpi
Binary files differ
diff --git a/testing/marionette/harness/marionette_harness/tests/unit/webextension-unsigned.xpi b/testing/marionette/harness/marionette_harness/tests/unit/webextension-unsigned.xpi
new file mode 100644
index 0000000000..cf0fad63b5
--- /dev/null
+++ b/testing/marionette/harness/marionette_harness/tests/unit/webextension-unsigned.xpi
Binary files differ